From 4205e8475d68339bde525ccc773ba5cd0ae7752a Mon Sep 17 00:00:00 2001 From: Nicolas Lau Date: Sun, 26 May 2024 21:10:03 +0800 Subject: [PATCH] feat(route): support podwise.ai (#15670) * feature: podwise.ai * refactor: simplify item, remove some unnessary code * feat: add podcast feed info * chore: remove comment * refactor: use import instead of require * convert enclosure type --- lib/routes/podwise/collections.ts | 46 ++++++++++++ lib/routes/podwise/episodes.ts | 119 ++++++++++++++++++++++++++++++ lib/routes/podwise/namespace.ts | 9 +++ 3 files changed, 174 insertions(+) create mode 100644 lib/routes/podwise/collections.ts create mode 100644 lib/routes/podwise/episodes.ts create mode 100644 lib/routes/podwise/namespace.ts diff --git a/lib/routes/podwise/collections.ts b/lib/routes/podwise/collections.ts new file mode 100644 index 000000000..438ab1adb --- /dev/null +++ b/lib/routes/podwise/collections.ts @@ -0,0 +1,46 @@ +import { Route } from '@/types'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; // 统一使用的请求库 + +export const route: Route = { + path: '/explore', + categories: ['multimedia'], + example: '/podwise/explore', + radar: [ + { + source: ['podwise.ai', 'podwise.ai/explore'], + }, + ], + name: 'Collections', + maintainers: ['lyling'], + handler: async () => { + const link = 'https://podwise.ai/explore'; + const response = await ofetch(link); + const $ = load(response); + const content = $('#navigator').next(); + // header/[div => content]/footer, content p(2) + const collectinDescription = content.find('p').eq(1).text(); + + const items = content + .find('.group') + .toArray() + .map((item) => { + item = $(item); + const title = item.find('a').first().text(); + const link = item.find('a').first().attr('href'); + const description = item.find('p').first().text(); + return { + title, + link: `https://podwise.ai${link}`, + description, + }; + }); + + return { + title: $('title').text(), + description: collectinDescription, + item: items, + link, + }; + }, +}; diff --git a/lib/routes/podwise/episodes.ts b/lib/routes/podwise/episodes.ts new file mode 100644 index 000000000..6066427cd --- /dev/null +++ b/lib/routes/podwise/episodes.ts @@ -0,0 +1,119 @@ +import { Route } from '@/types'; +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; // 统一使用的请求库 +import cache from '@/utils/cache'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +dayjs.extend(duration); + +export const route: Route = { + path: '/explore/:type', + categories: ['multimedia'], + example: '/podwise/explore/latest', + parameters: { type: 'latest or all episodes.' }, + radar: [ + { + source: ['podwise.ai/explore/:type'], + }, + ], + name: 'Episodes', + maintainers: ['lyling'], + handler: async (ctx) => { + const type = ctx.req.param('type'); + const link = `https://podwise.ai/explore/${type}`; + const response = await ofetch(link); + const $ = load(response); + const content = $('#navigator').next(); + // header/[div => content]/footer, content>div(2)>h1 + const title = content.find('h1').first().text(); + const description = content.find('p').eq(1).text(); + + const list = content + .find('.group') + .toArray() + .map((item) => { + item = $(item); + const link = item.find('a').first().attr('href'); + const description = item.find('p').first().text(); + const pubDate = item.find('a').next().children('span').text(); + + return { + link: `https://podwise.ai${link}`, + description, + pubDate: timezone(parseDate(pubDate, 'DD MMM YYYY', 'en'), 8), + }; + }); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + const $ = load(response); + + item.description = $('summary').first().html(); + + // duration + const $cover = $('img[alt="Podcast cover"]').eq(1); + const $duration = $cover.next().find('span').eq(2); + + const nextData = JSON.parse( + $('script:contains("podName")') + .first() + .text() + .match(/self\.__next_f\.push\((.+)\)/)?.[1] ?? '' + ); + const podcastData = JSON.parse(nextData[1].slice(2))[1][3].children[3].episode; + + // rss feed + item.title = podcastData.title; + item.author = podcastData.podName; + + // podcast feed + item.itunes_item_image = podcastData.cover; + item.itunes_duration = parseDuration($duration.text()); + item.enclosure_url = podcastData.link; + // item.enclosure_length: nothing can convert to. + item.enclosure_type = toEnclosureType(podcastData.linkType); + + return item; + }) + ) + ); + + return { + title, + description, + link, + item: items, + }; + }, +}; + +function parseDuration(durationStr) { + const matches = durationStr.match(/(\d+h)?(\d+m)?/); + const hours = matches[1] ? Number.parseInt(matches[1]) : 0; + const minutes = matches[2] ? Number.parseInt(matches[2]) : 0; + + // 使用 dayjs 的 duration 创建持续时间对象 + return dayjs + .duration({ + hours, + minutes, + }) + .asSeconds(); +} + +function toEnclosureType(linkType: string): string { + switch (linkType) { + case 'mp3': + return 'audio/mpeg'; + case 'm4a': + return 'audio/x-m4a'; + case 'mp4': + return 'video/mp4'; + default: + return linkType; + } +} diff --git a/lib/routes/podwise/namespace.ts b/lib/routes/podwise/namespace.ts new file mode 100644 index 000000000..3f93faed6 --- /dev/null +++ b/lib/routes/podwise/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Podwise', + url: 'podwise.ai', + zh: { + name: 'Podwise', + }, +};