// ==UserScript== // @name my-xmly // @version 0.0.2 // @description 登录后支持VIP音频下载,支持专辑批量下载,支持失败后自动重试,支持添加编号。(基于“黑客, uid:219866”,"Ming"代码) // @author Ming // @match *://www.ximalaya.com/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_download // @icon https://www.ximalaya.com/favicon.ico // @require https://registry.npmmirror.com/crypto-js/4.1.1/files/crypto-js.js // @license MIT // @namespace https://greasyfork.org/users/1438860 // @downloadURL https://update.greasyfork.icu/scripts/535106/my-xmly.user.js // @updateURL https://update.greasyfork.icu/scripts/535106/my-xmly.meta.js // ==/UserScript== (function() { 'use strict'; function showAlert(message, type = 'info') { const alertDiv = document.createElement('div'); alertDiv.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 10px 20px; background: ${type === 'error' ? '#fff2f0' : '#f6ffed'}; border: 1px solid ${type === 'error' ? '#ffccc7' : '#b7eb8f'}; color: ${type === 'error' ? '#cf1322' : '#389e0d'}; border-radius: 4px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; box-shadow: 0 2px 8px rgba(0,0,0,0.15); `; alertDiv.textContent = message; document.body.appendChild(alertDiv); setTimeout(() => { alertDiv.style.transition = 'opacity 0.3s'; alertDiv.style.opacity = '0'; setTimeout(() => alertDiv.remove(), 300); }, 3000); } function initSetting() { var setting; if (!GM_getValue('priate_script_xmly_data')) { GM_setValue('priate_script_xmly_data', { left: 20, top: 100, manualMusicURL: null, quality: 1, showNumber: true, numberOffset: 1 }) } setting = GM_getValue('priate_script_xmly_data') if (!setting.quality) setting.quality = 1; setting.quality = 1; // Always high quality if (setting.showNumber === null) setting.showNumber = true; if (!setting.numberOffset) setting.numberOffset = 1; GM_setValue('priate_script_xmly_data', setting) } // 手动获取音频地址功能 function manualGetMusicURL() { let windowID = getRandStr("1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM", 100) function getRandStr(chs, len) { let str = ""; while (len--) { str += chs[parseInt(Math.random() * chs.length)]; } return str; } (function() { let playOriginal = HTMLAudioElement.prototype.play; function play() { let link = this.src; window.top.postMessage(Array("audioVideoCapturer", link, windowID, "link"), "*"); return playOriginal.call(this); } HTMLAudioElement.prototype.play = play; HTMLAudioElement.prototype.play.toString = HTMLAudioElement.prototype.play.toString.bind(playOriginal); })(); if (window.top == window) { window.addEventListener("message", function(event) { if (event.data[0] == "audioVideoCapturer") { var setting = GM_getValue('priate_script_xmly_data') setting.manualMusicURL = event.data[1] GM_setValue('priate_script_xmly_data', setting) } }); } } manualGetMusicURL() function injectDiv() { try { // Check if div already exists if (document.getElementById('priate_script_div')) { return; } var priate_script_div = document.createElement("div"); priate_script_div.innerHTML = `
喜马拉雅下载器

音质: 标准 | 编号: 关闭

`; document.body.appendChild(priate_script_div); // Set initial position const setting = GM_getValue('priate_script_xmly_data'); const div = document.getElementById('priate_script_div'); if (div) { div.style.left = (setting.left || 20) + "px"; div.style.top = (setting.top || 100) + "px"; } } catch (error) { console.error('Failed to inject div:', error); } } function dragFunc(id) { var Drag = document.getElementById(id); var setting = GM_getValue('priate_script_xmly_data') Drag.onmousedown = function(event) { var ev = event || window.event; event.stopPropagation(); var disX = ev.clientX - Drag.offsetLeft; var disY = ev.clientY - Drag.offsetTop; document.onmousemove = function(event) { var ev = event || window.event; setting.left = ev.clientX - disX Drag.style.left = setting.left + "px"; setting.top = ev.clientY - disY Drag.style.top = setting.top + "px"; Drag.style.cursor = "move"; GM_setValue('priate_script_xmly_data', setting) }; }; Drag.onmouseup = function() { document.onmousemove = null; this.style.cursor = "default"; }; }; // 第一种获取musicURL的方式,任意用户均可获得,不可获得VIP音频 async function getSimpleMusicURL1(item) { var res = null; if (item.url) { res = item.url; } else { const timestamp = Date.parse(new Date()); var url = `https://mobwsa.ximalaya.com/mobile-playpage/playpage/tabs/${item.id}/${timestamp}`; try { const response = await fetch(url); const resp = await response.json(); if (resp.ret === 0) { const setting = GM_getValue('priate_script_xmly_data'); const trackInfo = resp.data.playpage.trackInfo; if (setting.quality == 0) { res = trackInfo.playUrl32; } else if (setting.quality == 1) { res = trackInfo.playUrl64; } } } catch (error) { console.error('Error fetching URL:', error); } } return res; } // 第二种获取musicURL的方式,任意用户均可获得,不可获得VIP音频 async function getSimpleMusicURL2(item) { var res = null; if (item.url) { res = item.url; } else { var url = `https://www.ximalaya.com/revision/play/v1/audio?id=${item.id}&ptype=1`; try { const response = await fetch(url); const resp = await response.json(); if (resp.ret == 200) { res = resp.data.src; } } catch (error) { console.error('Error fetching URL:', error); } } return res; } //获取任意音频方法 async function getAllMusicURL1(item) { var res = null var setting; if (item.url) { res = item.url } else { const all_li = document.querySelectorAll('.sound-list>ul li'); for (var num = 0; num < all_li.length; num++) { var li = all_li[num] const item_a = li.querySelector('a'); const id = item_a.href.split('/')[item_a.href.split('/').length - 1] if (id == item.id) { li.querySelector('div.all-icon').click() while (!res) { await Sleep(1) setting = GM_getValue('priate_script_xmly_data') res = setting.manualMusicURL } setting.manualMusicURL = null GM_setValue('priate_script_xmly_data', setting) li.querySelector('div.all-icon').click() break } } } if (!res && item.isSingle) { document.querySelector('div.play-btn').click() while (!res) { await Sleep(1) setting = GM_getValue('priate_script_xmly_data') res = setting.manualMusicURL } setting.manualMusicURL = null GM_setValue('priate_script_xmly_data', setting) document.querySelector('div.play-btn').click() } return res } // 通过解密数据的方式获取 URL async function getAllMusicURL2(item) { function decrypt(t) { return CryptoJS.AES.decrypt({ ciphertext: CryptoJS.enc.Base64url.parse(t) }, CryptoJS.enc.Hex.parse('aaad3e4fd540b0f79dca95606e72bf93'), { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }).toString(CryptoJS.enc.Utf8) } var res = null; if (item.url) { res = item.url; } else { const timestamp = Date.parse(new Date()); var url = `https://www.ximalaya.com/mobile-playpage/track/v3/baseInfo/${timestamp}?device=web&trackId=${item.id}`; try { const response = await fetch(url); const resp = await response.json(); try { res = decrypt(resp.trackInfo.playUrlList[0].url); } catch (e) { console.log("解密错误"); res = null; } } catch (error) { console.error('Error fetching URL:', error); } } return res; } class XimalayaDownloader { constructor() { this.setting = GM_getValue('priate_script_xmly_data'); this.data = []; this.musicList = []; this.isDownloading = false; this.cancelDownloadObj = null; this.stopDownload = false; this.autoDownloadEnabled = false; this.autoDownloadPaused = false; this.createButtons(); this.initElements(); this.initEventListeners(); this.updateUI(); } createButtons() { // Create auto download button only const autoDownloadBtn = document.createElement('button'); autoDownloadBtn.id = 'autoDownloadBtn'; autoDownloadBtn.textContent = '自动下载'; autoDownloadBtn.style.display = 'none'; const downloadBtn = document.getElementById('downloadBtn'); if (downloadBtn) { downloadBtn.parentNode.insertBefore(autoDownloadBtn, downloadBtn.nextSibling); } } initElements() { try { this.elements = { qualityBtn: document.getElementById('qualityBtn'), numberToggleBtn: document.getElementById('numberToggleBtn'), numberOffsetBtn: document.getElementById('numberOffsetBtn'), numberOffsetSpan: document.getElementById('numberOffsetSpan'), loadBtn: document.getElementById('loadBtn'), downloadBtn: document.getElementById('downloadBtn'), exportBtn: document.getElementById('exportBtn'), cancelBtn: document.getElementById('cancelBtn'), selectAllBtn: document.getElementById('selectAllBtn'), musicTable: document.getElementById('musicTable'), musicTableBody: document.getElementById('musicTableBody'), autoDownloadBtn: document.getElementById('autoDownloadBtn') }; // Verify all elements were found for (let key in this.elements) { if (!this.elements[key]) { throw new Error(`Element ${key} not found`); } } } catch (error) { console.error('Failed to initialize elements:', error); throw error; } } initEventListeners() { this.elements.qualityBtn.onclick = () => this.changeQuality(); this.elements.numberToggleBtn.onclick = () => this.switchShowNumber(); this.elements.numberOffsetBtn.onclick = () => this.addNumberOffset(); this.elements.numberOffsetBtn.oncontextmenu = (e) => { e.preventDefault(); this.subNumberOffset(); }; this.elements.loadBtn.onclick = () => this.loadMusic(); this.elements.downloadBtn.onclick = () => this.downloadAllMusics(); this.elements.exportBtn.onclick = () => this.exportAllMusicURL(); this.elements.cancelBtn.onclick = () => this.cancelDownload(); this.elements.selectAllBtn.onclick = () => this.selectAllMusic(); this.elements.autoDownloadBtn.onclick = () => this.toggleAutoDownload(); } updateUI() { // Update quality button const qualityColors = ["#946C00", "#55ACEE", "#00947e", "#337ab7"]; const qualityTexts = ["标准", "高清", "超高", "未知"]; const quality = (this.setting.quality >= 0 && this.setting.quality <= 2) ? this.setting.quality : 3; this.elements.qualityBtn.style.color = qualityColors[quality]; this.elements.qualityBtn.textContent = qualityTexts[quality]; // Update number toggle this.elements.numberToggleBtn.style.color = this.setting.showNumber ? "#00947e" : "#CC0F35"; this.elements.numberToggleBtn.textContent = this.setting.showNumber ? "开启" : "关闭"; this.elements.numberOffsetSpan.style.display = this.setting.showNumber ? "inline" : "none"; this.elements.numberOffsetBtn.textContent = this.setting.numberOffset; // Update buttons this.elements.loadBtn.textContent = this.data.length > 0 ? "重载" : "加载"; this.elements.downloadBtn.style.display = (!this.isDownloading && this.musicList.length > 0) ? "inline" : "none"; this.elements.exportBtn.style.display = (!this.isDownloading && this.musicList.length > 0) ? "inline" : "none"; this.elements.cancelBtn.style.display = this.isDownloading ? "inline" : "none"; this.elements.musicTable.style.display = this.data.length > 0 ? "block" : "none"; this.elements.autoDownloadBtn.style.display = (!this.isDownloading && this.musicList.length > 0) ? "inline" : "none"; this.elements.autoDownloadBtn.textContent = this.autoDownloadEnabled ? "停止自动" : "自动下载"; this.renderMusicList(); } renderMusicList() { this.elements.musicTableBody.innerHTML = ''; const dataToRender = this.isDownloading ? this.musicList : this.data; dataToRender.forEach(item => { const tr = document.createElement('tr'); tr.innerHTML = ` ${item.title} ${this.getActionButtonHTML(item)} `; // Add event listeners const checkbox = tr.querySelector('input'); checkbox.onchange = () => this.toggleMusicSelection(item); const downloadBtn = tr.querySelector('.downloadBtn'); if (downloadBtn) { downloadBtn.onclick = () => this.downloadMusic(item); } this.elements.musicTableBody.appendChild(tr); }); } getActionButtonHTML(item) { if (item.isDownloaded) return '完成'; if (item.isDownloading) return `${item.progress}`; if (item.isFailued) return '失败'; if (this.isDownloading) return ''; return '下载'; } async loadMusic() { const whiteList = ['sound', 'album']; const type = location.pathname.split('/')[location.pathname.split('/').length - 2]; if (whiteList.indexOf(type) < 0) { showAlert("请先进入一个专辑页面并等待页面完全加载!", "error"); this.data = []; this.musicList = []; this.updateUI(); return; } const all_li = document.querySelectorAll('.sound-list>ul li'); const result = []; all_li.forEach((item) => { const item_a = item.querySelector('a'); const number = item.querySelector('span.num') ? parseInt(item.querySelector('span.num').innerText) : 0; const title = item_a.title.trim() .replace(/\\|\/|\?|\?|\*|\"|\"|\"|\'|\'|\'|\<|\>|\{|\}|\[|\]|\【|\】|\:|\:|\、|\^|\$|\!|\~|\`|\|/g, '') .replace(/\./g, '-'); const formattedNumber = String(number + this.setting.numberOffset - 1).padStart(4, '0'); const music = { id: item_a.href.split('/')[item_a.href.split('/').length - 1], number, title: this.setting.showNumber ? `${formattedNumber}-${title}` : title, isDownloading: false, isDownloaded: false, progress: 0, }; result.push(music); }); // Handle single audio case if (result.length === 0 && type === 'sound') { const music = { id: location.pathname.split('/')[location.pathname.split('/').length - 1], title: this.setting.showNumber ? `0001-${document.querySelector('h1.title-wrapper')?.innerText || '未知标题'}` : document.querySelector('h1.title-wrapper')?.innerText || '未知标题', isDownloading: false, isDownloaded: false, progress: 0, isSingle: true }; result.push(music); } if (result.length === 0) { showAlert("未获取到数据,请进入一个专辑页面并等待页面完全加载!", "error"); } this.data = result; this.musicList = [...result]; this.updateUI(); } async downloadMusic(item) { if (this.stopDownload || item.isDownloading || item.isDownloaded) return; item.isDownloading = true; item.isFailued = false; this.updateUI(); try { const url = item.url || await this.getMusicURL(item); if (!url) { throw new Error('Failed to get music URL'); } return new Promise((resolve, reject) => { const details = { url: url, name: item.title + '.mp3', onload: () => { item.isDownloading = false; item.isDownloaded = true; this.updateUI(); resolve(); }, onerror: (e) => { item.isDownloading = false; if (e.error !== 'aborted') { item.isFailued = true; } this.updateUI(); reject(e); }, onprogress: (d) => { item.progress = (Math.round(d.loaded / d.total * 10000) / 100.00) + "%"; this.updateUI(); } }; this.cancelDownloadObj = GM_download(details); }); } catch (error) { console.error('Download failed:', error); item.isDownloading = false; item.isFailued = true; this.updateUI(); throw error; } } async getMusicURL(item) { let url = await getSimpleMusicURL1(item); if (!url) url = await getSimpleMusicURL2(item); if (!url) url = await getAllMusicURL2(item); if (!url) url = await getAllMusicURL1(item); return url; } async downloadAllMusics() { try { this.isDownloading = true; this.stopDownload = false; this.updateUI(); for (const item of this.musicList) { if (this.stopDownload) break; if (!item.isDownloaded && !item.isDownloading) { try { await this.downloadMusic(item); // Random delay between 3-6 seconds between downloads const delay = Math.floor(Math.random() * 2000) + 2000; await new Promise(resolve => setTimeout(resolve, delay)); } catch (error) { this.autoDownloadEnabled = false; throw error; } } } } catch (error) { console.error('Batch download failed:', error); showAlert('批量下载失败,已停止所有任务', 'error'); // 添加1秒后自动点击autoDownloadBtn的逻辑 setTimeout(() => { const autoDownloadBtn = document.getElementById('autoDownloadBtn'); if (autoDownloadBtn) { autoDownloadBtn.click(); } }, 10000); this.autoDownloadEnabled = false; } finally { this.isDownloading = false; this.stopDownload = false; this.updateUI(); } } cancelDownload() { this.stopDownload = true; if (this.cancelDownloadObj) { this.cancelDownloadObj.abort(); } this.isDownloading = false; this.updateUI(); } toggleMusicSelection(item) { const index = this.musicList.indexOf(item); if (index > -1) { this.musicList.splice(index, 1); } else { this.musicList.push(item); } this.updateUI(); } selectAllMusic() { if (this.musicList.length === this.data.length) { this.musicList = []; } else { this.musicList = [...this.data]; } this.updateUI(); } changeQuality() { showAlert("由于喜马拉雅接口变动,此功能暂时不可用,目前统一为高清。", "info"); this.setting.quality = 1; GM_setValue('priate_script_xmly_data', this.setting); this.updateUI(); } switchShowNumber() { this.setting.showNumber = !this.setting.showNumber; if (this.setting.showNumber) { this.setting.numberOffset = 1; } else { this.setting.numberOffset = 0; } GM_setValue('priate_script_xmly_data', this.setting); if (this.data.length > 0) { this.loadMusic(); } this.updateUI(); } addNumberOffset() { if (!this.setting.showNumber) { showAlert("请先开启编号功能再设置编号偏移量!", "error"); return; } this.setting.numberOffset += 1; GM_setValue('priate_script_xmly_data', this.setting); if (this.data.length > 0) { this.loadMusic(); } this.updateUI(); } subNumberOffset() { if (!this.setting.showNumber) { showAlert("请先开启编号功能再设置编号偏移量!", "error"); return; } this.setting.numberOffset -= 1; GM_setValue('priate_script_xmly_data', this.setting); if (this.data.length > 0) { this.loadMusic(); } this.updateUI(); } async exportAllMusicURL() { try { const urls = []; for (const item of this.musicList) { const url = await this.getMusicURL(item); if (url) { urls.push(url); } } if (urls.length > 0) { GM_setClipboard(urls.join('\n')); showAlert("音频链接已复制到剪贴板", "info"); } else { showAlert("没有可导出的链接", "error"); } } catch (error) { console.error('Export failed:', error); showAlert("导出失败,请重试", "error"); } } toggleAutoDownload() { this.autoDownloadEnabled = !this.autoDownloadEnabled; if (this.autoDownloadEnabled) { this.startAutoDownload(); } else { this.cancelDownload(); } this.updateUI(); } async startAutoDownload() { while (this.autoDownloadEnabled && !this.stopDownload) { try { await this.downloadAllMusics(); // If autoDownloadEnabled is false (set to false when download fails), don't continue if (!this.autoDownloadEnabled) { break; } // Check if there's a next page const nextPageBtn = document.querySelector('.page-next .page-link.N_t'); if (!nextPageBtn) { this.autoDownloadEnabled = false; showAlert("所有页面下载完成!", "info"); break; } // Random delay between 3-6 seconds before moving to next page const delay = Math.floor(Math.random() * 3000) + 3000; await new Promise(resolve => setTimeout(resolve, delay)); // Click next page and wait for content to load nextPageBtn.click(); await this.waitForContentLoad(); // Load new page content await this.loadMusic(); } catch (error) { console.error('Auto-download error:', error); this.autoDownloadPaused = true; showAlert("下载过程中遇到错误,已暂停自动下载", "error"); break; } } this.updateUI(); } waitForContentLoad() { return new Promise(resolve => { let attempts = 0; const checkContent = setInterval(() => { attempts++; const contentLoaded = document.querySelectorAll('.sound-list>ul li').length > 0; if (contentLoaded || attempts > 20) { // timeout after 10 seconds clearInterval(checkContent); setTimeout(resolve, 1000); // wait an extra second for good measure } }, 500); }); } } // Modify init function to use MutationObserver function init() { try { initSetting(); // Wait for body to be available if (document.body) { injectDiv(); setTimeout(() => { window.ximalayaDownloader = new XimalayaDownloader(); dragFunc("priate_script_div"); }, 100); // Increased timeout } else { // If body not available, wait for it const observer = new MutationObserver((mutations, obs) => { if (document.body) { obs.disconnect(); init(); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); } } catch (error) { console.error('Initialization failed:', error); } } // Remove DOMContentLoaded event listener and call init directly init(); })(); GM_addStyle(` #priate_script_div { font-size: 14px; position: fixed; background-color: #fff; color: #333; text-align: center; padding: 10px; z-index: 9999; border-radius: 8px; border: 1px solid #ddd; box-shadow: 0 2px 12px rgba(0,0,0,0.15); user-select: none; position: fixed !important; z-index: 2147483647 !important; background-color: #fff !important; } #priate_script_div button { border-radius: 4px; border: 1px solid #ddd; background-color: #fff; color: #333; padding: 4px 8px; margin: 4px 6px; cursor: pointer; } #priate_script_div table { text-align: center; margin: 5px auto; border-collapse: collapse; max-height: 400px; overflow-y: auto; } #priate_script_div td, #priate_script_div th { border: 1px solid #ddd; padding: 6px 10px; } .checkMusicBox { transform: scale(1.2); cursor: pointer; } #priate_script_div * { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; } `);