MyFreeMP3 API

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

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

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