rsshub 1.0.0-master.f71451d → 1.0.0-master.f75997f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/lib/config.ts +14 -0
  2. package/lib/errors/index.test.ts +2 -2
  3. package/lib/middleware/template.tsx +12 -3
  4. package/lib/routes/0x80/index.ts +87 -0
  5. package/lib/routes/0x80/namespace.ts +7 -0
  6. package/lib/routes/aljazeera/index.ts +17 -14
  7. package/lib/routes/apple/podcast.ts +64 -0
  8. package/lib/routes/bilibili/cache.ts +1 -1
  9. package/lib/routes/bing/daily-wallpaper.ts +9 -8
  10. package/lib/routes/byau/namespace.ts +6 -0
  11. package/lib/routes/byau/xinwen/index.ts +72 -0
  12. package/lib/routes/cpcaauto/index.ts +255 -0
  13. package/lib/routes/cpcaauto/namespace.ts +8 -0
  14. package/lib/routes/dehenglaw/index.ts +128 -0
  15. package/lib/routes/dehenglaw/namespace.ts +8 -0
  16. package/lib/routes/dehenglaw/templates/description.art +7 -0
  17. package/lib/routes/gov/stats/index.ts +25 -22
  18. package/lib/routes/gxmzu/ai.ts +1 -1
  19. package/lib/routes/gxmzu/lib.ts +9 -26
  20. package/lib/routes/gxmzu/utils/index.ts +31 -13
  21. package/lib/routes/gxmzu/yjs.ts +1 -1
  22. package/lib/routes/jou/utils/index.ts +35 -25
  23. package/lib/routes/lofter/tag.ts +3 -3
  24. package/lib/routes/lofter/user.ts +3 -3
  25. package/lib/routes/njxzc/utils/index.ts +31 -13
  26. package/lib/routes/qingting/podcast.ts +61 -39
  27. package/lib/routes/reuters/common.ts +2 -2
  28. package/lib/routes/sara/index.ts +66 -0
  29. package/lib/routes/sara/namespace.ts +6 -0
  30. package/lib/routes/tencent/news/author.ts +13 -11
  31. package/lib/routes/test/index.ts +11 -1
  32. package/lib/routes/twitter/api/mobile-api/login.ts +29 -28
  33. package/lib/routes/twitter/namespace.ts +2 -2
  34. package/lib/routes/twitter/user.ts +5 -0
  35. package/lib/routes/u3c3/index.ts +1 -1
  36. package/lib/routes/u3c3/namespace.ts +1 -1
  37. package/lib/routes/u9a9/index.ts +2 -2
  38. package/lib/routes/u9a9/namespace.ts +1 -1
  39. package/lib/routes/zsxq/group.ts +63 -0
  40. package/lib/routes/zsxq/namespace.ts +6 -0
  41. package/lib/routes/zsxq/types.ts +149 -0
  42. package/lib/routes/zsxq/user.ts +58 -0
  43. package/lib/routes/zsxq/utils.ts +70 -0
  44. package/lib/setup.test.ts +183 -12
  45. package/lib/utils/render.ts +1 -1
  46. package/lib/utils/request-rewriter/get.ts +8 -1
  47. package/lib/utils/wechat-mp.test.ts +411 -32
  48. package/lib/utils/wechat-mp.ts +447 -76
  49. package/lib/views/{rss3-ums.ts → rss3.ts} +2 -2
  50. package/package.json +14 -14
package/lib/config.ts CHANGED
@@ -230,6 +230,9 @@ export type Config = {
230
230
  pkubbs: {
231
231
  cookie?: string;
232
232
  };
233
+ qingting: {
234
+ id?: string;
235
+ };
233
236
  saraba1st: {
234
237
  cookie?: string;
235
238
  };
@@ -258,6 +261,7 @@ export type Config = {
258
261
  oauthTokenSecrets?: string[];
259
262
  username?: string;
260
263
  password?: string;
264
+ authenticationSecret?: string;
261
265
  cookie?: string;
262
266
  };
263
267
  weibo: {
@@ -294,6 +298,9 @@ export type Config = {
294
298
  zodgame: {
295
299
  cookie?: string;
296
300
  };
301
+ zsxq: {
302
+ accessToken?: string;
303
+ };
297
304
  };
298
305
 
299
306
  const value: Config | Record<string, any> = {};
@@ -575,6 +582,9 @@ const calculateValue = () => {
575
582
  pkubbs: {
576
583
  cookie: envs.PKUBBS_COOKIE,
577
584
  },
585
+ qingting: {
586
+ id: envs.QINGTING_ID,
587
+ },
578
588
  saraba1st: {
579
589
  cookie: envs.SARABA1ST_COOKIE,
580
590
  },
@@ -607,6 +617,7 @@ const calculateValue = () => {
607
617
  oauthTokenSecrets: envs.TWITTER_OAUTH_TOKEN_SECRET?.split(','),
608
618
  username: envs.TWITTER_USERNAME,
609
619
  password: envs.TWITTER_PASSWORD,
620
+ authenticationSecret: envs.TWITTER_AUTHENTICATION_SECRET,
610
621
  cookie: envs.TWITTER_COOKIE,
611
622
  },
612
623
  weibo: {
@@ -643,6 +654,9 @@ const calculateValue = () => {
643
654
  zodgame: {
644
655
  cookie: envs.ZODGAME_COOKIE,
645
656
  },
657
+ zsxq: {
658
+ accessToken: envs.ZSXQ_ACCESS_TOKEN,
659
+ },
646
660
  };
647
661
 
648
662
  for (const name in _value) {
@@ -66,13 +66,13 @@ describe('route throws an error', () => {
66
66
  expect(value).toBe('9');
67
67
  break;
68
68
  case 'Hot Routes:':
69
- expect(value).toBe('6 /test/:id<br>');
69
+ expect(value).toBe('6 /test/:id/:params?<br>');
70
70
  break;
71
71
  case 'Hot Paths:':
72
72
  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>');
73
73
  break;
74
74
  case 'Hot Error Routes:':
75
- expect(value).toBe('5 /test/:id<br>');
75
+ expect(value).toBe('5 /test/:id/:params?<br>');
76
76
  break;
77
77
  case 'Hot Error Paths:':
78
78
  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>');
@@ -1,4 +1,4 @@
1
- import { rss3Ums, json, RSS, Atom } from '@/utils/render';
1
+ import { rss3, json, RSS, Atom } from '@/utils/render';
2
2
  import { config } from '@/config';
3
3
  import { collapseWhitespace, convertDateToISO8601 } from '@/utils/common-utils';
4
4
  import type { MiddlewareHandler } from 'hono';
@@ -48,6 +48,14 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
48
48
  }
49
49
  }
50
50
 
51
+ if (item.description) {
52
+ // https://stackoverflow.com/questions/2507608/error-input-is-not-proper-utf-8-indicate-encoding-using-phps-simplexml-lo/40552083#40552083
53
+ // https://stackoverflow.com/questions/1497885/remove-control-characters-from-php-string/1497928#1497928
54
+ // remove unicode control characters
55
+ // see #14940 #14943 #15262
56
+ item.description = item.description.replaceAll(/[\u0000-\u0009\u000B\u000C\u000E-\u001F\u007F]/g, '');
57
+ }
58
+
51
59
  if (typeof item.author === 'string') {
52
60
  item.author = collapseWhitespace(item.author) || '';
53
61
  } else if (typeof item.author === 'object' && item.author !== null) {
@@ -86,8 +94,9 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
86
94
  return ctx.json(result);
87
95
  }
88
96
 
89
- if (outputType === 'ums') {
90
- return ctx.json(rss3Ums(result));
97
+ // retain .ums for backward compatibility
98
+ if (outputType === 'ums' || outputType === 'rss3') {
99
+ return ctx.json(rss3(result));
91
100
  } else if (outputType === 'json') {
92
101
  ctx.header('Content-Type', 'application/feed+json; charset=UTF-8');
93
102
  return ctx.body(json(result));
@@ -0,0 +1,87 @@
1
+ import { Route } from '@/types';
2
+
3
+ import cache from '@/utils/cache';
4
+ import got from '@/utils/got';
5
+ import { load } from 'cheerio';
6
+ import { parseDate } from '@/utils/parse-date';
7
+
8
+ export const route: Route = {
9
+ path: '/blog',
10
+ categories: ['blog'],
11
+ example: '/0x80/blog',
12
+ url: '0x80.pl/notesen.html',
13
+ name: 'Articles',
14
+ maintainers: ['xnum'],
15
+ handler,
16
+ };
17
+
18
+ function extractDateFromURL(url: string) {
19
+ const regex = /\d{4}-\d{2}-\d{2}/;
20
+ const match = url.match(regex);
21
+
22
+ return match ? match[0] : null;
23
+ }
24
+
25
+ async function handler() {
26
+ // The TLS cert is invalid, we are limited to use HTTP unfortunately.
27
+ const baseUrl = 'http://0x80.pl/';
28
+ const targetUrl = `${baseUrl}notesen.html`;
29
+
30
+ const response = await got({
31
+ method: 'get',
32
+ url: targetUrl,
33
+ });
34
+
35
+ const $ = load(response.data);
36
+
37
+ const alist = $('a.reference.external');
38
+
39
+ const list = alist
40
+ .toArray()
41
+ .map((item) => {
42
+ item = $(item);
43
+
44
+ const link = item.attr('href') || '';
45
+ const title = item.text() || '';
46
+ const pubDate = extractDateFromURL(link);
47
+
48
+ return {
49
+ title,
50
+ link,
51
+ pubDate,
52
+ category: 'Uncategoried',
53
+ };
54
+ })
55
+ .filter((item) => item.link.startsWith('notesen'));
56
+
57
+ const items = await Promise.all(
58
+ list.map((item) =>
59
+ cache.tryGet(item.link, async () => {
60
+ const articleUrl = `${baseUrl}${item.link}`;
61
+ const response = await got({
62
+ method: 'get',
63
+ url: articleUrl,
64
+ });
65
+
66
+ const $ = load(response.data);
67
+
68
+ const author = $('tr.author.field td.field-body').text();
69
+ const articlePubDate = $('tr.added-on.field td.field-body').text();
70
+
71
+ item.author = author;
72
+ // Some articles might be missing the added-on field.
73
+ // As a safeguard, if the date from url is null, fallbacks to the article one.
74
+ item.pubDate = parseDate(item.pubDate || articlePubDate);
75
+ item.description = $('div.document').first().html();
76
+
77
+ return item;
78
+ })
79
+ )
80
+ );
81
+
82
+ return {
83
+ title: '0x80.pl articles',
84
+ link: targetUrl,
85
+ item: items,
86
+ };
87
+ }
@@ -0,0 +1,7 @@
1
+ import type { Namespace } from '@/types';
2
+
3
+ export const namespace: Namespace = {
4
+ name: 'Wojciech Muła',
5
+ url: '0x80.pl',
6
+ description: '',
7
+ };
@@ -4,11 +4,10 @@ const __dirname = getCurrentPath(import.meta.url);
4
4
 
5
5
  import { getSubPath } from '@/utils/common-utils';
6
6
  import cache from '@/utils/cache';
7
- import got from '@/utils/got';
8
7
  import { load } from 'cheerio';
9
- import { parseDate } from '@/utils/parse-date';
10
8
  import { art } from '@/utils/render';
11
9
  import path from 'node:path';
10
+ import { ofetch } from 'ofetch';
12
11
 
13
12
  const languages = {
14
13
  arabic: {
@@ -45,12 +44,8 @@ async function handler(ctx) {
45
44
  const rootUrl = languages[language].rootUrl;
46
45
  const currentUrl = `${rootUrl}/${isRSS ? languages[language].rssUrl : params.join('/')}`;
47
46
 
48
- const response = await got({
49
- method: 'get',
50
- url: currentUrl,
51
- });
52
-
53
- const $ = load(response.data);
47
+ const response = await ofetch(currentUrl);
48
+ const $ = load(response);
54
49
 
55
50
  let items = isRSS
56
51
  ? response.data.match(new RegExp('<link>' + rootUrl + '/(.*?)</link>', 'g')).map((item) => ({
@@ -69,19 +64,27 @@ async function handler(ctx) {
69
64
  items = await Promise.all(
70
65
  items.slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 50).map((item) =>
71
66
  cache.tryGet(item.link, async () => {
72
- const detailResponse = await got({
73
- method: 'get',
74
- url: item.link,
75
- });
67
+ const detailResponse = await ofetch(item.link);
76
68
 
77
- const content = load(detailResponse.data);
69
+ const content = load(detailResponse);
78
70
 
79
71
  content('.more-on').parent().remove();
80
72
  content('.responsive-image img').removeAttr('srcset');
73
+ let pubDate;
74
+
75
+ const datePublished = detailResponse.match(/"datePublished": ?"(.*?)",/);
76
+ if (datePublished && datePublished.length > 1) {
77
+ pubDate = detailResponse.match(/"datePublished": ?"(.*?)",/)[1];
78
+ } else {
79
+ // uploadDate replaces datePublished for video articles
80
+ const uploadDate = detailResponse.match(/"uploadDate": ?"(.*?)",/)[1];
81
+
82
+ pubDate = uploadDate && uploadDate.length > 1 ? uploadDate : content('div.date-simple > span:nth-child(2)').text();
83
+ }
81
84
 
82
85
  item.title = content('h1').first().text();
83
86
  item.author = content('.author').text();
84
- item.pubDate = parseDate(detailResponse.data.match(/"datePublished": ?"(.*?)",/)[1]);
87
+ item.pubDate = pubDate;
85
88
  item.description = art(path.join(__dirname, 'templates/description.art'), {
86
89
  image: content('.article-featured-image').html(),
87
90
  description: content('.wysiwyg').html(),
@@ -0,0 +1,64 @@
1
+ import { Route } from '@/types';
2
+ import got from '@/utils/got';
3
+ import { load } from 'cheerio';
4
+ import { parseDate } from '@/utils/parse-date';
5
+
6
+ export const route: Route = {
7
+ path: '/podcast/:id',
8
+ categories: ['multimedia'],
9
+ example: '/apple/podcast/id1559695855',
10
+ parameters: { id: '播客id,可以在 Apple 播客app 内分享的播客的 URL 中找到' },
11
+ features: {
12
+ requireConfig: false,
13
+ requirePuppeteer: false,
14
+ antiCrawler: false,
15
+ supportBT: false,
16
+ supportPodcast: false,
17
+ supportScihub: false,
18
+ },
19
+ radar: [
20
+ {
21
+ source: ['podcasts.apple.com/cn/podcast/:id'],
22
+ },
23
+ ],
24
+ name: '播客',
25
+ maintainers: ['Acring'],
26
+ handler,
27
+ url: 'https://www.apple.com.cn/apple-podcasts/',
28
+ };
29
+
30
+ async function handler(ctx) {
31
+ const link = `https://podcasts.apple.com/cn/podcast/${ctx.req.param('id')}`;
32
+ const response = await got({
33
+ method: 'get',
34
+ url: link,
35
+ });
36
+
37
+ const $ = load(response.data);
38
+
39
+ const page_data = JSON.parse($('#shoebox-media-api-cache-amp-podcasts').text());
40
+
41
+ const data = JSON.parse(page_data[Object.keys(page_data)[0]]).d[0];
42
+ const attributes = data.attributes;
43
+
44
+ const episodes = data.relationships.episodes.data.map((item) => {
45
+ const attr = item.attributes;
46
+ return {
47
+ title: attr.name,
48
+ enclosure_url: attr.assetUrl,
49
+ itunes_duration: attr.durationInMilliseconds / 1000,
50
+ enclosure_type: 'audio/mp4',
51
+ link: attr.url,
52
+ pubDate: parseDate(attr.releaseDateTime),
53
+ description: attr.description.standard.replaceAll('\n', '<br>'),
54
+ };
55
+ });
56
+
57
+ return {
58
+ title: attributes.name,
59
+ link: attributes.url,
60
+ itunes_author: attributes.artistName,
61
+ item: episodes,
62
+ description: attributes.description.standard,
63
+ };
64
+ }
@@ -27,7 +27,7 @@ const getCookie = () => {
27
27
  await page.goto('https://space.bilibili.com/1/dynamic');
28
28
  const cookieString = await waitForRequest;
29
29
  logger.debug(`Got bilibili cookie: ${cookieString}`);
30
-
30
+ await browser.close();
31
31
  return cookieString;
32
32
  });
33
33
  };
@@ -1,5 +1,7 @@
1
1
  import { Route } from '@/types';
2
- import got from '@/utils/got';
2
+ import ofetch from '@/utils/ofetch';
3
+ import { parseDate } from '@/utils/parse-date';
4
+ import timezone from '@/utils/timezone';
3
5
 
4
6
  export const route: Route = {
5
7
  path: '/',
@@ -9,25 +11,23 @@ export const route: Route = {
9
11
  target: '',
10
12
  },
11
13
  ],
12
- name: 'Unknown',
14
+ name: '每日壁纸',
13
15
  maintainers: ['FHYunCai'],
14
16
  handler,
15
17
  url: 'cn.bing.com/',
16
18
  };
17
19
 
18
20
  async function handler(ctx) {
19
- const response = await got({
20
- method: 'get',
21
- prefixUrl: 'https://cn.bing.com',
22
- url: 'HPImageArchive.aspx',
23
- searchParams: {
21
+ const response = await ofetch('HPImageArchive.aspx', {
22
+ baseURL: 'https://cn.bing.com',
23
+ query: {
24
24
  format: 'js',
25
25
  idx: 0,
26
26
  n: ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 7,
27
27
  mkt: 'zh-CN',
28
28
  },
29
29
  });
30
- const data = response.data;
30
+ const data = response;
31
31
  return {
32
32
  title: 'Bing每日壁纸',
33
33
  link: 'https://cn.bing.com/',
@@ -35,6 +35,7 @@ async function handler(ctx) {
35
35
  title: item.copyright,
36
36
  description: `<img src="https://cn.bing.com${item.url}">`,
37
37
  link: item.copyrightlink,
38
+ pubDate: timezone(parseDate(item.fullstartdate), 0),
38
39
  })),
39
40
  };
40
41
  }
@@ -0,0 +1,6 @@
1
+ import type { Namespace } from '@/types';
2
+
3
+ export const namespace: Namespace = {
4
+ name: '黑龙江八一农垦大学',
5
+ url: 'byau.edu.cn',
6
+ };
@@ -0,0 +1,72 @@
1
+ import { Route } from '@/types';
2
+ import cache from '@/utils/cache';
3
+ import got from '@/utils/got';
4
+ import { load } from 'cheerio';
5
+ import { parseDate } from '@/utils/parse-date';
6
+ import timezone from '@/utils/timezone';
7
+
8
+ export const route: Route = {
9
+ path: '/news/:type_id',
10
+ categories: ['university'],
11
+ example: '/byau/news/3674',
12
+ parameters: { type_id: '栏目类型(从菜单栏获取对应 ID)' },
13
+ radar: [
14
+ {
15
+ source: ['xinwen.byau.edu.cn/:type_id/list.htm'],
16
+ target: '/news/:type_id',
17
+ },
18
+ ],
19
+ name: '新闻网',
20
+ maintainers: ['ueiu'],
21
+ handler,
22
+ url: 'xinwen.byau.edu.cn',
23
+ description: `| 学校要闻 | 校园动态 |
24
+ | ---- | ----------- |
25
+ | 3674 | 3676 |`,
26
+ };
27
+
28
+ async function handler(ctx) {
29
+ const baseUrl = 'http://xinwen.byau.edu.cn/';
30
+
31
+ const typeID = ctx.req.param('type_id');
32
+ const url = `${baseUrl}${typeID}/list.htm`;
33
+
34
+ const response = await got(url);
35
+ const $ = load(response.data);
36
+
37
+ const list = $('.news')
38
+ .toArray()
39
+ .map((item) => {
40
+ const $$ = load(item);
41
+
42
+ const originalItemUrl = $$('a').attr('href');
43
+ // 因为学校要闻的头两个像是固定了跳转专栏页面的,不能相同处理
44
+ const startsWithHttp = originalItemUrl.startsWith('http');
45
+ const itemUrl = startsWithHttp ? originalItemUrl : new URL(originalItemUrl, baseUrl).href;
46
+
47
+ return {
48
+ title: $$('a').text(),
49
+ link: itemUrl,
50
+ pubDate: timezone(parseDate($$('.news_meta').text()), +8),
51
+ };
52
+ });
53
+
54
+ const items = await Promise.all(
55
+ list.map((item) =>
56
+ cache.tryGet(item.link, async () => {
57
+ const response = await got(item.link);
58
+ const $ = load(response.data);
59
+
60
+ item.description = $('.col_news_con').html();
61
+
62
+ return item;
63
+ })
64
+ )
65
+ );
66
+
67
+ return {
68
+ title: $('title').text(),
69
+ link: url,
70
+ item: items,
71
+ };
72
+ }