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
This commit is contained in:
Nicolas Lau
2024-05-26 21:10:03 +08:00
committed by GitHub
parent 53edbc7e62
commit 4205e8475d
3 changed files with 174 additions and 0 deletions
+46
View File
@@ -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,
};
},
};
+119
View File
@@ -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;
}
}
+9
View File
@@ -0,0 +1,9 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Podwise',
url: 'podwise.ai',
zh: {
name: 'Podwise',
},
};