网易云音乐-MyFreeMP3扩展

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

目前為 2022-10-29 提交的版本,檢視 最新版本

/* 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            0.4
// @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            music.163.net
// @connect            music.126.net
// @icon               https://s1.music.126.net/style/favicon.ico
// @grant              GM_xmlhttpRequest
// @grant              GM_download
// @run-at             document-start
// @noframes
// ==/UserScript==

(function __MAIN__() {
    'use strict';
	const CONST = {
		Number: {
			Interval_Fastest: 1,
			Interval_Fast: 50,
			Interval_Balanced: 500,
		}
	}

	// Prepare
	const md5Script = document.createElement('script');
	md5Script.src = 'https://cdn.bootcdn.net/ajax/libs/blueimp-md5/2.18.0/js/md5.js';
	document.head.appendChild(md5Script);
	const addEventListener = getPureAEL();

	// Arguments: level=LogLevel.Info, logContent, asObject=false
    // Needs one call "DoLog();" to get it initialized before using it!
    function DoLog() {
        // Global log levels set
        unsafeWindow.LogLevel = {
            None: 0,
            Error: 1,
            Success: 2,
            Warning: 3,
            Info: 4,
        }
        unsafeWindow.LogLevelMap = {};
        unsafeWindow.LogLevelMap[LogLevel.None]     = {prefix: ''          , color: 'color:#ffffff'}
        unsafeWindow.LogLevelMap[LogLevel.Error]    = {prefix: '[Error]'   , color: 'color:#ff0000'}
        unsafeWindow.LogLevelMap[LogLevel.Success]  = {prefix: '[Success]' , color: 'color:#00aa00'}
        unsafeWindow.LogLevelMap[LogLevel.Warning]  = {prefix: '[Warning]' , color: 'color:#ffa500'}
        unsafeWindow.LogLevelMap[LogLevel.Info]     = {prefix: '[Info]'    , color: 'color:#888888'}
        unsafeWindow.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'}

        // Current log level
        DoLog.logLevel = (unsafeWindow ? unsafeWindow.isPY_DNG : window.isPY_DNG) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error

        // Log counter
        DoLog.logCount === undefined && (DoLog.logCount = 0);
        if (++DoLog.logCount > 512) {
            console.clear();
            DoLog.logCount = 0;
        }

        // Get args
        let level, logContent, asObject;
        switch (arguments.length) {
            case 1:
                level = LogLevel.Info;
                logContent = arguments[0];
                asObject = false;
                break;
            case 2:
                level = arguments[0];
                logContent = arguments[1];
                asObject = false;
                break;
            case 3:
                level = arguments[0];
                logContent = arguments[1];
                asObject = arguments[2];
                break;
            default:
                level = LogLevel.Info;
                logContent = 'DoLog initialized.';
                asObject = false;
                break;
        }

        // Log when log level permits
        if (level <= DoLog.logLevel) {
            let msg = '%c' + LogLevelMap[level].prefix;
            let subst = LogLevelMap[level].color;

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

            console.log(msg, subst, logContent);
        }
    }
    DoLog();

	main();
	function main() {
		// Wait for iframe
		if (!$('#g_iframe')) {
			setTimeout(main, CONST.Number.Interval_Fast);
		}

		// Commons
		hookPlay();

		// Page functions
		const 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) {
					callback(href, new_href);
					href = new_href;
				}
			}
		}) (deliverPageFuncs, true);
		ITM.time = CONST.Number.Interval_Fast;
		ITM.addTask(pageChangeDetecter);
		ITM.start();

		function deliverPageFuncs(href, new_href) {
			const pageFuncs = [
				[/^https?:\/\/music\.163\.com\/#\/song\?.+$/, pageSong, function() {
					const ifr = $('#g_iframe'); if (!ifr) {return false;}
					const oDoc = ifr.contentDocument; if (!oDoc) {return false;}
					const test = $(oDoc, '.cnt>.m-info');
					return test;
				}],
				[/^https?:\/\/music\.163\.com\/#\/artist\?.+$/, pageArtist]
			];
			for (const pageFunc of pageFuncs) {
				test_exec(pageFunc);
			}

			function test_exec(pageFunc) {
				pageFunc[0].test(location.href) && ((pageFunc[2] ? ({
					'string': () => ($(pageFunc[2])),
					'function': pageFunc[2],
				})[typeof pageFunc[2]]() : true) ? true : (setTimeout(test_exec.bind(null, pageFunc), 500), DoLog('waiting...'), false)) && pageFunc[1](href, new_href);
			}
		}
	}

	function hookPlay() {
		// Play
		try {
			const RATES = {
				'none': 0,
				'standard': 128000,
				'lossless': 999000,
			};
			const APIH = new APIHooker();

			let dlLevel, dlRate, plLevel, plRate;
			APIH.hook(/^https?:\/\/music\.163\.com\/weapi\/v3\/song\/detail(\?[a-zA-Z0-9=_]+)?$/, function(xhr) {
				const json = JSON.parse(xhr.response);
				const privilege = json['privileges'][0];
				dlLevel = privilege['downloadMaxBrLevel'];
				dlRate = RATES[dlLevel];
				plLevel = privilege['playMaxBrLevel'];
				plRate = RATES[plLevel];
				privilege['dlLevel'] = dlLevel;
				privilege['dl'] = dlRate;
				privilege['plLevel'] = plLevel;
				privilege['pl'] = plRate;
				const response = JSON.stringify(json)
				const propDesc = {
					value: response,
					writable: false,
					configurable: false,
					enumerable: true
				}
				Object.defineProperties(xhr, {
					'response': propDesc,
					'responseText': propDesc
				})
				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];
				const name = $('.play a.name').innerText;
				const artist = $('.play a[href^="/artist"]').innerText;
				const fname = replaceText('{$NAME} - {$ARTIST}', {'{$NAME}': name, '{$ARTIST}': artist});
				const cover = $('.m-playbar .head img').src;
				const cpath = getUrlPath(cover);

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

				search({
					text: fname,
					callback: function(s_json) {
						const list = s_json.data.list;
						const song = list.find(function(song) {
							// Search result
							const qualities = [2000, 320, 128];
							const q = qualities.find((q) => (song.quality.includes(q)));
							const s_url = song[({
								2000: 'url_flac',
								320: 'url_320',
								128: 'url_128'
							})[q]];
							const s_ftype = ({
								2000: 'flac',
								320: 'mp3',
								128: 'mp3'
							})[q];
							const s_lrc = song.lrc;
							const s_cover = song.cover;
							const s_name = song.name;
							const s_artist = song.artist;
							const s_fname = name + ' - ' + artist;
							const s_cpath = getUrlPath(s_cover);

							if (s_cpath === cpath) {
								// Song found, request final url
								song.url = s_url;
								return true;
							}
						}) || list[0];
						const abort = GM_xmlhttpRequest({
							method: 'GET',
							url: song.url,
							onprogress: function(e) {
								abort();
								// modify xhr and continue stack
								data['code'] = 200;
								data['br'] = plRate;
								data['level'] = plLevel;
								data['type'] = 'mp3';
								data['url'] = e.finalUrl;
								const response = JSON.stringify(json);
								const propDesc = {
									value: response,
									writable: false,
									configurable: true,
									enumerable: true
								};
								Object.defineProperties(xhr, {
									'response': propDesc,
									'responseText': propDesc
								});
								continueStack();
							}
						}).abort
					},
				});

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

				function continueStack() {
					onreadystatechange.apply(_this, args);;
				}
			});
		} catch (err) {
			console.error(err);
			DoLog(LogLevel.Error, 'hooking error');
		}
	}

	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 fname = replaceText('{$NAME} - {$ARTIST}', {'{$NAME}': name, '{$ARTIST}': artist});
		const cover = $(oDoc, '.u-cover>img.j-img').src;
		const cpath = getUrlPath(cover);

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

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

			// Text
			vip_play.title = '要听龙叔的话,V5不能屈,听完歌快去给你网易爸爸充VIP吧';
			vip_play.children[0].childNodes[1].nodeValue = '播放';
		}

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

		function dlOnclick(e) {
			e.stopPropagation();
			search({
				text: fname,
				callback: onsearch,
			});
		}

		function onsearch(json) {
			const list = json.data.list;
			for (const song of list) {
				const qualities = [2000, 320, 128];
				const q = qualities.find((q) => (song.quality.includes(q)));
				const s_url = song[({
					2000: 'url_flac',
					320: 'url_320',
					128: 'url_128'
				})[q]];
				const s_ftype = ({
					2000: 'flac',
					320: 'mp3',
					128: 'mp3'
				})[q];
				const s_lrc = song.lrc;
				const s_cover = song.cover;
				const s_name = song.name;
				const s_artist = song.artist;
				const s_fname = name + ' - ' + artist;
				const s_cpath = getUrlPath(s_cover);

				if (s_cpath === cpath) {
					dl_GM(s_url, s_fname + '.' + s_ftype);
					dl(s_lrc, s_fname + '.lrc');
					dl(s_cover, s_fname + s_cpath.match(/\.[a-zA-Z]+?$/)[0]);
					break;
				}
			}
		}
	}

	function pageArtist() {
		const iframe = $('#g_iframe');

		replacePredata();

		function replacePredata() {
			const oDoc = iframe.contentDocument;
			const oWin = iframe.contentWindow;
			const elmData = oDoc && $(oDoc, '#song-list-pre-data');
			if (!elmData) {
				if (oDoc && $(oDoc, '#song-list-pre-cache table')) {
					DoLog(LogLevel.Error, 'Predata hook failed.');
				} else {
					DoLog('No predata found, waiting...');
					setTimeout(replacePredata, CONST.Number.Interval_Fastest);
				}
				return false;
			}

			const RATES = {
				'none': 0,
				'standard': 128000,
				'lossless': 999000,
			};

			const list = JSON.parse(elmData.value);
			for (const song of list) {
				const privilege = song.privilege;
				const dlLevel = privilege.downloadMaxBrLevel;
				const dlRate = RATES[dlLevel];
				const plLevel = privilege.playMaxBrLevel;
				const plRate = RATES[plLevel];
				privilege.dlLevel = dlLevel;
				privilege.dl = dlRate;
				privilege.plLevel = plLevel;
				privilege.pl = plRate;
			}
			elmData.value = JSON.stringify(list);

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

	// Get unpolluted addEventListener
	function getPureAEL() {
		const ifr = document.createElement('iframe');
		ifr.srcdoc = '<html></html>';
		ifr.style.width = ifr.style.height = ifr.style.border = ifr.style.padding = ifr.style.margin = '0';
		document.body.appendChild(ifr);

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

		const AEL = oWin.EventTarget.prototype.addEventListener;
		return AEL.call.bind(AEL);
	}

	function search(details, retry=3) {
		const text = details.text;
		const page = details.page || '1';
		const type = details.type || 'YQD';
		const callback = details.callback;
		if (!text || !callback) {
			throw new Error('Argument text or callback missing');
		}

		const url = 'http://59.110.45.28/m/api/search';
		GM_xmlhttpRequest({
			method: 'POST',
			url: url,
			headers: {
				'Content-Type': 'application/x-www-form-urlencoded',
				'Referer': 'https://tools.liumingye.cn/music_old/'
			},
			data: encode('text='+text+'&page='+page+'&type='+type),
			onload: function(res) {
				let json;
				try {
					json = JSON.parse(res.responseText);
					if (json.code !== 200) {
						throw new Error('dataerror');
					} else {
						callback(json);
					}
				} catch(e) {
					--retry >= 0 && search(details, retry);
					return false;
				}
			}
		});
	}

	function encode(plainText) {
		const now = new Date().getTime();
		const md5Data = md5('<G6sX,Lk~^2:Y%4Z');
		let left = md5(md5Data.substr(0, 16));
		let right = md5(md5Data.substr(16, 32));
		let nowMD5 = md5(now).substr(-4);
		let Var_10 = (left + md5((left + nowMD5)));
		let Var_11 = Var_10['length'];
		let Var_12 = ((((now / 1000 + 86400) >> 0) + md5((plainText + right)).substr(0, 16)) + plainText);
		let Var_13 = '';
		for (let i = 0, Var_15 = Var_12.length;
			 (i < Var_15); i++) {
			let Var_16 = Var_12.charCodeAt(i);
			if ((Var_16 < 128)) {
				Var_13 += String['fromCharCode'](Var_16);
			} else if ((Var_16 > 127) && (Var_16 < 2048)) {
				Var_13 += String['fromCharCode'](((Var_16 >> 6) | 192));
				Var_13 += String['fromCharCode'](((Var_16 & 63) | 128));
			} else {
				Var_13 += String['fromCharCode'](((Var_16 >> 12) | 224));
				Var_13 += String['fromCharCode']((((Var_16 >> 6) & 63) | 128));
				Var_13 += String['fromCharCode'](((Var_16 & 63) | 128));
			}
		}
		let Var_17 = Var_13.length;
		let Var_18 = [];
		for (let i = 0; i <= 255; i++) {
			Var_18[i] = Var_10[(i % Var_11)].charCodeAt();
		}
		let Var_19 = [];
		for (let Var_04 = 0;
			 (Var_04 < 256); Var_04++) {
			Var_19.push(Var_04);
		}
		for (let Var_20 = 0, Var_04 = 0;
			 (Var_04 < 256); Var_04++) {
			Var_20 = (((Var_20 + Var_19[Var_04]) + Var_18[Var_04]) % 256);
			let Var_21 = Var_19[Var_04];
			Var_19[Var_04] = Var_19[Var_20];
			Var_19[Var_20] = Var_21;
		}
		let Var_22 = '';
		for (let Var_23 = 0, Var_20 = 0, Var_04 = 0;
			 (Var_04 < Var_17); Var_04++) {
			let Var_24 = '0|2|4|3|5|1'.split('|'),
				Var_25 = 0;
			while (true) {
				switch (Var_24[Var_25++]) {
					case '0':
						Var_23 = ((Var_23 + 1) % 256);
						continue;
					case '1':
						Var_22 += String.fromCharCode(Var_13[Var_04].charCodeAt() ^ Var_19[((Var_19[Var_23] + Var_19[Var_20]) % 256)]);
						continue;
					case '2':
						Var_20 = ((Var_20 + Var_19[Var_23]) % 256);
						continue;
					case '3':
						Var_19[Var_23] = Var_19[Var_20];
						continue;
					case '4':
						var Var_21 = Var_19[Var_23];
						continue;
					case '5':
						Var_19[Var_20] = Var_21;
						continue;
				}
				break;
			}
		}
		let Var_26 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
		for (var Var_27, Var_28, Var_29 = 0, Var_30 = Var_26, Var_31 = ''; Var_22.charAt((Var_29 | 0)) || (Var_30 = '=', (Var_29 % 1)); Var_31 += Var_30.charAt((63 & (Var_27 >> (8 - ((Var_29 % 1) * 8)))))) {
			Var_28 = Var_22['charCodeAt'](Var_29 += 0.75);
			Var_27 = ((Var_27 << 8) | Var_28);
		}
		Var_22 = (nowMD5 + Var_31.replace(/=/g, '')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '.');
		return (('data=' + Var_22) + '&v=2');
	}

	function dl(url, name) {
		GM_xmlhttpRequest({
			method: 'GET',
			url: url,
			responseType: 'blob',
			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);
			}
		});
	}

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

	function dl_GM(url, name) {
		GM_download(url, name);
	}

	// Basic functions
	// querySelector
	function $() {
		switch(arguments.length) {
			case 2:
				return arguments[0].querySelector(arguments[1]);
				break;
			default:
				return document.querySelector(arguments[0]);
		}
	}
	// querySelectorAll
	function $All() {
		switch(arguments.length) {
			case 2:
				return arguments[0].querySelectorAll(arguments[1]);
				break;
			default:
				return document.querySelectorAll(arguments[0]);
		}
	}
	// createElement
	function $CrE() {
		switch(arguments.length) {
			case 2:
				return arguments[0].createElement(arguments[1]);
				break;
			default:
				return document.createElement(arguments[0]);
		}
	}

	// Get the pathname of a given url
	function getUrlPath(url) {
		const a = $CrE('a');
		a.href = url;
		return a.pathname;
	}

	// Replace model text with no mismatching of replacing replaced text
	// e.g. replaceText('aaaabbbbccccdddd', {'a': 'b', 'b': 'c', 'c': 'd', 'd': 'e'}) === 'bbbbccccddddeeee'
	//      replaceText('abcdAABBAA', {'BB': 'AA', 'AAAAAA': 'This is a trap!'}) === 'abcdAAAAAA'
	//      replaceText('abcd{AAAA}BB}', {'{AAAA}': '{BB', '{BBBB}': 'This is a trap!'}) === 'abcd{BBBB}'
	//      replaceText('abcd', {}) === 'abcd'
	/* Note:
	    replaceText will replace in sort of replacer's iterating sort
	    e.g. currently replaceText('abcdAABBAA', {'BBAA': 'TEXT', 'AABB': 'TEXT'}) === 'abcdAATEXT'
	    but remember: (As MDN Web Doc said,) Although the keys of an ordinary Object are ordered now, this was
	    not always the case, and the order is complex. As a result, it's best not to rely on property order.
	    So, don't expect replaceText will treat replacer key-values in any specific sort. Use replaceText to
	    replace irrelevance replacer keys only.
	*/
	function replaceText(text, replacer) {
		if (Object.entries(replacer).length === 0) {return text;}
		const [models, targets] = Object.entries(replacer);
		const len = models.length;
		let text_arr = [{text: text, replacable: true}];
		for (const [model, target] of Object.entries(replacer)) {
			text_arr = replace(text_arr, model, target);
		}
		return text_arr.map((text_obj) => (text_obj.text)).join('');

		function replace(text_arr, model, target) {
			const result_arr = [];
			for (const text_obj of text_arr) {
				if (text_obj.replacable) {
					const splited = text_obj.text.split(model);
					for (const part of splited) {
						result_arr.push({text: part, replacable: true});
						result_arr.push({text: target, replacable: false});
					}
					result_arr.pop();
				} else {
					result_arr.push(text_obj);
				}
			}
			return result_arr;
		}
	}

	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
			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;
			}

			// Hook iframe location change
			if (!iframe.contentWindow.location.hooked) {
				// Save original objects
				const _win = iframe.contentWindow;
				const _location = _win.location;
				const _replace = _location.replace;
			}

			// Modified from: https://stackoverflow.com/questions/24603580/how-can-i-access-the-dom-elements-within-an-iframe/24603642#comment38157462_24603642
			// This function ONLY works for iframes of the same origin as their parent
			function iframeReady(iframe, fn, fromurl='about:blank') {
				let timer;

				function ready() {
					const doc = iframe.contentDocument || iframe.contentWindow.document;
					clearTimeout(timer);
					removeEventListener(iframe, 'load', iframeOnload);
					removeEventListener(doc, 'DOMContentLoaded', ready);
					removeEventListener(doc, "readystatechange", readyState);
					DoLog('iframe ready');
					fn.call(this);
				}

				function readyState() {
					if (this.readyState === "complete") {
						ready.call(this);
					}
				}

				// use iframe load as a backup - though the other events should occur first
				addEventListener(iframe, "load", iframeOnload);
				function iframeOnload() {
					ready.call(iframe.contentDocument || iframe.contentWindow.document);
				}

				function checkLoaded() {
					const doc = iframe.contentDocument || iframe.contentWindow.document;
					// We can tell if there is a dummy document installed because the dummy document
					// will have an URL that starts with "about:".  The real document will not have that URL
					if (doc.URL !== fromurl) {
						if (doc.readyState === "complete") {
							ready.call(doc);
						} else {
							// set event listener for DOMContentLoaded on the new document
							addEventListener(doc, "DOMContentLoaded", ready);
							addEventListener(doc, "readystatechange", readyState);
						}
					} else {
						// still same old original document, so keep looking for content or new document
						timer = setTimeout(checkLoaded, 1);
					}
				}
				checkLoaded();
			}

			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);
			}
		}

		// 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 addEventListener
		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 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);

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

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

			// Id
			const id = makeid();
			map[id] = {
				id: id,
				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 {
				map[id].parent = map[id].target;
				delete map[id];
			} catch(err) {}
		}

		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);
	}
})();

QingJ © 2025

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