喜马拉雅下载器!

喜马拉雅下载器,可以批量下載喜馬拉雅的專輯!

目前為 2022-12-18 提交的版本,檢視 最新版本

// ==UserScript==
// @name            喜马拉雅下载器!
// @version         1.3.0
// @description     喜马拉雅下载器,可以批量下載喜馬拉雅的專輯!
// @author          [email protected]
// @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://unpkg.com/vue@2
// @require         https://unpkg.com/[email protected]/dist/sweetalert.min.js
// @require         https://unpkg.com/[email protected]/dist/jquery.min.js
// @require         https://gf.qytechs.cn/scripts/435476-priatelib/code/PriateLib.js?version=1021495
// @require         https://unpkg.com/[email protected]/dist/ajaxhook.min.js
// @supportURL      https://gf.qytechs.cn/zh-CN/scripts/435495/feedback
// @homepageURL     https://gf.qytechs.cn/zh-CN/scripts/435495
// @contributionURL https://afdian.net/@cyberubbish
// @license         MIT
// @namespace       https://gf.qytechs.cn/users/219866
// ==/UserScript==

(function() {
	'use strict';
	// 用户自定义设置
	const global_setting = {
		number: true, // 是否在标题前添加编号 true-开启 false-关闭
		offset: 0, // 标题编号的偏移量(在原有的基础上进行加减,如1则为在原有编号的基础上加1,-3则为在原有编号的基础上减3)
		export: 'url', // 点击“导出数据”按钮时的功能 url-粘贴原始url到剪切板 csv-以csv格式粘贴到剪切板 aria2-调用aria2jsonrpc下载
		aria2_wsurl: "ws://127.0.0.1:6800/jsonrpc", // aria2 JSON rpc地址
		aria2_secret: "", // aria2 rpc-secret 设置的值
	}


	// |------------------------------------------------------|
    // |                   以下内容请勿修改                      |
    // |------------------------------------------------------|
	function initSetting() {
        //document.querySelectorAll('.xm-player-case').$remove(1);
		var setting;
		if (!GM_getValue('priate_script_xmly_data')) {
			GM_setValue('priate_script_xmly_data', {
				// 多线程下载
				multithreading: true,
				left: 20,
				top: 100,
				manualMusicURL: null,
				quality: 1
			})
		}
		setting = GM_getValue('priate_script_xmly_data')
		//后期添加内容
		if (setting.quality !== 0) setting.quality = setting.quality || 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() {
		var priate_script_div = document.createElement("div")
		priate_script_div.innerHTML = `
<div id="priate_script_div"><div><p style='margin:0 0;padding:5px;'>Ver <a href="//gf.qytechs.cn/zh-CN/scripts/435495" target="_blank" style='color:#5d718f'>{{version}}</a>&nbsp;&nbsp;音质 : <a @click='changeQuality' :style='"color:" + qualityColor'>{{qualityStr}}</a></p>
<button v-show="!isDownloading" @click="loadMusic">{{filterData.length > 0 ? '重新加载' : '加载数据'}}</button>
<button id='downall' @click="downloadAll" v-show="!isDownloading && (musicList.length > 0)">下载全部</button>
<button @click="exportAllMusicURL" v-show="!isDownloading && (musicList.length > 0)">导出数据 <b v-show="copyMusicURLProgress">{{copyMusicURLProgress}}%</b></button>
<button @click="cancelDownload" v-show="isDownloading">取消下载</button></br>
<table v-show="filterData.length > 0"><thead><tr><th><a style='color:#5d718f' @click='selectAllMusic'>全选</a></th><th>操作</th><th>标题</th></tr></thead>
<tbody id="priate_script_table">
<tr v-for="(item, index) in filterData" :key="index">
<td><input class="checkMusicBox" v-model="musicList" :value='item' type="checkbox" :disabled="item.isDownloaded || isDownloading"></td>
<td>
<a class="_down" v-show="!item.isDownloading && !item.isDownloaded && !isDownloading" style='color:#5d718f' @click="downloadMusic(item)">下载</a>
<a v-show="isDownloading && !item.isDownloading && !item.isDownloaded" style='color:gray'>等待中</a>
<a v-show="item.isDownloading" style='color:#C01D07'>{{item.progress}}</a>
<a v-show="item.isDownloaded" style='color:gray'>OK</a>
<a v-show="item.isFailued" style='color:blue'>失败</a> |
<a :style="'color:' + (item.url ? '#5d718f' : '#5d718f')" @click="copyMusic(item)">地址</a></td>
<td align="left"><a style='color:#5d718f'>{{item.title}}</a></td>
</tr></tbody></table></div></div>
`
		GM_addStyle(`
#priate_script_div{font-size:12px;background-color:#fff;color:#666;text-align:center;padding:5px;border-radius:5px;border:2px solid #5d718f;box-shadow:2px 2px 2px #bbb;z-index:9999;position:fixed;}
/*#priate_script_div:hover{box-shadow:3px 3px 3px #333;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:0;margin:5px auto;padding:2px;border-collapse:collapse;display:block;height:400px;overflow-y:scroll;}
#priate_script_div td{border:1px solid #ddd;padding:6px 6px 4px;max-width:300px;word-wrap:break-word;}
#priate_script_div th{border:1px solid #ddd;padding:3px 6px 4px;}
#priate_script_div button{font-size:12px;display:inline-block;box-shadow:2px 2px 2px #bbb;border-radius:4px;border:1px solid #31435e;background-color:#5d718f;color:#fff;text-decoration:none;padding:5px 10px;margin:5px 10px;}
#priate_script_div button:hover{cursor:pointer;box-shadow:0px 0px 0px #bbb;transition:box-shadow 0.2s;}
#priate_script_div .hide-button{z-index:2147483647;width:32px;height:32px;cursor:pointer;position:fixed;left:0px;bottom:0px;color:#660000;text-align:center;line-height:32px;margin:10px;border-width:1px;border-style:solid;border-color:#ccc;border-image:initial;border-radius:100%;}
#priate_script_div .hide-button:hover{background-color:rgba(240, 223, 175, 0.9);}
#priate_script_div textarea{height:50px;width:200px;background-color:#fff;border:1px solid #000000;padding:4px;}
.swal-button--low{background-color:#FFFAEB !important;color:#946C00;}
.swal-button--high{background-color:#ebfffc !important;color:#00947e;}
.swal-button--mid{background-color:#ECF6FD !important;color:#55ACEE;}
.checkMusicBox{transform:scale(1.5,1.5);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
	}

	initSetting() //初始化脚本设置
	injectDiv() //注入脚本div
	initQuality() //初始化音质修改

	async function getSimpleMusicURL1(item) { //第一种获取musicURL的方式,任意用户均可获得,不可获得VIP音频
		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
	}

	async function getSimpleMusicURL2(item) { //第二种获取musicURL的方式,任意用户均可获得,不可获得VIP音频
		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
	}

	var vm = new Vue({ //处理数据等逻辑
		el: '#priate_script_div',
		data: {
			version: "1.2.0",
			copyMusicURLProgress: 0,
			setting: GM_getValue('priate_script_xmly_data'),
			data: [],
			musicList: [],
			isDownloading: false,
			cancelDownloadObj: null,
			stopDownload: false
		},
		methods: {
			loadMusic() {
				const all_li = document.querySelectorAll('.sound-list>ul li');
				var result = [];
				all_li.forEach((item) => {
					const item_a = item.querySelector('a');
					const number = item.querySelector('span.num') ? parseInt(item.querySelector('span.num').innerText) + global_setting.offset : 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: global_setting.number ? `${number}-${title}` : title,
						isDownloading: false,
						isDownloaded: false,
						progress: 0,
					}
					result.push(music)
				})

				if (result.length == 0 && location.pathname.split('/')[location.pathname.split('/').length - 1]) { //如果没有取到数据,则判断为单个音频
					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 getAllMusicURL1(item)
				this.$set(item, 'url', res)
				return res
			},
			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.done / d.total * 10000 / 100.00))<100) ? (Math.round(d.done / d.total * 10000 / 100.00)) + "%" : '99%';
					}
				}
				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.done / 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)
				swal("复制成功!", {
					icon: "success",
					buttons: false,
					timer: 1000,
				});
			},
			async downloadAll() { //下载全部音频
				var all_down = document.querySelectorAll('#priate_script_table ._down');

				for (var num = 0; num < all_down.length; num++) {
                    all_down[num].click();
					//console.log(all_down[num].innerText)
					await Sleep(0.2);
				}
				all_down=null;
			},
			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(`${url},${dir},${item.title}`)
				}
				GM_setClipboard(res.join('\n'))
				swal("复制csv成功!", {
					icon: "success",
					buttons: false,
					timer: 1000,
				});
				this.copyMusicURLProgress = 0
			},
			async aria2AllMusicURL() {
				this.copyMusicURLProgress = 0
				const config = {
					wsurl: global_setting.aria2_wsurl,
					token: global_setting.aria2_secret
				}
				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: 4000,
				});
				this.copyMusicURLProgress = 0
			},
			async exportAllMusicURL() {
				switch (global_setting.export) {
					case 'url':
						await this.copyAllMusicURL();
						break;
					case "aria2":
						await this.aria2AllMusicURL();
						break;
					case "csv":
						await this.csvAllMusicURL();
					default:
						swal("用户自定义参数 export 设置错误,可选参数:[url, aria2, csv]", {
							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("请选择需要设置的音质,注意:此功能处于测试中,超高音质仅登陆后VIP可用,且部分音频不存在超高音质。(切换后将刷新页面)", {
					buttons: {
						low: "标准",
						mid: "高清",
						high: "超高(仅VIP)",
					},
				}).then((value) => {
					var setting = GM_getValue('priate_script_xmly_data')
					var changeFlag = true
					switch (value) {
						case "low":
							setting.quality = 0;
							break;
						case "mid":
							setting.quality = 1;
							break;
						case "high":
							setting.quality = 2;
							break;
						default:
							changeFlag = false
					}
					GM_setValue('priate_script_xmly_data', setting)
					_this.setting = setting
					changeFlag && location.reload()
				});
			},
			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 = ["#5d718f", "#55ACEE", "#00947e", "#337ab7"]
				return color[quality]
			}
		}
	})
	//设置div可拖动
	dragFunc("priate_script_div");
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址