rsshub 1.0.0-master.f6cb490 → 1.0.0-master.f6f0273
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.
- package/lib/api/index.ts +1 -6
- package/lib/routes/2048/index.ts +24 -23
- package/lib/routes/anthropic/news.ts +27 -13
- package/lib/routes/asianfanfics/namespace.ts +7 -0
- package/lib/routes/asianfanfics/tag.ts +89 -0
- package/lib/routes/asianfanfics/text-search.ts +68 -0
- package/lib/routes/blockworks/index.ts +128 -0
- package/lib/routes/blockworks/namespace.ts +7 -0
- package/lib/routes/cmu/andypavlo/blog.ts +55 -0
- package/lib/routes/cmu/namespace.ts +7 -0
- package/lib/routes/coindesk/{index.ts → consensus-magazine.ts} +17 -21
- package/lib/routes/coindesk/namespace.ts +2 -1
- package/lib/routes/coindesk/news.ts +47 -0
- package/lib/routes/coindesk/utils.ts +26 -0
- package/lib/routes/cointelegraph/index.ts +106 -0
- package/lib/routes/cointelegraph/namespace.ts +7 -0
- package/lib/routes/collabo-cafe/category.ts +37 -0
- package/lib/routes/collabo-cafe/index.ts +35 -0
- package/lib/routes/collabo-cafe/namespace.ts +9 -0
- package/lib/routes/collabo-cafe/parser.ts +29 -0
- package/lib/routes/collabo-cafe/tag.ts +37 -0
- package/lib/routes/cryptoslate/index.ts +98 -0
- package/lib/routes/cryptoslate/namespace.ts +7 -0
- package/lib/routes/decrypt/index.ts +115 -0
- package/lib/routes/decrypt/namespace.ts +7 -0
- package/lib/routes/discuz/discuz.ts +7 -9
- package/lib/routes/fangchan/list.ts +224 -0
- package/lib/routes/fangchan/namespace.ts +9 -0
- package/lib/routes/fangchan/templates/description.art +7 -0
- package/lib/routes/foreignaffairs/namespace.ts +7 -0
- package/lib/routes/foreignaffairs/rss.ts +55 -0
- package/lib/routes/forklog/index.ts +72 -0
- package/lib/routes/forklog/namespace.ts +7 -0
- package/lib/routes/gcores/categories.ts +129 -0
- package/lib/routes/gcores/collections.ts +129 -0
- package/lib/routes/gcores/topics.ts +63 -0
- package/lib/routes/gov/moa/gjs.ts +210 -0
- package/lib/routes/gov/tianjin/tjftz.ts +53 -0
- package/lib/routes/gov/tianjin/tjrcgzw.ts +51 -0
- package/lib/routes/grainoil/category.ts +207 -0
- package/lib/routes/grainoil/namespace.ts +9 -0
- package/lib/routes/huxiu/util.ts +11 -9
- package/lib/routes/ifanr/category.ts +7 -2
- package/lib/routes/ifanr/digest.ts +1 -1
- package/lib/routes/ifanr/index.ts +1 -1
- package/lib/routes/instructables/projects.ts +20 -15
- package/lib/routes/juejin/collections.ts +1 -1
- package/lib/routes/komiic/comic.ts +88 -0
- package/lib/routes/komiic/namespace.ts +7 -0
- package/lib/routes/leagueoflegends/namespace.ts +8 -0
- package/lib/routes/leagueoflegends/patch-notes.ts +76 -0
- package/lib/routes/likeshop/index.ts +43 -0
- package/lib/routes/likeshop/namespace.ts +7 -0
- package/lib/routes/ltaaa/article.ts +180 -0
- package/lib/routes/ltaaa/namespace.ts +9 -0
- package/lib/routes/ltaaa/templates/description.art +7 -0
- package/lib/routes/mashiro/index.ts +1 -0
- package/lib/routes/nhentai/util.ts +4 -1
- package/lib/routes/pinterest/user.ts +9 -0
- package/lib/routes/sohu/mp.ts +3 -2
- package/lib/routes/spotify/show.ts +1 -1
- package/lib/routes/stcn/index.ts +241 -136
- package/lib/routes/stcn/kx.ts +144 -0
- package/lib/routes/swjtu/namespace.ts +1 -1
- package/lib/routes/swjtu/{scai/bks.ts → scai.ts} +34 -20
- package/lib/routes/swjtu/sports.ts +77 -0
- package/lib/routes/theblock/index.ts +142 -0
- package/lib/routes/theblock/namespace.ts +7 -0
- package/lib/routes/theverge/index.ts +73 -62
- package/lib/routes/theverge/templates/header.art +19 -0
- package/lib/routes/threads/index.ts +73 -54
- package/lib/routes/threads/utils.ts +60 -78
- package/lib/routes/tmtpost/column.ts +298 -0
- package/lib/routes/tmtpost/new.ts +4 -199
- package/lib/routes/tmtpost/util.ts +207 -0
- package/lib/routes/toranoana/namespace.ts +7 -0
- package/lib/routes/toranoana/news.ts +110 -0
- package/lib/routes/wainao/templates/description.art +9 -0
- package/lib/routes/wainao/topics.ts +214 -0
- package/lib/routes/xiaoyuzhou/podcast.ts +27 -27
- package/lib/routes/xjtu/yz.ts +74 -0
- package/lib/routes/youmemark/index.ts +6 -6
- package/lib/routes/zaobao/util.ts +11 -3
- package/lib/routes/zhihu/answers.ts +26 -54
- package/package.json +36 -35
- package/lib/routes/gcores/category.ts +0 -171
- package/lib/routes/gcores/collection.ts +0 -161
- package/lib/routes-deprecated/ltaaa/index.js +0 -69
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Route } from '@/types';
|
|
2
|
+
import cache from '@/utils/cache';
|
|
3
|
+
import { load } from 'cheerio';
|
|
4
|
+
import { parseDate } from '@/utils/parse-date';
|
|
5
|
+
import { ofetch } from 'ofetch';
|
|
6
|
+
|
|
7
|
+
const rootURL = 'https://sports.swjtu.edu.cn';
|
|
8
|
+
const pageURL = `${rootURL}/xwzx.htm`;
|
|
9
|
+
|
|
10
|
+
export const route: Route = {
|
|
11
|
+
path: '/sports',
|
|
12
|
+
categories: ['university'],
|
|
13
|
+
example: '/swjtu/sports',
|
|
14
|
+
parameters: {},
|
|
15
|
+
features: {
|
|
16
|
+
requireConfig: false,
|
|
17
|
+
requirePuppeteer: false,
|
|
18
|
+
antiCrawler: true,
|
|
19
|
+
supportBT: false,
|
|
20
|
+
supportPodcast: false,
|
|
21
|
+
supportScihub: false,
|
|
22
|
+
},
|
|
23
|
+
radar: [
|
|
24
|
+
{
|
|
25
|
+
source: ['sports.swjtu.edu.cn/'],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
name: '体育学院',
|
|
29
|
+
description: '新闻资讯',
|
|
30
|
+
maintainers: ['AzureG03'],
|
|
31
|
+
handler,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const getItem = (item, cache) => {
|
|
35
|
+
const title = item.find('p.toe').text();
|
|
36
|
+
const link = `${rootURL}/${item.find('a').attr('href')}`;
|
|
37
|
+
|
|
38
|
+
return cache.tryGet(link, async () => {
|
|
39
|
+
const res = await ofetch(link);
|
|
40
|
+
const $ = load(res);
|
|
41
|
+
|
|
42
|
+
const pubDate = parseDate(
|
|
43
|
+
$('div.info span:nth-of-type(3)')
|
|
44
|
+
.text()
|
|
45
|
+
.slice(3)
|
|
46
|
+
.match(/\d{4}(-|\/|.)\d{1,2}\1\d{1,2}/)?.[0]
|
|
47
|
+
);
|
|
48
|
+
const description = $('div.detail-wrap').html();
|
|
49
|
+
return {
|
|
50
|
+
title,
|
|
51
|
+
pubDate,
|
|
52
|
+
link,
|
|
53
|
+
description,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
async function handler() {
|
|
59
|
+
const res = await ofetch(pageURL);
|
|
60
|
+
|
|
61
|
+
const $ = load(res);
|
|
62
|
+
const $list = $('div.news-list > ul > li');
|
|
63
|
+
|
|
64
|
+
const items = await Promise.all(
|
|
65
|
+
$list.toArray().map((i) => {
|
|
66
|
+
const $item = $(i);
|
|
67
|
+
return getItem($item, cache);
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
title: '西南交大体院-新闻资讯',
|
|
73
|
+
link: pageURL,
|
|
74
|
+
item: items,
|
|
75
|
+
allowEmpty: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Route, Data } from '@/types';
|
|
2
|
+
import cache from '@/utils/cache';
|
|
3
|
+
import ofetch from '@/utils/ofetch';
|
|
4
|
+
import { parseDate } from '@/utils/parse-date';
|
|
5
|
+
import { load } from 'cheerio';
|
|
6
|
+
import logger from '@/utils/logger';
|
|
7
|
+
|
|
8
|
+
export const route: Route = {
|
|
9
|
+
path: '/category/:category',
|
|
10
|
+
categories: ['finance'],
|
|
11
|
+
example: '/theblock/category/crypto-ecosystems',
|
|
12
|
+
parameters: { category: 'News category' },
|
|
13
|
+
features: {
|
|
14
|
+
requireConfig: false,
|
|
15
|
+
requirePuppeteer: false,
|
|
16
|
+
antiCrawler: false,
|
|
17
|
+
supportBT: false,
|
|
18
|
+
supportPodcast: false,
|
|
19
|
+
supportScihub: false,
|
|
20
|
+
},
|
|
21
|
+
name: 'Category',
|
|
22
|
+
maintainers: ['pseudoyu'],
|
|
23
|
+
handler,
|
|
24
|
+
radar: [
|
|
25
|
+
{
|
|
26
|
+
source: ['theblock.co/category/:category'],
|
|
27
|
+
target: '/category/:category',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
description: 'Get latest news from TheBlock by category. Note that due to website limitations, only article summaries may be available.',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
async function handler(ctx): Promise<Data> {
|
|
34
|
+
const category = ctx.req.param('category');
|
|
35
|
+
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 10;
|
|
36
|
+
|
|
37
|
+
const apiUrl = `https://www.theblock.co/api/category/${category}`;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const response = await ofetch(apiUrl);
|
|
41
|
+
|
|
42
|
+
// Extract articles from the nested data structure
|
|
43
|
+
const articles = response.data?.articles || [];
|
|
44
|
+
|
|
45
|
+
if (!articles.length) {
|
|
46
|
+
throw new Error(`No articles found for category: ${category}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const items = await Promise.all(
|
|
50
|
+
articles.slice(0, limit).map((article) =>
|
|
51
|
+
cache.tryGet(`theblock:article:${article.url}`, async () => {
|
|
52
|
+
try {
|
|
53
|
+
// Try to get the full article
|
|
54
|
+
const articleResponse = await ofetch(`https://www.theblock.co/api/post/${article.id}/`);
|
|
55
|
+
|
|
56
|
+
const post = articleResponse.post;
|
|
57
|
+
const $ = load(post.body, null, false);
|
|
58
|
+
|
|
59
|
+
// If we successfully got the article content
|
|
60
|
+
if (post.body.length) {
|
|
61
|
+
// Remove unwanted elements
|
|
62
|
+
$('.copyright').remove();
|
|
63
|
+
|
|
64
|
+
let fullText = '';
|
|
65
|
+
|
|
66
|
+
if (article.thumbnail) {
|
|
67
|
+
fullText += `<p><img src="${post.thumbnail}" alt="${article.title}"></p>`;
|
|
68
|
+
}
|
|
69
|
+
fullText += post.intro + $.html();
|
|
70
|
+
|
|
71
|
+
if (fullText) {
|
|
72
|
+
return {
|
|
73
|
+
title: article.title,
|
|
74
|
+
link: article.url,
|
|
75
|
+
pubDate: parseDate(post.published),
|
|
76
|
+
description: fullText,
|
|
77
|
+
author: article.authors?.map((a) => a.name).join(', ') || 'TheBlock',
|
|
78
|
+
category: [...new Set([post.categories.name, ...post.categories.map((cat) => cat.name), ...post.tags.map((tag) => tag.name)])],
|
|
79
|
+
guid: article.url,
|
|
80
|
+
image: article.thumbnail,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// If we couldn't extract specific content, fall back to a summary-based approach
|
|
86
|
+
logger.info(`Using summary-based approach for article: ${article.url}`);
|
|
87
|
+
return createSummaryItem(article);
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
// If we got a 403 error or any other error, use summary approach
|
|
90
|
+
logger.warn(`Couldn't fetch full content for ${article.url}: ${error.message}`);
|
|
91
|
+
return createSummaryItem(article);
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
title: `TheBlock - ${category.charAt(0).toUpperCase() + category.slice(1).replaceAll('-', ' ')}`,
|
|
99
|
+
link: `https://www.theblock.co/category/${category}`,
|
|
100
|
+
item: items,
|
|
101
|
+
description: `Latest articles from TheBlock in the ${category} category`,
|
|
102
|
+
language: 'en',
|
|
103
|
+
} as Data;
|
|
104
|
+
} catch (error: any) {
|
|
105
|
+
logger.error(`Error in TheBlock handler: ${error.message}`);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Helper function to create a summary-based item when full content isn't available
|
|
111
|
+
function createSummaryItem(article: any) {
|
|
112
|
+
let description = '';
|
|
113
|
+
|
|
114
|
+
// Add thumbnail if available
|
|
115
|
+
if (article.thumbnail) {
|
|
116
|
+
description += `<p><img src="${article.thumbnail}" alt="${article.title}"></p>`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Add subheading if available
|
|
120
|
+
if (article.subheading) {
|
|
121
|
+
description += `<p><strong>${article.subheading}</strong></p>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add preview if available
|
|
125
|
+
if (article.preview) {
|
|
126
|
+
description += `<p>${article.preview}</p>`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Add link to original article
|
|
130
|
+
description += `<p><a href="${article.url}">Read the full article at TheBlock</a></p>`;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
title: article.title,
|
|
134
|
+
link: article.url,
|
|
135
|
+
pubDate: parseDate(article.publishedFormatted, 'MMMM D, YYYY, h:mmA [EST]'),
|
|
136
|
+
description,
|
|
137
|
+
author: article.authors?.map((a) => a.name).join(', ') || 'TheBlock',
|
|
138
|
+
category: article.primaryCategory?.name || [],
|
|
139
|
+
guid: article.url,
|
|
140
|
+
image: article.thumbnail,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { Route } from '@/types';
|
|
2
2
|
import cache from '@/utils/cache';
|
|
3
|
-
import
|
|
3
|
+
import ofetch from '@/utils/ofetch';
|
|
4
4
|
import parser from '@/utils/rss-parser';
|
|
5
5
|
import { load } from 'cheerio';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { art } from '@/utils/render';
|
|
8
|
+
import { getCurrentPath } from '@/utils/helpers';
|
|
9
|
+
|
|
10
|
+
const __dirname = getCurrentPath(import.meta.url);
|
|
6
11
|
|
|
7
12
|
export const route: Route = {
|
|
8
13
|
path: '/:hub?',
|
|
@@ -22,7 +27,7 @@ export const route: Route = {
|
|
|
22
27
|
source: ['theverge.com/:hub', 'theverge.com/'],
|
|
23
28
|
},
|
|
24
29
|
],
|
|
25
|
-
name: '
|
|
30
|
+
name: 'Category',
|
|
26
31
|
maintainers: ['HenryQW', 'vbali'],
|
|
27
32
|
handler,
|
|
28
33
|
description: `| Hub | Hub name |
|
|
@@ -43,6 +48,37 @@ export const route: Route = {
|
|
|
43
48
|
Provides a better reading experience (full text articles) over the official one.`,
|
|
44
49
|
};
|
|
45
50
|
|
|
51
|
+
const renderBlock = (b) => {
|
|
52
|
+
switch (b.__typename) {
|
|
53
|
+
case 'CoreEmbedBlockType':
|
|
54
|
+
return b.embedHtml;
|
|
55
|
+
case 'CoreGalleryBlockType':
|
|
56
|
+
return b.images.map((i) => `<figure><img src="${i.image.thumbnails.horizontal.url.split('?')[0]}" alt="${i.alt}" /><figcaption>${i.caption.html}</figcaption></figure>`).join('');
|
|
57
|
+
case 'CoreHeadingBlockType':
|
|
58
|
+
return `<h${b.level}>${b.contents.html}</h${b.level}>`;
|
|
59
|
+
case 'CoreHTMLBlockType':
|
|
60
|
+
return b.markup;
|
|
61
|
+
case 'CoreImageBlockType':
|
|
62
|
+
return `<figure><img src="${b.thumbnail.url.split('?')[0]}" alt="${b.alt}" /><figcaption>${b.caption.html}</figcaption></figure>`;
|
|
63
|
+
case 'CoreListBlockType':
|
|
64
|
+
return `${b.ordered ? '<ol>' : '<ul>'}${b.items.map((i) => `<li>${i.contents.html}</li>`).join('')}${b.ordered ? '</ol>' : '</ul>'}`;
|
|
65
|
+
case 'CoreParagraphBlockType':
|
|
66
|
+
return b.contents.html;
|
|
67
|
+
case 'CorePullquoteBlockType':
|
|
68
|
+
return `<blockquote>${b.contents.html}</blockquote>`;
|
|
69
|
+
case 'CoreQuoteBlockType':
|
|
70
|
+
return `<blockquote>${b.children.map((child) => renderBlock(child)).join('')}</blockquote>`;
|
|
71
|
+
case 'CoreSeparatorBlockType':
|
|
72
|
+
return '<hr>';
|
|
73
|
+
case 'HighlightBlockType':
|
|
74
|
+
return b.children.map((c) => renderBlock(c)).join('');
|
|
75
|
+
case 'MethodologyAccordionBlockType':
|
|
76
|
+
return `<h2>${b.heading.html}</h2>${b.sections.map((s) => `<h3>${s.heading.html}</h3>${s.content.html}`).join('')}`;
|
|
77
|
+
default:
|
|
78
|
+
throw new Error(`Unsupported block type: ${b.__typename}`);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
46
82
|
async function handler(ctx) {
|
|
47
83
|
const link = ctx.req.param('hub') ? `https://www.theverge.com/${ctx.req.param('hub')}/rss/index.xml` : 'https://www.theverge.com/rss/index.xml';
|
|
48
84
|
|
|
@@ -51,73 +87,48 @@ async function handler(ctx) {
|
|
|
51
87
|
const items = await Promise.all(
|
|
52
88
|
feed.items.map((item) =>
|
|
53
89
|
cache.tryGet(item.link, async () => {
|
|
54
|
-
const response = await
|
|
55
|
-
|
|
56
|
-
const $ = load(response.data);
|
|
57
|
-
|
|
58
|
-
const content = $('#content');
|
|
59
|
-
const body = $('.duet--article--article-body-component-container');
|
|
60
|
-
|
|
61
|
-
// 处理封面图片
|
|
62
|
-
|
|
63
|
-
const cover = $('meta[property="og:image"]');
|
|
64
|
-
|
|
65
|
-
if (cover.length > 0) {
|
|
66
|
-
$(`<img src=${cover[0].attribs.content}>`).insertBefore(body[0].childNodes[0]);
|
|
67
|
-
}
|
|
90
|
+
const response = await ofetch(item.link);
|
|
68
91
|
|
|
69
|
-
|
|
70
|
-
$('div.l-col__main > div.c-video-embed, div.c-entry-hero > div.c-video-embed').each((i, e) => {
|
|
71
|
-
const src = `https://volume.vox-cdn.com/embed/${e.attribs['data-volume-uuid']}?autoplay=false`;
|
|
92
|
+
const $ = load(response);
|
|
72
93
|
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
const nextData = JSON.parse($('script#__NEXT_DATA__').text());
|
|
95
|
+
const node = nextData.props.pageProps.hydration.responses.find((x) => x.operationName === 'PostLayoutQuery' || x.operationName === 'StreamLayoutQuery').data.node;
|
|
75
96
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
let description = art(path.join(__dirname, 'templates/header.art'), {
|
|
98
|
+
featuredImage: node.featuredImage,
|
|
99
|
+
ledeMediaData: node.ledeMediaData,
|
|
79
100
|
});
|
|
80
101
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const lede = $('.duet--article--lede h2:first');
|
|
109
|
-
if (lede[0]) {
|
|
110
|
-
lede.insertBefore(body[0].childNodes[0]);
|
|
102
|
+
description += node.blocks
|
|
103
|
+
.filter((b) => b.__typename !== 'NewsletterBlockType' && b.__typename !== 'RelatedPostsBlockType' && b.__typename !== 'ProductBlockType' && b.__typename !== 'TableOfContentsBlockType')
|
|
104
|
+
.map((b) => renderBlock(b))
|
|
105
|
+
.join('<br><br>');
|
|
106
|
+
|
|
107
|
+
if (node.__typename === 'StreamResourceType') {
|
|
108
|
+
description += node.posts.edges
|
|
109
|
+
.map(({ node: n }) => {
|
|
110
|
+
let d =
|
|
111
|
+
`<h2><a href="${n.permalink}">${n.promo.headline || n.title}</a></h2>` +
|
|
112
|
+
art(path.join(__dirname, 'templates/header.art'), {
|
|
113
|
+
ledeMediaData: n.ledeMediaData,
|
|
114
|
+
});
|
|
115
|
+
switch (n.__typename) {
|
|
116
|
+
case 'PostResourceType':
|
|
117
|
+
d += n.excerpt.map((e) => e.contents.html).join('<br>');
|
|
118
|
+
break;
|
|
119
|
+
case 'QuickPostResourceType':
|
|
120
|
+
d += n.blocks.map((b) => renderBlock(b)).join('<br>');
|
|
121
|
+
break;
|
|
122
|
+
default:
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
return d;
|
|
126
|
+
})
|
|
127
|
+
.join('<br>');
|
|
111
128
|
}
|
|
112
129
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
content.find('.duet--recirculation--related-list').remove();
|
|
116
|
-
delete item.content;
|
|
117
|
-
delete item.contentSnippet;
|
|
118
|
-
delete item.isoDate;
|
|
119
|
-
|
|
120
|
-
item.description = body.html();
|
|
130
|
+
item.description = description;
|
|
131
|
+
item.category = node.categories;
|
|
121
132
|
|
|
122
133
|
return item;
|
|
123
134
|
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{{ if featuredImage }}
|
|
2
|
+
<figure>
|
|
3
|
+
<img src="{{ featuredImage.image.originalUrl.split('?')[0] }}" alt="{{ featuredImage.image.originalUrl.alt }}">
|
|
4
|
+
<figcaption>{{ featuredImage.image.title }}</figcaption>
|
|
5
|
+
</figure>
|
|
6
|
+
{{ /if }}
|
|
7
|
+
|
|
8
|
+
{{ if ledeMediaData }}
|
|
9
|
+
{{ if ledeMediaData.__typename === 'LedeMediaEmbedType'}}
|
|
10
|
+
{{@ ledeMediaData.embedHtml }}
|
|
11
|
+
{{ else if ledeMediaData.__typename === 'LedeMediaImageType' && !featuredImage }}
|
|
12
|
+
<figure>
|
|
13
|
+
<img src="{{ ledeMediaData.image.thumbnails.horizontal.url.split('?')[0] }}" alt="{{ ledeMediaData.image.title }}">
|
|
14
|
+
<figcaption>{{ ledeMediaData.image.credit.plaintext || ledeMediaData.image.title }}</figcaption>
|
|
15
|
+
</figure>
|
|
16
|
+
{{ else if ledeMediaData.__typename === 'LedeMediaVideoType'}}
|
|
17
|
+
<iframe src="https://volume.vox-cdn.com/embed/{{ ledeMediaData.video.volumeUuid }}" allowfullscreen></iframe>
|
|
18
|
+
{{ /if }}
|
|
19
|
+
{{ /if }}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Route, ViewType } from '@/types';
|
|
2
|
-
import ofetch from '@/utils/ofetch';
|
|
3
2
|
import { parseDate } from '@/utils/parse-date';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
3
|
+
import { threadUrl, profileUrl, extractTokens, getUserId, buildContent } from './utils';
|
|
4
|
+
import { JSDOM } from 'jsdom';
|
|
5
|
+
import { JSONPath } from 'jsonpath-plus';
|
|
6
|
+
import ofetch from '@/utils/ofetch';
|
|
8
7
|
|
|
9
8
|
export const route: Route = {
|
|
10
9
|
path: '/:user/:routeParams?',
|
|
@@ -25,13 +24,7 @@ Specify options (in the format of query string) in parameter \`routeParams\` to
|
|
|
25
24
|
| \`showAuthorAvatarInDesc\` | Show avatar of author in description (RSS body) (Not recommended if your RSS reader extracts images from description) | \`0\`/\`1\`/\`true\`/\`false\` | \`falseP\` |
|
|
26
25
|
| \`showEmojiForQuotesAndReply\` | Use "🔁" instead of "QT", "↩️" instead of "Re" | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` |
|
|
27
26
|
| \`showQuotedInTitle\` | Show quoted tweet in title | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` |
|
|
28
|
-
| \`replies\` | Show replies | \`0\`/\`1\`/\`true\`/\`false\` | \`true\`
|
|
29
|
-
|
|
30
|
-
Specify different option values than default values to improve readability. The URL
|
|
31
|
-
|
|
32
|
-
\`\`\`
|
|
33
|
-
https://rsshub.app/threads/zuck/showAuthorInTitle=1&showAuthorInDesc=1&showQuotedAuthorAvatarInDesc=1&showAuthorAvatarInDesc=1&showEmojiForQuotesAndReply=1&showQuotedInTitle=1
|
|
34
|
-
\`\`\``,
|
|
27
|
+
| \`replies\` | Show replies | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` |`,
|
|
35
28
|
},
|
|
36
29
|
},
|
|
37
30
|
name: 'User timeline',
|
|
@@ -42,10 +35,10 @@ https://rsshub.app/threads/zuck/showAuthorInTitle=1&showAuthorInDesc=1&showQuote
|
|
|
42
35
|
async function handler(ctx) {
|
|
43
36
|
const { user, routeParams } = ctx.req.param();
|
|
44
37
|
const { lsd } = await extractTokens(user);
|
|
45
|
-
const userId = await getUserId(user
|
|
38
|
+
const userId = await getUserId(user);
|
|
46
39
|
|
|
47
40
|
const params = new URLSearchParams(routeParams);
|
|
48
|
-
const debugJson = {
|
|
41
|
+
const debugJson: any = {
|
|
49
42
|
params: routeParams,
|
|
50
43
|
lsd,
|
|
51
44
|
};
|
|
@@ -60,48 +53,59 @@ async function handler(ctx) {
|
|
|
60
53
|
replies: params.get('replies') ?? false,
|
|
61
54
|
};
|
|
62
55
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
56
|
+
const response = await ofetch(profileUrl(user), {
|
|
57
|
+
headers: {
|
|
58
|
+
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
|
|
59
|
+
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
60
|
+
'Accept-Encoding': 'gzip, br',
|
|
61
|
+
'Accept-Language': 'zh-CN,zh;q=0.9',
|
|
62
|
+
'Cache-Control': 'no-cache',
|
|
63
|
+
Pragma: 'no-cache',
|
|
64
|
+
'Sec-Fetch-Dest': 'document',
|
|
65
|
+
'Sec-Fetch-Mode': 'navigate',
|
|
66
|
+
'Sec-Fetch-Site': 'none',
|
|
67
|
+
'Sec-Fetch-User': '?1',
|
|
68
|
+
'Upgrade-Insecure-Requests': '1',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const dom = new JSDOM(response);
|
|
73
|
+
|
|
74
|
+
let threadsData: ThreadItem[] | null = null;
|
|
75
|
+
for (const el of dom.window.document.querySelectorAll('script[data-sjs]')) {
|
|
76
|
+
try {
|
|
77
|
+
const data = JSONPath({
|
|
78
|
+
path: '$..thread_items[0]',
|
|
79
|
+
json: JSON.parse(el.textContent || ''),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (data?.length > 0) {
|
|
83
|
+
threadsData = data as ThreadItem[];
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// Skip invalid JSON
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!threadsData) {
|
|
92
|
+
throw new Error('Failed to fetch thread data');
|
|
93
|
+
}
|
|
82
94
|
|
|
83
95
|
debugJson.profileId = userId;
|
|
84
|
-
debugJson.response = {
|
|
85
|
-
response: threadsResponse,
|
|
86
|
-
};
|
|
96
|
+
debugJson.response = { response: threadsData };
|
|
87
97
|
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
description,
|
|
100
|
-
pubDate: parseDate(item.post.taken_at, 'X'),
|
|
101
|
-
link: threadUrl(item.post.code),
|
|
102
|
-
};
|
|
103
|
-
})
|
|
104
|
-
);
|
|
98
|
+
const userData: ThreadUser = threadsData[0]?.post?.user || { username: user, profile_pic_url: '' };
|
|
99
|
+
|
|
100
|
+
const items = threadsData
|
|
101
|
+
.filter((item) => user === item.post.user?.username)
|
|
102
|
+
.map((item) => ({
|
|
103
|
+
author: user,
|
|
104
|
+
title: buildContent(item, options).title,
|
|
105
|
+
description: buildContent(item, options).description,
|
|
106
|
+
pubDate: parseDate(item.post.taken_at, 'X'),
|
|
107
|
+
link: threadUrl(item.post.code),
|
|
108
|
+
}));
|
|
105
109
|
|
|
106
110
|
debugJson.items = items;
|
|
107
111
|
ctx.set('json', debugJson);
|
|
@@ -110,7 +114,22 @@ async function handler(ctx) {
|
|
|
110
114
|
title: `${user} (@${user}) on Threads`,
|
|
111
115
|
link: profileUrl(user),
|
|
112
116
|
image: userData?.profile_pic_url,
|
|
113
|
-
// description: userData.biography,
|
|
114
117
|
item: items,
|
|
115
118
|
};
|
|
116
119
|
}
|
|
120
|
+
|
|
121
|
+
interface ThreadUser {
|
|
122
|
+
username: string;
|
|
123
|
+
profile_pic_url: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface ThreadItem {
|
|
127
|
+
post: {
|
|
128
|
+
user?: ThreadUser;
|
|
129
|
+
taken_at: number;
|
|
130
|
+
code: string;
|
|
131
|
+
caption?: {
|
|
132
|
+
text: string;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
}
|