rsshub 1.0.0-master.f9003f9 → 1.0.0-master.f93bb8c

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.
@@ -1,7 +1,7 @@
1
1
  import { type NotFoundHandler, type ErrorHandler } from 'hono';
2
2
  import { getDebugInfo, setDebugInfo } from '@/utils/debug-info';
3
3
  import { config } from '@/config';
4
- import Sentry from '@sentry/node';
4
+ import * as Sentry from '@sentry/node';
5
5
  import logger from '@/utils/logger';
6
6
  import Error from '@/views/error';
7
7
 
@@ -53,7 +53,7 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
53
53
  // https://stackoverflow.com/questions/1497885/remove-control-characters-from-php-string/1497928#1497928
54
54
  // remove unicode control characters
55
55
  // see #14940 #14943 #15262
56
- item.description = item.description.replaceAll(/[\u0000-\u0009\u000B\u000C\u000E-\u001F\u007F]/g, '');
56
+ item.description = item.description.replaceAll(/[\u0000-\u0009\u000B\u000C\u000E-\u001F\u007F\u200B\uFFFF]/g, '');
57
57
  }
58
58
 
59
59
  if (typeof item.author === 'string') {
package/lib/router.js CHANGED
@@ -594,12 +594,6 @@ router.get('/zucc/cssearch/latest/:webVpn/:key', lazyloadRouteHandler('./routes/
594
594
  // checkee
595
595
  router.get('/checkee/:dispdate', lazyloadRouteHandler('./routes/checkee/index'));
596
596
 
597
- // Matters
598
- router.get('/matters/latest/:type?', lazyloadRouteHandler('./routes/matters/latest'));
599
- router.redirect('/matters/hot', '/matters/latest/heat'); // Deprecated
600
- router.get('/matters/tags/:tid', lazyloadRouteHandler('./routes/matters/tags'));
601
- router.get('/matters/author/:uid', lazyloadRouteHandler('./routes/matters/author'));
602
-
603
597
  // 古诗文网
604
598
  router.get('/gushiwen/recommend/:annotation?', lazyloadRouteHandler('./routes/gushiwen/recommend'));
605
599
 
@@ -26,7 +26,7 @@ function getDomList($, detailUrl) {
26
26
  return list;
27
27
  }
28
28
 
29
- function getItemList($, detailUrl, second) {
29
+ export function getItemList($, detailUrl, second) {
30
30
  const encoded = $('.article script[type]')
31
31
  .text()
32
32
  .match(/return p}\('(.*)',(\d+),(\d+),'(.*)'.split\(/);
@@ -0,0 +1,71 @@
1
+ import { Route } from '@/types';
2
+ import got from '@/utils/got';
3
+ import { load } from 'cheerio';
4
+ import { getItemList as detailItemList } from './detail';
5
+
6
+ import { defaultDomain, ensureDomain } from './utils';
7
+ import cache from '@/utils/cache';
8
+
9
+ function getItemList($) {
10
+ const list = $(`#vod .list-group-item`)
11
+ .toArray()
12
+ .map((item) => {
13
+ item = $(item);
14
+ return {
15
+ title: item.find('a').text(),
16
+ publishDate: item.find('b').text(),
17
+ link: `https://${defaultDomain}${item.find('a').attr('href')}`, // fixed domain for guid
18
+ };
19
+ })
20
+ .filter((item) => !item.title.includes('话') && !item.title.includes('集') && !item.title.includes('更新至'));
21
+ return list;
22
+ }
23
+
24
+ export const route: Route = {
25
+ path: '/latest_movie_bt',
26
+ categories: ['multimedia'],
27
+ example: '/domp4/latest_movie_bt',
28
+ parameters: {},
29
+ features: {
30
+ requireConfig: false,
31
+ requirePuppeteer: false,
32
+ antiCrawler: false,
33
+ supportBT: true,
34
+ supportPodcast: false,
35
+ supportScihub: false,
36
+ },
37
+ radar: [
38
+ {
39
+ source: ['domp4.cc/', 'domp4.cc/custom/update.html'],
40
+ },
41
+ ],
42
+ name: '最近更新的电源BT列表',
43
+ maintainers: ['xianghuawe'],
44
+ handler,
45
+ url: 'domp4.cc/',
46
+ };
47
+
48
+ async function handler(ctx) {
49
+ const { domain, second } = ctx.req.query();
50
+ const hostUrl = ensureDomain(ctx, domain);
51
+ const latestUrl = `${hostUrl}/custom/update.html`;
52
+ const res = await got.get(latestUrl);
53
+ const $ = load(res.data);
54
+ const list = getItemList($);
55
+ const process = await Promise.all(
56
+ list.map(
57
+ async (item) =>
58
+ await cache.tryGet(item.link, async () => {
59
+ const response = await got.get(item.link);
60
+ const $ = load(response.data);
61
+ return detailItemList($, item.link, second);
62
+ })
63
+ )
64
+ );
65
+
66
+ return {
67
+ link: latestUrl,
68
+ title: 'domp4电影',
69
+ item: process.filter((item) => item !== undefined).flat(),
70
+ };
71
+ }
@@ -2,5 +2,5 @@ import type { Namespace } from '@/types';
2
2
 
3
3
  export const namespace: Namespace = {
4
4
  name: 'fanbox',
5
- url: 'https://www.fanbox.cc',
5
+ url: 'www.fanbox.cc',
6
6
  };
@@ -0,0 +1,7 @@
1
+ import type { Namespace } from '@/types';
2
+
3
+ export const namespace: Namespace = {
4
+ name: '番茄小说',
5
+ url: 'fanqienovel.com',
6
+ categories: ['reading'],
7
+ };
@@ -0,0 +1,100 @@
1
+ import type { Data, Route } from '@/types';
2
+ import type { Context } from 'hono';
3
+ import ofetch from '@/utils/ofetch';
4
+ import * as cheerio from 'cheerio';
5
+ import { parseDate } from '@/utils/parse-date';
6
+
7
+ interface Chapter {
8
+ itemId: string;
9
+ needPay: number;
10
+ title: string;
11
+ isChapterLock: boolean;
12
+ isPaidPublication: boolean;
13
+ isPaidStory: boolean;
14
+ volume_name: string;
15
+ firstPassTime: string;
16
+ }
17
+
18
+ interface Page {
19
+ hasFetch: boolean;
20
+ author: string;
21
+ authorId: string;
22
+ bookId: string;
23
+ mediaId: string;
24
+ bookName: string;
25
+ status: number;
26
+ category: string;
27
+ categoryV2: string;
28
+ abstract: string;
29
+ thumbUri: string;
30
+ creationStatus: number;
31
+ wordNumber: number;
32
+ readCount: number;
33
+ description: string;
34
+ avatarUri: string;
35
+ creatorId: string;
36
+ lastPublishTime: string;
37
+ lastChapterItemId: string;
38
+ lastChapterTitle: string;
39
+ volumeNameList: string[];
40
+ chapterListWithVolume: Chapter[][];
41
+ chapterTotal: number;
42
+ followStatus: number;
43
+ itemIds: string[];
44
+ hasFetchDirectory: boolean;
45
+ chapterList: any[];
46
+ serverRendered: boolean;
47
+ genre: string;
48
+ platform: string;
49
+ type: string;
50
+ originalAuthors: string;
51
+ completeCategory: string;
52
+ }
53
+
54
+ export const route: Route = {
55
+ path: '/page/:bookId',
56
+ example: '/fanqienovel/page/6621052928482348040',
57
+ parameters: { bookId: '小说 ID,可在 URL 中找到' },
58
+ maintainers: ['TonyRL'],
59
+ name: '小说更新',
60
+ handler,
61
+ radar: [
62
+ {
63
+ source: ['fanqienovel.com/page/:bookId'],
64
+ },
65
+ ],
66
+ };
67
+
68
+ async function handler(ctx: Context): Promise<Data> {
69
+ const { bookId } = ctx.req.param();
70
+ const link = `https://fanqienovel.com/page/${bookId}`;
71
+
72
+ const response = await ofetch(link);
73
+ const $ = cheerio.load(response);
74
+
75
+ const initialState = JSON.parse(
76
+ $('script:contains("window.__INITIAL_STATE__")')
77
+ .text()
78
+ .match(/window\.__INITIAL_STATE__\s*=\s*(.*);/)?.[1] ?? '{}'
79
+ );
80
+
81
+ const page = initialState.page as Page;
82
+ const items = page.chapterListWithVolume.flatMap((volume) =>
83
+ volume.map((chapter) => ({
84
+ title: chapter.title,
85
+ link: `https://fanqienovel.com/reader/${chapter.itemId}`,
86
+ description: chapter.volume_name,
87
+ pubDate: parseDate(chapter.firstPassTime, 'X'),
88
+ author: page.author,
89
+ }))
90
+ );
91
+
92
+ return {
93
+ title: `${page.bookName} - ${page.author}`,
94
+ description: page.abstract,
95
+ link,
96
+ language: 'zh-CN',
97
+ image: page.thumbUri,
98
+ item: items,
99
+ };
100
+ }
@@ -0,0 +1,147 @@
1
+ import type { Data, Route } from '@/types';
2
+ import type { Context } from 'hono';
3
+ import ofetch from '@/utils/ofetch';
4
+ import { parseDate } from '@/utils/parse-date';
5
+ import timezone from '@/utils/timezone';
6
+ import md5 from '@/utils/md5';
7
+ import path from 'path';
8
+ import { getCurrentPath } from '@/utils/helpers';
9
+ import { art } from '@/utils/render';
10
+
11
+ const __dirname = getCurrentPath(import.meta.url);
12
+
13
+ interface User {
14
+ follow_type: number;
15
+ investment_style_id: null | string;
16
+ user_id: string;
17
+ nickname: string;
18
+ follow_status: number;
19
+ faction_id: string;
20
+ avatar: string;
21
+ medal_count: number;
22
+ style_str: null | string;
23
+ }
24
+
25
+ interface Stock {
26
+ stock_id: string;
27
+ name: string;
28
+ code: string;
29
+ }
30
+
31
+ interface ResultItem {
32
+ article_id: string;
33
+ is_top: number;
34
+ user_id: string;
35
+ is_like: number;
36
+ categoryIdSet: string[];
37
+ source_id: null | string;
38
+ title: string;
39
+ subtitle: string;
40
+ image: null | string;
41
+ cover: null | string;
42
+ url: null | string;
43
+ type: number;
44
+ read_limit: null | number;
45
+ comment_count: number;
46
+ collect_count: number;
47
+ like_count: number;
48
+ step_count: number;
49
+ forward_count: number;
50
+ integral: number;
51
+ is_user_top: number;
52
+ read_integral: number;
53
+ read_limit_time: null | string;
54
+ fans_limit: number;
55
+ copy_limit: number;
56
+ old_type: number;
57
+ hide: number;
58
+ create_time: string;
59
+ is_make_word_cloud: number;
60
+ sync_time: string;
61
+ is_flatter: number;
62
+ feature_img: null | string;
63
+ interest_disclosure: number;
64
+ new_interaction_time: string;
65
+ sensitive_words: null | string;
66
+ content: string;
67
+ user: User;
68
+ stock_list: Stock[];
69
+ }
70
+
71
+ interface CommunityData {
72
+ pageNo: number;
73
+ pageSize: number;
74
+ orderBy: null | string;
75
+ order: null | string;
76
+ autoCount: boolean;
77
+ map: Record<string, unknown>;
78
+ params: string;
79
+ result: ResultItem[];
80
+ totalCount: number;
81
+ first: number;
82
+ totalPages: number;
83
+ hasNext: boolean;
84
+ nextPage: number;
85
+ hasPre: boolean;
86
+ prePage: number;
87
+ }
88
+
89
+ interface Community {
90
+ msg: string;
91
+ data: CommunityData;
92
+ errCode: string;
93
+ serverTime: number;
94
+ }
95
+
96
+ const render = (data) => art(path.join(__dirname, 'templates', 'community-description.art'), data);
97
+
98
+ export const route: Route = {
99
+ path: '/community',
100
+ example: '/jiuyangongshe/community',
101
+ maintainers: ['TonyRL'],
102
+ name: '社群',
103
+ handler,
104
+ radar: [
105
+ {
106
+ source: ['www.jiuyangongshe.com'],
107
+ },
108
+ ],
109
+ };
110
+
111
+ async function handler(ctx: Context): Promise<Data> {
112
+ const link = `https://www.jiuyangongshe.com`;
113
+
114
+ const time = String(Date.now());
115
+ const response = await ofetch<Community>('https://app.jiuyangongshe.com/jystock-app/api/v2/article/community', {
116
+ method: 'POST',
117
+ headers: {
118
+ platform: '3',
119
+ timestamp: time,
120
+ token: md5(`Uu0KfOB8iUP69d3c:${time}`),
121
+ },
122
+ body: {
123
+ category_id: '',
124
+ limit: ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit') as string, 10) : 30,
125
+ order: 0,
126
+ start: 1,
127
+ type: 0,
128
+ back_garden: 0,
129
+ },
130
+ });
131
+
132
+ const items = response.data.result.map((item) => ({
133
+ title: item.title,
134
+ description: render({ cover: item.cover, content: item.content }),
135
+ link: `${link}/a/${item.article_id}`,
136
+ pubDate: timezone(parseDate(item.create_time, 'YYYY-MM-DD HH:mm:ss'), 8),
137
+ author: item.user.nickname,
138
+ category: item.stock_list.map((stock) => stock.name),
139
+ }));
140
+
141
+ return {
142
+ title: '社群 - 韭研公社-研究共享,茁壮成长(原韭菜公社)',
143
+ link,
144
+ language: 'zh-CN',
145
+ item: items,
146
+ };
147
+ }
@@ -0,0 +1,7 @@
1
+ import type { Namespace } from '@/types';
2
+
3
+ export const namespace: Namespace = {
4
+ name: '韭研公社',
5
+ url: 'www.jiuyangongshe.com',
6
+ categories: ['finance'],
7
+ };
@@ -0,0 +1,4 @@
1
+ {{ if cover }}
2
+ <img src="{{ cover }}"><br>
3
+ {{ /if }}
4
+ {{ content }}
@@ -0,0 +1,63 @@
1
+ import { Route } from '@/types';
2
+ import ofetch from '@/utils/ofetch';
3
+ import { baseUrl, gqlEndpoint, parseItem } from './utils';
4
+
5
+ const handler = async (ctx) => {
6
+ const { uid } = ctx.req.param();
7
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20;
8
+ const response = await ofetch(gqlEndpoint, {
9
+ method: 'POST',
10
+ body: {
11
+ query: `{
12
+ user(input: {userName: "${uid}"}) {
13
+ displayName
14
+ avatar
15
+ info {
16
+ description
17
+ }
18
+ articles(input: {first: ${limit}}) {
19
+ edges {
20
+ node {
21
+ shortHash
22
+ title
23
+ content
24
+ createdAt
25
+ author {
26
+ displayName
27
+ }
28
+ tags {
29
+ content
30
+ }
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }`,
36
+ },
37
+ });
38
+
39
+ const user = response.data.user;
40
+
41
+ return {
42
+ title: `Matters | ${user.displayName}`,
43
+ link: `${baseUrl}/@${uid}`,
44
+ description: user.info.description,
45
+ image: user.avatar,
46
+ item: user.articles.edges.map(({ node }) => parseItem(node)),
47
+ };
48
+ };
49
+
50
+ export const route: Route = {
51
+ path: '/author/:uid',
52
+ name: 'Author',
53
+ example: '/matters/author/robertu',
54
+ parameters: { uid: "Author id, can be found at author's homepage url" },
55
+ maintainers: ['Cerebrater', 'xosdy'],
56
+ handler,
57
+ radar: [
58
+ {
59
+ source: ['matters.town/:uid'],
60
+ target: (params) => `/matters/author/${params.uid.slice(1)}`,
61
+ },
62
+ ],
63
+ };
@@ -0,0 +1,74 @@
1
+ import { Route } from '@/types';
2
+ import ofetch from '@/utils/ofetch';
3
+ import { baseUrl, gqlEndpoint, parseItem } from './utils';
4
+
5
+ const handler = async (ctx) => {
6
+ const { type = 'latest' } = ctx.req.param();
7
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20;
8
+ const options = {
9
+ latest: {
10
+ title: '最新',
11
+ apiType: 'newest',
12
+ },
13
+ heat: {
14
+ title: '熱議',
15
+ apiType: 'hottest',
16
+ },
17
+ essence: {
18
+ title: '精華',
19
+ apiType: 'icymi',
20
+ },
21
+ };
22
+
23
+ const response = await ofetch(gqlEndpoint, {
24
+ method: 'POST',
25
+ body: {
26
+ query: `{
27
+ viewer {
28
+ recommendation {
29
+ feed: ${options[type].apiType}(input: {first: ${limit}}) {
30
+ edges {
31
+ node {
32
+ shortHash
33
+ title
34
+ content
35
+ createdAt
36
+ author {
37
+ displayName
38
+ }
39
+ tags {
40
+ content
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }`,
48
+ },
49
+ });
50
+
51
+ const item = response.data.viewer.recommendation.feed.edges.map(({ node }) => parseItem(node));
52
+
53
+ return {
54
+ title: `Matters | ${options[type].title}`,
55
+ link: baseUrl,
56
+ item,
57
+ };
58
+ };
59
+ export const route: Route = {
60
+ path: '/latest/:type?',
61
+ name: 'Latest, heat, essence',
62
+ example: '/matters/latest/heat',
63
+ parameters: { uid: 'Defaults to latest, see table below' },
64
+ maintainers: ['xyqfer', 'Cerebrater', 'xosdy'],
65
+ handler,
66
+ radar: [
67
+ {
68
+ source: ['matters.town'],
69
+ },
70
+ ],
71
+ description: `| 最新 | 热门 | 精华 |
72
+ | ------ | ---- | ------- |
73
+ | latest | heat | essence |`,
74
+ };
@@ -0,0 +1,7 @@
1
+ import type { Namespace } from '@/types';
2
+
3
+ export const namespace: Namespace = {
4
+ name: 'Matters',
5
+ url: 'matters.town',
6
+ categories: ['new-media'],
7
+ };
@@ -0,0 +1,85 @@
1
+ import { Route } from '@/types';
2
+ import ofetch from '@/utils/ofetch';
3
+ import * as cheerio from 'cheerio';
4
+ import cache from '@/utils/cache';
5
+ import { baseUrl, gqlEndpoint, parseItem } from './utils';
6
+
7
+ interface Tag {
8
+ type: string;
9
+ generated: boolean;
10
+ id: string;
11
+ typename: string;
12
+ }
13
+
14
+ const getTagId = (tid: string) =>
15
+ cache.tryGet(`matters:tags:${tid}`, async () => {
16
+ const response = await ofetch(`${baseUrl}/tags/${tid}`);
17
+ const $ = cheerio.load(response);
18
+ const nextData = JSON.parse($('script#__NEXT_DATA__').text());
19
+
20
+ const node = Object.entries(nextData.props.apolloState.data.ROOT_QUERY)
21
+ .find(([key]) => key.startsWith('node'))
22
+ ?.pop() as Tag;
23
+
24
+ return node?.id.split(':')[1];
25
+ });
26
+
27
+ const handler = async (ctx) => {
28
+ const { tid } = ctx.req.param();
29
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20;
30
+
31
+ const tagId = await getTagId(tid);
32
+
33
+ const gqlResponse = await ofetch(gqlEndpoint, {
34
+ method: 'POST',
35
+ body: {
36
+ query: `{
37
+ node(input: {id: "${tagId}"}) {
38
+ ... on Tag {
39
+ content
40
+ description
41
+ articles(input: {first: ${limit}}) {
42
+ edges {
43
+ node {
44
+ title
45
+ shortHash
46
+ content
47
+ createdAt
48
+ author {
49
+ displayName
50
+ }
51
+ tags {
52
+ content
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }`,
60
+ },
61
+ });
62
+
63
+ const node = gqlResponse.data.node;
64
+
65
+ return {
66
+ title: `Matters | ${node.content}`,
67
+ link: `${baseUrl}/tags/${tid}`,
68
+ description: node.description,
69
+ item: node.articles.edges.map(({ node }) => parseItem(node)),
70
+ };
71
+ };
72
+
73
+ export const route: Route = {
74
+ path: '/tags/:tid',
75
+ name: 'Tags',
76
+ example: '/matters/tags/972-哲學',
77
+ parameters: { tid: 'Tag id, can be found in the url of the tag page' },
78
+ maintainers: ['Cerebrater'],
79
+ handler,
80
+ radar: [
81
+ {
82
+ source: ['matters.town/tags/:tid'],
83
+ },
84
+ ],
85
+ };
@@ -0,0 +1,30 @@
1
+ import { parseDate } from '@/utils/parse-date';
2
+
3
+ export const baseUrl = 'https://matters.town';
4
+ export const gqlEndpoint = 'https://server.matters.town/graphql';
5
+
6
+ interface Tag {
7
+ content: string;
8
+ }
9
+
10
+ interface Author {
11
+ displayName: string;
12
+ }
13
+
14
+ interface Article {
15
+ shortHash: string;
16
+ title: string;
17
+ content: string;
18
+ createdAt: string;
19
+ author: Author;
20
+ tags: Tag[];
21
+ }
22
+
23
+ export const parseItem = (node: Article) => ({
24
+ title: node.title,
25
+ description: node.content,
26
+ link: `${baseUrl}/a/${node.shortHash}`,
27
+ author: node.author.displayName,
28
+ pubDate: parseDate(node.createdAt),
29
+ category: node.tags.map((tag) => tag.content),
30
+ });
@@ -8,15 +8,6 @@ export const route: Route = {
8
8
  path: '/asia',
9
9
  categories: ['traditional-media'],
10
10
  example: '/nikkei/asia',
11
- parameters: {},
12
- features: {
13
- requireConfig: false,
14
- requirePuppeteer: false,
15
- antiCrawler: false,
16
- supportBT: false,
17
- supportPodcast: false,
18
- supportScihub: false,
19
- },
20
11
  radar: [
21
12
  {
22
13
  source: ['asia.nikkei.com/'],
@@ -25,7 +16,7 @@ export const route: Route = {
25
16
  name: 'Nikkei Asia Latest News',
26
17
  maintainers: ['rainrdx'],
27
18
  handler,
28
- url: 'asia.nikkei.com/',
19
+ url: 'asia.nikkei.com',
29
20
  };
30
21
 
31
22
  async function handler() {
@@ -9,9 +9,45 @@ import parser from '@/utils/rss-parser';
9
9
 
10
10
  export const route: Route = {
11
11
  path: '/cn/*',
12
- name: 'Unknown',
13
- maintainers: [],
12
+ name: '中文版新闻',
13
+ example: '/nikkei/cn',
14
+ maintainers: ['nczitzk'],
14
15
  handler,
16
+ description: `::: tip
17
+ 如 [中国 经济 日经中文网](https://cn.nikkei.com/china/ceconomy.html) 的 URL 为 \`https://cn.nikkei.com/china/ceconomy.html\` 对应路由为 [\`/nikkei/cn/cn/china/ceconomy\`](https://rsshub.app/nikkei/cn/cn/china/ceconomy)
18
+
19
+ 如 [中國 經濟 日經中文網](https://zh.cn.nikkei.com/china/ceconomy.html) 的 URL 为 \`https://zh.cn.nikkei.com/china/ceconomy.html\` 对应路由为 [\`/nikkei/cn/zh/china/ceconomy\`](https://rsshub.app/nikkei/cn/zh/china/ceconomy)
20
+
21
+ 特别地,当 \`path\` 填入 \`rss\` 后(如路由为 [\`/nikkei/cn/cn/rss\`](https://rsshub.app/nikkei/cn/cn/rss)),此时返回的是 [官方 RSS 的内容](https://cn.nikkei.com/rss.html)
22
+ :::`,
23
+ radar: [
24
+ {
25
+ title: '中文版新闻',
26
+ source: ['cn.nikkei.com/:category/:type', 'cn.nikkei.com/:category', 'cn.nikkei.com/'],
27
+ target: (params) => {
28
+ if (params.category && params.type) {
29
+ return `/nikkei/cn/cn/${params.category}/${params.type.replace('.html', '')}`;
30
+ } else if (params.category && !params.type) {
31
+ return `/nikkei/cn/cn/${params.category.replace('.html', '')}`;
32
+ } else {
33
+ return `/nikkei/cn/cn`;
34
+ }
35
+ },
36
+ },
37
+ {
38
+ title: '中文版新聞',
39
+ source: ['zh.cn.nikkei.com/:category/:type', 'zh.cn.nikkei.com/:category', 'zh.cn.nikkei.com/'],
40
+ target: (params) => {
41
+ if (params.category && params.type) {
42
+ return `/nikkei/cn/zh/${params.category}/${params.type.replace('.html', '')}`;
43
+ } else if (params.category && !params.type) {
44
+ return `/nikkei/cn/zh/${params.category.replace('.html', '')}`;
45
+ } else {
46
+ return `/nikkei/cn/zh`;
47
+ }
48
+ },
49
+ },
50
+ ],
15
51
  };
16
52
 
17
53
  async function handler(ctx) {
@@ -40,7 +76,7 @@ async function handler(ctx) {
40
76
  officialFeed = await parser.parseURL(currentUrl);
41
77
  items = officialFeed.items.slice(0, limit).map((item) => ({
42
78
  title: item.title,
43
- link: new URL(item.attr('href'), rootUrl).href,
79
+ link: new URL(item.link, rootUrl).href,
44
80
  }));
45
81
  } else {
46
82
  const response = await got({
@@ -3,11 +3,12 @@ import got from '@/utils/got';
3
3
  import { load } from 'cheerio';
4
4
 
5
5
  export const route: Route = {
6
- path: ['/', '/index'],
7
- name: 'Unknown',
8
- maintainers: [],
6
+ path: '/index',
7
+ name: 'Home',
8
+ example: '/nikkei/index',
9
+ maintainers: ['zjysdhr'],
9
10
  handler,
10
- url: 'www.nikkei.com/',
11
+ url: 'www.nikkei.com',
11
12
  };
12
13
 
13
14
  async function handler() {
@@ -2,5 +2,5 @@ import type { Namespace } from '@/types';
2
2
 
3
3
  export const namespace: Namespace = {
4
4
  name: 'The Nikkei 日本経済新聞',
5
- url: 'asia.nikkei.com',
5
+ url: 'nikkei.com',
6
6
  };
@@ -10,9 +10,9 @@ import { art } from '@/utils/render';
10
10
  import path from 'node:path';
11
11
 
12
12
  export const route: Route = {
13
- path: '/:category/:article_type?',
13
+ path: '/news/:category/:article_type?',
14
14
  categories: ['traditional-media'],
15
- example: '/nikkei/news',
15
+ example: '/nikkei/news/news',
16
16
  parameters: { category: 'Category, see table below', article_type: 'Only includes free articles, set `free` to enable, disabled by default' },
17
17
  radar: [
18
18
  {
@@ -1,22 +1,45 @@
1
1
  import { Route } from '@/types';
2
- import { load } from 'cheerio';
3
- import logger from '@/utils/logger';
4
2
  import { parseDate } from '@/utils/parse-date';
5
- import puppeteer from '@/utils/puppeteer';
3
+ import ofetch from '@/utils/ofetch';
4
+
5
+ interface PlayerOptions {
6
+ link: string;
7
+ image: string;
8
+ flashplayer: string;
9
+ width: string;
10
+ height: string;
11
+ aspectratio: string;
12
+ title: string;
13
+ description: string;
14
+ autostart: boolean;
15
+ }
16
+
17
+ interface Element {
18
+ index: number;
19
+ href: string;
20
+ relativeHref: string;
21
+ text: string;
22
+ thumbnailText: string;
23
+ title: string;
24
+ image: string;
25
+ tags: string[];
26
+ filterTags: string;
27
+ publishDate: string;
28
+ playerOptions: PlayerOptions;
29
+ damSize: string;
30
+ template: string;
31
+ itemUrl: string;
32
+ itemWidth: string;
33
+ itemHeight: string;
34
+ isPage: boolean;
35
+ isVideo: boolean;
36
+ itemVideoTranscriptLink: string;
37
+ }
6
38
 
7
39
  export const route: Route = {
8
40
  path: '/strategyand/sustainability',
9
41
  categories: ['other'],
10
42
  example: '/pwc/strategyand/sustainability',
11
- parameters: {},
12
- features: {
13
- requireConfig: false,
14
- requirePuppeteer: true,
15
- antiCrawler: false,
16
- supportBT: false,
17
- supportPodcast: false,
18
- supportScihub: false,
19
- },
20
43
  radar: [
21
44
  {
22
45
  source: ['strategyand.pwc.com/at/en/functions/sustainability-strategy/publications.html', 'strategyand.pwc.com/'],
@@ -33,47 +56,28 @@ async function handler() {
33
56
  const feedLang = 'en';
34
57
  const feedDescription = 'Sustainability Publications from PwC Strategy&';
35
58
 
36
- const browser = await puppeteer();
37
- const page = await browser.newPage();
38
- logger.http(`Requesting ${baseUrl}`);
39
- await page.setRequestInterception(true);
40
- page.on('request', (request) => {
41
- request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort();
42
- });
43
- await page.goto(baseUrl, {
44
- waitUntil: 'domcontentloaded',
45
- });
46
- const response = await page.content();
47
- page.close();
48
-
49
- const $ = load(response);
50
-
51
- const list = $('div#wrapper article')
52
- .toArray()
53
- .map((item) => {
54
- item = $(item);
55
- const a = item.find('a').first();
56
- const div = item.find('div.collection__item-content').first();
57
-
58
- const link = a.attr('href');
59
- const title = div.find('h4').find('span').text();
60
- const pubDate = parseDate(div.find('time').attr('datetime'), 'DD/MM/YY');
61
- const description = div.find('p.paragraph').text();
62
-
63
- return {
64
- title,
65
- link,
66
- pubDate,
67
- description,
68
- };
69
- });
59
+ const response = await ofetch(
60
+ 'https://www.strategyand.pwc.com/content/pwc/03/en/functions/sustainability-strategy/publications/jcr:content/root/container/content-free-container/section_545483788/collection_v2.filter-dynamic.html',
61
+ {
62
+ query: {
63
+ currentPagePath: '/content/pwc/03/en/functions/sustainability-strategy/publications',
64
+ list: { menu_0: [] },
65
+ defaultImagePath: '/content/dam/pwc/network/strategyand-collection-fallback-images',
66
+ },
67
+ }
68
+ );
69
+ const elements = JSON.parse(response.elements) as Element[];
70
70
 
71
- const items = list;
71
+ const items = elements.map((item) => ({
72
+ title: item.title,
73
+ link: item.href,
74
+ pubDate: parseDate(item.publishDate, 'DD/MM/YY'),
75
+ description: item.text,
76
+ category: item.tags,
77
+ }));
72
78
 
73
79
  // TODO: Add full text support
74
80
 
75
- browser.close();
76
-
77
81
  return {
78
82
  title: 'PwC Strategy& - Sustainability Publications',
79
83
  link: baseUrl,
@@ -6,27 +6,18 @@ export const route: Route = {
6
6
  path: '/interactive-graphics',
7
7
  categories: ['traditional-media'],
8
8
  example: '/zaobao/interactive-graphics',
9
- parameters: {},
10
- features: {
11
- requireConfig: false,
12
- requirePuppeteer: false,
13
- antiCrawler: false,
14
- supportBT: false,
15
- supportPodcast: false,
16
- supportScihub: false,
17
- },
18
9
  name: '互动新闻',
19
10
  maintainers: ['shunf4'],
20
11
  handler,
21
12
  };
22
13
 
23
14
  async function handler(ctx) {
24
- const sectionLink = `/interactive-graphics`;
15
+ const sectionLink = '/interactive-graphics';
25
16
 
26
17
  const { resultList } = await parseList(ctx, sectionLink);
27
18
 
28
19
  return {
29
- title: `《联合早报》互动新闻`,
20
+ title: '《联合早报》互动新闻',
30
21
  link: baseUrl + sectionLink,
31
22
  description: '新加坡、中国、亚洲和国际的即时、评论、商业、体育、生活、科技与多媒体新闻,尽在联合早报。',
32
23
  item: resultList,
@@ -3,18 +3,10 @@ import { parseList } from './util';
3
3
  const baseUrl = 'https://www.zaobao.com';
4
4
 
5
5
  export const route: Route = {
6
- path: '/:type?/:section?',
6
+ path: '/other/:type?/:section?',
7
7
  categories: ['traditional-media'],
8
- example: '/zaobao/lifestyle/health',
8
+ example: '/zaobao/other/lifestyle/health',
9
9
  parameters: { type: 'https://www.zaobao.com/**lifestyle**/health 中的 **lifestyle**', section: 'https://www.zaobao.com/lifestyle/**health** 中的 **health**' },
10
- features: {
11
- requireConfig: false,
12
- requirePuppeteer: false,
13
- antiCrawler: false,
14
- supportBT: false,
15
- supportPodcast: false,
16
- supportScihub: false,
17
- },
18
10
  name: '其他栏目',
19
11
  maintainers: ['shunf4'],
20
12
  handler,
@@ -7,14 +7,6 @@ export const route: Route = {
7
7
  categories: ['traditional-media'],
8
8
  example: '/zaobao/realtime/china',
9
9
  parameters: { section: '分类,缺省为 china' },
10
- features: {
11
- requireConfig: false,
12
- requirePuppeteer: false,
13
- antiCrawler: false,
14
- supportBT: false,
15
- supportPodcast: false,
16
- supportScihub: false,
17
- },
18
10
  name: '即时新闻',
19
11
  maintainers: ['shunf4'],
20
12
  handler,
@@ -22,16 +22,20 @@ const got_ins = got.extend({
22
22
  *
23
23
  * @param {*} ctx RSSHub 的 ctx 参数,用来设置缓存
24
24
  * @param {string} sectionUrl 形如 /realtime/china 的字符串
25
- * @returns {Promise<{
26
- * title: string;
27
- * resultList: {
28
- * title: string;
29
- * description: string;
30
- * pubDate: string;
31
- * link: string;
32
- * }[];}>} 新闻标题以及新闻列表
25
+ * @returns 新闻标题以及新闻列表
33
26
  */
34
- const parseList = async (ctx, sectionUrl) => {
27
+ const parseList = async (
28
+ ctx,
29
+ sectionUrl: string
30
+ ): Promise<{
31
+ title: string;
32
+ resultList: {
33
+ title: string;
34
+ description: string;
35
+ pubDate: string;
36
+ link: string;
37
+ }[];
38
+ }> => {
35
39
  const response = await got_ins.get(baseUrl + sectionUrl);
36
40
  const $ = load(response.data);
37
41
  let data = $('.article-list').find('.article-type');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rsshub",
3
- "version": "1.0.0-master.f9003f9",
3
+ "version": "1.0.0-master.f93bb8c",
4
4
  "description": "Make RSS Great Again!",
5
5
  "keywords": [
6
6
  "RSS"
@@ -37,7 +37,7 @@
37
37
  "@hono/zod-openapi": "0.13.0",
38
38
  "@notionhq/client": "2.2.15",
39
39
  "@postlight/parser": "2.2.3",
40
- "@sentry/node": "7.114.0",
40
+ "@sentry/node": "8.3.0",
41
41
  "@tonyrl/rand-user-agent": "2.0.64",
42
42
  "aes-js": "3.1.2",
43
43
  "art-template": "4.13.2",
@@ -57,7 +57,7 @@
57
57
  "fanfou-sdk": "5.0.0",
58
58
  "form-data": "4.0.0",
59
59
  "googleapis": "137.1.0",
60
- "hono": "4.3.9",
60
+ "hono": "4.3.10",
61
61
  "html-to-text": "9.0.5",
62
62
  "https-proxy-agent": "7.0.4",
63
63
  "iconv-lite": "0.6.3",
@@ -98,9 +98,9 @@
98
98
  "tldts": "6.1.21",
99
99
  "tosource": "2.0.0-alpha.3",
100
100
  "tough-cookie": "4.1.4",
101
- "tsx": "4.10.5",
101
+ "tsx": "4.11.0",
102
102
  "twitter-api-v2": "1.16.4",
103
- "undici": "6.18.0",
103
+ "undici": "6.18.1",
104
104
  "uuid": "9.0.1",
105
105
  "winston": "3.13.0",
106
106
  "xxhash-wasm": "1.0.2",
@@ -150,7 +150,7 @@
150
150
  "got": "14.3.0",
151
151
  "husky": "9.0.11",
152
152
  "js-beautify": "1.15.1",
153
- "lint-staged": "15.2.2",
153
+ "lint-staged": "15.2.4",
154
154
  "mockdate": "3.0.5",
155
155
  "msw": "2.3.0",
156
156
  "prettier": "3.2.5",
@@ -1,49 +0,0 @@
1
- const got = require('@/utils/got');
2
- const { parseDate } = require('@/utils/parse-date');
3
-
4
- module.exports = async (ctx) => {
5
- const uid = ctx.params.uid;
6
- const host = `https://matters.news`;
7
- const url = `https://server.matters.news/graphql`;
8
- const response = await got({
9
- method: 'post',
10
- url,
11
- json: {
12
- query: `
13
- {
14
- user(input: { userName: "${uid}" }) {
15
- displayName
16
- info {
17
- description
18
- }
19
- articles(input: { first: 20 }) {
20
- edges {
21
- node {
22
- slug
23
- mediaHash
24
- title
25
- content
26
- createdAt
27
- }
28
- }
29
- }
30
- }
31
- }`,
32
- },
33
- });
34
-
35
- const user = response.data.data.user;
36
-
37
- ctx.state.data = {
38
- title: `Matters | ${user.displayName}`,
39
- link: `${host}/@${uid}`,
40
- description: user.info.description,
41
- item: user.articles.edges.map(({ node: article }) => ({
42
- title: article.title,
43
- author: user.displayName,
44
- description: article.content,
45
- link: `${host}/@${uid}/${article.slug}-${article.mediaHash}`,
46
- pubDate: parseDate(article.createdAt),
47
- })),
48
- };
49
- };
@@ -1,69 +0,0 @@
1
- const got = require('@/utils/got');
2
- const { parseDate } = require('@/utils/parse-date');
3
-
4
- module.exports = async (ctx) => {
5
- const type = ctx.params.type || 'latest';
6
- const url = `https://server.matters.news/graphql`;
7
- const options = {
8
- latest: {
9
- title: '最新',
10
- apiType: 'newest',
11
- },
12
- heat: {
13
- title: '熱議',
14
- apiType: 'hottest',
15
- },
16
- essence: {
17
- title: '精華',
18
- apiType: 'icymi',
19
- },
20
- };
21
-
22
- const response = await got({
23
- method: 'post',
24
- url,
25
- json: {
26
- query: `
27
- {
28
- viewer {
29
- id
30
- recommendation {
31
- feed: ${options[type].apiType}(input: { first: 10 }) {
32
- edges {
33
- node {
34
- author {
35
- userName
36
- displayName
37
- }
38
- slug
39
- mediaHash
40
- title
41
- content
42
- createdAt
43
- }
44
- }
45
- }
46
- }
47
- }
48
- }`,
49
- },
50
- });
51
-
52
- const item = response.data.data.viewer.recommendation.feed.edges.map(({ node }) => {
53
- const link = `https://matters.news/@${node.author.userName}/${node.slug}-${node.mediaHash}`;
54
- const article = node.content;
55
- return {
56
- title: node.title,
57
- link,
58
- description: article,
59
- author: node.author.displayName,
60
- pubDate: parseDate(node.createdAt),
61
- };
62
- });
63
-
64
- ctx.state.data = {
65
- title: `Matters | ${options[type].title}`,
66
- link: 'https://matters.news/',
67
- item,
68
- };
69
- };
@@ -1,56 +0,0 @@
1
- const got = require('@/utils/got');
2
- const { parseDate } = require('@/utils/parse-date');
3
-
4
- module.exports = async (ctx) => {
5
- const tid = ctx.params.tid;
6
- const host = `https://matters.news`;
7
- const url = `https://server.matters.news/graphql`;
8
- const response = await got({
9
- method: 'post',
10
- url,
11
- json: {
12
- query: `
13
- {
14
- node(input: { id: "${tid}" }) {
15
- ... on Tag {
16
- content
17
- articles(input:{first: 20}){
18
- edges {
19
- node {
20
- id
21
- title
22
- slug
23
- cover
24
- summary
25
- mediaHash
26
- content
27
- createdAt
28
- author {
29
- id
30
- userName
31
- displayName
32
- }
33
- }
34
- }
35
- }
36
- }
37
- }
38
- }`,
39
- },
40
- });
41
-
42
- const node = response.data.data.node;
43
-
44
- ctx.state.data = {
45
- title: `Matters | ${node.content}`,
46
- link: `${host}/tags/${tid}`,
47
- description: node.content,
48
- item: node.articles.edges.map(({ node: article }) => ({
49
- title: article.title,
50
- author: article.author.displayName,
51
- description: article.content,
52
- link: `${host}/@${article.author.id}/${article.slug}-${article.mediaHash}`,
53
- pubDate: parseDate(article.createdAt),
54
- })),
55
- };
56
- };