mirror of
https://github.com/DIYgod/RSSHub.git
synced 2026-09-24 23:12:34 +08:00
fix(route/ximalaya): fix isPaid (#19586)
* wip * refactor(utils): comment out unused getUrl function and optimize buffer handling
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import { Route } from '@/types';
|
||||
import cache from '@/utils/cache';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { load } from 'cheerio';
|
||||
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';
|
||||
import { Album, RichIntro, TrackInfoResponse } from './types';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
// Find category from: https://help.apple.com/itc/podcasts_connect/?lang=en#/itc9267a2f12
|
||||
const categoryDict = {
|
||||
@@ -18,51 +18,16 @@ const categoryDict = {
|
||||
IT科技: 'Technology',
|
||||
};
|
||||
|
||||
// function parseState(state_string) {
|
||||
// const data = {};
|
||||
// new Function('window', state_string)(data);
|
||||
// return data.__INITIAL_STATE__ || {};
|
||||
// }
|
||||
/**
|
||||
*
|
||||
* @param {string} stateString 网页中的有一处 script 标签内有语句
|
||||
* `window.__INITIAL_STATE__ = {"xx": "xx"};`
|
||||
*/
|
||||
function parseStateSafe(stateString) {
|
||||
const prefix = 'window.__INITIAL_STATE__ = ';
|
||||
const suffix = ';';
|
||||
stateString = stateString.slice(prefix.length, stateString.length - suffix.length);
|
||||
try {
|
||||
return JSON.parse(stateString);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function getTextFromElement(element) {
|
||||
if (element.children && element.children.length > 0) {
|
||||
return element.children[0].data.trim();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function parseAlbumData(category, album_id) {
|
||||
// 解析网页版的 HTML 获取该专辑的信息
|
||||
// 这里的 {category} 并不重要,系统会准确的返回该 id 对应专辑的信息
|
||||
// 现在 {category} 必须要精确到大的类别
|
||||
// https://www.ximalaya.com/{category}/{album_id}
|
||||
const response = await ofetch(`${baseUrl}/${category}/${album_id}`);
|
||||
const $ = load(response);
|
||||
const stateElement = $('script')
|
||||
.toArray()
|
||||
.filter((element) => getTextFromElement(element).startsWith('window.__INITIAL_STATE__'));
|
||||
if (stateElement.length > 0) {
|
||||
const stateString = getTextFromElement(stateElement[0]);
|
||||
if (stateString) {
|
||||
return parseStateSafe(stateString);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
function getAlbumData(albumId) {
|
||||
return cache.tryGet(`ximalaya:albumInfo:${albumId}`, async () => {
|
||||
const response = await ofetch(`${baseUrl}/revision/album/v1/simple`, {
|
||||
query: {
|
||||
albumId,
|
||||
},
|
||||
parseResponse: JSON.parse,
|
||||
});
|
||||
return response.data.albumPageMainInfo as Album;
|
||||
});
|
||||
}
|
||||
|
||||
function judgeTrue(str, ...validStrings) {
|
||||
@@ -85,7 +50,11 @@ export const route: Route = {
|
||||
path: ['/:type/:id/:all/:shownote?'],
|
||||
categories: ['multimedia'],
|
||||
example: '/ximalaya/album/299146',
|
||||
parameters: { type: '专辑类型, 通常可以使用 `album`,可在对应专辑页面的 URL 中找到', id: '专辑 id, 可在对应专辑页面的 URL 中找到', all: '是否需要获取全部节目,填入 `1`、`true`、`all` 视为获取所有节目,填入其他则不获取。' },
|
||||
parameters: {
|
||||
type: '专辑类型, 通常可以使用 `album`,可在对应专辑页面的 URL 中找到',
|
||||
id: '专辑 id, 可在对应专辑页面的 URL 中找到',
|
||||
all: '是否需要获取全部节目,填入 `1`、`true`、`all` 视为获取所有节目,填入其他则不获取。',
|
||||
},
|
||||
features: {
|
||||
requireConfig: [
|
||||
{
|
||||
@@ -118,28 +87,22 @@ async function handler(ctx) {
|
||||
const shouldShowNote = judgeTrue(ctx.req.param('shownote'), 'shownote');
|
||||
const pageSize = shouldAll ? 200 : 30;
|
||||
|
||||
const albumData = await parseAlbumData(type, id);
|
||||
const albumData = await getAlbumData(id);
|
||||
|
||||
const { albumPageMainInfo, currentCategory, anchorInfo } = albumData?.store?.AlbumDetailPage || {};
|
||||
const isPaid = albumData.isPaid;
|
||||
|
||||
const isPaid = albumPageMainInfo.isPaid;
|
||||
const author = albumData.anchorName;
|
||||
|
||||
const authorInfo = anchorInfo; // 作者数据
|
||||
const author = authorInfo.nickName;
|
||||
const albumTitle = albumData.albumTitle; // 专辑标题
|
||||
const albumCover = 'https:' + albumData.cover;
|
||||
const albumIntro = sanitizeHtml(albumData.detailRichIntro, { allowedTags: [], allowedAttributes: {} }); // 专辑介绍
|
||||
|
||||
const albumInfo = albumPageMainInfo; // 专辑数据
|
||||
const albumTitle = albumInfo.albumTitle; // 专辑标题
|
||||
const albumCover = 'http:' + albumInfo.cover.split('!')[0];
|
||||
const albumIntro = albumInfo.detailRichIntro; // 专辑介绍
|
||||
|
||||
const categoryInfo = currentCategory;
|
||||
const albumCategory = categoryInfo?.categoryTitle || albumPageMainInfo.categoryTitle; // 专辑分类名字
|
||||
const albumUrl = '/' + (categoryInfo?.categoryPinyin || type) + '/' + id + '/'; // 没有 categoryPinyin 这个字段了,用 type 兼容一下
|
||||
const albumCategory = albumData.categoryTitle; // 专辑分类名字
|
||||
|
||||
// sort 为 1 时是降序
|
||||
// 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 trackInfoApi = `https://mobile.ximalaya.com/mobile/v1/album/track/?albumId=${id}&pageSize=${pageSize}&pageId=`;
|
||||
const trackInfoResponse = await ofetch<TrackInfoResponse>(trackInfoApi + '1', {
|
||||
parseResponse: JSON.parse,
|
||||
});
|
||||
@@ -165,16 +128,15 @@ async function handler(ctx) {
|
||||
|
||||
await Promise.all(
|
||||
playList.map(async (item) => {
|
||||
const link = baseUrl + albumUrl + item.trackId;
|
||||
item.desc = await cache.tryGet('ximalaya:' + shouldShowNote.toString() + 'trackRichInfo' + link, async () => {
|
||||
let _desc;
|
||||
item.desc = await cache.tryGet(`ximalaya:trackRichInfo:${item.trackId}:${shouldShowNote.toString()}`, async () => {
|
||||
let _desc: string = '';
|
||||
if (shouldShowNote) {
|
||||
const trackRichInfoApi = `https://mobile.ximalaya.com/mobile-track/richIntro?trackId=${item.trackId}`;
|
||||
const trackRichInfoResponse = await ofetch<RichIntro>(trackRichInfoApi);
|
||||
_desc = trackRichInfoResponse.richIntro;
|
||||
}
|
||||
if (!_desc) {
|
||||
_desc = `<a href="${link}">在网页中查看</a>`;
|
||||
_desc = item.intro;
|
||||
}
|
||||
return _desc;
|
||||
});
|
||||
@@ -182,8 +144,8 @@ async function handler(ctx) {
|
||||
);
|
||||
|
||||
const token = config.ximalaya.token;
|
||||
const randomToken = getRandom16(8) + '-' + getRandom16(4) + '-' + getRandom16(4) + '-' + getRandom16(4) + '-' + getRandom16(12);
|
||||
if (isPaid && token) {
|
||||
const randomToken = getRandom16(8) + '-' + getRandom16(4) + '-' + getRandom16(4) + '-' + getRandom16(4) + '-' + getRandom16(12);
|
||||
await Promise.all(
|
||||
playList.map(async (item) => {
|
||||
const timestamp = Math.floor(Date.now());
|
||||
@@ -217,8 +179,8 @@ async function handler(ctx) {
|
||||
const resultItems = playList.map((item) => {
|
||||
const title = item.title;
|
||||
const trackId = item.trackId;
|
||||
const itunesItemImage = item.coverLarge ?? albumCover;
|
||||
const link = baseUrl + albumUrl + trackId;
|
||||
const itunesItemImage = item.coverLarge.split('!')[0] ?? albumCover;
|
||||
const link = `${baseUrl}/sound/${trackId}`;
|
||||
const pubDate = parseDate(item.createdAt, 'x');
|
||||
const duration = item.duration; // 时间长度:单位(秒)
|
||||
const enclosureUrl = item.playPathAacv224 || item.playPathAacv164;
|
||||
@@ -250,7 +212,7 @@ async function handler(ctx) {
|
||||
|
||||
return {
|
||||
title: albumTitle,
|
||||
link: baseUrl + albumUrl,
|
||||
link: `${baseUrl}/${type}/${id}`,
|
||||
description: albumIntro,
|
||||
image: albumCover,
|
||||
itunes_author: author,
|
||||
|
||||
@@ -66,3 +66,51 @@ export interface RichIntro {
|
||||
ret: number;
|
||||
richIntro: string;
|
||||
}
|
||||
|
||||
interface SubscriptInfo {
|
||||
albumSubscriptValue: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface Album {
|
||||
anchorUid: number;
|
||||
albumStatus: number;
|
||||
showApplyFinishBtn: boolean;
|
||||
showEditBtn: boolean;
|
||||
showTrackManagerBtn: boolean;
|
||||
showInformBtn: boolean;
|
||||
cover: string;
|
||||
albumTitle: string;
|
||||
updateDate: string;
|
||||
createDate: string;
|
||||
playCount: number;
|
||||
isPaid: boolean;
|
||||
isFinished: number;
|
||||
isSubscribe: boolean;
|
||||
richIntro: string;
|
||||
shortIntro: string;
|
||||
detailRichIntro: string;
|
||||
isPublic: boolean;
|
||||
hasBuy: boolean;
|
||||
vipType: number;
|
||||
canCopyText: boolean;
|
||||
subscribeCount: number;
|
||||
sellingPoint: object;
|
||||
personalDescription: string;
|
||||
bigshotRecommend: string;
|
||||
outline: string;
|
||||
customTitle: string;
|
||||
produceTeam: string;
|
||||
recommendReason: string;
|
||||
subscriptInfo: SubscriptInfo;
|
||||
albumSubscript: number;
|
||||
tags: string[];
|
||||
categoryId: number;
|
||||
ximiVipFreeType: number;
|
||||
joinXimi: boolean;
|
||||
freeExpiredTime: number;
|
||||
categoryTitle: string;
|
||||
anchorName: string;
|
||||
albumSeoTitle: string;
|
||||
visibleStatus: number;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import crypto from 'node:crypto';
|
||||
/*
|
||||
const getParams = (ep) => {
|
||||
const a1 = 'xkt3a41psizxrh9l';
|
||||
const a = [
|
||||
@@ -119,6 +120,7 @@ const getUrl = (r) => {
|
||||
const url = `https://audiopay.cos.xmcdn.com/download/${r.apiVersion}/${getPath(r.seed, r.fileId)}?${paramsArray.join('&')}`;
|
||||
return url;
|
||||
};
|
||||
*/
|
||||
|
||||
const getRandom16 = (len) =>
|
||||
crypto
|
||||
@@ -139,28 +141,28 @@ const decryptUrl = (encryptedUrl) => {
|
||||
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) {
|
||||
const encryptedData = Buffer.from(encryptedUrl.replace('_', '/').replace('-', '+') + padding, 'base64');
|
||||
if (encryptedData.length < 16) {
|
||||
return encryptedUrl;
|
||||
}
|
||||
const data = encrypted_data.slice(0, -16);
|
||||
const iv = encrypted_data.slice(-16);
|
||||
const data = encryptedData.subarray(0, -16);
|
||||
const iv = encryptedData.subarray(-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);
|
||||
const block = decryptedData.subarray(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);
|
||||
const block = decryptedData.subarray(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 };
|
||||
export { /* getUrl, */ getRandom16, decryptUrl };
|
||||
|
||||
Reference in New Issue
Block a user