Greasy Fork is available in English.

网易云音乐-MyFreeMP3扩展

利用MyFreeMP3扩展网易云音乐功能

/* eslint-disable no-multi-spaces */
/* eslint-disable dot-notation */

// ==UserScript==
// @name               网易云音乐-MyFreeMP3扩展
// @name:zh-CN         网易云音乐-MyFreeMP3扩展
// @name:en            Netease Music - MyFreeMP3 Extender
// @namespace          163Music-MyFreeMP3-Extender
// @version            2.0
// @description        利用MyFreeMP3扩展网易云音乐功能
// @description:zh-CN  利用MyFreeMP3扩展网易云音乐功能
// @description:en     Extend netease music with MyFreeMP3
// @author             PY-DNG
// @license            GPL-v3
// @match              http*://music.163.com/*
// @connect            59.110.45.28
// @connect            liumingye.cn
// @connect            *
// @connect            music.163.net
// @connect            music.126.net
// @require            https://greasyfork.org/scripts/456034-basic-functions-for-userscripts/code/script.js?version=1226884
// @require            https://greasyfork.org/scripts/457199/code/script.js?version=1132840
// @require            https://greasyfork.org/scripts/457244/code/script.js?version=1132550
// @require            https://greasyfork.org/scripts/474021-myfreemp3-api/code/MyFreeMP3%20API.js?version=1268225
// @icon               data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAABYZJREFUWEfFl3lQ1VUUxz+/92SRfRPZRFxK0UA0U3LSsAz33cqSVDIn0zR1nDQap6Yxtz9s0dEoR53E3JXUMKXScdBKS0kkNEUFHoKyySrw4P2ae+E9RX7vgTYN56/f755z7/2ee8/5nnMVWikZBHo7oE5QYSgo4UAXwKVxegVwA9SLCpyoQUkMJbeoNUsrLRldJyhcRf0AmAzYtWTfqDcC+xWUVV0xXLQ1xyqAy/i42uGwFngbaBGolU1UIN5Izfs9KSzXstFc+CrBvXXUJwLdW+lxS2bXTOgnPEF2+sOGzQDcIDDShHIMVLeWVn00vVKmQx3ehdzfHpzXBECD56Yzj7O5XY9uOAyIwFR8l3vJp1BrRRg087fMhG7QgydhAdB45+cf59g9P1mC5/KFlt2MV2+Q98Ir1BnytA7pmpGafuaYsADIJHATMMfWseq9PXGbO0OalKz4AlQVx0H9CTgtwgVq0y5j1z0Epb0jxR+u4e7K9daW+6obue8IpQTQmGqptqLdZep4fDauROfpLhfNCuhHfd4dPOLm4/XpUqp//ZNbg8Zj3+tJXGZMofTzzVJvLTsUlAiRohJAJoE7ganWrD2WzcNrlaACqL9dQMmKLynbsFX+u82bic+GFdReuoIh7MVHictd3ch9TREMZw/isjRJxnXGy3TY9plcuPLAUQpiF2Mqa0hpu67BuC2cjfv8WPlfvnU31SnnqNz3g8XGBiJjLfgr1wmYpaJs1jLU+/vS6copdK4uVHyXyJ2Y+fLeFXs7vFbH4fZuLIpdu2ZT64tKKFqwXM6xJQrqW0omAQmgTNMy9F73Ee6LZlOXZSCn11DUqnvSrOPeeJynjJbf1SlnG4Jx8EDExopej86jgUIK58ZRtulbGxjUHUomgYKrw5pZ6fV0zjuPvoM3hXOWURafIE2cJ4+i476vUevqKIhZQMXuQziNHIpf0nbq8wvICX2eDlvW4TxxBNTXY+g/itrUZgRo3i5NABAXaq5qFhz2T/UgKO1n+Z/lEya9E+KfvJP2wwZzd+1GipeulGMi7UKK01EcHTBERGPMuEpASiIOz/ShKukX8kdPt3YKFQKAKBjNxGnMMPwOb6Mu5xbZwQMs+s6FaQg+yB04hpqzInMbJODkPhyfj6Rg1hLKt+ySIAVYTCayfPtYHHh4I6sAXKZNxDdhPcbL18gJjboP4HYqel8fbj03kerT5yzjHQ9uxnnCCIoWfSw5AL2eLqUZKM5O5EW/LulZS6xegdOIKPyOJmAqLeemR6hlrt/3W3AaF03lnsPcflWSmYyToL9PoPfxIn9kDFU/npTjnTLPyFQVdsJeQ+QVaAZhu0A/gg1/yDmGvsMtgeQQ2Y9AQb06nRyrSU3HaXgUImWNVzIxhA+zFKKQokvovDxkDIhY0BARhNbTMOivZOzDQyldv1XmtVlcp0/BJ36NDDqz1N3MIX/sTMmIQuwjehN04Zj8zg7qT11uvsb+6g6bROQ25w18Nq1CNdaR2zea2vR/LIsIj50nj0bv5SE9rzx0HPVe9f2Y2P8NzpNGykAVAat5/4KIbFGxYLnA1OOywBivZ5M3ZJIVT5oub64PYjR/XCxVh5O19m+gYqGxVYzsw3oSkHIQnZurJJqCNxdTdfSEtkfOTrIyur83S+rLt+2RtcOKNBQjoWypHIvA80/abinFNb9foHJ/ErVpGTJLxHU4DonEJWaS5AghlXuPyNqh3RmhNinHjadgsyFpF+Qv+wGnsS9Z80iOC0DFcasbaoCqyXHCrGlDIkZa25I5PB2OICnHqGexC+mE4mBPfUmpTMmqIz9RkXAAU0WlLZDaLZmY8V+aUpvHYlEq1ptSs02btuVmEG36MDGDaNOn2YP32WaP04eD6v96nv8LNmVLjadPDdEAAAAASUVORK5CYII=
// @grant              GM_xmlhttpRequest
// @grant              GM_download
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_registerMenuCommand
// @grant              GM_unregisterMenuCommand
// @run-at             document-start
// @noframes
// ==/UserScript==

/* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager */
/* global md5 pop */
/* global Mfapi */

(function __MAIN__() {
    'use strict';
	const CONST = {
		Text: {
			V5NOCANQU: '要听龙叔的话,V5不能屈,听完歌快去给你网易爸爸充VIP吧',
			SongNotFound: '没有找到歌曲资源',
			ErrorOccured: `<span style="color: orange;">pageFunc Error</span>`,
			DownloadSetting: {
				Lrc: {
					Text: ['[x]同时下载歌词', '[√]同时下载歌词'],
					Tip: ['已关闭:下载歌曲时自动下载歌词', '已开启:下载歌曲时自动下载歌词']
				},
				Cover: {
					Text: ['[x]同时下载封面', '[√]同时下载封面'],
					Tip: ['已关闭:下载歌曲时自动下载封面', '已开启:下载歌曲时自动下载封面']
				}
			}
		},
		Number: {
			Interval_Fastest: 1,
			Interval_Fast: 50,
			Interval_Balanced: 500,
			MaxSearchPage: 3,
		},
		TYPE_INFO: {
			2000: 'flac',
			320: 'mp3',
			128: 'mp3'
		}
	}

	// Prepare
	const WEAPI = new Weapi();
	const PV = new Privileger();

	// function DoLog() [}
	// Arguments: level=LogLevel.Info, logContent, trace=false
	const [LogLevel, DoLog] = (function() {
		const LogLevel = {
			None: 0,
			Error: 1,
			Success: 2,
			Warning: 3,
			Info: 4,
		};

		return [LogLevel, DoLog];
		function DoLog() {
			// Get window
			const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window;

			const LogLevelMap = {};
			LogLevelMap[LogLevel.None] = {
				prefix: '',
				color: 'color:#ffffff'
			}
			LogLevelMap[LogLevel.Error] = {
				prefix: '[Error]',
				color: 'color:#ff0000'
			}
			LogLevelMap[LogLevel.Success] = {
				prefix: '[Success]',
				color: 'color:#00aa00'
			}
			LogLevelMap[LogLevel.Warning] = {
				prefix: '[Warning]',
				color: 'color:#ffa500'
			}
			LogLevelMap[LogLevel.Info] = {
				prefix: '[Info]',
				color: 'color:#888888'
			}
			LogLevelMap[LogLevel.Elements] = {
				prefix: '[Elements]',
				color: 'color:#000000'
			}

			// Current log level
			DoLog.logLevel = (win.isPY_DNG && win.userscriptDebugging) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error

			// Log counter
			DoLog.logCount === undefined && (DoLog.logCount = 0);

			// Get args
			let [level, logContent, trace] = parseArgs([...arguments], [
				[2],
				[1,2],
				[1,2,3]
			], [LogLevel.Info, 'DoLog initialized.', false]);

			// Log when log level permits
			if (level <= DoLog.logLevel) {
				let msg = '%c' + LogLevelMap[level].prefix + (typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + (LogLevelMap[level].prefix ? ' ' : '');
				let subst = LogLevelMap[level].color;

				switch (typeof(logContent)) {
					case 'string':
						msg += '%s';
						break;
					case 'number':
						msg += '%d';
						break;
					default:
						msg += '%o';
						break;
				}

				if (++DoLog.logCount > 512) {
					console.clear();
					DoLog.logCount = 0;
				}
				console[trace ? 'trace' : 'log'](msg, subst, logContent);
			}
		}
	}) ();

	main();
	function main() {
		// Wait for document.body
		if (!document.body) {
			setTimeout(main, CONST.Number.Interval_Fast);
			return false;
		}

		// Commons
		hookPlay();
		playlistDownload();

		// Page functions
		const ITM = unsafeWindow.ITM = new IntervalTaskManager();
		const pageChangeDetecter = (function(callback, emitOnInit=false) {
			let href = location.href;
			emitOnInit && callback(null, href);
			return function detecter() {
				const new_href = location.href;
				if (href !== new_href) {
					ITM.removeTask(ITM.tasks.indexOf(pageChangeDetecter));
					callback(href, new_href);
					href = new_href;
					ITM.addTask(inject_iframe);
				}
			}
		}) (deliverPageFuncs, true);
		ITM.time = CONST.Number.Interval_Fast;
		ITM.addTask(inject_iframe);
		ITM.start();

		function inject_iframe() {
			const ifr = $('#g_iframe') || {};
			const oWin = ifr.contentWindow;
			const oDoc = ifr.contentDocument;
			if (oWin && oDoc && oWin.location && oWin.location.host === 'music.163.com') {
				const AEL = getPureAEL();
				AEL.call(oWin, 'unload', function() {
					ITM.addTask(pageChangeDetecter);
				});
				ITM.removeTask(ITM.tasks.indexOf(inject_iframe));
			}
		}

		function deliverPageFuncs(href, new_href) {
			const pageFuncs = [{
				reg: /^https?:\/\/music\.163\.com\/#\/song\?.+$/,
				func: pageSong,
				checker: function() {
					const ifr = $('#g_iframe'); if (!ifr) {return false;}
					const oDoc = ifr.contentDocument; if (!oDoc) {return false;}
					const elm = !!$(oDoc, '.cnt>.m-info');
					return elm;
				}
			},{
				reg: /^https?:\/\/music\.163\.com\/#\/(artist|album|discover\/toplist)\?.+$/,
				func: replacePredata,
				sync: false
			},{
				reg: /^https?:\/\/music\.163\.com\/#\/(my\/m\/music\/)?playlist\?.+$/,
				func: replacePredata_encoded,
				sync: false
			},{
				reg: /^https?:\/\/music\.163\.com\//,
				func: listDownload,
				checker: function() {
					const ifr = $('#g_iframe'); if (!ifr) {return false;}
					const oDoc = ifr.contentDocument; if (!oDoc) {return false;}
					return !!oDoc.body;
				}
			},{
				reg: /^https?:\/\/music\.163\.com\/#\/album\?.+$/,
				func: pageAlbum,
				checker: function() {
					const ifr = $('#g_iframe'); if (!ifr) {return false;}
					const oDoc = ifr.contentDocument; if (!oDoc) {return false;}
					const elm = !!$(oDoc, '#content-operation');
					return elm;
				}
			},{
				reg: /^https?:\/\/music\.163\.com\//,
				func: settings
			}];
			for (const pageFunc of pageFuncs) {
				wrap(pageFunc);
			}
			for (const pageFunc of pageFuncs) {
				test_exec(pageFunc);
			}

			function wrap(pageFunc) {
				pageFunc.name = pageFunc.name || pageFunc.func.name;
				pageFunc.func = (function(func) {
					return function wrapper() {
						try {
							return func();
						} catch(err) {
							DoLog(LogLevel.Error, `Error executing pageFunc ${pageFunc.name}`);
							DoLog(LogLevel.Error, err, true);
							showErr(CONST.Text.ErrorOccured, true);
						}
					}
				}) (pageFunc.func);
			}

			function test_exec(pageFunc) {
				pageFunc.reg.test(location.href) && ((((pageFunc.sync || !pageFunc.hasOwnProperty('sync')) ? iframeDocSync() : true) && (pageFunc.checker ? ({
					'string': () => ($(pageFunc.checker)),
					'function': pageFunc.checker,
				})[typeof pageFunc.checker]() : true)) ? true : (setTimeout(test_exec.bind(null, pageFunc), CONST.Number.Interval_Balanced), DoLog(`waiting: ${location.href}, ${pageFunc.name}`), false)) && (DoLog('Exec ' + pageFunc.name), pageFunc.func(href, new_href));
			}
		}
	}

	function hookPlay() {
		// Access Checker: core_fbc43dc690327907cf6fdad6d52f7c31.js?:formatted:8988('l6f.tt2x = function(bi7b, action) {')
		// Play
		const APIH = new APIHooker();

		APIH.hook(/\/weapi\/v3\/song\/detail(\?[a-zA-Z0-9=_]+)?$/, function(xhr) {
			const json = JSON.parse(xhr.response);
			json.privileges.forEach(privilege => PV.fix(privilege));
			rewriteResponse(xhr, json);
			return true;
		});
		APIH.hook(/\/weapi\/v1\/play\/record(\?[a-zA-Z0-9=_]+)?$/, function(xhr) {
			const json = JSON.parse(xhr.response);
			(json.allData || []).concat(json.weekData || []).forEach(data => PV.fix(data.song.privilege));
			rewriteResponse(xhr, json);
			return true;
		});
		APIH.hook(/\/weapi\/v6\/playlist\/detail(\/?\?[a-zA-Z0-9=_]+)?$/, function(xhr) {
			const json = JSON.parse(xhr.response);
			json.privileges.forEach(privilege => PV.fix(privilege));
			rewriteResponse(xhr, json);
			return true;
		});
		APIH.hook(/\/weapi\/song\/enhance\/player\/url\/v1(\?[a-zA-Z0-9=_]+)?$/, function(xhr, _this, args, onreadystatechange) {
			const ifr = $('#g_iframe');
			const oDoc = ifr.contentDocument;

			// Get data
			const json = JSON.parse(xhr.response);
			const data = json['data'][0];

			// Only hook unplayable songs
			if (data['url']) {return true};

			search(data.id, function(song) {
				song ? reqSong(song) : showTip(CONST.Text.SongNotFound);

				function reqSong(song) {
					const qualities = Object.keys(CONST.TYPE_INFO).map(n => parseInt(n, 10)).sort((q1, q2) => q2 - q1);
					const q = qualities.find(q => song.quality.includes(q));
					const abort = GM_xmlhttpRequest({
						method: 'GET',
						url: song.url[q],
						onprogress: load,
						onload: load
					}).abort;

					function load(e) {
						// Abort request first
						abort();

						// Check if finalUrl differ from original url
						if (song.url === e.finalUrl) {
							DoLog(LogLevel.Warning, 'Searched song returned a useless url');
							showTip(CONST.Text.SongNotFound);
						}

						// modify xhr and continue stack
						data['code'] = 200;
						data['br'] = PV.levelData[data.id].plRate;
						data['level'] = PV.levelData[data.id].plLevel;
						data['type'] = 'mp3';
						data['url'] = e.finalUrl;
						rewriteResponse(xhr, json);
						continueStack();
					}
				}
			});

			// Suspend stack until search & find the song
			return false;

			function continueStack() {
				onreadystatechange.apply(_this, args);;
			}
		});

		function rewriteResponse(xhr, json) {
			const response = JSON.stringify(json);
			const propDesc = {
				value: response,
				writable: false,
				configurable: true,
				enumerable: true
			};
			Object.defineProperties(xhr, {
				'response': propDesc,
				'responseText': propDesc
			});
		}
	}

	function listDownload() {
		const iframe = $('#g_iframe');
		const oDoc = iframe.contentDocument;
		const body = oDoc.body;
		if (!body) {
			DoLog(LogLevel.Warning, 'listDownload: list not found');
			return false;
		}

		const AEL = getPureAEL();
		AEL.call(body, 'click', function(e) {
			const elm = e.target;
			if (elm.getAttribute('data-res-action') === 'download') {
				e.stopPropagation();
				downloadSong(elm.getAttribute('data-res-id') * 1);
			}
		}, {capture: true});

		function $T(elm, selector) {
			const e = $(elm, selector);
			return e ? e.innerText : null;
		}
	}

	function playlistDownload() {
		const AEL = getPureAEL();
		AEL.call(document.body, 'click', function(e) {
			const elm = e.target;
			if (elm.getAttribute('data-action') === 'download') {
				e.stopPropagation();
				downloadSong(elm.getAttribute('data-id') * 1);
			}
		}, {capture: true});
	}

	function pageSong() {
		const ifr = $('#g_iframe');
		const oDoc = ifr.contentDocument;
		const name = $(oDoc, '.tit>em').innerText;
		const artist = $(oDoc, '.cnt>.des>span>a').innerText;
		const cover = $(oDoc, '.u-cover>img.j-img').src;
		const AEL = getPureAEL();

		// GUI
		if ($(oDoc, '.vip-song')) {
			// vip song
			const content_operation = $(oDoc, '#content-operation');
			const vip_group = $(content_operation, '.u-vip-btn-group');
			const vip_play = $(vip_group || content_operation, 'a[data-res-action="play"]');
			const vip_add = $(vip_group || content_operation, 'a[data-res-action="addto"]');
			const vip_download = $(content_operation, '.u-btn-vip-download');

			// Style
			vip_play.classList.remove('u-btni-vipply');
			vip_play.classList.remove('u-btni-openvipply');
			vip_play.classList.add('u-btni-addply');
			vip_add && vip_add.classList.remove('u-btni-vipadd');
			vip_add && vip_add.classList.add('u-btni-add');
			vip_download.classList.remove('u-btn-vip-download');
			if (vip_group) {
				vip_add && content_operation.insertAdjacentElement('afterbegin', vip_add);
				content_operation.insertAdjacentElement('afterbegin', vip_play);
				content_operation.removeChild(vip_group);
			}

			// Text
			vip_play.title = CONST.Text.V5NOCANQU;
			vip_play.children[0].childNodes[1].nodeValue = '播放';
		}
		if ($(oDoc, '.u-btni-play-dis')) {
			// Copyright song
			// Data
			const cpr_play = $(oDoc, '.u-btni-play-dis');
			const cpr_fav = cpr_play.nextElementSibling;
			cpr_play.setAttribute('data-res-id', cpr_fav.getAttribute('data-res-id'));
			cpr_play.setAttribute('data-res-type', cpr_fav.getAttribute('data-res-type'));
			cpr_play.setAttribute('data-res-action', 'play');

			// Style
			cpr_play.classList.remove('u-btni-play-dis');
		}

		// Download
		const dlButton = $(oDoc, '#content-operation>a[data-res-action="download"]');
		AEL.call(dlButton, 'click', dlOnclick, {useCapture: true});

		function dlOnclick(e) {
			e.stopPropagation();
			downloadSong(dlButton.getAttribute('data-res-id') * 1);
		}
	}

	function pageAlbum() {
		const iframe = $('#g_iframe');
		const oDoc = iframe.contentDocument;
		const oWin = iframe.contentWindow;

		// GUI
		if ($(oDoc, '.vip-album')) {
			const content_operation = $(oDoc, '#content-operation');
			const vip_group = $(content_operation, '.u-vip-btn-group');
			const vip_play = $(vip_group || content_operation, 'a[data-res-action="play"]');
			const vip_add = $(vip_group || content_operation, 'a[data-res-action="addto"]');

			// Style
			vip_play.classList.remove('u-btni-vipply');
			vip_play.classList.remove('u-btni-openvipply');
			vip_play.classList.add('u-btni-addply');
			vip_add && vip_add.classList.remove('u-btni-vipadd');
			vip_add && vip_add.classList.add('u-btni-add');
			if (vip_group) {
				vip_add && content_operation.insertAdjacentElement('afterbegin', vip_add);
				content_operation.insertAdjacentElement('afterbegin', vip_play);
				content_operation.removeChild(vip_group);
			}

			// Text
			vip_play.title = CONST.Text.V5NOCANQU;
			vip_play.children[0].childNodes[1].nodeValue = '播放';
		}
	}

	function settings() {
		const DS = CONST.Text.DownloadSetting
		makeBooleanMenu(DS.Lrc.Text, DS.Lrc.Tip, 'lrc');
		makeBooleanMenu(DS.Cover.Text, DS.Cover.Tip, 'cover');

		function makeBooleanMenu(texts, tips, key) {
			const initialText = texts[GM_getValue(key, false) + 0];
			let id = GM_registerMenuCommand(initialText, onClick/*, {
				autoClose: false
			}*/);

			function onClick() {
				const newValue = !GM_getValue(key, false);
				const newText = texts[newValue + 0];
				GM_setValue(key, newValue);
				GM_unregisterMenuCommand(id);
				id = GM_registerMenuCommand(newText, onClick/*, {
					autoClose: false
				}*/);
				pop.info(tips[newValue + 0]);
			}
		}
	}

	function replacePredata() {
		const iframe = $('#g_iframe');
		const oDoc = iframe.contentDocument;
		const oWin = iframe.contentWindow;
		const envReady = oDoc && iframeDocSync();
		const elmData = oDoc && $(oDoc, '#song-list-pre-data');
		if (!elmData) {
			// No elmData found.
			if (envReady && $(oDoc, '#song-list-pre-cache table')) {
				// Too late. Data has already been dealed.
				DoLog(LogLevel.Error, 'Predata hook failed.');
				DoLog([$(oDoc, '#song-list-pre-cache table'), oDoc.URL, oWin.location.href]);
			} else {
				// Data has not been loaded!
				DoLog('No predata found');
				if (envReady) {
					// Hook Element.prototype.getElementsByTagName to make changeValue called.
					DoLog('Environment ready, hooking getElementsByTagName...');
					const hooker = new Hooker();
					const id = hooker.hook(oWin, 'Element.prototype.getElementsByTagName', false, false, {
						dealer: function(_this, args) {
							if (_this.id === 'song-list-pre-cache' && args[0] === 'textarea') {
								const elmData = $(_this, 'textarea');
								changeValue(elmData);
								hooker.unhook(id);
								DoLog('Value changed, getElementsByTagName unhooked...');
							}
							return [_this, args];
						}
					}).id;
					DoLog(LogLevel.Success, 'getElementsByTagName Hooked...');
				} else {
					// Environment not ready yet, wait for it
					DoLog('Environment not ready, waiting...');
					setTimeout(replacePredata, CONST.Number.Interval_Fastest);
				}
			}
			return false;
		} else {
			// elmData Found! Go change value directly.
			DoLog('Changing value directly');
			changeValue(elmData);
		}

		function changeValue(elmData) {
			const list = JSON.parse(elmData.value);
			list.forEach(song => PV.fix(song.privilege));
			elmData.value = JSON.stringify(list);

			DoLog(LogLevel.Success, 'Predata replaced');
		}
	}

	function replacePredata_encoded() {
		const iframe = $('#g_iframe');
		const oDoc = iframe.contentDocument;
		const oWin = iframe.contentWindow;
		const envReady = oDoc && iframeDocSync();
		const elmData = oDoc && $(oDoc, '#song-list-pre-data');
		if (!elmData) {
			// No elmData found.
			if (envReady && $(oDoc, '#song-list-pre-cache table')) {
				// Too late. Data has already been dealed.
				DoLog(LogLevel.Error, 'Predata hook failed.');
				DoLog([$(oDoc, '#song-list-pre-cache table'), oDoc.URL, oWin.location.href]);
			} else {
				// Data has not been loaded!
				DoLog('No predata found');
				if (envReady) {
					// Hook Element.prototype.getElementsByTagName to make changeValue called.
					DoLog('Environment ready, hooking getElementsByTagName...');
					const hooker = new Hooker();
					const id = hooker.hook(oWin, 'Element.prototype.getElementsByTagName', false, false, {
						dealer: function(_this, args) {
							if (_this.id === 'song-list-pre-cache' && args[0] === 'textarea') {
								const elmData = $(_this, 'textarea');
								changeValue(elmData);
								hooker.unhook(id);
								DoLog('Value changed, getElementsByTagName unhooked...');
							}
							return [_this, args];
						}
					}).id;
					DoLog(LogLevel.Success, 'getElementsByTagName Hooked...');
				} else {
					// Environment not ready yet, wait for it
					DoLog('Environment not ready, waiting...');
					setTimeout(replacePredata_encoded, CONST.Number.Interval_Fastest);
				}
			}
			return false;
		} else {
			// elmData Found! Go change value directly.
			DoLog('Changing value directly');
			changeValue(elmData);
		}

		function changeValue(elmData) {
			// Decrypt text
			const decode = Object.values(unsafeWindow.NEJ.P('nej.u')).find(f => f.toString().match(/function\([a-z0-9]+,[a-z0-9]+\)\{return [a-z0-9]+\.[a-z0-9]+\([a-z0-9]+\.[a-z0-9]+\([a-z0-9]+\),[a-z0-9]+\)\}/i));
			const decrypt = (str, key) => decode(str, key);
			const request = Object.values(unsafeWindow.NEJ.P('nej.j')).find(f => f.toString().includes('.replace("api","weapi")'));
			let encrypStr, position;
			request("/m/api/encryption/param/get", {
				sync: true,
				type: "json",
				query: {},
				method: "get",
				onload: function(data) {
					encrypStr = data.encrypStr;
					position = parseInt(data.position, 10);
				}
			});
			const str = elmData.value.slice(0, position) + elmData.value.slice(position + encrypStr.length);
			const key = 'undefined' + $(oDoc, '#m-playlist .j-img').dataset.key + $(oDoc, '#song-list-pre-cache a').getAttribute('href').slice(9,12);
			const text = decodeURIComponent(decrypt(str, key));

			// Parse & modify json data
			const data = JSON.parse(text);
			data.forEach(song => PV.fix(song.pv))

			// Hook JSON.parse
			const hooker = new Hooker();
			const id = hooker.hook(oWin, 'JSON.parse', false, false, {
				dealer: function(_this, args) {
					if (args[0] === text) {
						hooker.map[id].config.hook_return.value = data;
						hooker.unhook(id);
						DoLog('Value changed, JSON.parse unhooked...');
						DoLog(data);
					}
					return [_this, args];
				}
			}).id;
			DoLog(LogLevel.Success, 'JSON.parse Hooked...');

			/*Object.defineProperty(elmData, 'value', {
				get: e => {debugger;}
			});*/
		}
	}

	function Privileger() {
		const P = this;
		const levelData = {};
		P.levelData = MakeReadonlyObj(levelData);
		P.fix = fix;

		function fix(privilege) {
			const RATES = {
				'none': 0,
				'standard': 128000,
				'exhigh': 320000,
				'lossless': 999000,
			};

			const dlLevel = privilege.downloadMaxBrLevel;
			const dlRate = RATES[dlLevel];
			const plLevel = privilege.playMaxBrLevel;
			const plRate = RATES[plLevel];
			privilege.dlLevel = dlLevel; // Download
			privilege.dl = dlRate;       // Download
			privilege.plLevel = plLevel; // Play
			privilege.pl = plRate;       // Play
			privilege.st = 0;            // Copyright
			levelData[privilege.id] = {dlLevel, dlRate, plLevel, plRate};
		}
	}

	function downloadSong(id) {
		const qualities = Object.keys(CONST.TYPE_INFO).map(n => parseInt(n, 10)).sort((q1, q2) => q2 - q1);
		search(id, function(song) {
			if (song) {
				const q = qualities.find(q => song.quality.includes(q));
				const fname = `${song.name} - ${song.artist.join(',')}`;
				const ext = CONST.TYPE_INFO[q];
				const coverPath = new URL(song.cover).pathname;
				const coverExt = coverPath.match(/\.[a-zA-Z]+?$/) ? coverPath.match(/\.[a-zA-Z]+?$/)[0] : '.jpg';
				song.url[q] && dl(song.url[q], `${fname}.${ext}`, false);
				song.lrc && GM_getValue('lrc', false) && dl(song.lrc, `${fname}.lrc`, false);
				song.cover && GM_getValue('cover', false) && dl(song.cover, fname + coverExt, false);
			} else {
				showTip(CONST.Text.SongNotFound);
				DoLog(LogLevel.Warning, 'No search result matched.');
			}
		});
	}

	function search(id, callback) {
		// Get NeateaseMusic music info
		WEAPI.song_detail(id, function(data) {
			// Get info
			const song = data.songs[0];
			const name = song.name || '';
			const artist = song.ar.map((ar) => (ar.name)).join(',') || '';
			const cover = song.al.picUrl || '';

			// Gather info
			const fname = replaceText('{$NAME} - {$ARTIST}', {'{$NAME}': name, '{$ARTIST}': artist});
			const cpath = getUrlPath(cover);

			// Search MyFreeMP3
			search_song();

			function search_song(page=1, api='auto') {
				const fullList = [];
				doSearch(page, api);

				function doSearch(page, api) {
					Mfapi.search({
						text: fname,
						page: page,
						type: {
							old: 'YQB',
							new: 'YQD'
						},
						callback: onsearch,
						api
					});
				}

				function onsearch(json) {
					fullList.push.apply(fullList, json.list);
					const song = get_song(json.list, json.noMore || page >= 3);
					song ? callback(song) : doSearch(page+1, json.api);

					function get_song(list, force=false) {
						const exact = list.find(song => getUrlPath(song.cover) === cpath);
						const bestMatch = fullList.reduce((best, song) => {
							const nameMed = calcMed(song.name, name);
							const artistMed = calcMed(song.artist.join(','), artist);
							const med = nameMed + artistMed;
							if (med < best.med) {
								best.med = med;
								best.songs = [song];
							} else if (med === best.med) {
								best.songs.push(song);
							}
							return best;
						}, { med: Infinity, songs: [] });
						if (exact) {
							DoLog(['exact matched', exact]);
							return exact;
						} else if (bestMatch.med === 0) {
							DoLog(['name and artist matched', bestMatch.songs]);
							return getBestQualitySong(bestMatch);
						} else if (force) {
							DoLog(['matched', bestMatch]);
							return getBestQualitySong(bestMatch);
						} else {
							DoLog('not found');
							return null;
						}

						function getBestQualitySong(bestMatch) {
							return bestMatch.songs.reduce((best, cur) => Math.max(...best.quality) > Math.max(...cur.quality) ? best : cur);
						}
					}
				}
			}
		});
	}

	function Weapi() {
		const W = this;
		W.song_detail = song_detail;
		W.encrypt = encrypt;

		function song_detail(id, callback, onerror) {
			const data = {c: JSON.stringify([{id: id}]), csrfToken: ''};
			const xhr = new XMLHttpRequest();
			xhr.open('POST', 'https://music.163.com/weapi/v3/song/detail?csrf_token=');
			xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
			xhr.onerror = onerror;
			xhr.onload = function(e) {
				try {
					callback(JSON.parse(xhr.responseText));
				} catch(err) {
					if (onerror) {
						onerror(err);
					} else {
						throw err;
					}
				}
			};
			xhr.send(encrypt(data));
		};

		function encrypt(data) {
			const json = JSON.stringify(data);
			const encryted = unsafeWindow.asrsea(json, "010001", "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7", "0CoJUm6Qyw8W8jud");
			const xhr_text = 'params=' + encodeURIComponent(encryted.encText) + '&encSecKey=' + encodeURIComponent(encryted.encSecKey);
			return xhr_text;
		}
	}

	function dl(url, name) {
		const pop_id = pop.download(name, 'download');
		$('#pop-container').style.bottom = ($('.m-playbar').style.top.match(/\d+/)[0]*1 + 10).toString() + 'px';
		GM_xmlhttpRequest({
			method: 'GET',
			url: url,
			responseType: 'blob',
			onprogress: function(e) {
				e.lengthComputable /*&& c*/ && (pop.size(pop_id, bytesToSize(e.loaded) + " / " + bytesToSize(e.total)),
												pop.percent(pop_id, 100 * (e.loaded / e.total) >> 0))
			},
			onload: function(res) {
				const ourl = URL.createObjectURL(res.response);
				const a = document.createElement('a');
				a.download = name;
				a.href = ourl;
				a.click();
				setTimeout(function() {
					URL.revokeObjectURL(ourl);
				}, 0);
				pop.finished(pop_id);
				setTimeout(pop.close.bind(pop, pop_id), 2000);
			}
		});

		function bytesToSize(a) {
			if (0 === a) {return "0 B";}
			var b = 1024
			, c = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
			, d = Math.floor(Math.log(a) / Math.log(b));
			return (a / Math.pow(b, d)).toFixed(2) + " " + c[d];
		}
	}

	function showErr(text, html) {
		unsafeWindow.isPY_DNG && showTip(text, html);
	}

	function showTip(text, html=false) {
		const elm = $('span.tip');
		elm[html ? 'innerHTML' : 'innerText'] = text;
		elm.style.display = '';
		setTimeout(e => (elm.style.display = 'none'), 3000);
	}

	function dl_browser(url, name) {
		const a = $CrE('a');
		a.href = url;
		a.download = name;
		a.click();
	}

	function dl_GM(url, name, path=true) {
		name = path ? name : replaceOSSep(name);
		GM_download({
			url: url,
			name: name
		});
	}

	function replaceOSSep(text) {
		const sep = getOSSep();
		const rpl = ({'\\': '\', '/': '/'})[sep];
		return text.replaceAll(sep, rpl);
	}

	function getOSSep() {
		return ({
			'Windows': '\\',
			'Mac': '/',
			'Linux': '/',
			'Null': '-'
		})[getOS()];
	}

	function getOS() {
		const info = (navigator.platform || navigator.userAgent).toLowerCase();
		const test = (s) => (info.includes(s));
		const map = {
			'Windows': ['window', 'win32', 'win64', 'win86'],
			'Mac': ['mac', 'os x'],
			'Linux': ['linux']
		}
		for (const [sys, strs] of Object.entries(map)) {
			if (strs.some(test)) {
				return sys;
			}
		}
		return 'Null';
	}

	function MakeReadonlyObj(val) {
		return isObject(val) ? new Proxy(val, {
			get: function(target, property, receiver) {
				return MakeReadonlyObj(target[property]);
			},
			set: function(target, property, value, receiver) {
				return true;
			},
			has: function(target, prop) {}
		}) : val;

		function isObject(value) {
			return ['object', 'function'].includes(typeof value) && value !== null;
		}
	}

	// Get the pathname of a given url
	function getUrlPath(url) {
		return typeof url === 'string' ? new URL(url).pathname : null;
	}

	function iframeDocSync() {
		const iframe = $('#g_iframe');
		const oDoc = iframe && iframe.contentDocument;
		if (oDoc) {
			const top_path = document.URL.replace(/^https?:\/\/music\.163\.com\/(#\/)?/, '').replace(/^my\/m\//, '').replace('/m/', '/').replace('/#/', '/');
			const ifr_path = oDoc.URL.replace(/^https?:\/\/music\.163\.com\/?/, '').replace(/^my\/#\//, '').replace('/m/', '/').replace('/#/', '/').replace(/^discover$/, '');
			return top_path === ifr_path;
		} else {
			return false;
		}
	}

	// Get unpolluted addEventListener
	function getPureAEL(parentDocument=document) {
		const ifr = makeIfr(parentDocument);

		const oWin = ifr.contentWindow;
		const oDoc = ifr.contentDocument;

		const AEL = oWin.XMLHttpRequest.prototype.addEventListener;
		return AEL;
	}

	// Get unpolluted removeEventListener
	function getPureREL(parentDocument=document) {
		const ifr = makeIfr(parentDocument);

		const oWin = ifr.contentWindow;
		const oDoc = ifr.contentDocument;

		const REL = oWin.XMLHttpRequest.prototype.removeEventListener;
		return REL;
	}

	function makeIfr(parentDocument=document) {
		const ifr = $CrE(parentDocument, 'iframe');
		ifr.srcdoc = '<html></html>';
		ifr.style.width = ifr.style.height = ifr.style.border = ifr.style.padding = ifr.style.margin = '0';
		parentDocument.body.appendChild(ifr);
		return ifr;
	}

	function APIHooker() {
		const AH = this;
		const hooker = new Hooker();
		const hooker_hooks = [];
		const hooks = [];
		const addEventListener = (function() {
			const AEL = getPureAEL();
			return function() {
				const args = Array.from(arguments);
				const _this = args.shift();
				AEL.apply(_this, args);
			}
		}) ();
		const removeEventListener = (function() {
			const REL = getPureREL();
			return function() {
				const args = Array.from(arguments);
				const _this = args.shift();
				REL.apply(_this, args);
			}
		}) ();

		AH.hook = hook;
		AH.unhook = unhook;
		AH.pageOnchange = recover;

		inject();
		setInterval(inject, CONST.Number.Interval_Balanced);

		function hook(urlMatcher, xhrDealer) {
			return hooks.push({
				id: hooks.length,
				matcher: urlMatcher,
				dealer: xhrDealer,
				xhrs: []
			}) - 1;
		}

		function unhook(id) {
			hooks.splice(id, 1);
		}

		function inject() {
			const iframe = $('#g_iframe');
			const oWin = iframe ? iframe.contentWindow : null;

			const hook_dealers = {
				open: function(_this, args) {
					const xhr = _this;
					for (const hook of hooks) {
						matchUrl(args[1], hook.matcher) && hook.xhrs.push(xhr);
					}
					return [_this, args];
				},
				send: function(_this, args) {
					const xhr = _this;
					for (const hook of hooks) {
						if (hook.xhrs.includes(xhr)) {
							// After first readystatechange event, change onreadystatechange to our onProgress function
							let onreadystatechange;
							addEventListener(xhr, 'readystatechange', function(e) {
								onreadystatechange = xhr.onreadystatechange;
								xhr.onreadystatechange = onProgress;
							}, {
								capture: false,
								passive: true,
								once: true
							});

							// Recieves last 3 readystatechange event, apply dealer function, and continue onreadystatechange stack
							function onProgress(e) {
								let args = Array.from(arguments);

								// When onload, apply xhr dealer
								let continueStack = true;
								if (xhr.status === 200 && xhr.readyState === 4) {
									continueStack = hook.dealer(xhr, this, args, onreadystatechange);
								}

								continueStack && typeof onreadystatechange === 'function' && onreadystatechange.apply(this, args);
							}
						}
					}
					return [_this, args];
				},
			}
			let do_inject = false;

			// Hook open: filter all xhr that should be hooked
			try {
				if (window.XMLHttpRequest.prototype.open.name !== 'hooker') {
					hooker_hooks.push(hooker.hook(window, 'XMLHttpRequest.prototype.open', false, false, {
						dealer: hook_dealers.open
					}));
					do_inject = true;
				}
				if (oWin && oWin.XMLHttpRequest.prototype.open.name !== 'hooker') {
					hooker_hooks.push(hooker.hook(oWin, 'XMLHttpRequest.prototype.open', false, false, {
						dealer: hook_dealers.open
					}));
					do_inject = true;
				}

				// Hook send: change eventListeners for each hooked xhr, and apply xhr dealer
				if (window.XMLHttpRequest.prototype.send.name !== 'hooker') {
					hooker_hooks.push(hooker.hook(window, 'XMLHttpRequest.prototype.send', false, false, {
						dealer: hook_dealers.send
					}));
					do_inject = true;
				}
				if (oWin && oWin.XMLHttpRequest.prototype.send.name !== 'hooker') {
					hooker_hooks.push(hooker.hook(oWin, 'XMLHttpRequest.prototype.send', false, false, {
						dealer: hook_dealers.send
					}));
					do_inject = true;
				}
			} catch(err) {}

			do_inject && DoLog(LogLevel.Success, 'Hooker injected');
		}

		function recover() {
			hooker_hooks.forEach((hook) => (hooker.unhook(hook.id)));

			DoLog(LogLevel.Success, 'Hooker removed');
		}

		function matchUrl(url, matcher) {
			if (matcher instanceof RegExp) {
				return !!url.match(matcher);
			}
			if (typeof matcher === 'function') {
				return matcher(url);
			}
		}

		function idmaker() {
			let i = 0;
			return function() {
				return i++;
			}
		}
	}

	function Hooker() {
		const H = this;
		const makeid = idmaker();
		const map = H.map = {};
		H.hook = hook;
		H.unhook = unhook;

		function hook(base, path, log=false, apply_debugger=false, hook_return=false) {
			// target
			path = arrPath(path);
			let parent = base;
			for (let i = 0; i < path.length - 1; i++) {
				const prop = path[i];
				parent = parent[prop];
			}
			const prop = path[path.length-1];
			const target = parent[prop];

			// Only hook functions
			if (typeof target !== 'function') {
				throw new TypeError('hooker.hook: Hook functions only');
			}
			// Check args valid
			if (hook_return) {
				if (typeof hook_return !== 'object' || hook_return === null) {
					throw new TypeError('hooker.hook: Argument hook_return should be false or an object');
				}
				if (!hook_return.hasOwnProperty('value') && typeof hook_return.dealer !== 'function') {
					throw new TypeError('hooker.hook: Argument hook_return should contain one of following properties: value, dealer');
				}
				if (hook_return.hasOwnProperty('value') && typeof hook_return.dealer === 'function') {
					throw new TypeError('hooker.hook: Argument hook_return should not contain both of  following properties: value, dealer');
				}
			}

			// hooker function
			const hooker = function hooker() {
				let _this = this === H ? null : this;
				let args = Array.from(arguments);
				const config = map[id].config;
				const hook_return = config.hook_return;

				// hook functions
				config.log && console.log([base, path.join('.')], _this, args);
				if (config.apply_debugger) {debugger;}
				if (hook_return && typeof hook_return.dealer === 'function') {
					[_this, args] = hook_return.dealer(_this, args);
				}

				// continue stack
				return hook_return && hook_return.hasOwnProperty('value') ? hook_return.value : target.apply(_this, args);
			}
			parent[prop] = hooker;

			// Id
			const id = makeid();
			map[id] = {
				id: id,
				prop: prop,
				parent: parent,
				target: target,
				hooker: hooker,
				config: {
					log: log,
					apply_debugger: apply_debugger,
					hook_return: hook_return
				}
			};

			return map[id];
		}

		function unhook(id) {
			// unhook
			try {
				const hookObj = map[id];
				hookObj.parent[hookObj.prop] = hookObj.target;
				delete map[id];
			} catch(err) {
				console.error(err);
				DoLog(LogLevel.Error, 'unhook error');
			}
		}

		function arrPath(path) {
			return Array.isArray(path) ? path : path.split('.')
		}

		function idmaker() {
			let i = 0;
			return function() {
				return i++;
			}
		}
	}

	function IntervalTaskManager() {
		const tasks = this.tasks = [];
		this.time = 500;
		this.interval = -1;
		defineProperty(this, 'working', {
			get: () => (this.interval >= 0)
		});

		this.addTask = function(fn) {
			tasks.push(fn);
		}

		this.removeTask = function(fn_idx) {
			const idx = typeof fn_idx === 'number' ? fn_idx : tasks.indexOf(fn_idx)
			tasks.splice(idx, 1)
		}

		this.clearTasks = function() {
			tasks.splice(0, Infinity)
		}

		this.start = function() {
			if (!this.working) {
				this.interval = setInterval(this.do, this.time);
				return true;
			} else {
				return false;
			}
		}

		this.stop = function() {
			if (this.working) {
				clearInterval(this.interval);
				this.interval = -1;
				return true;
			} else {
				return false;
			}
		}

		this.do = function() {
			for (const task of tasks) {
				task();
			}
		}
	}

	function defineProperty(obj, prop, desc) {
		desc.configurable = false;
		desc.enumerable = true;
		Object.defineProperty(obj, prop, desc);
	}

	// Calculate 2 strings' similarity, return number lower means more similarity
	// MED: Minimal Edit Distance
	function calcMed(str1, str2) {
		// Create metrix
		const metrix = [];
		for (let i = 0; i < str1.length+1; i++) {
			metrix[i] = [];
		}

		// Fill metrix headers
		for (let i = 0; i < str1.length+1; i++) {
			metrix[i][0] = i;
		}
		for (let j = 0; j < str2.length+1; j++) {
			metrix[0][j] = j;
		}

		// Calc metrix grids
		for (let i = 1; i < str1.length+1; i++) {
			for (let j = 1; j < str2.length+1; j++) {
				const d1 = metrix[i-1][j] + 1;
				const d2 = metrix[i][j-1] + 1;
				const d3 = metrix[i-1][j-1] + (str1.charAt(i-1) === str2.charAt(j-1) ? 0 : 2);
				metrix[i][j] = Math.min(d1, d2, d3);
			}
		}

		return metrix[str1.length][str2.length];
	}
})();