Greasy Fork

Greasy Fork is available in English.

_喜马拉雅下载器!

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

当前为 2022-12-18 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            _喜马拉雅下载器!
// @version         1.3.1
// @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         http://greasyfork.icu/scripts/435476-priatelib/code/PriateLib.js?version=1021495
// @require         https://unpkg.com/[email protected]/dist/ajaxhook.min.js
// @supportURL      http://greasyfork.icu/zh-CN/scripts/435495/feedback
// @homepageURL     http://greasyfork.icu/zh-CN/scripts/435495
// @contributionURL https://afdian.net/@cyberubbish
// @license         MIT
// @namespace       http://greasyfork.icu/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="//greasyfork.org/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");
})();