Skip to content

Commit 59ae46b

Browse files
authored
Merge pull request #1 from DIYgod/master
[pull] master from diygod:master
2 parents 3e5ccab + e21e74a commit 59ae46b

5 files changed

Lines changed: 252 additions & 5 deletions

File tree

.github/dependabot.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,23 @@ updates:
2323
- dependency-name: jsdom
2424
versions: ['>=27.0.1']
2525
groups:
26+
cloudflare:
27+
patterns:
28+
- '@cloudflare/*'
29+
- 'wrangler'
2630
eslint:
2731
patterns:
2832
- 'eslint'
2933
- '@eslint/*'
3034
opentelemetry:
3135
patterns:
3236
- '@opentelemetry/*'
37+
oxc:
38+
patterns:
39+
- '@oxlint/*'
40+
- 'oxfmt'
41+
- 'oxlint'
42+
- 'oxlint-tsgolint'
3343
typescript-eslint:
3444
patterns:
3545
- '@typescript-eslint/*'

.github/workflows/lint.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ jobs:
4242
with:
4343
sarif_file: eslint-results.sarif
4444
wait-for-processing: true
45+
- name: Upload Artifact
46+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
47+
with:
48+
path: eslint-results.sarif
4549

4650
# https://github.com/amannn/action-semantic-pull-request
4751
title-lint:

lib/routes/bbc/utils.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,20 +355,26 @@ export const extractInitialData = ($: CheerioAPI): any => {
355355
const initialDataText = JSON.parse(
356356
$('script:contains("window.__INITIAL_DATA__")')
357357
.text()
358-
.match(/window\.__INITIAL_DATA__\s*=\s*(.*);/)?.[1] ?? '{}'
358+
.match(/window\.__INITIAL_DATA__\s*=\s*(.*);/)?.[1] ?? '"{}"'
359359
);
360360

361361
return JSON.parse(initialDataText);
362362
};
363363

364364
const extractArticleWithInitialData = ($: CheerioAPI, item) => {
365-
if (item.link.includes('/live/') || item.link.includes('/videos/') || item.link.includes('/extra/')) {
365+
if (item.link.includes('/live/') || item.link.includes('/videos/') || item.link.includes('/extra/') || item.link.includes('/sounds/play/')) {
366366
return {
367367
description: item.content,
368368
};
369369
}
370370

371371
const initialData = extractInitialData($);
372+
if (!initialData || !initialData.data) {
373+
return {
374+
description: item.content,
375+
};
376+
}
377+
372378
const article = Object.values(initialData.data).find((d) => d.name === 'article')?.data;
373379
const topics = Array.isArray(article?.topics) ? article.topics : [];
374380
const blocks = article?.content?.model?.blocks;

lib/routes/douban/tv/coming.ts

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import { webcrypto } from 'node:crypto';
2+
3+
import { config } from '@/config';
4+
import type { Route } from '@/types';
5+
import cache from '@/utils/cache';
6+
import got from '@/utils/got';
7+
import { parseDate } from '@/utils/parse-date';
8+
9+
const apiUrl = 'https://frodo.douban.com/api/v2/tv/coming_soon';
10+
const apiKey = '0dad551ec0f84ed02907ff5c42e8ec70';
11+
const apiSecret = 'bf7dddc7c9cfe6f7';
12+
const apiClientUa = 'api-client/1 com.douban.frodo/7.22.0.beta9(231) Android/23 product/Mate 40 vendor/HUAWEI model/Mate 40 brand/HUAWEI rom/android network/wifi platform/AndroidPad';
13+
14+
type ComingSoonSubject = {
15+
id: string;
16+
title: string;
17+
url?: string;
18+
sharing_url?: string;
19+
pubdate?: string[];
20+
wish_count?: number | string;
21+
card_subtitle?: string;
22+
intro?: string;
23+
genres?: string[];
24+
cover_url?: string;
25+
pic?: {
26+
large?: string;
27+
};
28+
};
29+
30+
type ComingSoonResponse = {
31+
count?: number;
32+
start?: number;
33+
total?: number;
34+
subjects?: ComingSoonSubject[];
35+
msg?: string;
36+
message?: string;
37+
reason?: string;
38+
};
39+
40+
const signRequest = async (url: string, ts: string, method = 'GET'): Promise<string> => {
41+
const urlPath = new URL(url).pathname;
42+
const rawSign = `${method.toUpperCase()}&${encodeURIComponent(urlPath)}&${ts}`;
43+
const keyData = new TextEncoder().encode(apiSecret);
44+
const messageData = new TextEncoder().encode(rawSign);
45+
const key = await webcrypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
46+
const signature = await webcrypto.subtle.sign('HMAC', key, messageData);
47+
return Buffer.from(signature).toString('base64');
48+
};
49+
50+
const getPubDateText = (pubdate?: string[]): string | undefined => pubdate?.[0];
51+
52+
const getPubDate = (pubdate?: string[]): Date | undefined => {
53+
const pubDateText = getPubDateText(pubdate);
54+
if (!pubDateText) {
55+
return undefined;
56+
}
57+
58+
const datePart = pubDateText.split('(')[0];
59+
return parseDate(datePart);
60+
};
61+
62+
const getSortTimestamp = (pubdate?: string[]): number => {
63+
const pubDateText = getPubDateText(pubdate);
64+
if (!pubDateText) {
65+
return Number.POSITIVE_INFINITY;
66+
}
67+
68+
const datePart = pubDateText.split('(')[0].trim();
69+
const match = /^(\d{4})(?:-(\d{1,2}))?(?:-(\d{1,2}))?/.exec(datePart);
70+
if (!match) {
71+
return Number.POSITIVE_INFINITY;
72+
}
73+
74+
const year = Number.parseInt(match[1], 10);
75+
const month = match[2] ? Number.parseInt(match[2], 10) : 1;
76+
const day = match[3] ? Number.parseInt(match[3], 10) : 1;
77+
const timestamp = Date.UTC(year, month - 1, day);
78+
return Number.isNaN(timestamp) ? Number.POSITIVE_INFINITY : timestamp;
79+
};
80+
81+
const getWishCount = (wishCount?: number | string): number => {
82+
if (typeof wishCount === 'number') {
83+
return wishCount;
84+
}
85+
if (typeof wishCount === 'string') {
86+
const parsed = Number.parseInt(wishCount, 10);
87+
return Number.isNaN(parsed) ? 0 : parsed;
88+
}
89+
return 0;
90+
};
91+
92+
const renderDescription = (subject: { intro?: string; wish_count?: number | string }): string => {
93+
const wishCount = getWishCount(subject.wish_count);
94+
const wishCountText = wishCount > 0 ? `想看人数:${wishCount}` : '';
95+
const introText = subject.intro ?? '';
96+
if (wishCountText && introText) {
97+
return `${wishCountText}${introText}`;
98+
}
99+
return wishCountText || introText;
100+
};
101+
102+
const buildFetchError = (error: unknown): Error => {
103+
const status = (error as { response?: { status?: number } })?.response?.status;
104+
if (status === 429) {
105+
return new Error('Douban 请求过于频繁(429)。请稍后重试,或降低请求频率。');
106+
}
107+
if (status === 403) {
108+
return new Error('Douban 拒绝访问(403),可能触发反爬策略。请稍后重试。');
109+
}
110+
return new Error('Douban 数据请求失败,可能触发反爬或限频,请稍后重试。');
111+
};
112+
113+
export const route: Route = {
114+
path: '/tv/coming/:sortBy?/:count?',
115+
categories: ['social-media'],
116+
example: '/douban/tv/coming',
117+
parameters: {
118+
sortBy: '排序方式,可选,支持 `hot` 或 `time`,默认 `hot`',
119+
count: '请求上游返回数量,可选,正整数,默认 `10`',
120+
},
121+
features: {
122+
requireConfig: false,
123+
requirePuppeteer: false,
124+
antiCrawler: false,
125+
supportBT: false,
126+
supportPodcast: false,
127+
supportScihub: false,
128+
},
129+
name: '即将播出的剧集',
130+
maintainers: ['honue'],
131+
handler,
132+
description: `| 路径参数 | 含义 | 接受的值 | 默认值 |
133+
| -------- | ---------------- | -------- | ------ |
134+
| sortBy | 排序方式 | hot/time | hot |
135+
| count | 请求上游返回数量 | 正整数 | 10 |
136+
137+
用例:\`/douban/tv/coming/hot/10\`
138+
139+
::: tip
140+
服务端请求固定使用 \`sortby=hot\` 拉取数据,再按 \`sortBy\` 参数在本地重排;条目数量可通过 \`count\` 调整,仍可叠加 RSSHub 通用参数 \`limit\`。
141+
:::`,
142+
};
143+
144+
async function handler(ctx) {
145+
const sortByParam = ctx.req.param('sortBy');
146+
const countParam = ctx.req.param('count');
147+
148+
const sortBy = sortByParam === 'time' ? 'time' : 'hot';
149+
const rawCount = Number.parseInt(countParam || '', 10);
150+
const requestCount = Number.isNaN(rawCount) || rawCount <= 0 ? 10 : rawCount;
151+
152+
const ts = new Date().toISOString().slice(0, 10).replaceAll('-', '');
153+
const searchParams: Record<string, string | number> = {
154+
start: 0,
155+
count: requestCount,
156+
sortby: 'hot',
157+
os_rom: 'android',
158+
apiKey,
159+
_ts: ts,
160+
_sig: await signRequest(apiUrl, ts),
161+
};
162+
163+
const cacheKey = `douban:tv:coming:${requestCount}`;
164+
const data = (await cache.tryGet(
165+
cacheKey,
166+
async () => {
167+
try {
168+
const response = await got({
169+
method: 'get',
170+
url: apiUrl,
171+
searchParams,
172+
headers: {
173+
Accept: 'application/json',
174+
'User-Agent': apiClientUa,
175+
},
176+
});
177+
return response.data as ComingSoonResponse;
178+
} catch (error) {
179+
throw buildFetchError(error);
180+
}
181+
},
182+
config.cache.routeExpire,
183+
false
184+
)) as ComingSoonResponse;
185+
186+
if (!Array.isArray(data.subjects)) {
187+
const details = data.msg || data.message || data.reason;
188+
throw new Error(`Douban 返回数据结构异常,可能触发反爬或限频。${details ? `上游信息:${details}` : ''}`);
189+
}
190+
if (data.subjects.length === 0) {
191+
throw new Error('Douban 返回空数据,可能触发反爬或限频。请稍后重试。');
192+
}
193+
194+
const subscriptionCount = data.count ?? 0;
195+
const total = data.total ?? 0;
196+
const sortedSubjects = data.subjects.toSorted((a, b) => {
197+
if (sortBy === 'time') {
198+
const timeDiff = getSortTimestamp(a.pubdate) - getSortTimestamp(b.pubdate);
199+
if (timeDiff !== 0) {
200+
return timeDiff;
201+
}
202+
return getWishCount(b.wish_count) - getWishCount(a.wish_count);
203+
}
204+
const wishDiff = getWishCount(b.wish_count) - getWishCount(a.wish_count);
205+
if (wishDiff !== 0) {
206+
return wishDiff;
207+
}
208+
return 0;
209+
});
210+
return {
211+
title: '豆瓣剧集-即将播出',
212+
link: 'https://movie.douban.com/tv/',
213+
description: `即将播出的剧集,请求参数: count=${subscriptionCount}, total=${total}, sortBy=${sortBy}, requestCount=${requestCount}`,
214+
item: sortedSubjects.map((subject) => {
215+
const link = subject.url || subject.sharing_url || `https://movie.douban.com/subject/${subject.id}/`;
216+
const category = subject.card_subtitle ? [subject.card_subtitle] : (subject.genres ?? []);
217+
const pubDate = sortBy === 'time' ? getPubDate(subject.pubdate) : undefined;
218+
return {
219+
title: subject.title,
220+
category: category.length > 0 ? category : undefined,
221+
pubDate,
222+
description: renderDescription(subject),
223+
link,
224+
guid: link,
225+
};
226+
}),
227+
};
228+
}

lib/routes/zhihu/answers.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Route } from '@/types';
22
import got from '@/utils/got';
33
import { parseDate } from '@/utils/parse-date';
44

5-
import { getSignedHeader, header } from './utils';
5+
import { getSignedHeader, header, processImage } from './utils';
66

77
export const route: Route = {
88
path: '/people/answers/:id',
@@ -53,10 +53,9 @@ async function handler(ctx) {
5353
const data = response.data.data;
5454
const items = data.map((item) => {
5555
const title = item.question.title;
56-
// let description = processImage(detail.content);
5756
const url = `https://www.zhihu.com/question/${item.question.id}/answer/${item.id}`;
5857
const author = item.author.name;
59-
const description = item.content;
58+
const description = processImage(item.content);
6059

6160
return {
6261
title,

0 commit comments

Comments
 (0)