MyFreeMP3 API

Music API for http://tool.liumingye.cn/music/ and http://tool.liumingye.cn/music_old/

Version vom 21.10.2023. Aktuellste Version

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/474021/1268225/MyFreeMP3%20API.js

/* eslint-disable no-multi-spaces */
/* eslint-disable no-return-assign */

// ==UserScript==
// @name               MyFreeMP3 API
// @namespace          PY-DNG userscripts
// @version            0.1.7
// @description        Music API for http://tool.liumingye.cn/music/ and http://tool.liumingye.cn/music_old/
// @author             PY-DNG
// @license            MIT
// ==/UserScript==

/* global md5 */

var Mfapi = (function __MAIN__() {
    'use strict';

	detectDom('head', head => loadMd5Script());

	return (function() {
		return {
			search,
			old: {
				search: search_old,
				link: link_old,
				encode: encode_old
			},
			new: {
				search: search_new,
				link: link_new,
				encode: encode_new
			}
		};

		function search(details, retry=3) {
			const onerror = details.onerror || function() {};
			const reqOld = onerror => req(search_old, dealResponse_old, onerror, 'old');
			const reqNew = onerror => req(search_new, dealResponse_new, onerror, 'new');
			({
				old: () => reqOld(onerror),
				new: () => reqNew(onerror),
				auto: () => reqNew(err => reqOld(err => --retry ? search(details, retry) : onerror(err)))
			})[details.api || 'auto']();

			function req(request, dealer, onerror, api) {
				request({
					text: getApiRes('text', api), page: getApiRes('page', api), type: getApiRes('type', api),
					callback: json => details.callback(dealer(json)),
					onerror: onerror
				}, 1);

				function getApiRes(prop, api) {
					const res = details[prop];
					return isObject(res) && res.hasOwnProperty(api) ? res[api] : res;
				}
			}

			function dealResponse_old(json) {
				return {
					list: json.data.list.map(song => ({
						name: song.name,
						artist: song.artist.split(','),
						cover: song.cover,
						lrc: song.lrc,
						quality: song.quality.map(q => typeof q === 'number' ? q : parseInt(q.name)),
						url: song.quality.reduce((url, q) => {
							url[q] = link_old(song, q);
							return url;
						}, {})
					})),
					noMore: !json.more,
					api: 'old'
				};
			}

			function dealResponse_new(json) {
				checkQuality();
				const newJson = {
					list: json.data.list.map(song => ({
						name: song.name,
						artist: song.artist.map(a => a.name),
						cover: (song.pic || song.album?.pic).replace(/[@\?][^@\?]*$/, ''),
						lrc: song.lyric ? `https://api.liumingye.cn/m/api/lyric/id/${encodeURIComponent(song.lyric)}/name/${encodeURIComponent(song.name)} - ${encodeURIComponent(song.artist.map(a => a.name).join(','))}` : null,
						quality: song.quality.map(q => typeof q === 'number' ? q : parseInt(q.name)).sort((q1, q2) => q1 - q2),
						url: new Proxy({}, {
							get: (target, property, receiver) => {
								const quality = parseInt(property, 10);
								return link_new(song, quality);
							},
							has: (target, property) => {
								const quality = parseInt(property, 10);
								return newJson.quality.includes(quality);
							},
							ownKeys: target => {
								return song.quality.map(q => q.toString());
							}
						}),
					})),
					noMore: !json.data.list.length,
					api: 'new'
				};
				return newJson;

				function checkQuality() {
					let alerted = false;
					json.data.list.forEach(song => song.quality.forEach(q => {
						const valid = typeof q === 'number' || (typeof q === 'object' && q !== null && typeof q.name === 'string' && /^\d+$/.test(q.name));
						if (!valid) {
							const str = JSON.stringify(q);
							if (str.length > 20) {
								str = str.substring(0, 20-3) + '...';
							}
							console.log(q);
							!alerted && alert(`MyFreeMP3 API: 该音频音质格式为(${str}),当前尚未支持,请向开发者反馈`);
							alerted = true;
						}
					}));
				}
			}
		}

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

			//const url = 'http://59.110.45.28/m/api/search';
			const url = 'http://api2.liumingye.cn/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_old('text='+text+'&page='+page+'&type='+type),
				timeout: 10 * 1000,
				onload: function(res) {
					let json;
					try {
						json = JSON.parse(res.responseText);
						if (json.code !== 200) {
							throw new Error('dataerror');
						} else {
							callback(json);
						}
					} catch(err) {
						--retry ? search_old(details, retry) : onerror(err);
						return false;
					}
				},
				onerror: err => --retry ? search_old(details, retry) : onerror(err),
				ontimeout: err => --retry ? search_old(details, retry) : onerror(err)
			});
		}

		function link_old(song, quality) {
			!song.quality.includes(quality) && (quality = Math.max.apply(Math, song.quality));
			const qname = ({
				96: 'url_m4a',
				128: 'url_128',
				320: 'url_320',
				2000: 'url_flac'
			})[quality];
			if (!qname) { setTimeout(e => alert(`MyFreeMP3 API: 该音频格式为${quality.toString()},当前尚未支持,请向开发者反馈`)); throw new Error('Unsupported MF3 quality name'); }
			return song[qname];
		}

		function encode_old(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 search_new(details, retry=3) {
			const callback = details.callback;
			const onerror = details.onerror || function() {};
			const data = {
				type: details.type || 'YQD',
				text: details.text,
				page: details.page || 1
			};
			doSearch();

			function doSearch() {
				// Set properties
				['_t', 'v', 'token'].forEach(key => delete data[key]);
				data.v = "beta";
				data._t = Date.now();
				data.token = encode_new(encodeURIComponent(JSON.stringify(data)));

				// Request
				GM_xmlhttpRequest({
					method: 'POST',
					url: 'https://api.liumingye.cn/m/api/search',
					headers: {
						'Accept': 'application/json, text/plain, */*',
						'Origin': 'https://tool.liumingye.cn',
						'content-type': 'application/json;charset=UTF-8',
					},
					responseType: 'json',
					data: JSON.stringify(data),
					timeout: 10 * 1000,
					onload: res => callback(res.response),
					onerror: err => --retry ? doSearch() : onerror(err),
					ontimeout: err => --retry ? doSearch() : onerror(err)
				});
			}
		}

		function link_new(song, quality) {
			!song.quality.includes(quality) && (quality = Math.max.apply(Math, song.quality));
			const params = {
				id: song.hash || song.id,
				quality,
				_t: Date.now()
			};
			params.token = encode_new(encodeURIComponent(JSON.stringify(params, (k, v) => {
				return typeof v === 'number' ? v.toString() : v;
			})));
			const paramsStr = (function() {
				let str = '';
				for (const [key, value] of Object.entries(params)) {
					str += `&${key.toString()}=${value.toString()}`;
				}
				str = str.slice(1);
				return str;
			}) ();
			const url = 'https://api.liumingye.cn/m/api/link?' + paramsStr;
			return url;
		}

		function encode_new() {
			if (!encode_new.encode) {
				encode_new.encode = (function() {
					/***** =19=19=>「Fuck You」<=8=10= *****/
					// This is what you put in your code, now I give it back to you.
					var As = "pW8jg/mke6cO1F4CTuaiswhZfQGzMyq5NJRLPVIvDxlA7=E3YrSUoH0b2BXKn9td+",
						Es = {
							encode: function(e, t) {
								(t === void 0) && (t = "yGz4n9XE9xYy2Oj5Ub7E6u9a5p5aIWZYe53Orq5wE5UgnjbWq0410WTvmLBO1Z2N");
								var i = Us(t.toString(), e.toString());
								return "20230327." + md5(Vs(i));
							}
						};
					return Es.encode;

					function Vs(e) {
						var s, o, a, i, l, c, u, d,
							f = 0,
							h = "";
						if (!e) {
							return e;
						}
						do {
							for (var y = "0|6|2|4|7|5|1|8|3".split("|"), g = 0;;) {
								switch (y[g++]) {
									case "0":
										s = e[f++];
										continue;
									case "1":
										c = 63 & (d >> 6);
										continue;
									case "2":
										a = e[f++];
										continue;
									case "3":
										h += As.charAt(i) + As.charAt(l) + As.charAt(c) + As.charAt(u);
										continue;
									case "4":
										d = ((s << 16) | (o << 8)) | a;
										continue;
									case "5":
										l = ((d >> 12) & 63);
										continue;
									case "6":
										o = e[f++];
										continue;
									case "7":
										i = (d >> 18 & 63);
										continue;
									case "8":
										u = (d & 63);
										continue
								}
								break
							}
						} while (f < e.length);
						var v = (e.length % 3);
						return (v ? h.slice(0, v - 3) : h) + "===".slice(v || 3)
					}

					function Hs(e, t) {
						return e.charCodeAt(Math.floor(t % e.length))
					}

					function Us(e, t) {
						var i = t.split("");
						return i.map(function(t, s) {
							return t.charCodeAt(0) ^ Hs(e, s)
						})
					}
				}) ();
			}
			return encode_new.encode.apply(this, arguments);
		}
	}) ();

	function loadMd5Script() {
		const s = document.createElement('script');
		s.src = 'https://cdn.bootcdn.net/ajax/libs/blueimp-md5/2.18.0/js/md5.js';
		document.head.appendChild(s);
	}

	// Get callback when specific dom/element loaded
	// detectDom({[root], selector, callback[, once]}) | detectDom(selector, callback) | detectDom(root, selector, callback) | detectDom(root, selector, callback, once)
	function detectDom() {
		const [root, selector, callback, once] = parseArgs([...arguments], [
			function(args, defaultValues) {
				const arg = args[0];
				return ['root', 'selector', 'callback', 'once'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i]);
			},
			[2,3],
			[1,2,3],
			[1,2,3,4]
		], [document, '', e => Err('detectDom: callback not found'), true]);

		if ($(root, selector)) {
			for (const elm of $All(root, selector)) {
				callback(elm);
				if (once) {
					return null;
				}
			}
		}

		const observer = new MutationObserver(mCallback);
		observer.observe(root, {
			childList: true,
			subtree: true
		});

		function mCallback(mutationList, observer) {
			const addedNodes = mutationList.reduce((an, mutation) => ((an.push.apply(an, mutation.addedNodes), an)), []);
			const addedSelectorNodes = addedNodes.reduce((nodes, anode) => {
				if (anode.matches && anode.matches(selector)) {
					nodes.add(anode);
				}
				const childMatches = anode.querySelectorAll ? $All(anode, selector) : [];
				for (const cm of childMatches) {
					nodes.add(cm);
				}
				return nodes;
			}, new Set());
			for (const node of addedSelectorNodes) {
				callback(node);
				if (once) {
					observer.disconnect();
					break;
				}
			}
		}

		return observer;
	}

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

	function parseArgs(args, rules, defaultValues=[]) {
		// args and rules should be array, but not just iterable (string is also iterable)
		if (!Array.isArray(args) || !Array.isArray(rules)) {
			throw new TypeError('parseArgs: args and rules should be array')
		}

		// fill rules[0]
		(!Array.isArray(rules[0]) || rules[0].length === 1) && rules.splice(0, 0, []);

		// max arguments length
		const count = rules.length - 1;

		// args.length must <= count
		if (args.length > count) {
			throw new TypeError(`parseArgs: args has more elements(${args.length}) longer than ruless'(${count})`);
		}

		// rules[i].length should be === i if rules[i] is an array, otherwise it should be a function
		for (let i = 1; i <= count; i++) {
			const rule = rules[i];
			if (Array.isArray(rule)) {
				if (rule.length !== i) {
					throw new TypeError(`parseArgs: rules[${i}](${rule}) should have ${i} numbers, but given ${rules[i].length}`);
				}
				if (!rule.every((num) => (typeof num === 'number' && num <= count))) {
					throw new TypeError(`parseArgs: rules[${i}](${rule}) should contain numbers smaller than count(${count}) only`);
				}
			} else if (typeof rule !== 'function') {
				throw new TypeError(`parseArgs: rules[${i}](${rule}) should be an array or a function.`)
			}
		}

		// Parse
		const rule = rules[args.length];
		let parsed;
		if (Array.isArray(rule)) {
			parsed = [...defaultValues];
			for (let i = 0; i < rule.length; i++) {
				parsed[rule[i]-1] = args[i];
			}
		} else {
			parsed = rule(args, defaultValues);
		}
		return parsed;
	}

	function isObject(val) {
		return typeof val === 'object' && val !== null;
	}

	// type: [Error, TypeError]
	function Err(msg, type=0) {
		throw new [Error, TypeError][type]((typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + msg);
	}
})();