MyFreeMP3 API

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

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/474021/1465958/MyFreeMP3%20API.js

  1. /* eslint-disable no-multi-spaces */
  2. /* eslint-disable no-return-assign */
  3.  
  4. // ==UserScript==
  5. // @name MyFreeMP3 API
  6. // @namespace PY-DNG userscripts
  7. // @version 0.1.8
  8. // @description Music API for http://tool.liumingye.cn/music/ and http://tool.liumingye.cn/music_old/
  9. // @author PY-DNG
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. /* global md5 */
  14.  
  15. var Mfapi = (function __MAIN__() {
  16. 'use strict';
  17.  
  18. detectDom('head', head => loadMd5Script());
  19.  
  20. return (function() {
  21. return {
  22. search,
  23. old: {
  24. search: search_old,
  25. link: link_old,
  26. encode: encode_old
  27. },
  28. new: {
  29. search: search_new,
  30. link: link_new,
  31. encode: encode_new
  32. }
  33. };
  34.  
  35. function search(details, retry=3) {
  36. const onerror = details.onerror || function() {};
  37. const reqOld = onerror => req(search_old, dealResponse_old, onerror, 'old');
  38. const reqNew = onerror => req(search_new, dealResponse_new, onerror, 'new');
  39. ({
  40. old: () => reqOld(onerror),
  41. new: () => reqNew(onerror),
  42. auto: () => reqNew(err => reqOld(err => --retry ? search(details, retry) : onerror(err)))
  43. })[details.api || 'new']();
  44.  
  45. function req(request, dealer, onerror, api) {
  46. request({
  47. text: getApiRes('text', api), page: getApiRes('page', api), type: getApiRes('type', api),
  48. callback: json => details.callback(dealer(json)),
  49. onerror: onerror
  50. }, 1);
  51.  
  52. function getApiRes(prop, api) {
  53. const res = details[prop];
  54. return isObject(res) && res.hasOwnProperty(api) ? res[api] : res;
  55. }
  56. }
  57.  
  58. function dealResponse_old(json) {
  59. return {
  60. list: json.data.list.map(song => ({
  61. name: song.name,
  62. artist: song.artist.split(','),
  63. cover: song.cover,
  64. lrc: song.lrc,
  65. quality: song.quality.map(q => typeof q === 'number' ? q : parseInt(q.name)),
  66. url: song.quality.reduce((url, q) => {
  67. url[q] = link_old(song, q);
  68. return url;
  69. }, {})
  70. })),
  71. noMore: !json.more,
  72. api: 'old'
  73. };
  74. }
  75.  
  76. function dealResponse_new(json) {
  77. checkQuality();
  78. const newJson = {
  79. list: json.data.list.map(song => ({
  80. name: song.name,
  81. artist: song.artist.map(a => a.name),
  82. cover: (song.pic || song.album?.pic).replace(/[@\?][^@\?]*$/, ''),
  83. 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,
  84. quality: song.quality.map(q => typeof q === 'number' ? q : parseInt(q.name)).sort((q1, q2) => q1 - q2),
  85. url: new Proxy({}, {
  86. get: (target, property, receiver) => {
  87. const quality = parseInt(property, 10);
  88. return link_new(song, quality);
  89. },
  90. has: (target, property) => {
  91. const quality = parseInt(property, 10);
  92. return newJson.quality.includes(quality);
  93. },
  94. ownKeys: target => {
  95. return song.quality.map(q => q.toString());
  96. }
  97. }),
  98. })),
  99. noMore: !json.data.list.length,
  100. api: 'new'
  101. };
  102. return newJson;
  103.  
  104. function checkQuality() {
  105. let alerted = false;
  106. json.data.list.forEach(song => song.quality.forEach(q => {
  107. const valid = typeof q === 'number' || (typeof q === 'object' && q !== null && typeof q.name === 'string' && /^\d+$/.test(q.name));
  108. if (!valid) {
  109. const str = JSON.stringify(q);
  110. if (str.length > 20) {
  111. str = str.substring(0, 20-3) + '...';
  112. }
  113. console.log(q);
  114. !alerted && alert(`MyFreeMP3 API: 该音频音质格式为(${str}),当前尚未支持,请向开发者反馈`);
  115. alerted = true;
  116. }
  117. }));
  118. }
  119. }
  120. }
  121.  
  122. function search_old(details, retry=3) {
  123. const text = details.text;
  124. const page = details.page || '1';
  125. const type = details.type || 'YQD';
  126. const callback = details.callback;
  127. const onerror = details.onerror || function() {};
  128. if (!text || !callback) {
  129. throw new Error('Argument text or callback missing');
  130. }
  131.  
  132. //const url = 'http://59.110.45.28/m/api/search';
  133. const url = 'http://api2.liumingye.cn/m/api/search';
  134. GM_xmlhttpRequest({
  135. method: 'POST',
  136. url: url,
  137. headers: {
  138. 'Content-Type': 'application/x-www-form-urlencoded',
  139. 'Referer': 'https://tools.liumingye.cn/music_old/'
  140. },
  141. data: encode_old('text='+text+'&page='+page+'&type='+type),
  142. timeout: 10 * 1000,
  143. onload: function(res) {
  144. let json;
  145. try {
  146. json = JSON.parse(res.responseText);
  147. if (json.code !== 200) {
  148. throw new Error('dataerror');
  149. } else {
  150. callback(json);
  151. }
  152. } catch(err) {
  153. --retry ? search_old(details, retry) : onerror(err);
  154. return false;
  155. }
  156. },
  157. onerror: err => --retry ? search_old(details, retry) : onerror(err),
  158. ontimeout: err => --retry ? search_old(details, retry) : onerror(err)
  159. });
  160. }
  161.  
  162. function link_old(song, quality) {
  163. !song.quality.includes(quality) && (quality = Math.max.apply(Math, song.quality));
  164. const qname = ({
  165. 96: 'url_m4a',
  166. 128: 'url_128',
  167. 320: 'url_320',
  168. 2000: 'url_flac'
  169. })[quality];
  170. if (!qname) { setTimeout(e => alert(`MyFreeMP3 API: 该音频格式为${quality.toString()},当前尚未支持,请向开发者反馈`)); throw new Error('Unsupported MF3 quality name'); }
  171. return song[qname];
  172. }
  173.  
  174. function encode_old(plainText) {
  175. const now = new Date().getTime();
  176. const md5Data = md5('<G6sX,Lk~^2:Y%4Z');
  177. let left = md5(md5Data.substr(0, 16));
  178. let right = md5(md5Data.substr(16, 32));
  179. let nowMD5 = md5(now).substr(-4);
  180. let Var_10 = (left + md5((left + nowMD5)));
  181. let Var_11 = Var_10.length;
  182. let Var_12 = ((((now / 1000 + 86400) >> 0) + md5((plainText + right)).substr(0, 16)) + plainText);
  183. let Var_13 = '';
  184. for (let i = 0, Var_15 = Var_12.length;
  185. (i < Var_15); i++) {
  186. let Var_16 = Var_12.charCodeAt(i);
  187. if ((Var_16 < 128)) {
  188. Var_13 += String.fromCharCode(Var_16);
  189. } else if ((Var_16 > 127) && (Var_16 < 2048)) {
  190. Var_13 += String.fromCharCode(((Var_16 >> 6) | 192));
  191. Var_13 += String.fromCharCode(((Var_16 & 63) | 128));
  192. } else {
  193. Var_13 += String.fromCharCode(((Var_16 >> 12) | 224));
  194. Var_13 += String.fromCharCode((((Var_16 >> 6) & 63) | 128));
  195. Var_13 += String.fromCharCode(((Var_16 & 63) | 128));
  196. }
  197. }
  198. let Var_17 = Var_13.length;
  199. let Var_18 = [];
  200. for (let i = 0; i <= 255; i++) {
  201. Var_18[i] = Var_10[(i % Var_11)].charCodeAt();
  202. }
  203. let Var_19 = [];
  204. for (let Var_04 = 0;
  205. (Var_04 < 256); Var_04++) {
  206. Var_19.push(Var_04);
  207. }
  208. for (let Var_20 = 0, Var_04 = 0;
  209. (Var_04 < 256); Var_04++) {
  210. Var_20 = (((Var_20 + Var_19[Var_04]) + Var_18[Var_04]) % 256);
  211. let Var_21 = Var_19[Var_04];
  212. Var_19[Var_04] = Var_19[Var_20];
  213. Var_19[Var_20] = Var_21;
  214. }
  215. let Var_22 = '';
  216. for (let Var_23 = 0, Var_20 = 0, Var_04 = 0;
  217. (Var_04 < Var_17); Var_04++) {
  218. let Var_24 = '0|2|4|3|5|1'.split('|'),
  219. Var_25 = 0;
  220. while (true) {
  221. switch (Var_24[Var_25++]) {
  222. case '0':
  223. Var_23 = ((Var_23 + 1) % 256);
  224. continue;
  225. case '1':
  226. Var_22 += String.fromCharCode(Var_13[Var_04].charCodeAt() ^ Var_19[((Var_19[Var_23] + Var_19[Var_20]) % 256)]);
  227. continue;
  228. case '2':
  229. Var_20 = ((Var_20 + Var_19[Var_23]) % 256);
  230. continue;
  231. case '3':
  232. Var_19[Var_23] = Var_19[Var_20];
  233. continue;
  234. case '4':
  235. var Var_21 = Var_19[Var_23];
  236. continue;
  237. case '5':
  238. Var_19[Var_20] = Var_21;
  239. continue;
  240. }
  241. break;
  242. }
  243. }
  244. let Var_26 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
  245. 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)))))) {
  246. Var_28 = Var_22.charCodeAt(Var_29 += 0.75);
  247. Var_27 = ((Var_27 << 8) | Var_28);
  248. }
  249. Var_22 = (nowMD5 + Var_31.replace(/=/g, '')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '.');
  250. return (('data=' + Var_22) + '&v=2');
  251. }
  252.  
  253. function search_new(details, retry=3) {
  254. const callback = details.callback;
  255. const onerror = details.onerror || function() {};
  256. const data = {
  257. type: details.type || 'YQD',
  258. text: details.text,
  259. page: details.page || 1
  260. };
  261. doSearch();
  262.  
  263. function doSearch() {
  264. // Set properties
  265. ['_t', 'v', 'token'].forEach(key => delete data[key]);
  266. data.v = "beta";
  267. data._t = Date.now();
  268. data.token = encode_new(encodeURIComponent(JSON.stringify(data)));
  269.  
  270. // Request
  271. GM_xmlhttpRequest({
  272. method: 'POST',
  273. url: 'https://api.liumingye.cn/m/api/search',
  274. headers: {
  275. 'Accept': 'application/json, text/plain, */*',
  276. 'Origin': 'https://tool.liumingye.cn',
  277. 'content-type': 'application/json;charset=UTF-8',
  278. },
  279. responseType: 'json',
  280. data: JSON.stringify(data),
  281. timeout: 10 * 1000,
  282. onload: res => callback(res.response),
  283. onerror: err => --retry ? doSearch() : onerror(err),
  284. ontimeout: err => --retry ? doSearch() : onerror(err)
  285. });
  286. }
  287. }
  288.  
  289. function link_new(song, quality) {
  290. !song.quality.includes(quality) && (quality = Math.max.apply(Math, song.quality));
  291. const params = {
  292. id: song.hash || song.id,
  293. quality,
  294. _t: Date.now()
  295. };
  296. params.token = encode_new(encodeURIComponent(JSON.stringify(params, (k, v) => {
  297. return typeof v === 'number' ? v.toString() : v;
  298. })));
  299. const paramsStr = (function() {
  300. let str = '';
  301. for (const [key, value] of Object.entries(params)) {
  302. str += `&${key.toString()}=${value.toString()}`;
  303. }
  304. str = str.slice(1);
  305. return str;
  306. }) ();
  307. const url = 'https://api.liumingye.cn/m/api/link?' + paramsStr;
  308. return url;
  309. }
  310.  
  311. function encode_new() {
  312. // 感谢 snyssss 提供的新算法
  313. if (!encode_new.encode) {
  314. encode_new.encode = (function () {
  315. const version = "20240531.";
  316. const defaultKey =
  317. "4b9qrOXu305U5Ex5U1yYv69jZO5EbznZq9nWaY5e5NW2GImw27aEBjL4OgW01Tpy";
  318.  
  319. const customAlphabet =
  320. "hQxDsS6geBiG1MTOPZzoHkt8Wyf4AnLU7FqJbp+0N=udc2j/VY9aICrmX3Rvl5KwE";
  321.  
  322. return (value, key = defaultKey) => {
  323. const xor = value.replace(/./g, (char, index) =>
  324. String.fromCharCode(
  325. char.charCodeAt(0) ^ key.charCodeAt(index % key.length)
  326. )
  327. );
  328.  
  329. const base64 = btoa(xor);
  330.  
  331. const result = base64.replace(/./g, (char) => {
  332. const standardAlphabet =
  333. "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  334.  
  335. if (char === standardAlphabet[standardAlphabet.length - 1]) {
  336. return char;
  337. }
  338.  
  339. return customAlphabet[standardAlphabet.indexOf(char)];
  340. });
  341.  
  342. return version + md5(result);
  343. };
  344. })();
  345. }
  346. return encode_new.encode.apply(this, arguments);
  347. }
  348. }) ();
  349.  
  350. function loadMd5Script() {
  351. const s = document.createElement('script');
  352. s.src = 'https://cdn.bootcdn.net/ajax/libs/blueimp-md5/2.18.0/js/md5.js';
  353. document.head.appendChild(s);
  354. }
  355.  
  356. // Get callback when specific dom/element loaded
  357. // detectDom({[root], selector, callback[, once]}) | detectDom(selector, callback) | detectDom(root, selector, callback) | detectDom(root, selector, callback, once)
  358. function detectDom() {
  359. const [root, selector, callback, once] = parseArgs([...arguments], [
  360. function(args, defaultValues) {
  361. const arg = args[0];
  362. return ['root', 'selector', 'callback', 'once'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i]);
  363. },
  364. [2,3],
  365. [1,2,3],
  366. [1,2,3,4]
  367. ], [document, '', e => Err('detectDom: callback not found'), true]);
  368.  
  369. if ($(root, selector)) {
  370. for (const elm of $All(root, selector)) {
  371. callback(elm);
  372. if (once) {
  373. return null;
  374. }
  375. }
  376. }
  377.  
  378. const observer = new MutationObserver(mCallback);
  379. observer.observe(root, {
  380. childList: true,
  381. subtree: true
  382. });
  383.  
  384. function mCallback(mutationList, observer) {
  385. const addedNodes = mutationList.reduce((an, mutation) => ((an.push.apply(an, mutation.addedNodes), an)), []);
  386. const addedSelectorNodes = addedNodes.reduce((nodes, anode) => {
  387. if (anode.matches && anode.matches(selector)) {
  388. nodes.add(anode);
  389. }
  390. const childMatches = anode.querySelectorAll ? $All(anode, selector) : [];
  391. for (const cm of childMatches) {
  392. nodes.add(cm);
  393. }
  394. return nodes;
  395. }, new Set());
  396. for (const node of addedSelectorNodes) {
  397. callback(node);
  398. if (once) {
  399. observer.disconnect();
  400. break;
  401. }
  402. }
  403. }
  404.  
  405. return observer;
  406. }
  407.  
  408. // querySelector
  409. function $() {
  410. switch(arguments.length) {
  411. case 2:
  412. return arguments[0].querySelector(arguments[1]);
  413. break;
  414. default:
  415. return document.querySelector(arguments[0]);
  416. }
  417. }
  418. // querySelectorAll
  419. function $All() {
  420. switch(arguments.length) {
  421. case 2:
  422. return arguments[0].querySelectorAll(arguments[1]);
  423. break;
  424. default:
  425. return document.querySelectorAll(arguments[0]);
  426. }
  427. }
  428.  
  429. function parseArgs(args, rules, defaultValues=[]) {
  430. // args and rules should be array, but not just iterable (string is also iterable)
  431. if (!Array.isArray(args) || !Array.isArray(rules)) {
  432. throw new TypeError('parseArgs: args and rules should be array')
  433. }
  434.  
  435. // fill rules[0]
  436. (!Array.isArray(rules[0]) || rules[0].length === 1) && rules.splice(0, 0, []);
  437.  
  438. // max arguments length
  439. const count = rules.length - 1;
  440.  
  441. // args.length must <= count
  442. if (args.length > count) {
  443. throw new TypeError(`parseArgs: args has more elements(${args.length}) longer than ruless'(${count})`);
  444. }
  445.  
  446. // rules[i].length should be === i if rules[i] is an array, otherwise it should be a function
  447. for (let i = 1; i <= count; i++) {
  448. const rule = rules[i];
  449. if (Array.isArray(rule)) {
  450. if (rule.length !== i) {
  451. throw new TypeError(`parseArgs: rules[${i}](${rule}) should have ${i} numbers, but given ${rules[i].length}`);
  452. }
  453. if (!rule.every((num) => (typeof num === 'number' && num <= count))) {
  454. throw new TypeError(`parseArgs: rules[${i}](${rule}) should contain numbers smaller than count(${count}) only`);
  455. }
  456. } else if (typeof rule !== 'function') {
  457. throw new TypeError(`parseArgs: rules[${i}](${rule}) should be an array or a function.`)
  458. }
  459. }
  460.  
  461. // Parse
  462. const rule = rules[args.length];
  463. let parsed;
  464. if (Array.isArray(rule)) {
  465. parsed = [...defaultValues];
  466. for (let i = 0; i < rule.length; i++) {
  467. parsed[rule[i]-1] = args[i];
  468. }
  469. } else {
  470. parsed = rule(args, defaultValues);
  471. }
  472. return parsed;
  473. }
  474.  
  475. function isObject(val) {
  476. return typeof val === 'object' && val !== null;
  477. }
  478.  
  479. // type: [Error, TypeError]
  480. function Err(msg, type=0) {
  481. throw new [Error, TypeError][type]((typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + msg);
  482. }
  483. })();