mirror of
https://github.com/DIYgod/RSSHub.git
synced 2025-12-04 19:59:54 +08:00
feat(core): json feed (#11494)
* feat(core): json feed * docs: json feed description * fix: remove extra comma if no elements exist after author * test: json feed test * feat: generate json feed without art-template * test: fix coverage * style: 4 spaces json * docs: fix parameter
This commit is contained in:
@@ -4,17 +4,17 @@
|
||||
|
||||
Parameters here are actually URI query and can be linked together with `&` to generate a complex feed.
|
||||
|
||||
Parameters here need to be placed after the route path. Some routes may have <span color=green>**custom route parameters**</span> and <span color=violet>**parameters here**</span> need to be placed after them.
|
||||
Parameters here need to be placed after the route path. Some routes may have <span style="color: green">**custom route parameters**</span> and <span style="color: violet">**parameters here**</span> need to be placed after them.
|
||||
|
||||
E.g.
|
||||
|
||||
<a href="https://rsshub.app/twitter/user/durov/readable=1&includeRts=0?brief=100&limit=5">https://rsshub.app/twitter/user/durov/<span color=green><b>readable=1&includeRts=0</b></span>?<span color=violet><b>brief=100&limit=5</b></span></a>
|
||||
<a href="https://rsshub.app/twitter/user/durov/readable=1&includeRts=0?brief=100&limit=5">https://rsshub.app/twitter/user/durov/<span style="green"><b>readable=1&includeRts=0</b></span>?<span style="color: violet"><b>brief=100&limit=5</b></span></a>
|
||||
|
||||
If a <span color=magenta>**output format**</span> (`.atom`, `.rss`, `.debug.json`) is set, it needs to be placed between the route path (including <span color=green>**custom route parameters**</span>) and <span color=violet>**other parameters**</span>.
|
||||
If a <span style="color: magenta">**output format**</span> (`.atom`, `.rss`, `.json`, `.debug.json`) is set, it needs to be placed between the route path (including <span style="green">**custom route parameters**</span>) and <span style="color: violet">**other parameters**</span>.
|
||||
|
||||
E.g.
|
||||
|
||||
<a href="https://rsshub.app/twitter/user/durov/readable=1&includeRts=0.atom?brief=100&limit=5">https://rsshub.app/twitter/user/durov/<span color=green><b>readable=1&includeRts=0</b></span><span color=magenta><b>.atom</b></span>?<span color=violet><b>brief=100&limit=5</b></span></a>
|
||||
<a href="https://rsshub.app/twitter/user/durov/readable=1&includeRts=0.atom?brief=100&limit=5">https://rsshub.app/twitter/user/durov/<span style="color: green"><b>readable=1&includeRts=0</b></span><span style="color: magenta"><b>.atom</b></span>?<span style="color: violet"><b>brief=100&limit=5</b></span></a>
|
||||
|
||||
:::
|
||||
|
||||
@@ -138,13 +138,14 @@ There are more details in the [FAQ](/en/faq.html).
|
||||
|
||||
## Output Formats
|
||||
|
||||
RSSHub conforms to RSS 2.0 and Atom Standard, simply append `.rss` `.atom` to the end of the feed address to obtain the feed in corresponding format. The default output format is RSS 2.0.
|
||||
RSSHub conforms to RSS 2.0, Atom and JSON Feed Standard, simply append `.rss`, `.atom` or `.json` to the end of the feed address to obtain the feed in corresponding format. The default output format is RSS 2.0.
|
||||
|
||||
E.g.
|
||||
|
||||
- Default (RSS 2.0) - [https://rsshub.app/dribbble/popular](https://rsshub.app/dribbble/popular)
|
||||
- RSS 2.0 - [https://rsshub.app/dribbble/popular.rss](https://rsshub.app/dribbble/popular.rss)
|
||||
- Atom - [https://rsshub.app/dribbble/popular.atom](https://rsshub.app/dribbble/popular.atom)
|
||||
- JSON Feed - [https://rsshub.app/twitter/user/DIYgod.json](https://rsshub.app/twitter/user/DIYgod.json)
|
||||
- Apply filters or URL query - [https://rsshub.app/dribbble/popular.atom?filterout=Blue|Yellow|Black](https://rsshub.app/dribbble/popular.atom?filterout=Blue|Yellow|Black)
|
||||
|
||||
### Debug
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
|
||||
通用参数实际上是 URI 中的 query,可以使用 `&` 连接组合使用,效果叠加。
|
||||
|
||||
通用参数需要置于路由路径的最后。有些路由在路由路径(route path)的最后引入了<span color=green>**自定义参数**</span>,<span color=violet>**通用参数**</span>也需要置于它们之后。
|
||||
通用参数需要置于路由路径的最后。有些路由在路由路径(route path)的最后引入了<span style="color: green">**自定义参数**</span>,<span style="color: violet">**通用参数**</span>也需要置于它们之后。
|
||||
|
||||
举例:
|
||||
|
||||
<a href="https://rsshub.app/twitter/user/durov/readable=1&includeRts=0?brief=100&limit=5"><https://rsshub.app/twitter/user/durov/><span color=green><b>readable=1\&includeRts=0</b></span>?<span color=violet><b>brief=100\&limit=5</b></span></a>
|
||||
<a href="https://rsshub.app/twitter/user/durov/readable=1&includeRts=0?brief=100&limit=5"><https://rsshub.app/twitter/user/durov/><span style="color: green"><b>readable=1\&includeRts=0</b></span>?<span style=violet><b>brief=100\&limit=5</b></span></a>
|
||||
|
||||
如果设置了<span color=magenta>**输出格式**</span>(`.atom`, `.rss`, `.debug.json`),则需要置于路由路径(含<span color=green>**自定义参数**</span>)与<span color=violet>**其它通用参数**</span>之间。
|
||||
如果设置了<span style="color: magenta">**输出格式**</span>(`.atom`, `.rss`, `.json`, `.debug.json`),则需要置于路由路径(含<span style="color: green">**自定义参数**</span>)与<span style="color: violet">**其它通用参数**</span>之间。
|
||||
|
||||
举例:
|
||||
|
||||
<a href="https://rsshub.app/twitter/user/durov/readable=1&includeRts=0.atom?brief=100&limit=5"><https://rsshub.app/twitter/user/durov/><span color=green><b>readable=1\&includeRts=0</b></span><span color=magenta><b>.atom</b></span>?<span color=violet><b>brief=100\&limit=5</b></span></a>
|
||||
<a href="https://rsshub.app/twitter/user/durov/readable=1&includeRts=0.atom?brief=100&limit=5"><https://rsshub.app/twitter/user/durov/><span style="color: green"><b>readable=1\&includeRts=0</b></span><span style="color: magenta"><b>.atom</b></span>?<span style="color: violet"><b>brief=100\&limit=5</b></span></a>
|
||||
|
||||
:::
|
||||
|
||||
@@ -137,13 +137,14 @@ Telegram 即时预览模式需要在官网制作页面处理模板,请前往[
|
||||
|
||||
## 输出格式
|
||||
|
||||
RSSHub 同时支持 RSS 2.0 和 Atom 输出格式,在路由末尾添加 `.rss` 或 `.atom` 即可请求对应输出格式,缺省为 RSS 2.0
|
||||
RSSHub 同时支持 RSS 2.0、Atom 和 JSON Feed 输出格式,在路由末尾添加 `.rss`、`.atom` 或 `.json` 即可请求对应输出格式,缺省为 RSS 2.0
|
||||
|
||||
举例:
|
||||
|
||||
- 缺省 RSS 2.0 - <https://rsshub.app/jianshu/home>
|
||||
- RSS 2.0 - <https://rsshub.app/jianshu/home.rss>
|
||||
- Atom - <https://rsshub.app/jianshu/home.atom>
|
||||
- JSON Feed - <https://rsshub.app/twitter/user/DIYgod.json>
|
||||
- 和 filter 或其他 URL query 一起使用 - `https://rsshub.app/bilibili/user/coin/2267573.atom?filter=微小微|赤九玖|暴走大事件`
|
||||
|
||||
### debug
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const art = require('art-template');
|
||||
const { art, json } = require('@/utils/render');
|
||||
const path = require('path');
|
||||
const config = require('@/config').value;
|
||||
const typeRegex = /\.(atom|rss|debug\.json)$/;
|
||||
const typeRegex = /\.(atom|rss|debug\.json|json)$/;
|
||||
const { collapseWhitespace, convertDateToISO8601 } = require('@/utils/common-utils');
|
||||
|
||||
module.exports = async (ctx, next) => {
|
||||
@@ -14,21 +14,25 @@ module.exports = async (ctx, next) => {
|
||||
|
||||
await next();
|
||||
|
||||
if (ctx.state.type[1] === 'debug.json' && config.debugInfo) {
|
||||
const outputType = ctx.state.type[1] || 'rss';
|
||||
|
||||
if (outputType === 'debug.json' && config.debugInfo) {
|
||||
ctx.set({
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
});
|
||||
if (ctx.state.json) {
|
||||
ctx.body = JSON.stringify(ctx.state.json, null, 4);
|
||||
} else {
|
||||
ctx.body = JSON.stringify({ message: 'plugin does not set json' });
|
||||
ctx.body = JSON.stringify({ message: 'plugin does not set debug json' });
|
||||
}
|
||||
}
|
||||
if (outputType === 'json') {
|
||||
ctx.set({ 'Content-Type': 'application/feed+json; charset=UTF-8' });
|
||||
}
|
||||
|
||||
if (!ctx.body) {
|
||||
let template;
|
||||
|
||||
const outputType = ctx.state.type[1];
|
||||
switch (outputType) {
|
||||
case 'atom':
|
||||
template = path.resolve(__dirname, '../views/atom.art');
|
||||
@@ -60,7 +64,16 @@ module.exports = async (ctx, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof item.author === 'string') {
|
||||
item.author = collapseWhitespace(item.author);
|
||||
} else if (typeof item.author === 'object') {
|
||||
for (const a of item.author) {
|
||||
a.name = collapseWhitespace(a.name);
|
||||
}
|
||||
if (outputType !== 'json') {
|
||||
item.author = item.author.map((a) => a.name).join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
if (item.itunes_duration && ((typeof item.itunes_duration === 'string' && item.itunes_duration.indexOf(':') === -1) || (typeof item.itunes_duration === 'number' && !isNaN(item.itunes_duration)))) {
|
||||
item.itunes_duration = +item.itunes_duration;
|
||||
@@ -68,7 +81,7 @@ module.exports = async (ctx, next) => {
|
||||
Math.floor(item.itunes_duration / 3600) + ':' + (Math.floor((item.itunes_duration % 3600) / 60) / 100).toFixed(2).slice(-2) + ':' + (((item.itunes_duration % 3600) % 60) / 100).toFixed(2).slice(-2);
|
||||
}
|
||||
|
||||
if (outputType === 'atom') {
|
||||
if (outputType !== 'rss') {
|
||||
item.pubDate = convertDateToISO8601(item.pubDate);
|
||||
item.updated = convertDateToISO8601(item.updated);
|
||||
}
|
||||
@@ -86,10 +99,15 @@ module.exports = async (ctx, next) => {
|
||||
};
|
||||
if (config.isPackage) {
|
||||
ctx.body = data;
|
||||
} else {
|
||||
if (template) {
|
||||
return;
|
||||
}
|
||||
if (!template) {
|
||||
return;
|
||||
}
|
||||
if (outputType !== 'json') {
|
||||
ctx.body = art(template, data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ctx.body = json(data);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,51 @@ const art = require('art-template');
|
||||
|
||||
// We may add more control over it later
|
||||
|
||||
/**
|
||||
* This function should be used by RSSHub middleware only.
|
||||
* @param {object} data ctx.state.data
|
||||
* @returns `JSON.stringify`-ed [JSON Feed](https://www.jsonfeed.org/)
|
||||
*/
|
||||
const json = (data) => {
|
||||
const jsonFeed = {
|
||||
version: 'https://jsonfeed.org/version/1.1',
|
||||
title: data.title || 'RSSHub',
|
||||
home_page_url: data.link || 'https://docs.rsshub.app',
|
||||
feed_url: data.feedLink,
|
||||
description: `${data.description || data.title} - Made with love by RSSHub(https://github.com/DIYgod/RSSHub)`,
|
||||
icon: data.image,
|
||||
authors: typeof data.author === 'string' ? [{ name: data.author }] : data.author,
|
||||
language: data.language || 'zh-cn',
|
||||
items: data.item.map((item) => ({
|
||||
id: item.guid || item.id || item.link,
|
||||
url: item.link,
|
||||
title: item.title,
|
||||
content_html: (item.content && item.content.html) || item.description || item.title,
|
||||
content_text: item.content && item.content.text,
|
||||
image: item.image,
|
||||
banner_image: item.banner,
|
||||
date_published: item.pubDate,
|
||||
date_modified: item.updated,
|
||||
authors: typeof item.author === 'string' ? [{ name: item.author }] : item.author,
|
||||
tags: typeof item.category === 'string' ? [item.category] : item.category,
|
||||
language: item.language,
|
||||
attachments: item.enclosure_url
|
||||
? [
|
||||
{
|
||||
url: item.enclosure_url,
|
||||
mime_type: item.enclosure_type,
|
||||
title: item.enclosure_title,
|
||||
size_in_bytes: item.enclosure_length,
|
||||
duration_in_seconds: item.itunes_duration,
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
})),
|
||||
};
|
||||
return JSON.stringify(jsonFeed, null, 4);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
art,
|
||||
json,
|
||||
};
|
||||
|
||||
@@ -176,6 +176,46 @@ module.exports = async (ctx) => {
|
||||
pubDate: new Date(1546272000000).toUTCString(),
|
||||
author: `DIYgod0`,
|
||||
});
|
||||
} else if (ctx.params.id === 'json') {
|
||||
item.push(
|
||||
{
|
||||
title: 'Title0',
|
||||
pubDate: new Date(`2019-3-1`).toUTCString(),
|
||||
link: `https://github.com/DIYgod/RSSHub/issues/-3`,
|
||||
},
|
||||
{
|
||||
title: 'Title1',
|
||||
description: 'Description1',
|
||||
pubDate: new Date(`2019-3-1`).toUTCString(),
|
||||
link: `https://github.com/DIYgod/RSSHub/issues/-2`,
|
||||
author: `DIYgod0 `,
|
||||
category: 'Category0',
|
||||
},
|
||||
{
|
||||
title: 'Title2 HTML in description',
|
||||
description: '<a href="https://github.com/DIYgod/RSSHub">RSSHub</a>',
|
||||
pubDate: new Date(`2019-3-1`).toUTCString(),
|
||||
updated: new Date(`2019-3-2`).toUTCString(),
|
||||
link: `https://github.com/DIYgod/RSSHub/issues/-1`,
|
||||
author: [{ name: ' DIYgod1' }, { name: 'DIYgod2 ' }],
|
||||
category: ['Category0', 'Category1'],
|
||||
},
|
||||
{
|
||||
title: 'Title3 HTML in content',
|
||||
content: {
|
||||
html: '<a href="https://github.com/DIYgod/RSSHub">DIYgod/RSSHub</a>',
|
||||
},
|
||||
pubDate: new Date(`2019-3-1`).toUTCString(),
|
||||
updated: new Date(`2019-3-2`).toUTCString(),
|
||||
link: `https://github.com/DIYgod/RSSHub/issues/0`,
|
||||
author: [{ name: ' DIYgod3' }, { name: 'DIYgod4 ' }, { name: 'DIYgod5 ' }],
|
||||
category: ['Category1'],
|
||||
enclosure_url: 'https://github.com/DIYgod/RSSHub/issues/0',
|
||||
enclosure_type: 'image/jpeg',
|
||||
enclosure_length: 3661,
|
||||
itunes_duration: 36610,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 1; i < 6; i++) {
|
||||
|
||||
@@ -63,9 +63,27 @@ describe('template', () => {
|
||||
});
|
||||
|
||||
it(`.json`, async () => {
|
||||
const response = await request.get('/test/1.json');
|
||||
const responseXML = await request.get('/test/1.rss');
|
||||
expect(response.text.slice(0, 50)).toEqual(responseXML.text.slice(0, 50));
|
||||
const jsonResponse = await request.get('/test/1.json');
|
||||
const rssResponse = await request.get('/test/1.rss');
|
||||
const jsonParsed = JSON.parse(jsonResponse.text);
|
||||
const rssParsed = await parser.parseString(rssResponse.text);
|
||||
|
||||
expect(jsonResponse.headers['content-type']).toBe('application/feed+json; charset=UTF-8');
|
||||
|
||||
expect(jsonParsed.items[0].title).toEqual(rssParsed.items[0].title);
|
||||
expect(jsonParsed.items[0].url).toEqual(rssParsed.items[0].link);
|
||||
expect(jsonParsed.items[0].id).toEqual(rssParsed.items[0].guid);
|
||||
expect(jsonParsed.items[0].date_published).toEqual(expectPubDate.toISOString());
|
||||
expect(jsonParsed.items[0].content_html).toEqual(rssParsed.items[0].content);
|
||||
expect(jsonParsed.items[0].authors[0].name).toEqual(rssParsed.items[0].author);
|
||||
expect(jsonParsed.items.every((item) => item.authors.every((author) => author.name.includes(' ')))).toBe(false);
|
||||
});
|
||||
|
||||
it('flatten author object', async () => {
|
||||
const response = await request.get('/test/json');
|
||||
const parsed = await parser.parseString(response.text);
|
||||
expect(parsed.items[2].author).toBe(['DIYgod1', 'DIYgod2'].map((name) => name).join(', '));
|
||||
expect(parsed.items[3].author).toBe(['DIYgod3', 'DIYgod4', 'DIYgod5'].map((name) => name).join(', '));
|
||||
});
|
||||
|
||||
it(`long title`, async () => {
|
||||
|
||||
Reference in New Issue
Block a user