From 31f578d694954e0af5d478027b999fd55ac6ce58 Mon Sep 17 00:00:00 2001 From: karasu Date: Wed, 17 Apr 2024 14:34:41 +0800 Subject: [PATCH] fix(route): qingting (#15251) * fix(route): qingting * fix: set default value for `qingtingId` * fix: typo --- lib/config.ts | 6 ++ lib/routes/qingting/podcast.ts | 101 ++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 39 deletions(-) diff --git a/lib/config.ts b/lib/config.ts index 0d55e685f9a76d..2f1793201e599f 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -230,6 +230,9 @@ export type Config = { pkubbs: { cookie?: string; }; + qingting: { + id?: string; + }; saraba1st: { cookie?: string; }; @@ -575,6 +578,9 @@ const calculateValue = () => { pkubbs: { cookie: envs.PKUBBS_COOKIE, }, + qingting: { + id: envs.QINGTING_ID, + }, saraba1st: { cookie: envs.SARABA1ST_COOKIE, }, diff --git a/lib/routes/qingting/podcast.ts b/lib/routes/qingting/podcast.ts index 9211f3e8c9df41..f71ad53b96b3aa 100644 --- a/lib/routes/qingting/podcast.ts +++ b/lib/routes/qingting/podcast.ts @@ -1,9 +1,12 @@ -import { Route } from '@/types'; +import type { DataItem, Route } from '@/types'; import cache from '@/utils/cache'; import crypto from 'crypto'; import got from '@/utils/got'; import timezone from '@/utils/timezone'; import { parseDate } from '@/utils/parse-date'; +import { config } from '@/config'; + +const qingtingId = config.qingting.id ?? ''; export const route: Route = { path: '/podcast/:id', @@ -11,12 +14,14 @@ export const route: Route = { example: '/qingting/podcast/293411', parameters: { id: '专辑id, 可在专辑页 URL 中找到' }, features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, supportPodcast: true, - supportScihub: false, + requireConfig: [ + { + name: 'QINGTING_ID', + optional: true, + description: '用户id, 部分专辑需要会员身份,用户id可以通过从网页端登录蜻蜓fm后使用开发者工具,在控制台中运行JSON.parse(localStorage.getItem("user")).qingting_id获取', + }, + ], }, radar: [ { @@ -29,21 +34,35 @@ export const route: Route = { description: `获取的播放 URL 有效期只有 1 天,需要开启播客 APP 的自动下载功能。`, }; +function getMediaUrl(channelId: string, mediaId: string) { + const path = `/audiostream/redirect/${channelId}/${mediaId}?access_token=&device_id=MOBILESITE&qingting_id=${qingtingId}&t=${Date.now()}`; + const sign = crypto.createHmac('md5', 'fpMn12&38f_2e').update(path).digest('hex').toString(); + return `https://audio.qingting.fm${path}&sign=${sign}`; +} + async function handler(ctx) { - const channelUrl = `https://i.qingting.fm/capi/v3/channel/${ctx.req.param('id')}`; - let response = await got({ + const channelId = ctx.req.param('id'); + + const channelUrl = `https://i.qingting.fm/capi/v3/channel/${channelId}`; + const response = await got({ method: 'get', url: channelUrl, headers: { Referer: 'https://www.qingting.fm/', }, }); + const title = response.data.data.title; const channel_img = response.data.data.thumbs['400_thumb']; const authors = response.data.data.podcasters.map((author) => author.nick_name).join(','); const desc = response.data.data.description; - const programUrl = `https://i.qingting.fm/capi/channel/${ctx.req.param('id')}/programs/${response.data.data.v}?curpage=1&pagesize=10&order=asc`; - response = await got({ + const programUrl = `https://i.qingting.fm/capi/channel/${channelId}/programs/${response.data.data.v}?curpage=1&pagesize=10&order=asc`; + + const { + data: { + data: { programs }, + }, + } = await got({ method: 'get', url: programUrl, headers: { @@ -51,45 +70,49 @@ async function handler(ctx) { }, }); + let isPaid = false; + + if (qingtingId) { + const { + data: { data: channelInfo }, + } = await got(`https://i.qingting.fm/capi/v3/channel/${channelId}?user_id=${qingtingId}`); + isPaid = channelInfo.user_relevance.sale_status === 'paid'; + } + const resultItems = await Promise.all( - response.data.data.programs.map((item) => - cache.tryGet(`qingting:podcast:${ctx.req.param('id')}:${item.id}`, async () => { - const link = `https://www.qingting.fm/channels/${ctx.req.param('id')}/programs/${item.id}/`; - - const path = `/audiostream/redirect/${ctx.req.param('id')}/${item.id}?access_token=&device_id=MOBILESITE&qingting_id=&t=${Date.now()}`; - const sign = crypto.createHmac('md5', 'fpMn12&38f_2e').update(path).digest('hex').toString(); - - const [detailRes, mediaRes] = await Promise.all([ - got({ - method: 'get', - url: link, - headers: { - Referer: 'https://www.qingting.fm/', - }, - }), - got({ - method: 'get', - url: `https://audio.qingting.fm${path}&sign=${sign}`, - headers: { - Referer: 'https://www.qingting.fm/', - }, - }), - ]); + programs.map(async (item) => { + const data = (await cache.tryGet(`qingting:podcast:${channelId}:${item.id}`, async () => { + const link = `https://www.qingting.fm/channels/${channelId}/programs/${item.id}/`; + + const detailRes = await got({ + method: 'get', + url: link, + headers: { + Referer: 'https://www.qingting.fm/', + }, + }); const detail = JSON.parse(detailRes.data.match(/},"program":(.*?),"plist":/)[1]); - return { + const rssItem = { title: item.title, link, itunes_item_image: item.cover, itunes_duration: item.duration, pubDate: timezone(parseDate(item.update_time), +8), description: detail.richtext, - enclosure_url: mediaRes.url, - enclosure_type: 'audio/x-m4a', }; - }) - ) + + return rssItem; + })) as DataItem; + + if (isPaid || item.isfree) { + data.enclosure_url = getMediaUrl(channelId, item.id); + data.enclosure_type = 'audio/x-m4a'; + } + + return data; + }) ); return { @@ -97,7 +120,7 @@ async function handler(ctx) { description: desc, itunes_author: authors, image: channel_img, - link: `https://www.qingting.fm/channels/${ctx.req.param('id')}`, + link: `https://www.qingting.fm/channels/${channelId}`, item: resultItems, }; }