rsshub
Version:
Make RSS Great Again!
223 lines (199 loc) • 8.19 kB
text/typescript
import { Route } from '@/types';
import cache from '@/utils/cache';
import ofetch from '@/utils/ofetch';
import { getRandom16, decryptUrl } from './utils';
const baseUrl = 'https://www.ximalaya.com';
import { config } from '@/config';
import { parseDate } from '@/utils/parse-date';
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 = {
人文: 'Society & Culture',
历史: 'History',
头条: 'News',
娱乐: 'Leisure',
音乐: 'Music',
IT科技: 'Technology',
};
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) {
if (!str) {
return false;
}
str = str.toLowerCase();
if (str === 'true' || str === '1') {
return true;
}
for (const _s of validStrings) {
if (str === _s) {
return true;
}
}
return false;
}
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` 视为获取所有节目,填入其他则不获取。',
},
features: {
requireConfig: [
{
name: 'XIMALAYA_TOKEN',
description: '',
},
],
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: true,
supportScihub: false,
},
name: '专辑',
maintainers: ['lengthmin', 'jjeejj', 'prnake'],
handler,
description: `目前喜马拉雅的 API 只能一集一集的获取各节目上的 ShowNote,会极大的占用系统资源,所以默认为不获取节目的 ShowNote。
::: warning
专辑类型即 url 中的分类拼音,使用通用分类 \`album\` 通常是可行的,专辑 id 是跟在**分类拼音**后的那个 id, 不要输成某集的 id 了
**付费内容需要配置好已购买账户的 token 才能收听,详情见部署页面的配置模块**
:::`,
};
async function handler(ctx) {
const type = ctx.req.param('type'); // 专辑分类
const id = ctx.req.param('id'); // 专辑id
const shouldAll = judgeTrue(ctx.req.param('all'), 'all');
const shouldShowNote = judgeTrue(ctx.req.param('shownote'), 'shownote');
const pageSize = shouldAll ? 200 : 30;
const albumData = await getAlbumData(id);
const isPaid = albumData.isPaid;
const author = albumData.anchorName;
const albumTitle = albumData.albumTitle; // 专辑标题
const albumCover = 'https:' + albumData.cover;
const albumIntro = sanitizeHtml(albumData.detailRichIntro, { allowedTags: [], allowedAttributes: {} }); // 专辑介绍
const albumCategory = albumData.categoryTitle; // 专辑分类名字
// sort 为 1 时是降序
// const isAsc = albumData.store.AlbumDetailTrackList.sort === 0;
// 喜马拉雅的 API 的 query 参数 isAsc=0 时才是升序,不写就是降序。
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,
});
const maxPageId = trackInfoResponse.data.maxPageId; // 最大页数
let playList = trackInfoResponse.data.list;
if (shouldAll) {
const promises = [];
for (let i = 2; i <= maxPageId; i++) {
// string + number -> string
promises.push(
ofetch<TrackInfoResponse>(trackInfoApi + i, {
parseResponse: JSON.parse,
})
);
}
const responses = await Promise.all(promises);
for (const j of responses) {
playList = [...playList, ...j.data.list];
}
}
await Promise.all(
playList.map(async (item) => {
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 = item.intro;
}
return _desc;
});
})
);
const token = config.ximalaya.token;
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());
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 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 (!trackInfo.isAuthorized) {
return _item;
}
_item.playPathAacv224 = decryptUrl(trackInfo.playUrlList[0].url);
return _item;
});
if (data.playPathAacv224) {
item.playPathAacv224 = data.playPathAacv224;
}
if (data.desc) {
item.desc = data.desc;
}
})
);
}
const resultItems = playList.map((item) => {
const title = item.title;
const trackId = item.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;
let resultItem = {
title,
link,
description: item.desc || '',
pubDate,
itunes_item_image: itunesItemImage,
};
if (enclosureUrl) {
if (isPaid) {
resultItem.description = '[该内容需付费] ' + resultItem.description;
}
resultItem = {
...resultItem,
enclosure_url: enclosureUrl,
itunes_duration: duration,
enclosure_type: 'audio/x-m4a',
};
} else {
resultItem.description = '[该内容需付费] ' + resultItem.description;
}
return resultItem;
});
return {
title: albumTitle,
link: `${baseUrl}/${type}/${id}`,
description: albumIntro,
image: albumCover,
itunes_author: author,
itunes_category: categoryDict[albumCategory] || albumCategory,
item: resultItems,
};
}