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