rsshub 1.0.0-master.f9b85f5 → 1.0.0-master.f9c381a

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 (166) hide show
  1. package/lib/config.ts +10 -0
  2. package/lib/middleware/cache.ts +4 -0
  3. package/lib/middleware/parameter.ts +1 -1
  4. package/lib/registry.ts +6 -2
  5. package/lib/routes/51cto/utils.ts +1 -1
  6. package/lib/routes/abc/index.ts +1 -1
  7. package/lib/routes/acgvinyl/namespace.ts +6 -0
  8. package/lib/routes/acgvinyl/news.ts +86 -0
  9. package/lib/routes/ally/rail.ts +1 -1
  10. package/lib/routes/anthropic/news.ts +13 -11
  11. package/lib/routes/apnews/mobile-api.ts +1 -1
  12. package/lib/routes/apnews/sitemap.ts +1 -1
  13. package/lib/routes/apple/apps.ts +2 -2
  14. package/lib/routes/apple/podcast.ts +58 -25
  15. package/lib/routes/bangumi.tv/group/reply.ts +1 -1
  16. package/lib/routes/bilibili/cache.ts +18 -2
  17. package/lib/routes/bilibili/dynamic.ts +31 -7
  18. package/lib/routes/bilibili/page.ts +1 -1
  19. package/lib/routes/bilibili/ranking.ts +23 -17
  20. package/lib/routes/bilibili/wasm-exec.ts +1 -1
  21. package/lib/routes/bilibili/weekly-recommend.ts +22 -6
  22. package/lib/routes/bjp/apod.ts +1 -1
  23. package/lib/routes/buaa/jiaowu.ts +1 -1
  24. package/lib/routes/bullionvault/gold-news.ts +3 -3
  25. package/lib/routes/cast/index.ts +1 -1
  26. package/lib/routes/coolapk/utils.ts +5 -4
  27. package/lib/routes/coolbuy/index.ts +106 -0
  28. package/lib/routes/{xinhuanet → coolbuy}/namespace.ts +3 -3
  29. package/lib/routes/coolbuy/templates/description.art +48 -0
  30. package/lib/routes/coolidge/film-guide.ts +60 -0
  31. package/lib/routes/coolidge/namespace.ts +7 -0
  32. package/lib/routes/coolidge/news.ts +65 -0
  33. package/lib/routes/coolidge/templates/description.art +4 -0
  34. package/lib/routes/copymanga/comic.ts +1 -1
  35. package/lib/routes/cpta/handler.ts +1 -1
  36. package/lib/routes/creative-comic/book.ts +1 -1
  37. package/lib/routes/daum/potplayer.ts +1 -1
  38. package/lib/routes/dockerhub/utils.ts +1 -1
  39. package/lib/routes/dora-world/article.ts +2 -2
  40. package/lib/routes/ehentai/ehapi.ts +3 -3
  41. package/lib/routes/eventbrite/events.ts +152 -0
  42. package/lib/routes/eventbrite/namespace.ts +7 -0
  43. package/lib/routes/github/activity.ts +1 -1
  44. package/lib/routes/google/jules.ts +63 -0
  45. package/lib/routes/gov/mem/namespace.ts +7 -0
  46. package/lib/routes/gov/mem/zfxxgkpt.ts +96 -0
  47. package/lib/routes/gov/mot/index.ts +158 -53
  48. package/lib/routes/hameln/chapter.ts +1 -1
  49. package/lib/routes/hpoi/banner-item.ts +28 -11
  50. package/lib/routes/hpoi/info.ts +1 -1
  51. package/lib/routes/huggingface/daily-papers.ts +1 -1
  52. package/lib/routes/hupu/index.ts +158 -74
  53. package/lib/routes/hupu/types.ts +163 -0
  54. package/lib/routes/hust/gs.ts +1 -1
  55. package/lib/routes/hust/mse.ts +1 -1
  56. package/lib/routes/huxiu/util.ts +2 -2
  57. package/lib/routes/infoq/presentations.ts +1 -1
  58. package/lib/routes/instagram/common-utils.ts +3 -3
  59. package/lib/routes/itch/devlog.ts +7 -3
  60. package/lib/routes/javbus/index.ts +1 -1
  61. package/lib/routes/javlibrary/utils.ts +1 -1
  62. package/lib/routes/jbma/namespace.ts +17 -0
  63. package/lib/routes/jbma/report.ts +473 -0
  64. package/lib/routes/jetbrains/comments.ts +1 -1
  65. package/lib/routes/jingzhengu/utils.ts +1 -1
  66. package/lib/routes/juejin/aicoding.ts +102 -0
  67. package/lib/routes/juejin/utils.ts +36 -51
  68. package/lib/routes/kakuyomu/works.ts +1 -1
  69. package/lib/routes/kemono/index.ts +2 -2
  70. package/lib/routes/komiic/comic.ts +1 -1
  71. package/lib/routes/koyso/index.ts +338 -0
  72. package/lib/routes/koyso/namespace.ts +9 -0
  73. package/lib/routes/koyso/templates/description.art +13 -0
  74. package/lib/routes/letterboxd/index.ts +65 -0
  75. package/lib/routes/letterboxd/namespace.ts +8 -0
  76. package/lib/routes/maccms/index.ts +1 -1
  77. package/lib/routes/mercari/util.ts +1 -1
  78. package/lib/routes/mingpao/index.ts +1 -1
  79. package/lib/routes/nankai/ai-notice.ts +142 -0
  80. package/lib/routes/nankai/graduate-notice.ts +162 -0
  81. package/lib/routes/natgeo/natgeo.ts +1 -0
  82. package/lib/routes/nhk/news-web-easy.ts +1 -1
  83. package/lib/routes/nicovideo/mylist.ts +39 -0
  84. package/lib/routes/nicovideo/types.ts +27 -0
  85. package/lib/routes/nicovideo/utils.ts +27 -1
  86. package/lib/routes/nikkei/cn/index.ts +1 -4
  87. package/lib/routes/now/news.ts +1 -1
  88. package/lib/routes/nytimes/index.ts +1 -1
  89. package/lib/routes/pixiv/novel-api/user-novels/sfw.ts +1 -1
  90. package/lib/routes/pixivision/utils.ts +1 -1
  91. package/lib/routes/pku/hr.ts +1 -1
  92. package/lib/routes/pku/scc/recruit.ts +1 -1
  93. package/lib/routes/producthunt/templates/description.art +2 -2
  94. package/lib/routes/producthunt/today.ts +17 -8
  95. package/lib/routes/ps/trophy.ts +1 -1
  96. package/lib/routes/pubscholar/utils.ts +14 -1
  97. package/lib/routes/qweather/3days.ts +14 -14
  98. package/lib/routes/qweather/now.ts +12 -10
  99. package/lib/routes/qweather/util.tsx +89 -0
  100. package/lib/routes/qwenlm/blog.ts +75 -0
  101. package/lib/routes/qwenlm/namespace.ts +6 -0
  102. package/lib/routes/radio-canada/latest.ts +30 -17
  103. package/lib/routes/ruc/ai.ts +1 -1
  104. package/lib/routes/ruc/hr.ts +1 -1
  105. package/lib/routes/samrdprc/index.ts +241 -0
  106. package/lib/routes/samrdprc/namespace.ts +1 -1
  107. package/lib/routes/sdo/ff14risingstones/api.ts +78 -0
  108. package/lib/routes/sdo/ff14risingstones/constant.ts +338 -0
  109. package/lib/routes/sdo/ff14risingstones/posts.ts +80 -0
  110. package/lib/routes/sdo/ff14risingstones/strats.ts +75 -0
  111. package/lib/routes/sdo/ff14risingstones/templates/duties-party.art +41 -0
  112. package/lib/routes/sdo/ff14risingstones/templates/fc-party.art +26 -0
  113. package/lib/routes/sdo/ff14risingstones/templates/novice-network-party.art +9 -0
  114. package/lib/routes/sdo/ff14risingstones/templates/rp-party.art +15 -0
  115. package/lib/routes/sdo/ff14risingstones/timeline.ts +31 -0
  116. package/lib/routes/sdo/ff14risingstones/types/dynamic.ts +50 -0
  117. package/lib/routes/sdo/ff14risingstones/types/index.ts +3 -0
  118. package/lib/routes/sdo/ff14risingstones/types/other.ts +57 -0
  119. package/lib/routes/sdo/ff14risingstones/types/party.ts +111 -0
  120. package/lib/routes/sdo/ff14risingstones/user-dynamics.ts +32 -0
  121. package/lib/routes/sdo/ff14risingstones/user-posts.ts +32 -0
  122. package/lib/routes/sdo/ff14risingstones/user-resently.ts +38 -0
  123. package/lib/routes/sdo/ff14risingstones/user-strats.ts +32 -0
  124. package/lib/routes/sdo/ff14risingstones/utils.ts +215 -0
  125. package/lib/routes/sdo/namespace.ts +7 -0
  126. package/lib/routes/showstart/utils.ts +1 -1
  127. package/lib/routes/sohu/mp.ts +1 -1
  128. package/lib/routes/sotwe/user.ts +1 -1
  129. package/lib/routes/surfshark/blog.ts +273 -77
  130. package/lib/routes/surfshark/templates/description.art +16 -4
  131. package/lib/routes/sustainabilitymag/articles.ts +1 -1
  132. package/lib/routes/syosetu/dev.ts +1 -1
  133. package/lib/routes/syosetu/ranking-isekai.ts +1 -1
  134. package/lib/routes/szse/disclosure/listed-notice.ts +44 -6
  135. package/lib/routes/telegram/channel-media.ts +249 -0
  136. package/lib/routes/telegram/channel.ts +5 -4
  137. package/lib/routes/telegram/stories.ts +130 -0
  138. package/lib/routes/telegram/tglib/channel.ts +136 -118
  139. package/lib/routes/telegram/tglib/client.ts +37 -139
  140. package/lib/routes/tesla/cx.ts +1 -1
  141. package/lib/routes/theverge/index.ts +20 -6
  142. package/lib/routes/threads/utils.ts +7 -3
  143. package/lib/routes/tidb/blog.ts +1 -1
  144. package/lib/routes/toutiao/user.ts +2 -2
  145. package/lib/routes/twitter/api/mobile-api/api.ts +1 -1
  146. package/lib/routes/txrjy/fornumtopic.ts +2 -2
  147. package/lib/routes/typst/universe.ts +1 -1
  148. package/lib/routes/uber/blog.ts +87 -46
  149. package/lib/routes/weibo/utils.ts +17 -9
  150. package/lib/routes/xiaohongshu/user.ts +1 -1
  151. package/lib/routes/xjtu/ee-jzxx.ts +1 -1
  152. package/lib/routes/yahoo/news/utils.ts +1 -1
  153. package/lib/routes/ymgal/article.ts +1 -1
  154. package/lib/routes/yoasobi-music/media.ts +1 -1
  155. package/lib/routes/youtube/api/youtubei.ts +1 -1
  156. package/lib/routes/youtube/community.ts +4 -4
  157. package/lib/routes/zaker/utils.ts +1 -1
  158. package/lib/routes/zaobao/util.tsx +1 -1
  159. package/lib/server.ts +1 -1
  160. package/lib/types.ts +1 -1
  161. package/lib/utils/puppeteer-utils.test.ts +2 -2
  162. package/lib/views/index.tsx +4 -4
  163. package/package.json +40 -40
  164. package/lib/routes/qweather/templates/3days.art +0 -22
  165. package/lib/routes/qweather/templates/now.art +0 -16
  166. package/lib/routes/xinhuanet/app.ts +0 -109
package/lib/config.ts CHANGED
@@ -187,6 +187,7 @@ export type Config = {
187
187
  };
188
188
  hefeng: {
189
189
  key?: string;
190
+ apiHost?: string;
190
191
  };
191
192
  infzm: {
192
193
  cookie?: string;
@@ -325,6 +326,10 @@ export type Config = {
325
326
  scihub: {
326
327
  host?: string;
327
328
  };
329
+ sdo: {
330
+ ff14risingstones?: string;
331
+ ua?: string;
332
+ };
328
333
  sis001: {
329
334
  baseUrl?: string;
330
335
  };
@@ -659,6 +664,7 @@ const calculateValue = () => {
659
664
  },
660
665
  hefeng: {
661
666
  key: envs.HEFENG_KEY,
667
+ apiHost: envs.HEFENG_API_HOST,
662
668
  },
663
669
  infzm: {
664
670
  cookie: envs.INFZM_COOKIE,
@@ -797,6 +803,10 @@ const calculateValue = () => {
797
803
  scihub: {
798
804
  host: envs.SCIHUB_HOST || 'https://sci-hub.se/',
799
805
  },
806
+ sdo: {
807
+ ff14risingstones: envs.SDO_FF14RISINGSTONES,
808
+ ua: envs.SDO_UA,
809
+ },
800
810
  sis001: {
801
811
  baseUrl: envs.SIS001_BASE_URL || 'https://sis001.com',
802
812
  },
@@ -55,6 +55,10 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
55
55
  // Doesn't hit the cache? We need to let others know!
56
56
  await cacheModule.globalCache.set(controlKey, '1', config.cache.requestTimeout);
57
57
 
58
+ // let routers control cache
59
+ ctx.set('cacheKey', key);
60
+ ctx.set('cacheControlKey', controlKey);
61
+
58
62
  try {
59
63
  await next();
60
64
  } catch (error) {
@@ -80,7 +80,7 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
80
80
 
81
81
  // sort items
82
82
  if (ctx.req.query('sorted') !== 'false') {
83
- data.item = data.item.sort((a: DataItem, b: DataItem) => +new Date(b.pubDate || 0) - +new Date(a.pubDate || 0));
83
+ data.item = data.item.toSorted((a: DataItem, b: DataItem) => +new Date(b.pubDate || 0) - +new Date(a.pubDate || 0));
84
84
  }
85
85
 
86
86
  const handleItem = (item: DataItem) => {
package/lib/registry.ts CHANGED
@@ -155,7 +155,7 @@ const sortRoutes = (
155
155
  }
156
156
  >
157
157
  ) =>
158
- Object.entries(routes).sort(([pathA], [pathB]) => {
158
+ Object.entries(routes).toSorted(([pathA], [pathB]) => {
159
159
  const segmentsA = pathA.split('/');
160
160
  const segmentsB = pathB.split('/');
161
161
  const lenA = segmentsA.length;
@@ -198,7 +198,11 @@ for (const namespace in namespaces) {
198
198
  routeData.handler = route.handler;
199
199
  }
200
200
  }
201
- ctx.set('data', await routeData.handler(ctx));
201
+ const response = await routeData.handler(ctx);
202
+ if (response instanceof Response) {
203
+ return response;
204
+ }
205
+ ctx.set('data', response);
202
206
  }
203
207
  };
204
208
  subApp.get(path, wrappedHandler);
@@ -16,6 +16,6 @@ export const getToken = () =>
16
16
  export const sign = (requestPath: string, payload: Record<string, any> = {}, timestamp: number, token: string) => {
17
17
  payload.timestamp = timestamp;
18
18
  payload.token = token;
19
- const sortedParams = Object.keys(payload).sort();
19
+ const sortedParams = Object.keys(payload).toSorted();
20
20
  return md5(md5(requestPath) + md5(sortedParams + md5(token) + timestamp));
21
21
  };
@@ -132,7 +132,7 @@ async function handler(ctx) {
132
132
  if (enclosureMatches) {
133
133
  const enclosureMatch = enclosureMatches
134
134
  .map((e) => e.match(new RegExp(enclosurePattern)))
135
- .sort((a, b) => Number.parseInt(a[2], 10) - Number.parseInt(b[2], 10))
135
+ .toSorted((a, b) => Number.parseInt(a[2], 10) - Number.parseInt(b[2], 10))
136
136
  .pop();
137
137
 
138
138
  item.enclosure_url = enclosureMatch[3];
@@ -0,0 +1,6 @@
1
+ import type { Namespace } from '@/types';
2
+
3
+ export const namespace: Namespace = {
4
+ name: 'ACG Vinyl - 黑胶',
5
+ url: 'www.acgvinyl.com',
6
+ };
@@ -0,0 +1,86 @@
1
+ import { Route } from '@/types';
2
+ import cache from '@/utils/cache';
3
+ import ofetch from '@/utils/ofetch';
4
+ import { load } from 'cheerio';
5
+ import { parseDate } from '@/utils/parse-date';
6
+
7
+ export const route: Route = {
8
+ path: '/news',
9
+ categories: ['anime'],
10
+ example: '/news',
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: ['www.acgvinyl.com'],
22
+ target: '/news',
23
+ },
24
+ ],
25
+ name: 'News',
26
+ maintainers: ['williamgateszhao'],
27
+ handler,
28
+ url: 'www.acgvinyl.com/col.jsp?id=103',
29
+ zh: {
30
+ name: '黑胶新闻',
31
+ },
32
+ };
33
+
34
+ async function handler(ctx) {
35
+ const rootUrl = 'http://www.acgvinyl.com';
36
+
37
+ const newsIndexResponse = await ofetch(`${rootUrl}/col.jsp?id=103`);
38
+ const $ = load(newsIndexResponse);
39
+ const newsIndexJsonText = $('script:contains("window.__INITIAL_STATE__")').text().replaceAll('window.__INITIAL_STATE__=', '');
40
+ const newsIndexJson = JSON.parse(newsIndexJsonText);
41
+
42
+ const newsListResponse = await ofetch(`${rootUrl}/rajax/news_h.jsp?cmd=getWafNotCk_getList`, {
43
+ method: 'POST',
44
+ headers: {
45
+ 'content-type': 'application/x-www-form-urlencoded',
46
+ },
47
+ body: new URLSearchParams({
48
+ page: '1',
49
+ pageSize: String(ctx.req.query('limit') ?? 20),
50
+ fromMid: newsIndexJson.modules.module366.id,
51
+ idList: `[${newsIndexJson.modules.module366.prop3}]`,
52
+ sortKey: newsIndexJson.modules.module366.blob0.sortKey,
53
+ sortType: newsIndexJson.modules.module366.blob0.sortType,
54
+ }).toString(),
55
+ });
56
+ const list = JSON.parse(newsListResponse);
57
+
58
+ if (!list?.success || !Array.isArray(list?.list)) {
59
+ return null;
60
+ }
61
+
62
+ const items = await Promise.all(
63
+ list.list.map((item) =>
64
+ cache.tryGet(item.url, async () => {
65
+ const detailResponse = await ofetch(`${rootUrl}${item.url}`);
66
+ const $ = load(detailResponse);
67
+ const detailJsonText = $('script:contains("window.__INITIAL_STATE__")').text().replaceAll('window.__INITIAL_STATE__=', '');
68
+ const detailJson = JSON.parse(detailJsonText);
69
+ const detail = load(detailJson.modules.module2.newsInfo.content);
70
+ detail('[style]').removeAttr('style');
71
+ return {
72
+ title: item.title,
73
+ link: `${rootUrl}${item.url}`,
74
+ pubDate: parseDate(item.date),
75
+ description: detail.html(),
76
+ };
77
+ })
78
+ )
79
+ );
80
+
81
+ return {
82
+ title: 'ACG Vinyl - 黑胶 - 黑胶新闻',
83
+ link: 'http://www.acgvinyl.com/col.jsp?id=103',
84
+ item: items,
85
+ };
86
+ }
@@ -86,7 +86,7 @@ async function handler(ctx) {
86
86
  uniqueItems.push(item!);
87
87
  }
88
88
  }
89
- items = uniqueItems.sort((a, b) => b.pubDate - a.pubDate).slice(0, ctx.req.query('limit') || 20);
89
+ items = uniqueItems.toSorted((a, b) => b.pubDate - a.pubDate).slice(0, ctx.req.query('limit') || 20);
90
90
 
91
91
  items = await Promise.all(
92
92
  items.map((item) =>
@@ -1,7 +1,7 @@
1
1
  import ofetch from '@/utils/ofetch';
2
2
  import { load } from 'cheerio';
3
3
  import cache from '@/utils/cache';
4
- import { Route } from '@/types';
4
+ import { DataItem, Route } from '@/types';
5
5
  import pMap from 'p-map';
6
6
 
7
7
  export const route: Route = {
@@ -20,18 +20,20 @@ export const route: Route = {
20
20
  url: 'www.anthropic.com/news',
21
21
  };
22
22
 
23
- async function handler() {
23
+ async function handler(ctx) {
24
24
  const link = 'https://www.anthropic.com/news';
25
25
  const response = await ofetch(link);
26
26
  const $ = load(response);
27
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20;
27
28
 
28
- const list = $('.contentFadeUp a')
29
+ const list: DataItem[] = $('.contentFadeUp a')
29
30
  .toArray()
30
- .map((e) => {
31
- e = $(e);
32
- const title = e.find('h3[class^="PostCard_post-heading__"]').text().trim();
33
- const href = e.attr('href');
34
- const pubDate = e.find('div[class^="PostList_post-date__"]').text().trim();
31
+ .slice(0, limit)
32
+ .map((el) => {
33
+ const $el = $(el);
34
+ const title = $el.find('h3').text().trim();
35
+ const href = $el.attr('href') ?? '';
36
+ const pubDate = $el.find('p.detail-m.agate').text().trim() || $el.find('div[class^="PostList_post-date__"]').text().trim(); // legacy selector used roughly before Jan 2025
35
37
  const fullLink = href.startsWith('http') ? href : `https://www.anthropic.com${href}`;
36
38
  return {
37
39
  title,
@@ -43,8 +45,8 @@ async function handler() {
43
45
  const out = await pMap(
44
46
  list,
45
47
  (item) =>
46
- cache.tryGet(item.link, async () => {
47
- const response = await ofetch(item.link);
48
+ cache.tryGet(item.link!, async () => {
49
+ const response = await ofetch(item.link!);
48
50
  const $ = load(response);
49
51
 
50
52
  $('div[class^="PostDetail_b-social-share"]').remove();
@@ -61,7 +63,7 @@ async function handler() {
61
63
  }
62
64
  });
63
65
 
64
- item.description = content.html();
66
+ item.description = content.html() ?? undefined;
65
67
 
66
68
  return item;
67
69
  }),
@@ -81,7 +81,7 @@ async function handler(ctx) {
81
81
  }
82
82
  })
83
83
  .filter(Boolean)
84
- .sort((a, b) => b.pubDate - a.pubDate)
84
+ .toSorted((a, b) => b.pubDate - a.pubDate)
85
85
  .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20);
86
86
 
87
87
  const items = ctx.req.query('fulltext') === 'true' ? await pMap(list, (item) => fetchArticle(item), { concurrency: 10 }) : list;
@@ -79,7 +79,7 @@ async function handler(ctx) {
79
79
  return res;
80
80
  })
81
81
  .filter((e) => Boolean(e.link) && !new URL(e.link).pathname.split('/').includes('hub'))
82
- .sort((a, b) => (a.pubDate && b.pubDate ? b.pubDate - a.pubDate : b.lastmod - a.lastmod))
82
+ .toSorted((a, b) => (a.pubDate && b.pubDate ? b.pubDate - a.pubDate : b.lastmod - a.lastmod))
83
83
  .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20);
84
84
 
85
85
  const items = ctx.req.query('fulltext') === 'true' ? await pMap(list, (item) => fetchArticle(item), { concurrency: 20 }) : list;
@@ -130,7 +130,7 @@ async function handler(ctx) {
130
130
  return {
131
131
  title: `${appName} ${item.versionDisplay} for ${p}`,
132
132
  link: currentUrl,
133
- description: item.releaseNotes?.replace(/\n/g, '<br>'),
133
+ description: item.releaseNotes?.replaceAll('\n', '<br>'),
134
134
  category: [p],
135
135
  guid: `apple/apps/${country}/${id}/${pid}#${item.versionDisplay}`,
136
136
  pubDate: parseDate(item.releaseTimestamp),
@@ -147,7 +147,7 @@ async function handler(ctx) {
147
147
  item: items,
148
148
  title: `${title} - Apple App Store`,
149
149
  link: currentUrl,
150
- description: description?.replace(/\n/g, ' '),
150
+ description: description?.replaceAll('\n', ' '),
151
151
  language: $('html').prop('lang'),
152
152
  image: $('meta[property="og:image"]').prop('content'),
153
153
  icon,
@@ -1,7 +1,9 @@
1
1
  import { Route } from '@/types';
2
- import got from '@/utils/got';
2
+ import ofetch from '@/utils/ofetch';
3
3
  import { load } from 'cheerio';
4
4
  import { parseDate } from '@/utils/parse-date';
5
+ import cache from '@/utils/cache';
6
+ import { config } from '@/config';
5
7
 
6
8
  export const route: Route = {
7
9
  path: '/podcast/:id/:region?',
@@ -21,7 +23,7 @@ export const route: Route = {
21
23
  },
22
24
  radar: [
23
25
  {
24
- source: ['podcasts.apple.com/:region/podcast/:id'],
26
+ source: ['podcasts.apple.com/:region/podcast/:showName/:id', 'podcasts.apple.com/:region/podcast/:id'],
25
27
  },
26
28
  ],
27
29
  name: '播客',
@@ -32,42 +34,73 @@ export const route: Route = {
32
34
 
33
35
  async function handler(ctx) {
34
36
  const { id, region } = ctx.req.param();
35
- const link = `https://podcasts.apple.com/${region || `cn`}/podcast/${id}`;
36
- const response = await got({
37
- method: 'get',
38
- url: link,
39
- });
37
+ const numericId = id.match(/id(\d+)/)?.[1];
38
+ const baseUrl = 'https://podcasts.apple.com';
39
+ const link = `${baseUrl}/${region || `cn`}/podcast/${id}`;
40
40
 
41
- const $ = load(response.data);
41
+ const response = await ofetch(link);
42
42
 
43
- const schemaShow = JSON.parse($(String.raw`#schema\:show`).text());
44
- const serializedServerData = JSON.parse($('#serialized-server-data').text());
43
+ const $ = load(response);
45
44
 
46
- const seoEpisodes = schemaShow.workExample;
47
- const originEpisodes = serializedServerData[0].data.shelves.find((item) => item.contentType === 'episode').items;
45
+ const serializedServerData = JSON.parse($('#serialized-server-data').text());
48
46
  const header = serializedServerData[0].data.shelves.find((item) => item.contentType === 'showHeaderRegular').items[0];
49
47
 
50
- const episodes = originEpisodes.map((item) => {
48
+ const bearerToken = await cache.tryGet(
49
+ 'apple:podcast:bearer',
50
+ async () => {
51
+ const moduleAddress = new URL($('head script[type="module"]').attr('src'), baseUrl).href;
52
+ const modulesResponse = await ofetch(moduleAddress, {
53
+ parseResponse: (txt) => txt,
54
+ });
55
+ const bearerToken = modulesResponse.match(/="(eyJhbGci.*?)",/)[1];
56
+
57
+ return bearerToken as string;
58
+ },
59
+ config.cache.contentExpire,
60
+ false
61
+ );
62
+
63
+ const episodeReponse = await ofetch(`https://amp-api.podcasts.apple.com/v1/catalog/us/podcasts/${numericId}/episodes`, {
64
+ query: {
65
+ 'extend[podcast-channels]': 'editorialArtwork,subscriptionArtwork,subscriptionOffers',
66
+ include: 'channel',
67
+ limit: 25,
68
+ with: 'entitlements',
69
+ l: 'en-US',
70
+ },
71
+ headers: {
72
+ Authorization: `Bearer ${bearerToken}`,
73
+ Origin: baseUrl,
74
+ },
75
+ });
76
+
77
+ const episodes = episodeReponse.data.map(({ attributes: item }) => {
51
78
  // Try to keep line breaks in the description
52
- const matchedSeoEpisode = seoEpisodes.find((seoEpisode) => seoEpisode.name === item.title) || null;
53
- const episodeDescription = (matchedSeoEpisode ? matchedSeoEpisode.description : item.summary).replaceAll('\n', '<br>');
79
+ const offer = item.offers[0];
54
80
 
55
81
  return {
56
- title: item.title,
57
- enclosure_url: item.playAction.episodeOffer.streamUrl,
58
- enclosure_type: 'audio/mp4',
59
- itunes_duration: item.duration,
60
- link: item.playAction.episodeOffer.storeUrl,
61
- pubDate: parseDate(item.releaseDate),
62
- description: episodeDescription,
82
+ title: item.name,
83
+ enclosure_url: item.assetUrl || offer.hlsUrl,
84
+ enclosure_type: item.assetUrl ? 'audio/mp4' : 'application/vnd.apple.mpegurl',
85
+ itunes_duration: (item.durationInMilliseconds || offer.durationInMilliseconds) / 1000,
86
+ link: item.url,
87
+ pubDate: parseDate(item.releaseDateTime),
88
+ description: item.description.standard.replaceAll('\n', '<br>'),
89
+ author: item.artistName,
90
+ itunes_item_image: item.artwork.url.replace(/\{w\}x\{h\}(?:\{c\}|bb)\.\{f\}/, '3000x3000bb.webp'),
91
+ category: item.genreNames,
63
92
  };
64
93
  });
65
94
 
95
+ const channel = episodeReponse.data.find((d) => d.type === 'podcast-episodes').relationships.channel.data.find((d) => d.type === 'podcast-channels').attributes;
96
+
66
97
  return {
67
- title: header.title,
68
- link: header.contextAction.podcastOffer.storeUrl,
98
+ title: channel.name,
99
+ link: channel.url,
69
100
  itunes_author: header.contextAction.podcastOffer.author,
70
101
  item: episodes,
71
- description: header.description.replaceAll('\n', ' '),
102
+ description: (header.description || channel.description.standard).replaceAll('\n', ' '),
103
+ image: (channel.logoArtwork || channel.subscriptionArtwork).url.replace(/\{w\}x\{h\}(?:\{c\}|bb)\.\{f\}/, '3000x3000bb.webp'),
104
+ itunes_category: header.metadata.find((d) => Object.hasOwn(d, 'category')).category?.title || header.metadata.find((d) => Object.hasOwn(d, 'category')).category,
72
105
  };
73
106
  }
@@ -56,7 +56,7 @@ async function handler(ctx) {
56
56
  date: $el.children().first().find('small').children().remove().end().text().slice(3),
57
57
  };
58
58
  });
59
- const finalLatestReplies = [...latestReplies, ...latestSubReplies].sort((a, b) => (a.id < b.id ? 1 : -1));
59
+ const finalLatestReplies = [...latestReplies, ...latestSubReplies].toSorted((a, b) => (a.id < b.id ? 1 : -1));
60
60
 
61
61
  const postTopic = {
62
62
  title,
@@ -6,6 +6,17 @@ import { config } from '@/config';
6
6
  import logger from '@/utils/logger';
7
7
  import { getPuppeteerPage } from '@/utils/puppeteer';
8
8
  import { JSDOM } from 'jsdom';
9
+ import { RateLimiterMemory, RateLimiterQueue } from 'rate-limiter-flexible';
10
+
11
+ const subtitleLimiter = new RateLimiterMemory({
12
+ points: 5,
13
+ duration: 1,
14
+ execEvenly: true,
15
+ });
16
+
17
+ const subtitleLimiterQueue = new RateLimiterQueue(subtitleLimiter, {
18
+ maxQueueSize: 4800,
19
+ });
9
20
 
10
21
  const getCookie = (disableConfig = false) => {
11
22
  if (Object.keys(config.bilibili.cookies).length > 0 && !disableConfig) {
@@ -200,7 +211,7 @@ const getCidFromId = (aid, pid, bvid) => {
200
211
  const { data } = await got(`https://api.bilibili.com/x/web-interface/view?${bvid ? `bvid=${bvid}` : `aid=${aid}`}`, {
201
212
  referer: `https://www.bilibili.com/video/${bvid || `av${aid}`}`,
202
213
  });
203
- return data.data.pages[pid - 1].cid;
214
+ return data?.data?.pages[pid - 1]?.cid;
204
215
  });
205
216
  };
206
217
 
@@ -244,8 +255,13 @@ const getVideoSubtitle = async (
244
255
  }
245
256
 
246
257
  const cid = await getCidFromId(undefined, 1, bvid);
258
+ if (!cid) {
259
+ return [];
260
+ }
261
+
247
262
  const cookie = await getCookie();
248
263
  return cache.tryGet(`bili-video-subtitle-${bvid}`, async () => {
264
+ await subtitleLimiterQueue.removeTokens(1);
249
265
  const response = await got(`https://api.bilibili.com/x/player/wbi/v2?bvid=${bvid}&cid=${cid}`, {
250
266
  headers: {
251
267
  Referer: `https://www.bilibili.com/video/${bvid}`,
@@ -274,7 +290,7 @@ const getVideoSubtitle = async (
274
290
  const getVideoSubtitleAttachment = async (bvid: string) => {
275
291
  const subtitles = await getVideoSubtitle(bvid);
276
292
  return subtitles.map((subtitle) => ({
277
- url: `data:text/plain;charset=utf-8,${subtitle.content}`,
293
+ url: `data:text/plain;charset=utf-8,${encodeURIComponent(subtitle.content)}`,
278
294
  mime_type: 'text/srt',
279
295
  title: `字幕 - ${subtitle.lan_doc}`,
280
296
  }));
@@ -126,34 +126,58 @@ const getIframe = (data?: Modules, embed: boolean = true) => {
126
126
  };
127
127
 
128
128
  const getImgs = (data?: Modules) => {
129
- const imgUrls: string[] = [];
129
+ const imgUrls: {
130
+ url: string;
131
+ width?: number;
132
+ height?: number;
133
+ }[] = [];
130
134
  const major = data?.module_dynamic?.major;
131
135
  if (!major) {
132
136
  return '';
133
137
  }
134
138
  // 动态图片
135
139
  if (major.opus?.pics?.length) {
136
- imgUrls.push(...major.opus.pics.map((e) => e.url));
140
+ imgUrls.push(
141
+ ...major.opus.pics.map((e) => ({
142
+ url: e.url,
143
+ width: e.width,
144
+ height: e.height,
145
+ }))
146
+ );
137
147
  }
138
148
  // 专栏封面
139
149
  if (major.article?.covers?.length) {
140
- imgUrls.push(...major.article.covers);
150
+ imgUrls.push(
151
+ ...major.article.covers.map((e) => ({
152
+ url: e,
153
+ }))
154
+ );
141
155
  }
142
156
  // 相簿
143
157
  if (major.draw?.items?.length) {
144
- imgUrls.push(...major.draw.items.map((e) => e.src));
158
+ imgUrls.push(
159
+ ...major.draw.items.map((e) => ({
160
+ url: e.src,
161
+ width: e.width,
162
+ height: e.height,
163
+ }))
164
+ );
145
165
  }
146
166
  // 正在直播的动态
147
167
  if (major.live_rcmd?.content) {
148
- imgUrls.push(JSON.parse(major.live_rcmd.content)?.live_play_info?.cover);
168
+ imgUrls.push({
169
+ url: JSON.parse(major.live_rcmd.content)?.live_play_info?.cover,
170
+ });
149
171
  }
150
172
  const type = major.type.replace('MAJOR_TYPE_', '').toLowerCase();
151
173
  if (major[type]?.cover) {
152
- imgUrls.push(major[type].cover);
174
+ imgUrls.push({
175
+ url: major[type]?.cover,
176
+ });
153
177
  }
154
178
  return imgUrls
155
179
  .filter(Boolean)
156
- .map((url) => `<img src="${url}">`)
180
+ .map((img) => `<img src="${img.url}" ${img.width ? `width="${img.width}"` : ''} ${img.height ? `height="${img.height}"` : ''}>`)
157
181
  .join('');
158
182
  };
159
183
 
@@ -45,7 +45,7 @@ async function handler(ctx) {
45
45
  link,
46
46
  description: `视频 ${name} 的视频选集列表`,
47
47
  item: data
48
- .sort((a, b) => b.page - a.page)
48
+ .toSorted((a, b) => b.page - a.page)
49
49
  .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 10)
50
50
  .map((item) => ({
51
51
  title: item.part,
@@ -2,6 +2,8 @@ import { Route, ViewType } from '@/types';
2
2
  import got from '@/utils/got';
3
3
  import utils, { getVideoUrl } from './utils';
4
4
  import { parseDuration } from '@/utils/helpers';
5
+ import { config } from '@/config';
6
+ import cache from './cache';
5
7
 
6
8
  // https://www.bilibili.com/v/popular/rank/all
7
9
 
@@ -212,22 +214,26 @@ async function handler(ctx) {
212
214
  return {
213
215
  title: `bilibili 排行榜-${ridChinese}`,
214
216
  link,
215
- item: list.map((item) => ({
216
- title: item.title,
217
- description: utils.renderUGCDescription(embed, item.pic, item.description || item.title, item.aid, undefined, item.bvid),
218
- pubDate: item.create && new Date(item.create).toUTCString(),
219
- author: item.author,
220
- link: !item.create || (new Date(item.create).getTime() / 1000 > utils.bvidTime && item.bvid) ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`,
221
- image: item.pic,
222
- attachments: item.bvid
223
- ? [
224
- {
225
- url: getVideoUrl(item.bvid),
226
- mime_type: 'text/html',
227
- duration_in_seconds: parseDuration(item.duration),
228
- },
229
- ]
230
- : undefined,
231
- })),
217
+ item: list.map(async (item) => {
218
+ const subtitles = !config.bilibili.excludeSubtitles && item.bvid ? await cache.getVideoSubtitleAttachment(item.bvid) : [];
219
+ return {
220
+ title: item.title,
221
+ description: utils.renderUGCDescription(embed, item.pic, item.description || item.title, item.aid, undefined, item.bvid),
222
+ pubDate: item.create && new Date(item.create).toUTCString(),
223
+ author: item.author,
224
+ link: !item.create || (new Date(item.create).getTime() / 1000 > utils.bvidTime && item.bvid) ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`,
225
+ image: item.pic,
226
+ attachments: item.bvid
227
+ ? [
228
+ {
229
+ url: getVideoUrl(item.bvid),
230
+ mime_type: 'text/html',
231
+ duration_in_seconds: parseDuration(item.duration),
232
+ },
233
+ ...subtitles,
234
+ ]
235
+ : undefined,
236
+ };
237
+ }),
232
238
  };
233
239
  }
@@ -595,7 +595,7 @@
595
595
  }
596
596
  argvPtrs.push(0);
597
597
 
598
- const keys = Object.keys(this.env).sort();
598
+ const keys = Object.keys(this.env).toSorted();
599
599
  for (const key of keys) {
600
600
  argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
601
601
  }