Skip to content

Commit

Permalink
Merge pull request #2476 from DIYgod/master
Browse files Browse the repository at this point in the history
[pull] master from diygod:master
  • Loading branch information
pull[bot] authored Apr 18, 2024
2 parents a63313f + a11404e commit e4ca8c0
Show file tree
Hide file tree
Showing 13 changed files with 1,337 additions and 372 deletions.
6 changes: 6 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ export type Config = {
pkubbs: {
cookie?: string;
};
qingting: {
id?: string;
};
saraba1st: {
cookie?: string;
};
Expand Down Expand Up @@ -575,6 +578,9 @@ const calculateValue = () => {
pkubbs: {
cookie: envs.PKUBBS_COOKIE,
},
qingting: {
id: envs.QINGTING_ID,
},
saraba1st: {
cookie: envs.SARABA1ST_COOKIE,
},
Expand Down
4 changes: 2 additions & 2 deletions lib/errors/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ describe('route throws an error', () => {
expect(value).toBe('9');
break;
case 'Hot Routes:':
expect(value).toBe('6 /test/:id<br>');
expect(value).toBe('6 /test/:id/:params?<br>');
break;
case 'Hot Paths:':
expect(value).toBe('2 /test/error<br>2 /test/slow<br>1 /test/httperror<br>1 /test/config-not-found-error<br>1 /test/invalid-parameter-error<br>1 /thisDoesNotExist<br>1 /<br>');
break;
case 'Hot Error Routes:':
expect(value).toBe('5 /test/:id<br>');
expect(value).toBe('5 /test/:id/:params?<br>');
break;
case 'Hot Error Paths:':
expect(value).toBe('2 /test/error<br>1 /test/httperror<br>1 /test/slow<br>1 /test/config-not-found-error<br>1 /test/invalid-parameter-error<br>1 /thisDoesNotExist<br>');
Expand Down
87 changes: 87 additions & 0 deletions lib/routes/0x80/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Route } from '@/types';

import cache from '@/utils/cache';
import got from '@/utils/got';
import { load } from 'cheerio';
import { parseDate } from '@/utils/parse-date';

export const route: Route = {
path: '/blog',
categories: ['blog'],
example: '/0x80/blog',
url: '0x80.pl/notesen.html',
name: 'Articles',
maintainers: ['xnum'],
handler,
};

function extractDateFromURL(url: string) {
const regex = /\d{4}-\d{2}-\d{2}/;
const match = url.match(regex);

return match ? match[0] : null;
}

async function handler() {
// The TLS cert is invalid, we are limited to use HTTP unfortunately.
const baseUrl = 'http://0x80.pl/';
const targetUrl = `${baseUrl}notesen.html`;

const response = await got({
method: 'get',
url: targetUrl,
});

const $ = load(response.data);

const alist = $('a.reference.external');

const list = alist
.toArray()
.map((item) => {
item = $(item);

const link = item.attr('href') || '';
const title = item.text() || '';
const pubDate = extractDateFromURL(link);

return {
title,
link,
pubDate,
category: 'Uncategoried',
};
})
.filter((item) => item.link.startsWith('notesen'));

const items = await Promise.all(
list.map((item) =>
cache.tryGet(item.link, async () => {
const articleUrl = `${baseUrl}${item.link}`;
const response = await got({
method: 'get',
url: articleUrl,
});

const $ = load(response.data);

const author = $('tr.author.field td.field-body').text();
const articlePubDate = $('tr.added-on.field td.field-body').text();

item.author = author;
// Some articles might be missing the added-on field.
// As a safeguard, if the date from url is null, fallbacks to the article one.
item.pubDate = parseDate(item.pubDate || articlePubDate);
item.description = $('div.document').first().html();

return item;
})
)
);

return {
title: '0x80.pl articles',
link: targetUrl,
item: items,
};
}
7 changes: 7 additions & 0 deletions lib/routes/0x80/namespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Namespace } from '@/types';

export const namespace: Namespace = {
name: 'Wojciech Muła',
url: '0x80.pl',
description: '',
};
64 changes: 64 additions & 0 deletions lib/routes/apple/podcast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Route } from '@/types';
import got from '@/utils/got';
import { load } from 'cheerio';
import { parseDate } from '@/utils/parse-date';

export const route: Route = {
path: '/podcast/:id',
categories: ['multimedia'],
example: '/apple/podcast/id1559695855',
parameters: { id: '播客id,可以在 Apple 播客app 内分享的播客的 URL 中找到' },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['podcasts.apple.com/cn/podcast/:id'],
},
],
name: '播客',
maintainers: ['Acring'],
handler,
url: 'https://www.apple.com.cn/apple-podcasts/',
};

async function handler(ctx) {
const link = `https://podcasts.apple.com/cn/podcast/${ctx.req.param('id')}`;
const response = await got({
method: 'get',
url: link,
});

const $ = load(response.data);

const page_data = JSON.parse($('#shoebox-media-api-cache-amp-podcasts').text());

const data = JSON.parse(page_data[Object.keys(page_data)[0]]).d[0];
const attributes = data.attributes;

const episodes = data.relationships.episodes.data.map((item) => {
const attr = item.attributes;
return {
title: attr.name,
enclosure_url: attr.assetUrl,
itunes_duration: attr.durationInMilliseconds / 1000,
enclosure_type: 'audio/mp4',
link: attr.url,
pubDate: parseDate(attr.releaseDateTime),
description: attr.description.standard.replaceAll('\n', '<br>'),
};
});

return {
title: attributes.name,
link: attributes.url,
itunes_author: attributes.artistName,
item: episodes,
description: attributes.description.standard,
};
}
2 changes: 1 addition & 1 deletion lib/routes/bilibili/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const getCookie = () => {
await page.goto('https://space.bilibili.com/1/dynamic');
const cookieString = await waitForRequest;
logger.debug(`Got bilibili cookie: ${cookieString}`);

await browser.close();
return cookieString;
});
};
Expand Down
100 changes: 61 additions & 39 deletions lib/routes/qingting/podcast.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
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',
categories: ['multimedia'],
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: [
{
Expand All @@ -29,75 +34,92 @@ 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: {
Referer: 'https://www.qingting.fm/',
},
});

const {
data: { data: channelInfo },
} = await got(`https://i.qingting.fm/capi/v3/channel/${channelId}?user_id=${qingtingId}`);

const isCharged = channelInfo.purchase?.item_type !== 0;

const 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 (!isCharged || isPaid || item.isfree) {
data.enclosure_url = getMediaUrl(channelId, item.id);
data.enclosure_type = 'audio/x-m4a';
}

return data;
})
);

return {
title: `${title} - 蜻蜓FM`,
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,
};
}
12 changes: 11 additions & 1 deletion lib/routes/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { config } from '@/config';
import got from '@/utils/got';
import wait from '@/utils/wait';
import cache from '@/utils/cache';
import { fetchArticle } from '@/utils/wechat-mp';
import ConfigNotFoundError from '@/errors/types/config-not-found';
import InvalidParameterError from '@/errors/types/invalid-parameter';

let cacheIndex = 0;

export const route: Route = {
path: '/:id',
path: '/:id/:params?',
name: 'Unknown',
maintainers: ['DIYgod', 'NeverBehave'],
handler,
Expand Down Expand Up @@ -384,6 +385,15 @@ async function handler(ctx) {
];
}

if (ctx.req.param('id') === 'wechat-mp') {
const params = ctx.req.param('params');
if (!params) {
throw new InvalidParameterError('Invalid parameter');
}
const mpUrl = 'https:/mp.weixin.qq.com/s' + (params.includes('&') ? '?' : '/') + params;
item = [await fetchArticle(mpUrl)];
}

return {
title: `Test ${ctx.req.param('id')}`,
itunes_author: ctx.req.param('id') === 'enclosure' ? 'DIYgod' : null,
Expand Down
Loading

0 comments on commit e4ca8c0

Please sign in to comment.