index.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. import axios from "axios";
  2. import CryptoJs = require("crypto-js");
  3. import qs = require("qs");
  4. import bigInt = require("big-integer");
  5. import dayjs = require("dayjs");
  6. import cheerio = require("cheerio");
  7. /** 内部的函数 */
  8. function a() {
  9. var d,
  10. e,
  11. b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
  12. c = "";
  13. for (d = 0; 16 > d; d += 1)
  14. (e = Math.random() * b.length), (e = Math.floor(e)), (c += b.charAt(e));
  15. return c;
  16. }
  17. function b(a, b) {
  18. var c = CryptoJs.enc.Utf8.parse(b),
  19. d = CryptoJs.enc.Utf8.parse("0102030405060708"),
  20. e = CryptoJs.enc.Utf8.parse(a),
  21. f = CryptoJs.AES.encrypt(e, c, {
  22. iv: d,
  23. mode: CryptoJs.mode.CBC,
  24. });
  25. return f.toString();
  26. }
  27. function c(text) {
  28. text = text.split("").reverse().join("");
  29. const d = "010001";
  30. const e =
  31. "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7";
  32. const hexText = text
  33. .split("")
  34. .map((_) => _.charCodeAt(0).toString(16))
  35. .join("");
  36. const res = bigInt(hexText, 16)
  37. .modPow(bigInt(d, 16), bigInt(e, 16))
  38. .toString(16);
  39. return Array(256 - res.length)
  40. .fill("0")
  41. .join("")
  42. .concat(res);
  43. }
  44. function getParamsAndEnc(text) {
  45. const first = b(text, "0CoJUm6Qyw8W8jud");
  46. const rand = a();
  47. const params = b(first, rand);
  48. const encSecKey = c(rand);
  49. return {
  50. params,
  51. encSecKey,
  52. };
  53. }
  54. function formatMusicItem(_) {
  55. const album = _.al || _.album;
  56. return {
  57. id: _.id,
  58. artwork: album?.picUrl,
  59. title: _.name,
  60. artist: (_.ar || _.artists)[0].name,
  61. album: album?.name,
  62. url: `https://music.163.com/song/media/outer/url?id=${_.id}.mp3`,
  63. qualities: {
  64. low: {
  65. size: (_.l || {})?.size,
  66. },
  67. standard: {
  68. size: (_.m || {})?.size,
  69. },
  70. high: {
  71. size: (_.h || {})?.size,
  72. },
  73. super: {
  74. size: (_.sq || {})?.size,
  75. },
  76. },
  77. copyrightId: _?.copyrightId
  78. };
  79. }
  80. function formatAlbumItem(_) {
  81. return {
  82. id: _.id,
  83. artist: _.artist.name,
  84. title: _.name,
  85. artwork: _.picUrl,
  86. description: "",
  87. date: dayjs.unix(_.publishTime / 1000).format("YYYY-MM-DD"),
  88. };
  89. }
  90. function musicCanPlayFilter(_) {
  91. return (_.fee === 0 || _.fee === 8) && (!_.privilege || _.privilege?.st >= 0);
  92. }
  93. const pageSize = 30;
  94. async function searchBase(query, page, type) {
  95. const data = {
  96. s: query,
  97. limit: pageSize,
  98. type: type,
  99. offset: (page - 1) * pageSize,
  100. csrf_token: "",
  101. };
  102. const pae = getParamsAndEnc(JSON.stringify(data));
  103. const paeData = qs.stringify(pae);
  104. const headers = {
  105. authority: "music.163.com",
  106. "user-agent":
  107. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
  108. "content-type": "application/x-www-form-urlencoded",
  109. accept: "*/*",
  110. origin: "https://music.163.com",
  111. "sec-fetch-site": "same-origin",
  112. "sec-fetch-mode": "cors",
  113. "sec-fetch-dest": "empty",
  114. referer: "https://music.163.com/search/",
  115. "accept-language": "zh-CN,zh;q=0.9",
  116. };
  117. const res = (
  118. await axios({
  119. method: "post",
  120. url: "https://music.163.com/weapi/search/get",
  121. headers,
  122. data: paeData,
  123. })
  124. ).data;
  125. return res;
  126. }
  127. async function searchMusic(query, page) {
  128. const res = await searchBase(query, page, 1);
  129. const songs = res.result.songs
  130. .filter(musicCanPlayFilter)
  131. .map(formatMusicItem);
  132. return {
  133. isEnd: res.result.songCount <= page * pageSize,
  134. data: songs,
  135. };
  136. }
  137. async function searchAlbum(query, page) {
  138. const res = await searchBase(query, page, 10);
  139. const albums = res.result.albums.map(formatAlbumItem);
  140. return {
  141. isEnd: res.result.albumCount <= page * pageSize,
  142. data: albums,
  143. };
  144. }
  145. async function searchArtist(query, page) {
  146. const res = await searchBase(query, page, 100);
  147. const artists = res.result.artists.map((_) => ({
  148. name: _.name,
  149. id: _.id,
  150. avatar: _.img1v1Url,
  151. worksNum: _.albumSize,
  152. }));
  153. return {
  154. isEnd: res.result.artistCount <= page * pageSize,
  155. data: artists,
  156. };
  157. }
  158. async function searchMusicSheet(query, page) {
  159. const res = await searchBase(query, page, 1000);
  160. const playlists = res.result.playlists.map((_) => ({
  161. title: _.name,
  162. id: _.id,
  163. coverImg: _.coverImgUrl,
  164. artist: _.creator?.nickname,
  165. playCount: _.playCount,
  166. worksNum: _.trackCount,
  167. }));
  168. return {
  169. isEnd: res.result.playlistCount <= page * pageSize,
  170. data: playlists,
  171. };
  172. }
  173. async function searchLyric(query, page) {
  174. const res = await searchBase(query, page, 1006);
  175. const lyrics =
  176. res.result.songs?.map((it) => ({
  177. title: it.name,
  178. artist: it.ar?.map((_) => _.name).join(", "),
  179. id: it.id,
  180. artwork: it.al?.picUrl,
  181. album: it.al?.name,
  182. rawLrcTxt: it.lyrics?.join("\n"),
  183. })) ?? [];
  184. return {
  185. isEnd: res.result.songCount <= page * pageSize,
  186. data: lyrics,
  187. };
  188. }
  189. async function getArtistWorks(artistItem, page, type) {
  190. const data = {
  191. csrf_token: "",
  192. };
  193. const pae = getParamsAndEnc(JSON.stringify(data));
  194. const paeData = qs.stringify(pae);
  195. const headers = {
  196. authority: "music.163.com",
  197. "user-agent":
  198. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
  199. "content-type": "application/x-www-form-urlencoded",
  200. accept: "*/*",
  201. origin: "https://music.163.com",
  202. "sec-fetch-site": "same-origin",
  203. "sec-fetch-mode": "cors",
  204. "sec-fetch-dest": "empty",
  205. referer: "https://music.163.com/search/",
  206. "accept-language": "zh-CN,zh;q=0.9",
  207. };
  208. if (type === "music") {
  209. const res = (
  210. await axios({
  211. method: "post",
  212. url: `https://music.163.com/weapi/v1/artist/${artistItem.id}?csrf_token=`,
  213. headers,
  214. data: paeData,
  215. })
  216. ).data;
  217. return {
  218. isEnd: true,
  219. data: res.hotSongs.filter(musicCanPlayFilter).map(formatMusicItem),
  220. };
  221. } else if (type === "album") {
  222. const res = (
  223. await axios({
  224. method: "post",
  225. url: `https://music.163.com/weapi/artist/albums/${artistItem.id}?csrf_token=`,
  226. headers,
  227. data: paeData,
  228. })
  229. ).data;
  230. return {
  231. isEnd: true,
  232. data: res.hotAlbums.map(formatAlbumItem),
  233. };
  234. }
  235. }
  236. async function getTopListDetail(topListItem) {
  237. const musicList = await getSheetMusicById(topListItem.id);
  238. return {
  239. ...topListItem,
  240. musicList,
  241. };
  242. }
  243. async function getLyric(musicItem) {
  244. const headers = {
  245. Referer: "https://y.music.163.com/",
  246. Origin: "https://y.music.163.com/",
  247. authority: "music.163.com",
  248. "User-Agent":
  249. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
  250. "Content-Type": "application/x-www-form-urlencoded",
  251. };
  252. const data = { id: musicItem.id, lv: -1, tv: -1, csrf_token: "" };
  253. const pae = getParamsAndEnc(JSON.stringify(data));
  254. const paeData = qs.stringify(pae);
  255. const result = (
  256. await axios({
  257. method: "post",
  258. url: `https://interface.music.163.com/weapi/song/lyric?csrf_token=`,
  259. headers,
  260. data: paeData,
  261. })
  262. ).data;
  263. return {
  264. rawLrc: result.lrc.lyric,
  265. };
  266. }
  267. async function getAlbumInfo(albumItem) {
  268. const headers = {
  269. Referer: "https://y.music.163.com/",
  270. Origin: "https://y.music.163.com/",
  271. authority: "music.163.com",
  272. "User-Agent":
  273. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
  274. "Content-Type": "application/x-www-form-urlencoded",
  275. };
  276. const data = {
  277. resourceType: 3,
  278. resourceId: albumItem.id,
  279. limit: 15,
  280. csrf_token: "",
  281. };
  282. const pae = getParamsAndEnc(JSON.stringify(data));
  283. const paeData = qs.stringify(pae);
  284. const res = (
  285. await axios({
  286. method: "post",
  287. url: `https://interface.music.163.com/weapi/v1/album/${albumItem.id}?csrf_token=`,
  288. headers,
  289. data: paeData,
  290. })
  291. ).data;
  292. return {
  293. albumItem: { description: res.album.description },
  294. musicList: (res.songs || [])
  295. .filter(musicCanPlayFilter)
  296. .map(formatMusicItem),
  297. };
  298. }
  299. async function getValidMusicItems(trackIds) {
  300. const headers = {
  301. Referer: "https://y.music.163.com/",
  302. Origin: "https://y.music.163.com/",
  303. authority: "music.163.com",
  304. "User-Agent":
  305. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
  306. "Content-Type": "application/x-www-form-urlencoded",
  307. };
  308. try {
  309. const data = {
  310. csrf_token: "",
  311. ids: `[${trackIds.join(",")}]`,
  312. level: "standard",
  313. encodeType: "flac",
  314. };
  315. const pae = getParamsAndEnc(JSON.stringify(data));
  316. const urlencoded = qs.stringify(pae);
  317. const res = (
  318. await axios({
  319. method: "post",
  320. url: `https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=`,
  321. headers,
  322. data: urlencoded,
  323. })
  324. ).data;
  325. const validTrackIds = res.data.filter((_) => _.url).map((_) => _.id);
  326. const songDetails = (
  327. await axios.get(
  328. `https://music.163.com/api/song/detail/?id=${
  329. validTrackIds[0]
  330. }&ids=[${validTrackIds.join(",")}]`,
  331. { headers }
  332. )
  333. ).data;
  334. const validMusicItems = songDetails.songs
  335. .filter((_) => _.fee === 0 || _.fee === 8)
  336. .map(formatMusicItem);
  337. return validMusicItems;
  338. } catch (e) {
  339. return [];
  340. }
  341. }
  342. async function getSheetMusicById(id) {
  343. const headers = {
  344. Referer: "https://y.music.163.com/",
  345. Origin: "https://y.music.163.com/",
  346. authority: "music.163.com",
  347. "User-Agent":
  348. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
  349. };
  350. const sheetDetail = (
  351. await axios.get(
  352. `https://music.163.com/api/v3/playlist/detail?id=${id}&n=5000`,
  353. {
  354. headers,
  355. }
  356. )
  357. ).data;
  358. const trackIds = sheetDetail.playlist.trackIds.map((_) => _.id);
  359. let result = [];
  360. let idx = 0;
  361. while (idx * 200 < trackIds.length) {
  362. const res = await getValidMusicItems(
  363. trackIds.slice(idx * 200, (idx + 1) * 200)
  364. );
  365. result = result.concat(res);
  366. ++idx;
  367. }
  368. return result;
  369. }
  370. async function importMusicSheet(urlLike) {
  371. const matchResult = urlLike.match(
  372. /(?:https:\/\/y\.music\.163.com\/m\/playlist\?id=([0-9]+))|(?:https?:\/\/music\.163\.com\/playlist\/([0-9]+)\/.*)|(?:https?:\/\/music.163.com(?:\/#)?\/playlist\?id=(\d+))|(?:^\s*(\d+)\s*$)/
  373. );
  374. const id =
  375. matchResult[1] || matchResult[2] || matchResult[3] || matchResult[4];
  376. return getSheetMusicById(id);
  377. }
  378. async function getTopLists() {
  379. const res = await axios.get("https://music.163.com/discover/toplist", {
  380. headers: {
  381. referer: "https://music.163.com/",
  382. "user-agent":
  383. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.54",
  384. },
  385. });
  386. const $ = cheerio.load(res.data);
  387. const children = $(".n-minelst").children();
  388. const groups = [];
  389. let currentGroup: Record<string, any> = {};
  390. for (let c of children) {
  391. if (c.tagName == "h2") {
  392. if (currentGroup.title) {
  393. groups.push(currentGroup);
  394. }
  395. currentGroup = {};
  396. currentGroup.title = $(c).text();
  397. currentGroup.data = [];
  398. } else if (c.tagName === "ul") {
  399. let sections = $(c).children();
  400. currentGroup.data = sections
  401. .map((index, element) => {
  402. const ele = $(element);
  403. const id = ele.attr("data-res-id");
  404. const coverImg = ele.find("img").attr("src");
  405. const title = ele.find("p.name").text();
  406. const description = ele.find("p.s-fc4").text();
  407. return {
  408. id,
  409. coverImg,
  410. title,
  411. description,
  412. };
  413. })
  414. .toArray();
  415. }
  416. }
  417. if (currentGroup.title) {
  418. groups.push(currentGroup);
  419. }
  420. return groups;
  421. }
  422. const qualityLevels: Record<IMusic.IQualityKey, string> = {
  423. low: "",
  424. standard: "standard",
  425. high: "exhigh",
  426. super: "lossless",
  427. };
  428. /** 获取音乐源 */
  429. async function getMediaSource(
  430. musicItem: IMusic.IMusicItem,
  431. quality: IMusic.IQualityKey
  432. ) {
  433. if (quality !== "standard") {
  434. return;
  435. }
  436. return {
  437. url: `https://music.163.com/song/media/outer/url?id=${musicItem.id}.mp3`,
  438. };
  439. }
  440. const headers = {
  441. authority: "music.163.com",
  442. "user-agent":
  443. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
  444. "content-type": "application/x-www-form-urlencoded",
  445. accept: "*/*",
  446. origin: "https://music.163.com",
  447. "sec-fetch-site": "same-origin",
  448. "sec-fetch-mode": "cors",
  449. "sec-fetch-dest": "empty",
  450. referer: "https://music.163.com/",
  451. "accept-language": "zh-CN,zh;q=0.9",
  452. };
  453. /** 推荐歌单tag */
  454. async function getRecommendSheetTags() {
  455. const data = {
  456. csrf_token: "",
  457. };
  458. const pae = getParamsAndEnc(JSON.stringify(data));
  459. const paeData = qs.stringify(pae);
  460. const res = (
  461. await axios({
  462. method: "post",
  463. url: "https://music.163.com/weapi/playlist/catalogue",
  464. headers,
  465. data: paeData,
  466. })
  467. ).data;
  468. const cats = res.categories;
  469. const map = {};
  470. const catData = Object.entries(cats).map((_) => {
  471. const tagData = {
  472. title: _[1],
  473. data: [],
  474. };
  475. map[_[0]] = tagData;
  476. return tagData;
  477. });
  478. const pinned = [];
  479. res.sub.forEach((tag) => {
  480. const _tag = {
  481. id: tag.name,
  482. title: tag.name,
  483. };
  484. if (tag.hot) {
  485. pinned.push(_tag);
  486. }
  487. map[tag.category].data.push(_tag);
  488. });
  489. return {
  490. pinned,
  491. data: catData,
  492. };
  493. }
  494. async function getRecommendSheetsByTag(tag, page: number) {
  495. const pageSize = 20;
  496. const data = {
  497. cat: tag.id || "全部",
  498. order: "hot", // hot,new
  499. limit: pageSize,
  500. offset: (page - 1) * pageSize,
  501. total: true,
  502. csrf_token: "",
  503. };
  504. const pae = getParamsAndEnc(JSON.stringify(data));
  505. const paeData = qs.stringify(pae);
  506. const res = (
  507. await axios({
  508. method: "post",
  509. url: "https://music.163.com/weapi/playlist/list",
  510. headers,
  511. data: paeData,
  512. })
  513. ).data;
  514. const playLists = res.playlists.map((_) => ({
  515. id: _.id,
  516. artist: _.creator.nickname,
  517. title: _.name,
  518. artwork: _.coverImgUrl,
  519. playCount: _.playCount,
  520. createUserId: _.userId,
  521. createTime: _.createTime,
  522. description: _.description,
  523. }));
  524. return {
  525. isEnd: !(res.more === true),
  526. data: playLists,
  527. };
  528. }
  529. async function getMusicSheetInfo(sheet: IMusicSheet.IMusicSheetItem, page) {
  530. let trackIds = sheet._trackIds;
  531. if (!trackIds) {
  532. const id = sheet.id;
  533. const headers = {
  534. Referer: "https://y.music.163.com/",
  535. Origin: "https://y.music.163.com/",
  536. authority: "music.163.com",
  537. "User-Agent":
  538. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
  539. };
  540. const sheetDetail = (
  541. await axios.get(
  542. `https://music.163.com/api/v3/playlist/detail?id=${id}&n=5000`,
  543. {
  544. headers,
  545. }
  546. )
  547. ).data;
  548. trackIds = sheetDetail.playlist.trackIds.map((_) => _.id);
  549. }
  550. const pageSize = 40;
  551. const currentPageIds = trackIds.slice((page - 1) * pageSize, page * pageSize);
  552. const res = await getValidMusicItems(currentPageIds);
  553. let extra = {};
  554. if (page <= 1) {
  555. extra = {
  556. _trackIds: trackIds,
  557. };
  558. }
  559. return {
  560. isEnd: trackIds.length <= page * pageSize,
  561. musicList: res,
  562. ...extra,
  563. };
  564. }
  565. module.exports = {
  566. platform: "网易云",
  567. version: "0.2.1",
  568. appVersion: ">0.1.0-alpha.0",
  569. srcUrl:
  570. "https://gitee.com/maotoumao/MusicFreePlugins/raw/v0.1/dist/netease/index.js",
  571. cacheControl: "no-store",
  572. hints: {
  573. importMusicSheet: [
  574. "网易云移动端:APP点击分享,然后复制链接",
  575. "网易云H5/PC端:复制URL,或者直接输入歌单ID即可",
  576. "默认歌单无法导入,先新建一个空白歌单复制过去再导入新歌单即可",
  577. "导入过程中会过滤掉所有VIP/试听/收费音乐,导入时间和歌单大小有关,请耐心等待",
  578. ],
  579. },
  580. supportedSearchType: ["music", "album", "sheet", "artist", "lyric"],
  581. async search(query, page, type) {
  582. if (type === "music") {
  583. return await searchMusic(query, page);
  584. }
  585. if (type === "album") {
  586. return await searchAlbum(query, page);
  587. }
  588. if (type === "artist") {
  589. return await searchArtist(query, page);
  590. }
  591. if (type === "sheet") {
  592. return await searchMusicSheet(query, page);
  593. }
  594. if (type === "lyric") {
  595. return await searchLyric(query, page);
  596. }
  597. },
  598. getMediaSource,
  599. getAlbumInfo,
  600. getLyric,
  601. getArtistWorks,
  602. importMusicSheet,
  603. getTopLists,
  604. getTopListDetail,
  605. getRecommendSheetTags,
  606. getMusicSheetInfo,
  607. getRecommendSheetsByTag,
  608. };
  609. // async function getValidMusicItemsBak(trackIds: Array<number|string>) {
  610. // let idsData = {
  611. // c:
  612. // '[' +
  613. // trackIds
  614. // .slice(0, 5)
  615. // .map((item) => '{"id":' + item + '}')
  616. // .join(',') +
  617. // ']',
  618. // }
  619. // const pae = getParamsAndEnc(JSON.stringify(idsData));
  620. // const paeData = qs.stringify(pae);
  621. // const res = (
  622. // await axios({
  623. // method: "post",
  624. // url: "https://music.163.com/weapi/v3/song/detail",
  625. // headers,
  626. // data: paeData,
  627. // })
  628. // ).data;
  629. // return res.songs.filter(musicCanPlayFilter)
  630. // }
  631. // g();