index.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. import axios from "axios";
  2. import { load } from "cheerio";
  3. import CryptoJS = require("crypto-js");
  4. import dayjs = require("dayjs");
  5. const pageSize = 20;
  6. const headers = {
  7. "user-agent":
  8. "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
  9. };
  10. /** 工具函数 */
  11. function nonce(e = 10) {
  12. let n = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
  13. r = "";
  14. for (let i = 0; i < e; i++)
  15. r += n.charAt(Math.floor(Math.random() * n.length));
  16. return r;
  17. }
  18. function getNormalizedParams(parameters) {
  19. const sortedKeys = [];
  20. const normalizedParameters = [];
  21. for (let e in parameters) {
  22. sortedKeys.push(_encode(e));
  23. }
  24. sortedKeys.sort();
  25. for (let idx = 0; idx < sortedKeys.length; idx++) {
  26. const e = sortedKeys[idx];
  27. var n,
  28. r,
  29. i = _decode(e),
  30. a = parameters[i];
  31. for (a.sort(), n = 0; n < a.length; n++)
  32. (r = _encode(a[n])), normalizedParameters.push(e + "=" + r);
  33. }
  34. return normalizedParameters.join("&");
  35. }
  36. function _encode(e) {
  37. return e
  38. ? encodeURIComponent(e)
  39. .replace(/[!'()]/g, escape)
  40. .replace(/\*/g, "%2A")
  41. : "";
  42. }
  43. function _decode(e) {
  44. return e ? decodeURIComponent(e) : "";
  45. }
  46. function u(e) {
  47. (this._parameters = {}), this._loadParameters(e || {});
  48. }
  49. u.prototype = {
  50. _loadParameters: function (e) {
  51. e instanceof Array
  52. ? this._loadParametersFromArray(e)
  53. : "object" == typeof e && this._loadParametersFromObject(e);
  54. },
  55. _loadParametersFromArray: function (e) {
  56. var n;
  57. for (n = 0; n < e.length; n++) this._loadParametersFromObject(e[n]);
  58. },
  59. _loadParametersFromObject: function (e) {
  60. var n;
  61. for (n in e)
  62. if (e.hasOwnProperty(n)) {
  63. var r = this._getStringFromParameter(e[n]);
  64. this._loadParameterValue(n, r);
  65. }
  66. },
  67. _loadParameterValue: function (e, n) {
  68. var r;
  69. if (n instanceof Array) {
  70. for (r = 0; r < n.length; r++) {
  71. var i = this._getStringFromParameter(n[r]);
  72. this._addParameter(e, i);
  73. }
  74. 0 == n.length && this._addParameter(e, "");
  75. } else this._addParameter(e, n);
  76. },
  77. _getStringFromParameter: function (e) {
  78. var n = e || "";
  79. try {
  80. ("number" == typeof e || "boolean" == typeof e) && (n = e.toString());
  81. } catch (e) {}
  82. return n;
  83. },
  84. _addParameter: function (e, n) {
  85. this._parameters[e] || (this._parameters[e] = []),
  86. this._parameters[e].push(n);
  87. },
  88. get: function () {
  89. return this._parameters;
  90. },
  91. };
  92. // 获取签名
  93. function getSignature(
  94. method: string,
  95. urlPath: string,
  96. params,
  97. secret = "f3ac5b086f3eab260520d8e3049561e6"
  98. ) {
  99. urlPath = urlPath.split("?")[0];
  100. urlPath = urlPath.startsWith("http")
  101. ? urlPath
  102. : "https://api.audiomack.com/v1" + urlPath;
  103. const r = new u(params).get();
  104. const httpMethod = method.toUpperCase();
  105. const normdParams = getNormalizedParams(r);
  106. const l =
  107. _encode(httpMethod) + "&" + _encode(urlPath) + "&" + _encode(normdParams);
  108. const hash = CryptoJS.HmacSHA1(l, secret + "&").toString(CryptoJS.enc.Base64);
  109. return hash;
  110. }
  111. function formatMusicItem(raw) {
  112. return {
  113. id: raw.id,
  114. artwork: raw.image || raw.image_base,
  115. duration: +raw.duration,
  116. title: raw.title,
  117. artist: raw.artist,
  118. album: raw.album,
  119. url_slug: raw.url_slug,
  120. };
  121. }
  122. function formatAlbumItem(raw) {
  123. return {
  124. artist: raw.artist,
  125. artwork: raw.image || raw.image_base,
  126. id: raw.id,
  127. date: dayjs.unix(+raw.released).format("YYYY-MM-DD"),
  128. title: raw.title,
  129. _musicList: raw?.tracks?.map?.((it) => ({
  130. id: it.song_id || it.id,
  131. artwork: raw.image || raw.image_base,
  132. duration: +it.duration,
  133. title: it.title,
  134. artist: it.artist,
  135. album: raw.title,
  136. })),
  137. };
  138. }
  139. function formatMusicSheetItem(raw) {
  140. return {
  141. worksNum: raw.track_count,
  142. id: raw.id,
  143. title: raw.title,
  144. artist: raw.artist?.name,
  145. artwork: raw.image || raw.image_base,
  146. artistItem: {
  147. id: raw.artist?.id,
  148. avatar: raw.artist?.image || raw.artist?.image_base,
  149. name: raw.artist?.name,
  150. url_slug: raw.artist?.url_slug,
  151. },
  152. createAt: dayjs.unix(+raw.created).format("YYYY-MM-DD"),
  153. url_slug: raw.url_slug,
  154. };
  155. }
  156. async function searchBase(query, page, show) {
  157. const params = {
  158. limit: pageSize,
  159. oauth_consumer_key: "audiomack-js",
  160. oauth_nonce: nonce(32),
  161. oauth_signature_method: "HMAC-SHA1",
  162. oauth_timestamp: Math.round(Date.now() / 1e3),
  163. oauth_version: "1.0",
  164. page: page,
  165. q: query,
  166. show: show,
  167. sort: "popular",
  168. };
  169. const oauth_signature = getSignature("GET", "/search", params);
  170. const results = (
  171. await axios.get("https://api.audiomack.com/v1/search", {
  172. headers,
  173. params: {
  174. ...params,
  175. oauth_signature,
  176. },
  177. })
  178. ).data.results;
  179. return results;
  180. }
  181. async function searchMusic(query, page) {
  182. const results = await searchBase(query, page, "songs");
  183. return {
  184. isEnd: results.length < pageSize,
  185. data: results.map(formatMusicItem),
  186. };
  187. }
  188. async function searchAlbum(query, page) {
  189. const results = await searchBase(query, page, "albums");
  190. return {
  191. isEnd: results.length < pageSize,
  192. data: results.map(formatAlbumItem),
  193. };
  194. }
  195. async function searchMusicSheet(query, page) {
  196. const results = await searchBase(query, page, "playlists");
  197. return {
  198. isEnd: results.length < pageSize,
  199. data: results.map(formatMusicSheetItem),
  200. };
  201. }
  202. async function searchArtist(query, page) {
  203. const results = await searchBase(query, page, "artists");
  204. return {
  205. isEnd: results.length < pageSize,
  206. data: results.map((raw) => ({
  207. name: raw.name,
  208. id: raw.id,
  209. avatar: raw.image || raw.image_base,
  210. url_slug: raw.url_slug,
  211. })),
  212. };
  213. }
  214. let dataUrlBase;
  215. async function getDataUrlBase() {
  216. if (dataUrlBase) {
  217. return dataUrlBase;
  218. }
  219. const rawHtml = (await axios.get("https://audiomack.com/")).data;
  220. const $ = load(rawHtml);
  221. const script = $("script#__NEXT_DATA__").text();
  222. const jsonObj = JSON.parse(script);
  223. if (jsonObj.buildId) {
  224. dataUrlBase = `https://audiomack.com/_next/data/${jsonObj.buildId}`
  225. }
  226. return dataUrlBase;
  227. }
  228. async function getArtistWorks(artistItem, page, type) {
  229. if (type === "music") {
  230. const params = {
  231. artist_id: artistItem.id,
  232. limit: pageSize,
  233. oauth_consumer_key: "audiomack-js",
  234. oauth_nonce: nonce(32),
  235. oauth_signature_method: "HMAC-SHA1",
  236. oauth_timestamp: Math.round(Date.now() / 1e3),
  237. oauth_version: "1.0",
  238. page: page,
  239. sort: "rank",
  240. type: "songs",
  241. };
  242. const oauth_signature = getSignature(
  243. "GET",
  244. "/search_artist_content",
  245. params
  246. );
  247. const results = (
  248. await axios.get("https://api.audiomack.com/v1/search_artist_content", {
  249. headers,
  250. params: {
  251. ...params,
  252. oauth_signature,
  253. },
  254. })
  255. ).data.results;
  256. return {
  257. isEnd: results.length < pageSize,
  258. data: results.map(formatMusicItem),
  259. };
  260. } else if (type === "album") {
  261. const params = {
  262. artist_id: artistItem.id,
  263. limit: pageSize,
  264. oauth_consumer_key: "audiomack-js",
  265. oauth_nonce: nonce(32),
  266. oauth_signature_method: "HMAC-SHA1",
  267. oauth_timestamp: Math.round(Date.now() / 1e3),
  268. oauth_version: "1.0",
  269. page: page,
  270. sort: "rank",
  271. type: "albums",
  272. };
  273. const oauth_signature = getSignature(
  274. "GET",
  275. "/search_artist_content",
  276. params
  277. );
  278. const results = (
  279. await axios.get("https://api.audiomack.com/v1/search_artist_content", {
  280. headers,
  281. params: {
  282. ...params,
  283. oauth_signature,
  284. },
  285. })
  286. ).data.results;
  287. return {
  288. isEnd: results.length < pageSize,
  289. data: results.map(formatAlbumItem),
  290. };
  291. }
  292. }
  293. async function getMusicSheetInfo(sheet: IMusicSheet.IMusicSheetItem, page) {
  294. const _dataUrlBase = await getDataUrlBase();
  295. const res = (
  296. await axios.get(
  297. `${_dataUrlBase}/${sheet.artistItem.url_slug}/playlist/${sheet.url_slug}.json`,
  298. {
  299. params: {
  300. page_slug: sheet.artistItem.url_slug,
  301. playlist_slug: sheet.url_slug,
  302. },
  303. headers: {
  304. ...headers,
  305. },
  306. }
  307. )
  308. ).data;
  309. const musicPage = res.pageProps.initialState.musicPage;
  310. const targetKey = Object.keys(musicPage).find((it) =>
  311. it.startsWith("musicMusicPage")
  312. );
  313. const tracks = musicPage[targetKey].results.tracks;
  314. return {
  315. isEnd: true,
  316. musicList: tracks.map(formatMusicItem),
  317. };
  318. }
  319. async function getMediaSource(musicItem, quality: IMusic.IQualityKey) {
  320. if (quality !== "standard") {
  321. return;
  322. }
  323. const params = {
  324. environment: "desktop-web",
  325. hq: true,
  326. oauth_consumer_key: "audiomack-js",
  327. oauth_nonce: nonce(32),
  328. oauth_signature_method: "HMAC-SHA1",
  329. oauth_timestamp: Math.round(Date.now() / 1e3),
  330. oauth_version: "1.0",
  331. section: "/search",
  332. };
  333. const oauth_signature = getSignature(
  334. "GET",
  335. `/music/play/${musicItem.id}`,
  336. params
  337. );
  338. const res = (
  339. await axios.get(`https://api.audiomack.com/v1/music/play/${musicItem.id}`, {
  340. headers: {
  341. ...headers,
  342. origin: "https://audiomack.com",
  343. },
  344. params: {
  345. ...params,
  346. oauth_signature,
  347. },
  348. })
  349. ).data;
  350. return {
  351. url: res.signedUrl,
  352. };
  353. }
  354. async function getAlbumInfo(albumItem) {
  355. return {
  356. // 老版本有bug
  357. musicList: albumItem._musicList.map((it) => ({ ...it })),
  358. };
  359. }
  360. async function getRecommendSheetTags() {
  361. const rawHtml = (await axios.get("https://audiomack.com/playlists")).data;
  362. const $ = load(rawHtml);
  363. const script = $("script#__NEXT_DATA__").text();
  364. const jsonObj = JSON.parse(script);
  365. return {
  366. data: [
  367. {
  368. data: jsonObj.props.pageProps.categories,
  369. },
  370. ],
  371. };
  372. }
  373. async function getRecommendSheetsByTag(tag, page) {
  374. if (!tag.id) {
  375. tag = { id: "34", title: "What's New", url_slug: "whats-new" };
  376. }
  377. const params = {
  378. featured: "yes",
  379. limit: pageSize,
  380. oauth_consumer_key: "audiomack-js",
  381. oauth_nonce: nonce(32),
  382. oauth_signature_method: "HMAC-SHA1",
  383. oauth_timestamp: Math.round(Date.now() / 1e3),
  384. oauth_version: "1.0",
  385. page: page,
  386. slug: tag.url_slug,
  387. };
  388. const oauth_signature = getSignature("GET", "/playlist/categories", params);
  389. const results = (
  390. await axios.get("https://api.audiomack.com/v1/playlist/categories", {
  391. headers,
  392. params: {
  393. ...params,
  394. oauth_signature,
  395. },
  396. })
  397. ).data.results.playlists;
  398. return {
  399. isEnd: results.length < pageSize,
  400. data: results.map(formatMusicSheetItem),
  401. };
  402. }
  403. async function getTopLists() {
  404. const genres = [
  405. {
  406. title: "All Genres",
  407. url_slug: null,
  408. },
  409. {
  410. title: "Afrosounds",
  411. url_slug: "afrobeats",
  412. },
  413. {
  414. title: "Hip-Hop/Rap",
  415. url_slug: "rap",
  416. },
  417. {
  418. title: "Latin",
  419. url_slug: "latin",
  420. },
  421. {
  422. title: "Caribbean",
  423. url_slug: "caribbean",
  424. },
  425. {
  426. title: "Pop",
  427. url_slug: "pop",
  428. },
  429. {
  430. title: "R&B",
  431. url_slug: "rb",
  432. },
  433. {
  434. title: "Gospel",
  435. url_slug: "gospel",
  436. },
  437. {
  438. title: "Electronic",
  439. url_slug: "electronic",
  440. },
  441. {
  442. title: "Rock",
  443. url_slug: "rock",
  444. },
  445. {
  446. title: "Punjabi",
  447. url_slug: "punjabi",
  448. },
  449. {
  450. title: "Country",
  451. url_slug: "country",
  452. },
  453. {
  454. title: "Instrumental",
  455. url_slug: "instrumental",
  456. },
  457. {
  458. title: "Podcast",
  459. url_slug: "podcast",
  460. },
  461. ];
  462. return [
  463. {
  464. title: "Trending Songs",
  465. data: genres.map((it) => ({
  466. ...it,
  467. type: "trending",
  468. id: it.url_slug ?? it.title,
  469. })),
  470. },
  471. {
  472. title: "Recently Added Music",
  473. data: genres.map((it) => ({
  474. ...it,
  475. type: "recent",
  476. id: it.url_slug ?? it.title,
  477. })),
  478. },
  479. ];
  480. }
  481. // TODO: 支持分页
  482. async function getTopListDetail(topListItem, page = 1) {
  483. const type = topListItem.type;
  484. const partialUrl = `/music/${
  485. topListItem.url_slug ? `${topListItem.url_slug}/` : ""
  486. }${type}/page/${page}`;
  487. const url = `https://api.audiomack.com/v1${partialUrl}`;
  488. const params = {
  489. oauth_consumer_key: "audiomack-js",
  490. oauth_nonce: nonce(32),
  491. oauth_signature_method: "HMAC-SHA1",
  492. oauth_timestamp: Math.round(Date.now() / 1e3),
  493. oauth_version: "1.0",
  494. type: "song",
  495. };
  496. const oauth_signature = getSignature("GET", partialUrl, params);
  497. const results = (
  498. await axios.get(url, {
  499. headers,
  500. params: {
  501. ...params,
  502. oauth_signature,
  503. },
  504. })
  505. ).data.results;
  506. return {
  507. musicList: results.map(formatMusicItem),
  508. };
  509. }
  510. module.exports = {
  511. platform: "Audiomack",
  512. version: "0.0.2",
  513. author: '猫头猫',
  514. primaryKey: ["id", "url_slug"],
  515. srcUrl:
  516. "https://gitee.com/maotoumao/MusicFreePlugins/raw/v0.1/dist/audiomack/index.js",
  517. cacheControl: "no-cache",
  518. supportedSearchType: ['music', 'album', 'sheet', 'artist'],
  519. async search(query, page, type) {
  520. if (type === "music") {
  521. return await searchMusic(query, page);
  522. } else if (type === "album") {
  523. return await searchAlbum(query, page);
  524. } else if (type === "sheet") {
  525. return await searchMusicSheet(query, page);
  526. } else if (type === "artist") {
  527. return await searchArtist(query, page);
  528. }
  529. },
  530. getMediaSource,
  531. getAlbumInfo,
  532. getMusicSheetInfo,
  533. getArtistWorks,
  534. getRecommendSheetTags,
  535. getRecommendSheetsByTag,
  536. getTopLists,
  537. getTopListDetail,
  538. };
  539. // getMediaSource(
  540. // {
  541. // id: 14925926,
  542. // artwork:
  543. // "https://assets.audiomack.com/1756162437/a16444eb63eb3b3244e31c39ee98066f10c510429bde00f0949b9a0796f5b859.jpeg",
  544. // duration: 227,
  545. // title: "夜曲",
  546. // artist: "周杰伦",
  547. // album: "十一月的萧邦",
  548. // },
  549. // "standard"
  550. // ).then(console.log);
  551. // searchMusicSheet("周杰伦", 1).then(e => console.log(JSON.stringify(e.data[0])));