// ==UserScript== // @name 喜马拉雅专辑下载器 // @version 1.3.1 // @description 可能是你见过最丝滑的喜马拉雅下载器啦!登录后支持VIP音频下载,支持专辑批量下载,支持添加编号,链接导出、调用aria2等功能,直接下载M4A,MP3、MP4文件。 // @author Priate // @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/vue/2.7.16/files/dist/vue.min.js // @require https://registry.npmmirror.com/sweetalert/2.1.2/files/dist/sweetalert.min.js // @require https://registry.npmmirror.com/jquery/3.2.1/files/dist/jquery.min.js // @require https://greasyfork.org/scripts/435476-priatelib/code/PriateLib.js?version=1202493 // @require https://registry.npmmirror.com/ajax-hook/2.0.3/files/dist/ajaxhook.min.js // @require https://registry.npmmirror.com/crypto-js/4.1.1/files/crypto-js.js // @supportURL https://greasyfork.org/zh-CN/scripts/435495/feedback // @homepageURL https://greasyfork.org/zh-CN/scripts/435495 // @contributionURL https://afdian.net/@cyberubbish // @license MIT // @namespace https://greasyfork.org/users/219866 // ==/UserScript== (function() { 'use strict'; function initSetting() { var setting; if (!GM_getValue('priate_script_xmly_data')) { GM_setValue('priate_script_xmly_data', { // 多线程下载 multithreading: false, left: 20, top: 100, manualMusicURL: null, quality: 1, showNumber: true, numberOffset: 0, pageSize: 30, aria2: "ws://127.0.0.1:16800/jsonrpc" }) } setting = GM_getValue('priate_script_xmly_data') //后期添加内容 if (!setting.quality) setting.quality = 1; // 暂时统一为高清音质 setting.quality = 1 if (setting.showNumber === null) setting.showNumber = true; if (!setting.numberOffset) setting.numberOffset = 0; if (!setting.pageSize) setting.pageSize = 30; if (!setting.aria2) setting.aria2 = "ws://127.0.0.1:16800/jsonrpc" 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() { var priate_script_div = document.createElement("div") priate_script_div.innerHTML = `
喜马拉雅专辑下载器

❤️ by Priate | v {{version}} | 音质 : {{qualityStr}}
编号 : {{ setting.showNumber ? "开启" : "关闭"}} - {{ setting.numberOffset }} | 数量 : {{ setting.pageSize }} |


全选标题操作
{{item.title}} 下载 等待中 {{item.progress}} 已下载 下载失败 | 地址
` GM_addStyle(` #priate_script_div{ font-size : 15px; position: fixed; background-color: rgb(240, 223, 175); color : #660000; text-align : center; padding: 10px; z-index : 9999; border-radius : 20px; border:2px solid #660000; font-weight: 300; -webkit-text-stroke: 0.5px; text-stroke: 0.5px; box-shadow: 5px 15px 15px rgba(0,0,0,0.4); user-select : none; -webkit-user-select : none; -moz-user-select : none; -ms-user-select:none; } #priate_script_div:hover{ box-shadow: 5px 15px 15px rgba(0,0,0,0.8); transition: box-shadow 0.3s; } .priate_script_hide{ padding: 0 !important; border:none !important; } a{ cursor : pointer; text-decoration : none; } /*表格样式*/ #priate_script_div table{ text-align: center; // border:2px solid #660000; margin: 5px auto; padding: 2px; border-collapse: collapse; display: block; height : 400px; overflow-y: scroll; } /*表格框样式*/ #priate_script_div td{ border:2px solid #660000; padding: 8px 12px; max-width : 300px; word-wrap : break-word; } /*表头样式*/ #priate_script_div th{ border:2px solid #660000; padding: 8px 12px; font-weight: 300; -webkit-text-stroke: 0.5px; text-stroke: 0.5px; } /*脚本按钮样式*/ #priate_script_div button{ display: inline-block; border-radius: 4px; border: 1px solid #660000; background-color: transparent; color: #660000; text-decoration: none; padding: 5px 10px; margin : 5px 10px; font-weight: 300; -webkit-text-stroke: 0.5px; text-stroke: 0.5px; } /*脚本按钮悬浮样式*/ #priate_script_div button:hover{ cursor : pointer; color: rgb(240, 223, 175); background-color: #660000; transition: background-color 0.2s; } /*设置区域 p 标签*/ #priate_script_setting{ user-select : none; -webkit-user-select : none; -moz-user-select : none; -ms-user-select:none; } /*输入框样式*/ #priate_script_div textarea{ height : 50px; width : 200px; background-color: #fff; border:1px solid #000000; padding: 4px; } /*swal按钮*/ .swal-button--1{ background-color: #FFFAEB !important; color: #946C00; } .swal-button--2{ background-color: #ebfffc !important; color: #00947e; } .swal-button--3{ background-color: #ECF6FD !important; color: #55ACEE; } .checkMusicBox{ transform: scale(1.7,1.7); cursor: pointer; } `); document.querySelector("html").appendChild(priate_script_div) var setting = GM_getValue('priate_script_xmly_data') document.getElementById("priate_script_div").style.left = (setting.left || 20) + "px"; document.getElementById("priate_script_div").style.top = (setting.top || 100) + "px"; } 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"; }; }; // 初始化音质修改 function initQuality() { ah.proxy({ onRequest: (config, handler) => { handler.next(config); }, onError: (err, handler) => { handler.next(err) }, onResponse: (response, handler) => { const setting = GM_getValue('priate_script_xmly_data') // hook返回数据 if (response.config.url.indexOf("mobile.ximalaya.com/mobile-playpage/track/v3/baseInfo") != -1) { const setting = GM_getValue('priate_script_xmly_data') const data = JSON.parse(response.response) const playUrlList = data.trackInfo.playUrlList var replaceUrl; for (var num = 0; num < playUrlList.length; num++) { var item = playUrlList[num] if (item.qualityLevel == setting.quality) { replaceUrl = item.url break } } replaceUrl && playUrlList.forEach((item) => { item.url = replaceUrl }) response.response = JSON.stringify(data) } // hook普通音频获取高品质,实际上只需删除获取到的src即可 if (setting.quality == 2 && response.config.url.indexOf("www.ximalaya.com/revision/play/v1/audio") != -1) { const setting = GM_getValue('priate_script_xmly_data') var resp = JSON.parse(response.response) var data = resp.data delete data.src response.response = JSON.stringify(resp) } handler.next(response) } }) unsafeWindow.XMLHttpRequest = XMLHttpRequest } // 修改翻页大小 function initPageSize() { const originFetch = fetch; const setting = GM_getValue('priate_script_xmly_data') window.unsafeWindow.fetch = (url, options) => { if (url.indexOf('/revision/album/v1/getTracksList') != -1) { url = url.replace('pageSize=30', `pageSize=${setting.pageSize}`) } return originFetch(url, options).then(async (response) => { return response; }); }; } //初始化脚本设置 initSetting() //注入脚本div injectDiv() // 初始化音质修改 initQuality() // 修改翻页大小 initPageSize() // 第一种获取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}` $.ajax({ type: 'get', url: url, async: false, dataType: "json", success: function(resp) { 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 } // res = res || trackInfo.downloadUrl } } }); } 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` $.ajax({ type: 'get', url: url, async: false, dataType: "json", success: function(resp) { if (resp.ret == 200) res = resp.data.src; } }); } 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}` $.ajax({ type: 'get', url: url, async: false, dataType: "json", success: function(resp) { try { res = decrypt(resp.trackInfo.playUrlList[0].url) } catch (e) { console.log("解密错误") res = null } } }); } return res } // 处理数据等逻辑 var vm = new Vue({ el: '#priate_script_div', data: { version: "1.3.1", copyMusicURLProgress: 0, setting: GM_getValue('priate_script_xmly_data'), data: [], musicList: [], isDownloading: false, cancelDownloadObj: null, stopDownload: false, }, methods: { loadMusic() { const whiteList = ['sound', 'album'] const type = location.pathname.split('/')[location.pathname.split('/').length - 2] if (whiteList.indexOf(type) < 0) { swal("请先进入一个专辑页面并等待页面完全加载!", { icon: "error", buttons: false, timer: 3000, }); this.data = [] this.musicList = [] return } const all_li = document.querySelectorAll('.sound-list>ul li'); var result = []; var _this = this 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 music = { id: item_a.href.split('/')[item_a.href.split('/').length - 1], number, title: _this.setting.showNumber ? `${number + _this.setting.numberOffset - 1}-${title}` : title, isDownloading: false, isDownloaded: false, progress: 0, } result.push(music) }) // 如果没有获取到数据,则判断为单个音频 if (result.length == 0 && type == 'sound') { const music = { id: location.pathname.split('/')[location.pathname.split('/').length - 1], title: document.querySelector('h1.title-wrapper').innerText, isDownloading: false, isDownloaded: false, progress: 0, isSingle: true } result.push(music) } // 如果仍未获取到数据 if (result.length == 0) { swal("未获取到数据,请进入一个专辑页面并等待页面完全加载!", { icon: "error", buttons: false, timer: 3000, }); } this.data = result this.musicList = [] this.data.forEach((item) => { this.musicList.push(item) }) }, async getMusicURL(item) { var res = await getSimpleMusicURL1(item) res = res || await getSimpleMusicURL2(item) res = res || await getAllMusicURL2(item) res = res || await getAllMusicURL1(item) this.$set(item, 'url', res) return res }, async openMusicURL(item) { item.url = item.url || await this.getMusicURL(item) window.open(item.url) }, async downloadMusic(item) { //this.isDownloading = true item.isDownloading = true item.isFailued = false var _this = this const details = { url: item.url || await this.getMusicURL(item), name: item.title.trim().replace(/\\|\/|\?|\?|\*|\"|\“|\”|\'|\‘|\’|\<|\>|\{|\}|\[|\]|\【|\】|\:|\:|\、|\^|\$|\!|\~|\`|\|/g, '').replace(/\./g, '-'), onload: function(e) { _this.isDownloading = false item.isDownloading = false item.isDownloaded = true _this.selectAllMusic() }, onerror: function(e) { _this.isDownloading = false console.log(e) item.isDownloading = false if (e.error != 'aborted') item.isFailued = true }, onprogress: function(d) { item.progress = (Math.round(d.loaded / d.total * 10000) / 100.00) + "%"; } } this.cancelDownloadObj = GM_download(details) }, // 顺序下载 async sequenceDownload(index, data) { this.isDownloading = true const item = data[index] if (!item) { this.isDownloading = false this.selectAllMusic() this.stopDownload = false return; }; if (item.isDownloading || item.isDownloaded || this.stopDownload) return this.sequenceDownload(index + 1, data); item.isDownloading = true item.isFailued = false const _this = this const details = { url: item.url || await this.getMusicURL(item), name: item.title.trim().replace(/\\|\/|\?|\?|\*|\"|\“|\”|\'|\‘|\’|\<|\>|\{|\}|\[|\]|\【|\】|\:|\:|\、|\^|\$|\!|\~|\`|\|/g, '').replace(/\./g, '-'), onload: function(e) { item.isDownloading = false item.isDownloaded = true _this.cancelDownloadObj = _this.sequenceDownload(index + 1, data) }, onerror: function(e) { console.log(e) item.isDownloading = false if (e.error != 'aborted') item.isFailued = true _this.cancelDownloadObj = _this.sequenceDownload(index + 1, data) }, onprogress: function(d) { item.progress = (Math.round(d.loaded / d.total * 10000) / 100.00) + "%"; } } this.cancelDownloadObj = GM_download(details) return this.cancelDownloadObj }, async copyMusic(item) { item.url = item.url || await this.getMusicURL(item) GM_setClipboard(item.url) }, // 下载当前列表全部音频 async downloadAllMusics() { await this.sequenceDownload(0, this.musicList) }, async copyAllMusicURL() { this.copyMusicURLProgress = 0 var res = [] for (var num = 0; num < this.musicList.length; num++) { var item = this.musicList[num]; const url = await this.getMusicURL(item) await Sleep(0.01) this.copyMusicURLProgress = Math.round((num + 1) / this.musicList.length * 10000) / 100.00; res.push(url) } GM_setClipboard(res.join('\n')) swal("复制成功!", { icon: "success", buttons: false, timer: 1000, }); this.copyMusicURLProgress = 0 }, async csvAllMusicURL() { this.copyMusicURLProgress = 0 var dir = document.querySelector('h1.title').innerText dir = dir || Date.parse(new Date()) / 1000 // var res = ["url,subfolder,filename"] var res = [] for (var num = 0; num < this.musicList.length; num++) { var item = this.musicList[num]; const url = await this.getMusicURL(item) await Sleep(0.01) this.copyMusicURLProgress = Math.round((num + 1) / this.musicList.length * 10000) / 100.00; res.push(`${item.number},${item.title.replaceAll(',',',')},${url},${dir}`) } GM_setClipboard(res.join('\n')) this.copyMusicURLProgress = 0 function download(filename, text) { var element = document.createElement('a'); element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); element.setAttribute('download', filename); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); } download(`${dir}.csv`, res.join('\n')); swal("下载 CSV 文件成功!", { icon: "success", buttons: false, timer: 1000, }); }, async aria2AllMusicURL(wsurl) { this.setting.aria2 = wsurl GM_setValue('priate_script_xmly_data', this.setting) this.copyMusicURLProgress = 0 const config = { wsurl } var dir = document.querySelector('h1.title').innerText dir = dir || (Date.parse(new Date()) / 1000 + '') dir = dir.trim().replace(/\\|\/|\?|\?|\*|\"|\“|\”|\'|\‘|\’|\<|\>|\{|\}|\[|\]|\【|\】|\:|\:|\、|\^|\$|\!|\~|\`|\|/g, '').replace(/\./g, '-') + '/' for (var num = 0; num < this.musicList.length; num++) { var item = this.musicList[num]; const url = await this.getMusicURL(item) var ext = url.split('.')[url.split('.').length - 1] ext = ext.toLowerCase() if (ext != 'mp3' || ext != 'm4a') { ext = 'mp3' } await Sleep(0.01) this.copyMusicURLProgress = Math.round((num + 1) / this.musicList.length * 10000) / 100.00; Aria2(url, dir + item.title + '.' + ext, config) } swal(`Aria2 任务下发成功!文件保存至 ${dir} ,请自行检查下载状态。`, { icon: "success", buttons: false, timer: 5000, }); this.copyMusicURLProgress = 0 }, async exportAllMusicURL() { var _this = this var swalField = document.createElement('input'); swalField.setAttribute("placeholder", "Aria2 下载地址"); swalField.setAttribute("value", this.setting.aria2); swalField.setAttribute("class", "swal-content__input"); swal("URL : 仅复制选中音频的 URL,不附带文件名等信息。\n\nCSV : 下载包含专辑名、音频名、URL等信息的 CSV 文件,可用 EXCEL 编辑,便于自己实现批量下载。\n\nAria2 : 调用 Aria2 进行下载,请先在文本框中填写 RPC 地址,并在 RPC 服务端将授权密钥设置为空,默认为 Motrix 的 RPC 下载地址。", { buttons: { 1: "URL", 2: "CSV", 3: "Aria2", }, content: swalField, }).then(async (value) => { const method = parseInt(value) switch (method) { case 1: await _this.copyAllMusicURL(); break; case 2: await _this.csvAllMusicURL(); break; case 3: await _this.aria2AllMusicURL(swalField.value); break; default: if (value) swal(`导出失败!导出方法 ${value} 不存在。`, { icon: "error", buttons: false, timer: 3000, }); break; } }) }, selectAllMusic() { if (this.musicList.length == this.notDownloadedData.length) { this.musicList = [] } else { this.musicList = [] this.data.forEach((item) => { !item.isDownloaded && this.musicList.push(item) }) } }, //取消下载功能 cancelDownload() { this.stopDownload = true this.cancelDownloadObj.abort() }, // 修改音质功能 changeQuality() { const _this = this swal("由于喜马拉雅接口变动,此功能暂时不可用,目前统一为高清。", { buttons: false, timer: 3000, // buttons: { // 1: "标准", // 2: "高清", // 3: "超高(仅VIP)", // }, }).then((value) => { var changeFlag = true switch (value) { case "1": _this.setting.quality = 0; break; case "2": _this.setting.quality = 1; break; case "3": _this.setting.quality = 2; break; default: changeFlag = false } _this.setting.quality = 1 GM_setValue('priate_script_xmly_data', _this.setting) changeFlag && location.reload() }); }, // 切换是否显示编号功能 switchShowNumber() { this.setting.showNumber = !this.setting.showNumber this.setting.numberOffset = 0 GM_setValue('priate_script_xmly_data', this.setting) if (this.filterData.length > 0) { this.loadMusic() } }, // 增加编号偏移量 addNumberOffset() { if (!this.setting.showNumber) swal("请先开启编号功能再设置编号偏移量!", { buttons: false, timer: 2000, }) if (this.setting.showNumber) this.setting.numberOffset += 1 GM_setValue('priate_script_xmly_data', this.setting) if (this.filterData.length > 0) { this.loadMusic() } }, // 减少编号偏移量 subNumberOffset() { if (!this.setting.showNumber) swal("请先开启编号功能再设置编号偏移量!", { buttons: false, timer: 2000, }) if (this.setting.showNumber) this.setting.numberOffset -= 1 GM_setValue('priate_script_xmly_data', this.setting) if (this.filterData.length > 0) { this.loadMusic() } }, // 修改每页容量 changePageSize() { const _this = this swal("请设置每页展示的音频数量,最小为 30 ,最大为 100。\n\n注意:此设置仅会改变每页展示数据,底部的分页导航不受影响,因此后面部分页面出现空白为正常现象。\n\n设置后将刷新页面。", { content: { element: "input", attributes: { placeholder: "每页展示的音频数量", type: "number", value: _this.setting.pageSize ? _this.setting.pageSize : 30 } } }).then((value) => { const number = parseInt(value) if (number > 100 || number < 30) { swal(`每页数量不得超过 100 或少于 30!`, { icon: "error", buttons: false, timer: 4000, }); } else { _this.setting.pageSize = number || _this.setting.pageSize GM_setValue('priate_script_xmly_data', _this.setting) if (value) location.reload() } }); }, clearMusicData() { if (this.data.length == 0) swal(`已经是最简形态了!`, { buttons: false, timer: 2000, }); this.data = [] this.musicList = [] }, openDonate() { showDonate() } }, computed: { filterData() { if (this.isDownloading) { return this.musicList } else { return this.data } }, notDownloadedData() { return this.data.filter((item) => { return item.isDownloaded == false }) }, qualityStr() { var quality = (this.setting.quality >= 0 && this.setting.quality <= 2) ? this.setting.quality : 3 const str = ["标准", "高清", "超高", "未知"] return str[quality] }, qualityColor() { var quality = (this.setting.quality >= 0 && this.setting.quality <= 2) ? this.setting.quality : 3 const color = ["#946C00", "#55ACEE", "#00947e", "#337ab7"] return color[quality] } }, mounted() {} }) //设置div可拖动 dragFunc("priate_script_div"); })();