My Free MP3+

解锁MyFreeMP3的QQ音乐、酷狗音乐、酷我音乐,过广告拦截器检测,所有下载全部转为页面内直链下载

目前为 2023-08-30 提交的版本。查看 最新版本

// ==UserScript==
// @name         My Free MP3+
// @namespace    http://tampermonkey.net/My Free MP3 Plus
// @version      0.2.5
// @description  解锁MyFreeMP3的QQ音乐、酷狗音乐、酷我音乐,过广告拦截器检测,所有下载全部转为页面内直链下载
// @author       PY-DNG
// @require      https://gf.qytechs.cn/scripts/456034-basic-functions-for-userscripts/code/script.js?version=1226884
// @match        http*://tool.liumingye.cn/music_old/*
// @match        http*://tools.liumingye.cn/music_old/*
// @match        http*://tool.liumingye.cn/music/*
// @match        http*://tools.liumingye.cn/music/*
// @connect      kugou.com
// @connect      *
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @run-at       document-start
// ==/UserScript==

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

(function() {
    'use strict';

	// Main loader
	main();

	function main() {
		// Collect all funcs from page objs
		const pages = [music, music_old].map(f => f());
		const func_immediate = [], func_load = [];
		for (const page of pages) {
			page.regurl.test(location.href) &&
				page.funcs.forEach(funcobj => (funcobj.onload ? func_load : func_immediate).push(funcobj.func));
		}

		// Exec
		const exec = funcs => funcs.forEach(func => func());
		exec(func_immediate);
		$AEL(window, 'load', exec.bind(null, func_load));
	}

	// 新版页面
	function music() {
		return {
			regurl: /^https?:\/\/tools?\.liumingye\.cn\/music\//,
			funcs: [{
				func: downloadInPage,
				onload: false
			}]
		}

		function downloadInPage() {
			const hooker = new Hooker();

			const xhrs = [];
			const hookedURLs = ['https://api.liumingye.cn/m/api/search', 'https://api.liumingye.cn/m/api/home/recommend', 'https://api.liumingye.cn/m/api/top/song'];

			const openHooerId = hooker.hook(XMLHttpRequest.prototype, 'open', false, false, {
				dealer(_this, args) {
					if (hookedURLs.some(url => args[1].includes(url))) {
						xhrs.push(_this);
					}
					return [_this, args];
				}
			});

			const sendHooerId = hooker.hook(XMLHttpRequest.prototype, 'send', false, false, {
				dealer(_this, args) {
					if (xhrs.includes(_this)) {
						const callbackName = 'onloadend' in _this ? 'onloadend' : 'onreadystatechange';
						const callback = _this[callbackName];
						_this[callbackName] = function() {
							const json = JSON.parse(this.response);
							json.data.list.forEach(song => song.quality.forEach((q, i) => typeof q !== 'number' && (song.quality[i] = parseInt(q.name, 10))));
							rewriteResponse(this, json);
							callback.apply(this, arguments);
						}
						xhrs.splice(xhrs.indexOf(_this), 1);
					}
					return [_this, args];
				}
			});

			DoLog(`XMLHttpRequest Hooked: ${openHooerId}, ${sendHooerId}`);
		}
	}

	// 旧版页面
	function music_old() {
		return {
			regurl: /^https?:\/\/tools?\.liumingye\.cn\/music_old\//,
			funcs: [{
				func: unlockTencent,
				onload: true
			}, {
				func: downloadInPage,
				onload: true
			}, {
				func: bypassAdkillerDetector,
				onload: false
			}]
		};

		// 解锁QQ音乐、酷狗音乐、酷我音乐函数
		function unlockTencent() {
			// 模拟双击
			const search_title = $('#search .home-title');
			const eDblclick = new Event('dblclick');
			search_title.dispatchEvent(eDblclick);
			// 去除双击事件
			const p = search_title.parentElement;
			const new_search_title = $CrE('div');
			new_search_title.className = search_title.className;
			new_search_title.innerHTML = search_title.innerHTML;
			p.removeChild(search_title);
			p.insertBefore(new_search_title, p.children[0]);
		}

		// Hook掉下载按钮实现全部下载均采用页面内下载方式(重写下载逻辑)
		function downloadInPage() {
			$AEL(document.body, 'click', onclick, {capture: true});

			function onclick(e) {
				const elm = e.target;
				const parent = elm ? elm.parentElement : null;
				match(elm);
				match(parent);

				function match(elm) {
					const tag = elm.tagName.toUpperCase();
					const clList = [...elm.classList];
					if (tag === 'A' && clList.includes('download') || clList.includes('pic_download')) {
						e.stopPropagation();
						e.preventDefault();;
						download(elm);
					}
				}
			}

			function download(a) {
				const elm_data = a.parentElement.previousElementSibling;
				const url = elm_data.value;
				const name = $("#name").value;
				const pop_id = pop.download(name, 'download');
				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))
					},
					onerror: function(e) {
						console.log(e);
						window.open(url);
					},
					onload: function(response) {
						const blob = response.response;
						const dataUrl = URL.createObjectURL(blob);
						const ext = getExtname(elm_data.id, blob.type.split(';')[0]);
						saveFile(dataUrl, `${name}.${ext}`);
						setTimeout(URL.revokeObjectURL.bind(URL, dataUrl), 1000);
						pop.finished(pop_id);
						setTimeout(pop.close.bind(pop, pop_id), 2000);
					}
				});

				function getExtname(...args) {
					const map = {
						url_dsd: "flac",
						url_flac: "flac",
						url_ape: "ape",
						url_320: "mp3",
						url_128: "mp3",
						url_m4a: "m4a",
						url_lrc: "lrc",
						'image/png': 'png',
						'image/jpg': 'jpg',
						'image/gif': 'gif',
						'image/bmp': 'bmp',
						'image/jpeg': 'jpeg',
						'image/webp': 'webp',
						'image/tiff': 'tiff',
						'image/vnd.microsoft.icon': 'ico',
					};
					return map[args.find(a => map[a])];
				}

				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 bypassAdkillerDetector() {
			/*
		// 拦截广告拦截检测器的setTimeout延迟启动器
		// 优点:不用考虑#music_tool是否存在,不用反复执行;缺点:需要在setTimeout启动器注册(不可用)前执行,如果脚本加载缓慢,就来不及了
		const setTimeout = unsafeWindow.setTimeout;
		unsafeWindow.setTimeout = function(func, time) {
			if (func && func.toString().includes('$("#music_tool").html()')) {
				func = function() {};
			}
			setTimeout.call(this, func, time);
		}
		*/
			/*
		// 拦截广告拦截检测器的innerHTML检测
		// 优点:对浏览器API没有影响,对DOM影响极小,在检测前执行即可;缺点:需要#music_tool存在,需要反复检测执行,影响性能,稳定性差
		const bypasser = () => {
			const elm = $('#music_tool');
			elm && Object.defineProperty($('#music_tool'), 'innerHTML', {get: () => '<iframe></iframe>'});
		};
		setTimeout(bypasser, 2000);
		bypasser();
		*/
			// 在页面添加干扰元素
			// 优点:对浏览器API没有影响,对DOM几乎没有影响,在检测前执行即可,不用考虑#music_tool是否存在,不用反复执行;缺点:可能影响广告功能(乐
			document.body.firstChild.insertAdjacentHTML('beforebegin', '<ins id="music_tool" style="display: none !important;">sometext</ins>');
		}
	}

	// Save dataURL to file
	function saveFile(dataURL, filename) {
		const a = $CrE('a');
		a.href = dataURL;
		a.download = filename;
		a.click();
	}

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

QingJ © 2025

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