fix(ximalaya): parse pay audio address (#15715)
* fix(ximalaya): parse pay audio address * chore: add types * fix: add json.parse to parseResponse option ---------
This commit is contained in:
parent
31e2a43a00
commit
d5ab30ea30
|
|
@ -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<TrackInfoResponse>(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<TrackInfoResponse>(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<RichIntro>(trackRichInfoApi);
|
||||
_desc = trackRichInfoResponse.richIntro;
|
||||
}
|
||||
if (!_desc) {
|
||||
_desc = `<a href="${link}">在网页中查看</a>`;
|
||||
|
|
@ -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 + '<br/>' + trackPayInfoResponse.data.msg;
|
||||
if (!trackInfo.isAuthorized) {
|
||||
return _item;
|
||||
}
|
||||
_item.playPathAacv224 = decryptUrl(trackInfo.playUrlList[0].url);
|
||||
return _item;
|
||||
});
|
||||
|
||||
if (data.playPathAacv224) {
|
||||
item.playPathAacv224 = data.playPathAacv224;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Reference in New Issue