From d5ab30ea30c281130db4f20c5ec31c2012c53929 Mon Sep 17 00:00:00 2001 From: Terry Chan Date: Sun, 26 May 2024 08:39:47 -0700 Subject: [PATCH] fix(ximalaya): parse pay audio address (#15715) * fix(ximalaya): parse pay audio address * chore: add types * fix: add json.parse to parseResponse option --------- --- lib/routes/ximalaya/album.ts | 44 +++++++++++++---------- lib/routes/ximalaya/types.ts | 68 ++++++++++++++++++++++++++++++++++++ lib/routes/ximalaya/utils.ts | 39 ++++++++++++++++++++- 3 files changed, 132 insertions(+), 19 deletions(-) create mode 100644 lib/routes/ximalaya/types.ts diff --git a/lib/routes/ximalaya/album.ts b/lib/routes/ximalaya/album.ts index ffde92729..d5a2a9438 100644 --- a/lib/routes/ximalaya/album.ts +++ b/lib/routes/ximalaya/album.ts @@ -1,11 +1,12 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; -import { getUrl, getRandom16 } from './utils'; +import { getRandom16, decryptUrl } from './utils'; const baseUrl = 'https://www.ximalaya.com'; import { config } from '@/config'; import { parseDate } from '@/utils/parse-date'; +import { RichIntro, TrackInfoResponse } from './types'; // Find category from: https://help.apple.com/itc/podcasts_connect/?lang=en#/itc9267a2f12 const categoryDict = { @@ -50,9 +51,8 @@ async function parseAlbumData(category, album_id) { // 这里的 {category} 并不重要,系统会准确的返回该 id 对应专辑的信息 // 现在 {category} 必须要精确到大的类别 // https://www.ximalaya.com/{category}/{album_id} - const response = await got(`${baseUrl}/${category}/${album_id}`); - const data = response.body; - const $ = load(data); + const response = await ofetch(`${baseUrl}/${category}/${album_id}`); + const $ = load(response); const stateElement = $('script') .toArray() .filter((element) => getTextFromElement(element).startsWith('window.__INITIAL_STATE__')); @@ -140,20 +140,26 @@ async function handler(ctx) { // const isAsc = albumData.store.AlbumDetailTrackList.sort === 0; // 喜马拉雅的 API 的 query 参数 isAsc=0 时才是升序,不写就是降序。 const trackInfoApi = `http://mobile.ximalaya.com/mobile/v1/album/track/?albumId=${id}&pageSize=${pageSize}&pageId=`; - const trackInfoResponse = await got(trackInfoApi + '1'); - const maxPageId = trackInfoResponse.data.data.maxPageId; // 最大页数 + const trackInfoResponse = await ofetch(trackInfoApi + '1', { + parseResponse: JSON.parse, + }); + const maxPageId = trackInfoResponse.data.maxPageId; // 最大页数 - let playList = trackInfoResponse.data.data.list; + let playList = trackInfoResponse.data.list; if (shouldAll) { const promises = []; for (let i = 2; i <= maxPageId; i++) { // string + number -> string - promises.push(got(trackInfoApi + i)); + promises.push( + ofetch(trackInfoApi + i, { + parseResponse: JSON.parse, + }) + ); } const responses = await Promise.all(promises); for (const j of responses) { - playList = [...playList, ...j.data.data.list]; + playList = [...playList, ...j.data.list]; } } @@ -164,8 +170,8 @@ async function handler(ctx) { let _desc; if (shouldShowNote) { const trackRichInfoApi = `https://mobile.ximalaya.com/mobile-track/richIntro?trackId=${item.trackId}`; - const trackRichInfoResponse = await got(trackRichInfoApi); - _desc = trackRichInfoResponse.data.richIntro; + const trackRichInfoResponse = await ofetch(trackRichInfoApi); + _desc = trackRichInfoResponse.richIntro; } if (!_desc) { _desc = `在网页中查看`; @@ -180,22 +186,24 @@ async function handler(ctx) { if (isPaid && token) { await Promise.all( playList.map(async (item) => { - const trackPayInfoApi = `https://mpay.ximalaya.com/mobile/track/pay/${item.trackId}/?device=pc`; + const timestamp = Math.floor(Date.now()); + const trackPayInfoApi = `https://www.ximalaya.com/mobile-playpage/track/v3/baseInfo/${timestamp}?device=www2&trackQualityLevel=2&trackId=${item.trackId}`; const data = await cache.tryGet('ximalaya:trackPayInfo' + trackPayInfoApi, async () => { - const trackPayInfoResponse = await got(trackPayInfoApi, { + const trackPayInfoResponse = await ofetch(trackPayInfoApi, { headers: { 'user-agent': 'ting_6.7.9(GM1900,Android29)', cookie: `1&_device=android&${randomToken}&6.7.9;1&_token=${token}`, }, }); + const trackInfo = trackPayInfoResponse.trackInfo; const _item = {}; - if (trackPayInfoResponse.data.ep) { - _item.playPathAacv224 = getUrl(trackPayInfoResponse.data); - } else if (trackPayInfoResponse.data.msg) { - _item.desc = item.desc + '
' + trackPayInfoResponse.data.msg; + if (!trackInfo.isAuthorized) { + return _item; } + _item.playPathAacv224 = decryptUrl(trackInfo.playUrlList[0].url); return _item; }); + if (data.playPathAacv224) { item.playPathAacv224 = data.playPathAacv224; } diff --git a/lib/routes/ximalaya/types.ts b/lib/routes/ximalaya/types.ts new file mode 100644 index 000000000..c6606d7b7 --- /dev/null +++ b/lib/routes/ximalaya/types.ts @@ -0,0 +1,68 @@ +interface Track { + trackId: number; + trackRecordId: number; + uid: number; + playUrl64: string; + playUrl32: string; + playPathAacv164: string; + playPathAacv224: string; + playPathHq: string; + title: string; + duration: number; + albumId: number; + albumTitle: string; + isPaid: boolean; + isFree: boolean; + isVideo: boolean; + isDraft: boolean; + isAuthorized: boolean; + priceTypeId: number; + sampleDuration: number; + priceTypeEnum: number; + type: number; + relatedId: number; + orderNo: number; + isHoldCopyright: boolean; + vipFirstStatus: number; + paidType: number; + isSample: boolean; + ximiFirstStatus: number; + coverLarge: string; + permissionSource: string; + playtimes: number; + labelType: number; + processState: number; + createdAt: number; + coverSmall: string; + coverMiddle: string; + nickname: string; + smallLogo: string; + userSource: number; + opType: number; + isPublic: boolean; + likes: number; + comments: number; + shares: number; + status: number; + videoCover: string; + intro: string; + labelList: string[]; + isTrailer: number; +} + +export interface TrackInfoResponse { + msg: string; + ret: number; + data: { + list: Track[]; + pageId: number; + pageSize: number; + maxPageId: number; + totalCount: number; + }; +} + +export interface RichIntro { + ret: number; + richIntro: string; +} diff --git a/lib/routes/ximalaya/utils.ts b/lib/routes/ximalaya/utils.ts index c5b01864f..142e694db 100644 --- a/lib/routes/ximalaya/utils.ts +++ b/lib/routes/ximalaya/utils.ts @@ -126,4 +126,41 @@ const getRandom16 = (len) => .toString('hex') .slice(0, len); -export { getUrl, getRandom16 }; +const decryptUrl = (encryptedUrl) => { + const o = [ + 183, 174, 108, 16, 131, 159, 250, 5, 239, 110, 193, 202, 153, 137, 251, 176, 119, 150, 47, 204, 97, 237, 1, 71, 177, 42, 88, 218, 166, 82, 87, 94, 14, 195, 69, 127, 215, 240, 225, 197, 238, 142, 123, 44, 219, 50, 190, 29, + 181, 186, 169, 98, 139, 185, 152, 13, 141, 76, 6, 157, 200, 132, 182, 49, 20, 116, 136, 43, 155, 194, 101, 231, 162, 242, 151, 213, 53, 60, 26, 134, 211, 56, 28, 223, 107, 161, 199, 15, 229, 61, 96, 41, 66, 158, 254, 21, 165, + 253, 103, 89, 3, 168, 40, 246, 81, 95, 58, 31, 172, 78, 99, 45, 148, 187, 222, 124, 55, 203, 235, 64, 68, 149, 180, 35, 113, 207, 118, 111, 91, 38, 247, 214, 7, 212, 209, 189, 241, 18, 115, 173, 25, 236, 121, 249, 75, 57, + 216, 10, 175, 112, 234, 164, 70, 206, 198, 255, 140, 230, 12, 32, 83, 46, 245, 0, 62, 227, 72, 191, 156, 138, 248, 114, 220, 90, 84, 170, 128, 19, 24, 122, 146, 80, 39, 37, 8, 34, 22, 11, 93, 130, 63, 154, 244, 160, 144, 79, + 23, 133, 92, 54, 102, 210, 65, 67, 27, 196, 201, 106, 143, 52, 74, 100, 217, 179, 48, 233, 126, 117, 184, 226, 85, 171, 167, 86, 2, 147, 17, 135, 228, 252, 105, 30, 192, 129, 178, 120, 36, 145, 51, 163, 77, 205, 73, 4, 188, + 125, 232, 33, 243, 109, 224, 104, 208, 221, 59, 9, + ]; + + const a = [204, 53, 135, 197, 39, 73, 58, 160, 79, 24, 12, 83, 180, 250, 101, 60, 206, 30, 10, 227, 36, 95, 161, 16, 135, 150, 235, 116, 242, 116, 165, 171]; + + const padding = '='.repeat((4 - (encryptedUrl.length % 4)) % 4); + const encrypted_data = Buffer.from(encryptedUrl.replace('_', '/').replace('-', '+') + padding, 'base64'); + if (encrypted_data.length < 16) { + return encryptedUrl; + } + const data = encrypted_data.slice(0, -16); + const iv = encrypted_data.slice(-16); + const decryptedData = new Uint8Array(data); + for (let i = 0; i < decryptedData.length; i++) { + decryptedData[i] = o[decryptedData[i]]; + } + for (let i = 0; i < decryptedData.length; i += 16) { + const block = decryptedData.slice(i, i + 16); + for (const [j, element] of block.entries()) { + decryptedData[i + j] = element ^ iv[j]; + } + } + for (let i = 0; i < decryptedData.length; i += 32) { + const block = decryptedData.slice(i, i + 32); + for (const [j, element] of block.entries()) { + decryptedData[i + j] = element ^ a[j]; + } + } + return Buffer.from(decryptedData).toString('utf8'); +}; +export { getUrl, getRandom16, decryptUrl };