UNPKG

rsshub

Version:
476 lines (471 loc) 9.12 kB
import "./esm-shims-CzJ_djXG.mjs"; import { t as config } from "./config-C37vj7VH.mjs"; import "./dist-BInvbO1W.mjs"; import "./logger-Czu8UMNd.mjs"; import { t as ofetch_default } from "./ofetch-BIyrKU3Y.mjs"; import { t as parseDate } from "./parse-date-BrP7mxXf.mjs"; import { t as cache_default } from "./cache-Bo__VnGm.mjs"; import crypto from "node:crypto"; import sanitizeHtml from "sanitize-html"; //#region lib/routes/ximalaya/utils.ts const getRandom16 = (len) => crypto.randomBytes(Math.ceil(len / 2)).toString("hex").slice(0, len); 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 encryptedData = Buffer.from(encryptedUrl.replace("_", "/").replace("-", "+") + padding, "base64"); if (encryptedData.length < 16) return encryptedUrl; 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.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.subarray(i, i + 32); for (const [j, element] of block.entries()) decryptedData[i + j] = element ^ a[j]; } return Buffer.from(decryptedData).toString("utf8"); }; //#endregion //#region lib/routes/ximalaya/album.ts const baseUrl = "https://www.ximalaya.com"; const categoryDict = { 人文: "Society & Culture", 历史: "History", 头条: "News", 娱乐: "Leisure", 音乐: "Music", IT科技: "Technology" }; function getAlbumData(albumId) { return cache_default.tryGet(`ximalaya:albumInfo:${albumId}`, async () => { return (await ofetch_default(`${baseUrl}/revision/album/v1/simple`, { query: { albumId }, parseResponse: JSON.parse })).data.albumPageMainInfo; }); } 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; } const 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"); 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; const trackInfoApi = `https://mobile.ximalaya.com/mobile/v1/album/track/?albumId=${id}&pageSize=${pageSize}&pageId=`; const trackInfoResponse = await ofetch_default(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++) promises.push(ofetch_default(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_default.tryGet(`ximalaya:trackRichInfo:${item.trackId}:${shouldShowNote.toString()}`, async () => { let _desc = ""; if (shouldShowNote) _desc = (await ofetch_default(`https://mobile.ximalaya.com/mobile-track/richIntro?trackId=${item.trackId}`)).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 trackPayInfoApi = `https://www.ximalaya.com/mobile-playpage/track/v3/baseInfo/${Math.floor(Date.now())}?device=www2&trackQualityLevel=2&trackId=${item.trackId}`; const data = await cache_default.tryGet("ximalaya:trackPayInfo" + trackPayInfoApi, async () => { const trackInfo = (await ofetch_default(trackPayInfoApi, { headers: { "user-agent": "ting_6.7.9(GM1900,Android29)", cookie: `1&_device=android&${randomToken}&6.7.9;1&_token=${token}` } })).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 }; } //#endregion export { route };