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/1465958/MyFreeMP3%20API.js
/* eslint-disable no-multi-spaces */
/* eslint-disable no-return-assign */
// ==UserScript==
// @name MyFreeMP3 API
// @namespace PY-DNG userscripts
// @version 0.1.8
// @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 || 'new']();
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() {
// 感谢 snyssss 提供的新算法
if (!encode_new.encode) {
encode_new.encode = (function () {
const version = "20240531.";
const defaultKey =
"4b9qrOXu305U5Ex5U1yYv69jZO5EbznZq9nWaY5e5NW2GImw27aEBjL4OgW01Tpy";
const customAlphabet =
"hQxDsS6geBiG1MTOPZzoHkt8Wyf4AnLU7FqJbp+0N=udc2j/VY9aICrmX3Rvl5KwE";
return (value, key = defaultKey) => {
const xor = value.replace(/./g, (char, index) =>
String.fromCharCode(
char.charCodeAt(0) ^ key.charCodeAt(index % key.length)
)
);
const base64 = btoa(xor);
const result = base64.replace(/./g, (char) => {
const standardAlphabet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
if (char === standardAlphabet[standardAlphabet.length - 1]) {
return char;
}
return customAlphabet[standardAlphabet.indexOf(char)];
});
return version + md5(result);
};
})();
}
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);
}
})();