MyFreeMP3 API

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

当前为 2023-08-27 提交的版本,查看 最新版本

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