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
@@ -1,101 +1,297 @@
1
- import { Route } from '@/types';
1
+ import { type Data, type DataItem, type Route, ViewType } from '@/types';
2
2
 
3
+ import { art } from '@/utils/render';
3
4
  import cache from '@/utils/cache';
4
- import got from '@/utils/got';
5
- import { load } from 'cheerio';
5
+ import ofetch from '@/utils/ofetch';
6
6
  import { parseDate } from '@/utils/parse-date';
7
- import { art } from '@/utils/render';
7
+
8
+ import { type CheerioAPI, type Cheerio, load } from 'cheerio';
9
+ import type { Element } from 'domhandler';
10
+ import { type Context } from 'hono';
8
11
  import path from 'node:path';
9
12
 
10
- export const route: Route = {
11
- path: '/blog/:category{.+}?',
12
- name: 'Unknown',
13
- maintainers: [],
14
- handler,
15
- };
13
+ export const handler = async (ctx: Context): Promise<Data> => {
14
+ const { category } = ctx.req.param();
15
+ const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10);
16
16
 
17
- async function handler(ctx) {
18
- const category = ctx.req.param('category');
19
- const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15;
17
+ const domain: string = 'surfshark.com';
18
+ const baseUrl: string = `https://${domain}`;
19
+ const targetUrl: string = new URL(`blog${category ? `/${category}` : ''}`, baseUrl).href;
20
20
 
21
- const rootUrl = 'https://surfshark.com';
22
- const currentUrl = new URL(`blog${category ? `/${category}` : ''}`, rootUrl).href;
21
+ const headers = {
22
+ host: 'surfshark.com',
23
+ origin: baseUrl,
24
+ referer: targetUrl,
25
+ };
23
26
 
24
- const { data: response } = await got(currentUrl);
27
+ const response = await ofetch(targetUrl, {
28
+ headers,
29
+ });
30
+ const $: CheerioAPI = load(response);
31
+ const language = $('html').attr('lang') ?? 'en';
25
32
 
26
- const $ = load(response);
33
+ let items: DataItem[] = [];
27
34
 
28
- let items = $('div.article-info')
35
+ items = $('div.dg-article-single-card')
29
36
  .slice(0, limit)
30
37
  .toArray()
31
- .map((item) => {
32
- item = $(item);
33
-
34
- const a = item.find('a');
35
-
36
- return {
37
- title: a.prop('title'),
38
- link: a.prop('href'),
39
- author: item.find('div.author, div.name').text().split('in')[0].trim(),
40
- category: item
41
- .find('div.author, div.name')
42
- .find('a')
43
- .toArray()
44
- .map((c) => $(c).text()),
45
- pubDate: parseDate(item.find('div.date, div.time').text().split('·')[0].trim(), 'YYYY, MMMM D'),
38
+ .map((el): Element => {
39
+ const $el: Cheerio<Element> = $(el);
40
+ const $aEl: Cheerio<Element> = $el.find('div.dg-article-content__info a[title]');
41
+
42
+ const title: string = $aEl.text();
43
+ const image: string | undefined = $el.find('a.dg-article-image img').attr('src');
44
+ const description: string | undefined = art(path.join(__dirname, 'templates/description.art'), {
45
+ images: image
46
+ ? [
47
+ {
48
+ src: image,
49
+ alt: title,
50
+ },
51
+ ]
52
+ : undefined,
53
+ });
54
+
55
+ const pubDateEl: Cheerio<Element> = $el.find('span.js-date');
56
+ const pubDateStr: string | undefined = `${pubDateEl.attr('data-year')}-${pubDateEl.attr('data-month')}-${pubDateEl.attr('data-day')}`;
57
+
58
+ const linkUrl: string | undefined = $aEl.attr('href');
59
+ const categoryEls: Element[] = $el.find('div.dg-blog-breadcrumbs a').toArray();
60
+ const categories: string[] = [
61
+ ...new Set(
62
+ categoryEls
63
+ .map((el) => $(el).text())
64
+ .slice(1)
65
+ .filter(Boolean)
66
+ ),
67
+ ];
68
+ const authorEls: Element[] = $el.find('a.author-link ').toArray();
69
+ const authors: DataItem['author'] = authorEls.map((authorEl) => {
70
+ const $authorEl: Cheerio<Element> = $(authorEl);
71
+
72
+ return {
73
+ name: $authorEl.text(),
74
+ url: $authorEl.attr('href'),
75
+ avatar: undefined,
76
+ };
77
+ });
78
+ const upDatedStr: string | undefined = pubDateStr;
79
+
80
+ const processedItem: DataItem = {
81
+ title,
82
+ description,
83
+ pubDate: parseDate(pubDateStr),
84
+ link: linkUrl,
85
+ category: categories,
86
+ author: authors,
87
+ content: {
88
+ html: description,
89
+ text: description,
90
+ },
91
+ image,
92
+ banner: image,
93
+ updated: parseDate(upDatedStr),
94
+ language,
46
95
  };
96
+
97
+ return processedItem;
47
98
  });
48
99
 
49
100
  items = await Promise.all(
50
- items.map((item) =>
51
- cache.tryGet(item.link, async () => {
52
- const { data: detailResponse } = await got(item.link);
53
-
54
- const content = load(detailResponse);
55
-
56
- content('div.post-main-img').each(function () {
57
- const image = content(this).find('img');
58
-
59
- content(this).replaceWith(
60
- art(path.join(__dirname, 'templates/description.art'), {
61
- image: {
62
- src: image
63
- .prop('srcset')
64
- .match(/(https?:.*?\.\w+\s)/g)
65
- .pop()
66
- .trim(),
67
- alt: image.prop('alt'),
68
- },
69
- })
70
- );
101
+ items.map((item) => {
102
+ if (!item.link) {
103
+ return item;
104
+ }
105
+
106
+ return cache.tryGet(item.link, async (): Promise<DataItem> => {
107
+ const detailResponse = await ofetch(item.link, {
108
+ headers,
71
109
  });
110
+ const $$: CheerioAPI = load(detailResponse);
72
111
 
73
- item.title = content('div.blog-post-title').text();
74
- item.description = content('div.post-content').html();
75
- item.author = content('meta[name="author"]').prop('content');
76
- item.category = content('div.name')
77
- .find('a')
78
- .toArray()
79
- .map((c) => content(c).text());
80
- item.pubDate = parseDate(content('meta[property="article:published_time"]').prop('content'));
112
+ const title: string = $$('meta[property="og:title"]').attr('content') ?? item.title;
113
+ const image: string | undefined = $$('div.dg-featured-img-container img').attr('src');
114
+ const description: string | undefined = art(path.join(__dirname, 'templates/description.art'), {
115
+ images: image
116
+ ? [
117
+ {
118
+ src: image,
119
+ alt: title,
120
+ },
121
+ ]
122
+ : undefined,
123
+ description: $$('div.dg-blog-post-blocks').html(),
124
+ });
125
+ const pubDateStr: string | undefined = $$('meta[property="article:published_time"]').attr('content');
126
+ const authorEls: Element[] = $$('div.dg-blog-post-author-top').toArray();
127
+ const authors: DataItem['author'] = authorEls.map((authorEl) => {
128
+ const $$authorEl: Cheerio<Element> = $$(authorEl);
81
129
 
82
- return item;
83
- })
84
- )
85
- );
130
+ return {
131
+ name: $$authorEl.find('a.author-link').last().text(),
132
+ url: $$authorEl.find('a.author-link').last().attr('href'),
133
+ avatar: $$authorEl.find('a.author-avatar img').attr('src'),
134
+ };
135
+ });
136
+ const upDatedStr: string | undefined = $$('meta[property="article:modified_time"]').attr('content');
137
+
138
+ const processedItem: DataItem = {
139
+ title,
140
+ description,
141
+ pubDate: pubDateStr ? parseDate(pubDateStr) : item.pubDate,
142
+ author: authors,
143
+ content: {
144
+ html: description,
145
+ text: description,
146
+ },
147
+ image,
148
+ banner: image,
149
+ updated: upDatedStr ? parseDate(upDatedStr) : item.updated,
150
+ language,
151
+ };
86
152
 
87
- const icon = new URL($('link[rel="apple-touch-icon"]').prop('href'), rootUrl).href;
153
+ return {
154
+ ...item,
155
+ ...processedItem,
156
+ };
157
+ });
158
+ })
159
+ );
88
160
 
89
161
  return {
90
- item: items,
91
162
  title: $('title').text(),
92
- link: currentUrl,
93
- description: $('meta[property="og:description"]').prop('content'),
94
- language: $('html').prop('lang'),
95
- image: $('meta[property="og:image"]').prop('content'),
96
- icon,
97
- logo: icon,
98
- subtitle: $('meta[property="og:title"]').prop('content'),
99
- author: $('meta[property="og:site_name"]').prop('content'),
163
+ description: $('meta[property="og:description"]').attr('content'),
164
+ link: targetUrl,
165
+ item: items,
166
+ allowEmpty: true,
167
+ image: $('meta[property="og:image"]').attr('content'),
168
+ author: $('meta[property="og:site_name"]').attr('content'),
169
+ language,
170
+ id: $('meta[property="og:url"]').attr('content'),
100
171
  };
101
- }
172
+ };
173
+
174
+ export const route: Route = {
175
+ path: '/blog/:category{.+}?',
176
+ name: 'Blog',
177
+ url: 'surfshark.com',
178
+ maintainers: ['nczitzk'],
179
+ handler,
180
+ example: '/surfshark/blog',
181
+ parameters: {
182
+ category: {
183
+ description: 'Category, All by default',
184
+ options: [
185
+ {
186
+ label: 'All',
187
+ value: '',
188
+ },
189
+ {
190
+ label: 'Cybersecurity',
191
+ value: 'cybersecurity',
192
+ },
193
+ {
194
+ label: 'All things VPN',
195
+ value: 'all-things-vpn',
196
+ },
197
+ {
198
+ label: 'Internet censorship',
199
+ value: 'internet-censorship',
200
+ },
201
+ {
202
+ label: 'Entertainment',
203
+ value: 'entertainment',
204
+ },
205
+ {
206
+ label: 'Expert Insights',
207
+ value: 'expert-insights',
208
+ },
209
+ {
210
+ label: 'Video',
211
+ value: 'video',
212
+ },
213
+ {
214
+ label: 'News',
215
+ value: 'news',
216
+ },
217
+ ],
218
+ },
219
+ },
220
+ description: `:::tip
221
+ To subscribe to [Cybersecurity](https://surfshark.com/blog/cybersecurity), where the source URL is \`https://surfshark.com/blog/cybersecurity\`, extract the certain parts from this URL to be used as parameters, resulting in the route as [\`/surfshark/blog/cybersecurity\`](https://rsshub.app/surfshark/blog/cybersecurity).
222
+ :::
223
+
224
+ <details>
225
+ <summary>More categories</summary>
226
+
227
+ | Category | ID |
228
+ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
229
+ | [All](https://surfshark.com/blog) | (empty) |
230
+ | [Cybersecurity](https://surfshark.com/blog/cybersecurity) | [cybersecurity](https://rsshub.app/surfshark/blog/cybersecurity) |
231
+ | [All things VPN](https://surfshark.com/blog/all-things-vpn) | [all-things-vpn](https://rsshub.app/surfshark/blog/all-things-vpn) |
232
+ | [Internet censorship](https://surfshark.com/blog/internet-censorship) | [internet-censorship](https://rsshub.app/surfshark/blog/internet-censorship) |
233
+ | [Entertainment](https://surfshark.com/blog/entertainment) | [entertainment](https://rsshub.app/surfshark/blog/entertainment) |
234
+ | [Expert Insights](https://surfshark.com/blog/expert-insights) | [expert-insights](https://rsshub.app/surfshark/blog/expert-insights) |
235
+ | [Video](https://surfshark.com/blog/video) | [video](https://rsshub.app/surfshark/blog/video) |
236
+ | [News](https://surfshark.com/blog/news) | [news](https://rsshub.app/surfshark/blog/news) |
237
+
238
+ </details>
239
+ `,
240
+ categories: ['new-media'],
241
+ features: {
242
+ requireConfig: false,
243
+ requirePuppeteer: false,
244
+ antiCrawler: false,
245
+ supportRadar: true,
246
+ supportBT: false,
247
+ supportPodcast: false,
248
+ supportScihub: false,
249
+ },
250
+ radar: [
251
+ {
252
+ source: ['surfshark.com/blog/:category'],
253
+ target: '/blog/:category',
254
+ },
255
+ {
256
+ title: 'All',
257
+ source: ['surfshark.com/blog'],
258
+ target: '/blog',
259
+ },
260
+ {
261
+ title: 'Cybersecurity',
262
+ source: ['surfshark.com/blog/cybersecurity'],
263
+ target: '/blog/cybersecurity',
264
+ },
265
+ {
266
+ title: 'All things VPN',
267
+ source: ['surfshark.com/blog/all-things-vpn'],
268
+ target: '/blog/all-things-vpn',
269
+ },
270
+ {
271
+ title: 'Internet censorship',
272
+ source: ['surfshark.com/blog/internet-censorship'],
273
+ target: '/blog/internet-censorship',
274
+ },
275
+ {
276
+ title: 'Entertainment',
277
+ source: ['surfshark.com/blog/entertainment'],
278
+ target: '/blog/entertainment',
279
+ },
280
+ {
281
+ title: 'Expert Insights',
282
+ source: ['surfshark.com/blog/expert-insights'],
283
+ target: '/blog/expert-insights',
284
+ },
285
+ {
286
+ title: 'Video',
287
+ source: ['surfshark.com/blog/video'],
288
+ target: '/blog/video',
289
+ },
290
+ {
291
+ title: 'News',
292
+ source: ['surfshark.com/blog/news'],
293
+ target: '/blog/news',
294
+ },
295
+ ],
296
+ view: ViewType.Articles,
297
+ };
@@ -1,5 +1,17 @@
1
- {{ if image }}
2
- <figure>
3
- <img src="{{ image.src }}" alt="{{ image.alt }}">
4
- </figure>
1
+ {{ if images }}
2
+ {{ each images image }}
3
+ {{ if image?.src }}
4
+ <figure>
5
+ <img
6
+ {{ if image.alt }}
7
+ alt="{{ image.alt }}"
8
+ {{ /if }}
9
+ src="{{ image.src }}">
10
+ </figure>
11
+ {{ /if }}
12
+ {{ /each }}
13
+ {{ /if }}
14
+
15
+ {{ if description }}
16
+ {{@ description }}
5
17
  {{ /if }}
@@ -31,7 +31,7 @@ export const route: Route = {
31
31
  const findLargestImgKey = (images) =>
32
32
  Object.keys(images)
33
33
  .filter((key) => key.startsWith('inline_free_') || key.startsWith('hero_landscape_'))
34
- .sort((a, b) => Number.parseInt(b.split('_')[2]) - Number.parseInt(a.split('_')[2]))[0];
34
+ .toSorted((a, b) => Number.parseInt(b.split('_')[2]) - Number.parseInt(a.split('_')[2]))[0];
35
35
 
36
36
  const renderFigure = (url, caption) => `<figure><img src="${url}" alt="${caption}" /><figcaption>${caption}</figcaption></figure>`;
37
37
 
@@ -49,7 +49,7 @@ async function handler(): Promise<Data> {
49
49
  const updates = dates
50
50
  .map((date, index) => ({
51
51
  date,
52
- content: contents[index]?.replace(/\n/g, '<br>') ?? '',
52
+ content: contents[index]?.replaceAll('\n', '<br>') ?? '',
53
53
  }))
54
54
  .filter((update) => update.content);
55
55
 
@@ -70,7 +70,7 @@ export async function handleIsekaiRanking(type: string, limit: number): Promise<
70
70
  }
71
71
 
72
72
  const items = uniqueNovels
73
- .sort((a, b) => (b[pointField] || 0) - (a[pointField] || 0))
73
+ .toSorted((a, b) => (b[pointField] || 0) - (a[pointField] || 0))
74
74
  .map((novel, index) => ({
75
75
  title: `#${index + 1} ${novel.title}`,
76
76
  link: `https://ncode.syosetu.com/${String(novel.ncode).toLowerCase()}`,
@@ -3,14 +3,52 @@ import { type Data, type DataItem, type Route, ViewType } from '@/types';
3
3
  import ofetch from '@/utils/ofetch';
4
4
  import { parseDate } from '@/utils/parse-date';
5
5
  import timezone from '@/utils/timezone';
6
-
7
6
  import { type CheerioAPI, load } from 'cheerio';
8
7
  import { type Context } from 'hono';
9
8
 
9
+ function isValidDate(dateString: string): boolean {
10
+ // 正则表达式检查格式:YYYY-MM-DD
11
+ const regex = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
12
+ if (!regex.test(dateString)) {
13
+ return false;
14
+ }
15
+
16
+ // 解析日期并验证有效性(避免像 2025-02-30 这样的无效日期)
17
+ const [year, month, day] = dateString.split('-').map(Number);
18
+ const date = new Date(year, month - 1, day); // 月份从 0 开始
19
+
20
+ // 检查解析后的日期是否与输入一致
21
+ return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
22
+ }
23
+
10
24
  export const handler = async (ctx: Context): Promise<Data> => {
11
25
  const { category = '' } = ctx.req.param();
12
26
  const limit: number = Number.parseInt(ctx.req.query('limit') ?? '50', 10);
13
-
27
+ const query: string = ctx.req.param('query') ?? '';
28
+ const queries: Record<string, string> = {
29
+ stock: '',
30
+ beginDate: '',
31
+ endDate: '',
32
+ };
33
+ if (query) {
34
+ for (const pair of query.split('&')) {
35
+ const [key, value] = pair.split('=');
36
+ if (key) {
37
+ queries[key] = value;
38
+ }
39
+ }
40
+ }
41
+ if (queries.beginDate && !isValidDate(queries.beginDate)) {
42
+ throw new Error('Invalid beginDate format. Expected YYYY-MM-DD');
43
+ }
44
+ if (queries.endDate) {
45
+ if (!isValidDate(queries.endDate)) {
46
+ throw new Error('Invalid endDate format. Expected YYYY-MM-DD');
47
+ }
48
+ } else if (queries.beginDate) {
49
+ // 如果只提供了开始日期,则将结束日期设置为开始日期
50
+ queries.endDate = queries.beginDate;
51
+ }
14
52
  const baseUrl: string = 'https://www.szse.cn';
15
53
  const staticBaseUrl: string = 'https://disc.static.szse.cn';
16
54
  const apiUrl: string = new URL('api/disc/announcement/annList', baseUrl).href;
@@ -19,11 +57,11 @@ export const handler = async (ctx: Context): Promise<Data> => {
19
57
  const targetResponse = await ofetch(targetUrl);
20
58
  const $: CheerioAPI = load(targetResponse);
21
59
  const language = $('html').attr('lang') ?? 'zh-CN';
22
-
23
60
  const response = await ofetch(apiUrl, {
24
61
  method: 'POST',
25
62
  body: {
26
- seDate: ['', ''],
63
+ stock: queries.stock ? [queries.stock] : [],
64
+ seDate: [queries.beginDate, queries.endDate],
27
65
  channelCode: ['listedNotice_disc'],
28
66
  pageSize: limit,
29
67
  pageNum: 1,
@@ -83,13 +121,13 @@ export const handler = async (ctx: Context): Promise<Data> => {
83
121
  };
84
122
 
85
123
  export const route: Route = {
86
- path: '/disclosure/listed/notice',
124
+ path: '/disclosure/listed/notice/:query?',
87
125
  name: '上市公司公告',
88
126
  url: 'www.szse.cn',
89
127
  maintainers: ['nczitzk'],
90
128
  handler,
91
129
  example: '/szse/disclosure/listed/notice',
92
- parameters: undefined,
130
+ parameters: { query: 'Filter options. can filte by "stock","beginDate","endDate". example:"stock=000001&beginDate=2025-07-01&endDate=2025-08-30"' },
93
131
  description: undefined,
94
132
  categories: ['finance'],
95
133
  features: {