diff --git a/docs/blog.md b/docs/blog.md index 4dbb36f5be9b65..d0df65a9d18292 100644 --- a/docs/blog.md +++ b/docs/blog.md @@ -4,6 +4,18 @@ pageClass: routes # 博客 +## Amazon + +### AWS 博客 + + + +| zh_CN | en_US | fr_FR | de_DE | ja_JP | ko_KR | pt_BR | es_ES | ru_RU | id_ID | tr_TR | +| ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | +| 汉语 | 英语 | 法语 | 德语 | 日语 | 韩语 | 葡萄牙语 | 西班牙语 | 俄语 | 印尼语 | 土耳其语 | + + + ## archdaily ### 首页 diff --git a/docs/en/blog.md b/docs/en/blog.md index d525db7aa4f985..2c6da143b36bb0 100644 --- a/docs/en/blog.md +++ b/docs/en/blog.md @@ -4,6 +4,18 @@ pageClass: routes # Blog +## Amazon + +### AWS Blogs + + + +| zh_CN | en_US | fr_FR | de_DE | ja_JP | ko_KR | pt_BR | es_ES | ru_RU | id_ID | tr_TR | +| ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | +| Chinese | English | French | German | Japanese | Korean | Portuguese | Spainish | Russian | Indonesian | Turkish | + + + ## archdaily ### Home diff --git a/docs/en/other.md b/docs/en/other.md index d88422ede20fb6..79a9f6239aa656 100644 --- a/docs/en/other.md +++ b/docs/en/other.md @@ -214,6 +214,12 @@ For example: +## oshwhub + +### OpenSource Square + + + ## Panda ### Feeds diff --git a/docs/en/social-media.md b/docs/en/social-media.md index 85745f17959f9c..e88fc9dcfdbc4c 100644 --- a/docs/en/social-media.md +++ b/docs/en/social-media.md @@ -437,7 +437,8 @@ Specify options (in the format of query string) in parameter `routeParams` to co | `showAuthorInDesc` | Show author name in description (RSS body) | `0`/`1`/`true`/`false` | `false` (`true` in `/twitter/followings`) | | `showQuotedAuthorAvatarInDesc` | Show avatar of quoted Tweet's author in description (RSS body) (Not recommended if your RSS reader extracts images from description) | `0`/`1`/`true`/`false` | `false` | | `showAuthorAvatarInDesc` | Show avatar of author in description (RSS body) (Not recommended if your RSS reader extracts images from description) | `0`/`1`/`true`/`false` | `false` | -| `showEmojiForRetweetAndReply` | Use "🔁" instead of "Rt", "↩️" & "💬" instead of "Re" | `0`/`1`/`true`/`false` | `false` | +| `showEmojiForRetweetAndReply` | Use "🔁" instead of "RT", "↩️" & "💬" instead of "Re" | `0`/`1`/`true`/`false` | `false` | +| `showSymbolForRetweetAndReply` | Use " RT " instead of "", " Re " instead of "" | `0`/`1`/`true`/`false` | `true` | | `showRetweetTextInTitle` | Show quote comments in title (if `false`, only the retweeted tweet will be shown in the title) | `0`/`1`/`true`/`false` | `true` | | `addLinkForPics` | Add clickable links for Tweet pictures | `0`/`1`/`true`/`false` | `false` | | `showTimestampInDescription` | Show timestamp in description | `0`/`1`/`true`/`false` | `false` | diff --git a/docs/en/traditional-media.md b/docs/en/traditional-media.md index 00685cf76f30b7..9fe39a5e537228 100644 --- a/docs/en/traditional-media.md +++ b/docs/en/traditional-media.md @@ -574,6 +574,20 @@ Language +## The Atlantic + +### News + + + +| Popular | Latest | Politics | Technology | Business | +| ------------ | ------ | -------- | ---------- | -------- | +| most-popular | latest | politics | technology | business | + +More categories (except photo) can be found within the navigation bar at + + + ## The Economist ### Category @@ -678,7 +692,17 @@ Provides all of the articles by the specified New York Times author. ### News - + + +en_us +| World | U.S. | Politics | Economy | Business | Tech | Markets | Opinion | Books & Arts | Real Estate | Life & Work | Sytle | Sports | +| ------ | ------- | -------- | -------- | ----- | --------- | --------- | --------- | --------- | --------- |--------- | --------- | --------- | +| world | us | politics | economy | business | technology | markets | opinion | books-arts | realestate | life-work | style-entertainment | sports | + +zh-cn / zh-tw +| 国际 | 中国 | 金融市场 | 经济 | 商业 | 科技 | 派 | 专栏与观点 | +| ------ | ------- | -------- | -------- | ----- | --------- | --------- | --------- | +| world | china | markets | economy | business | technology | life-arts | opinion | Provide full article RSS for WSJ topics. diff --git a/docs/game.md b/docs/game.md index e3d7f32de82f70..d0f03ec88d591f 100644 --- a/docs/game.md +++ b/docs/game.md @@ -236,9 +236,9 @@ pageClass: routes ### 游戏折扣 -| switch | ps4 | ps5 | xbox | steam | epic | -| ------ | --- | ---- | ---- | ---- | ---- | -| 可用 | 可用 | 可用 | 不可用 | 可用 | 不可用 | +| switch | ps4 | ps5 | xbox | steam | epic | +| ------ | ---- | ---- | ------ | ----- | ------ | +| 可用 | 可用 | 可用 | 不可用 | 可用 | 不可用 | | filter | switch | ps4 | ps5 | steam | | ------ | ------ | --- | --- | ----- | @@ -311,8 +311,8 @@ pageClass: routes ### Feed The Beast (FTB) 模组包更新 -| 参数 | 说明 | -| ------| ------------ | +| 参数 | 说明 | +| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | modpackEntry | 模组包的短名从模组包的页面链接中找到,例如 `https://www.feed-the-beast.com/modpack/ftb_presents_direwolf20_1_16`,短名就是 `ftb_presents_direwolf20_1_16`。 | @@ -913,6 +913,19 @@ Example:`https://www.iyingdi.com/tz/people/55547` ,id 是 `55547` +## 遊戲基地 Gamebase + +### 新聞 + + + +类型 + +| newslist | r18list | +| -------- | ------- | + + + ## 掌上英雄联盟 ### 推荐 diff --git a/docs/other.md b/docs/other.md index c79f045a790f06..c13c8675e2364d 100644 --- a/docs/other.md +++ b/docs/other.md @@ -629,6 +629,12 @@ type 为 all 时,category 参数不支持 cost 和 free +## 立创开源硬件平台 + +### 开源广场 + + + ## 律师事务所文章 ### 君合 diff --git a/docs/social-media.md b/docs/social-media.md index 20fe79c5708fc4..97f8dfe6b6c0b1 100644 --- a/docs/social-media.md +++ b/docs/social-media.md @@ -500,7 +500,7 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性 ### 用户 / 标签 - Private API - + | 用户时间线 | 标签 | | ----- | ---- | @@ -798,6 +798,7 @@ Instagram Stories 没有可靠的 guid,你的 RSS 阅读器可能将同一条 | `showQuotedAuthorAvatarInDesc` | 是否在正文处显示被转推的推文的作者头像(若阅读器会提取正文图片,不建议开启) | `0`/`1`/`true`/`false` | `false` | | `showAuthorAvatarInDesc` | 是否在正文处显示作者头像(若阅读器会提取正文图片,不建议开启) | `0`/`1`/`true`/`false` | `false` | | `showEmojiForRetweetAndReply` | 显示 “🔁” 取代 “Rt”、“↩️” 取代 “Re” | `0`/`1`/`true`/`false` | `false` | +| `showSymbolForRetweetAndReply` | 显示 “RT” 取代 “”、“ Re ” 取代 “” | `0`/`1`/`true`/`false` | `true` | | `showRetweetTextInTitle` | 在标题处显示转推评论(置为 `false` 则在标题只显示被转推推文) | `0`/`1`/`true`/`false` | `true` | | `addLinkForPics` | 为图片添加可点击的链接 | `0`/`1`/`true`/`false` | `false` | | `showTimestampInDescription` | 在正文处显示推特的时间戳 | `0`/`1`/`true`/`false` | `false` | @@ -946,7 +947,8 @@ YouTube 官方亦有提供频道 RSS,形如 @@ -1257,6 +1260,35 @@ YouTube 官方亦有提供频道 RSS,形如 +### 榜单与集合 + + + +| 榜单 / 集合 | 路由(type) | +| --------- | -------------------------- | +| 实时热门书影音 | subject_real_time_hotest | +| 影院热映 | movie_showing | +| 实时热门电影 | movie_real_time_hotest | +| 实时热门电视 | tv_real_time_hotest | +| 一周口碑电影榜 | movie_weekly_best | +| 华语口碑剧集榜 | tv_chinese_best_weekly | +| 全球口碑剧集榜 | tv_global_best_weekly | +| 国内口碑综艺榜 | show_chinese_best_weekly | +| 国外口碑综艺榜 | show_global_best_weekly | +| 虚构类小说热门榜 | book_fiction_hot_weekly | +| 非虚构类小说热门榜 | book_nonfiction_hot_weekly | +| 热门单曲榜 | music_single | +| 华语新碟榜 | music_chinese | +| ... | ... | + +> 上面的榜单 / 集合并没有列举完整。 +> +> 如何找到榜单对应的路由参数: +> 在豆瓣手机 APP 中,对应地榜单页面右上角,点击分享链接。链接路径 `subject_collection` 后的路径就是路由参数 `type`。 +> 如:小说热门榜的分享链接为:`https://m.douban.com/subject_collection/ECDIHUN4A`,其对应本 RSS 路由的 `type` 为 `ECDIHUN4A`,对应的订阅链接路由:[`/douban/list/ECDIHUN4A`](https://rsshub.app/douban/list/ECDIHUN4A) + + + ## 饭否 ::: warning 注意 diff --git a/docs/traditional-media.md b/docs/traditional-media.md index d80084716efdd8..eef02620f7ee3c 100644 --- a/docs/traditional-media.md +++ b/docs/traditional-media.md @@ -468,6 +468,20 @@ Solidot 提供的 feed: +## The Atlantic + +### News + + + +| Popular | Latest | Politics | Technology | Business | +| ------------ | ------ | -------- | ---------- | -------- | +| most-popular | latest | politics | technology | business | + +More categories (except photo) can be found within the navigation bar at + + + ## The Economist ### 分类 @@ -1182,7 +1196,19 @@ IT・科学 tech_science ### 新闻 - + + +en_us + +| World | U.S. | Politics | Economy | Business | Tech | Markets | Opinion | Books & Arts | Real Estate | Life & Work | Sytle | Sports | +| ----- | ---- | -------- | ------- | -------- | ---------- | ------- | ------- | ------------ | ----------- | ----------- | ------------------- | ------ | +| world | us | politics | economy | business | technology | markets | opinion | books-arts | realestate | life-work | style-entertainment | sports | + +zh-cn / zh-tw + +| 国际 | 中国 | 金融市场 | 经济 | 商业 | 科技 | 派 | 专栏与观点 | +| ----- | ----- | ------- | ------- | -------- | ---------- | --------- | ------- | +| world | china | markets | economy | business | technology | life-arts | opinion | 通过提取文章全文,以提供比官方源更佳的阅读体验。 diff --git a/docs/university.md b/docs/university.md index ab1019801ff301..42d872bc46999d 100644 --- a/docs/university.md +++ b/docs/university.md @@ -3045,6 +3045,22 @@ jsjxy.hbut.edu.cn 证书链不全,自建 RSSHub 可设置环境变量 NODE_TLS ## 西南交通大学 +### 教务网 + + + +### 扬华素质网 + + + +栏目列表: + +| 通知公告 | 扬华新闻 | 多彩学院 | 学工之家 | +| ---- | ---- | ---- | ---- | +| tzgg | yhxw | dcxy | xgzj | + + + ### 就业招聘信息 diff --git a/lib/v2/amazon/awsblogs.js b/lib/v2/amazon/awsblogs.js new file mode 100644 index 00000000000000..5baa0822b50e45 --- /dev/null +++ b/lib/v2/amazon/awsblogs.js @@ -0,0 +1,27 @@ +const got = require('@/utils/got'); +const { parseDate } = require('@/utils/parse-date'); + +module.exports = async (ctx) => { + const locale = ctx.params.locale ?? 'zh_CN'; + + const response = await got({ + url: `https://aws.amazon.com/api/dirs/items/search?item.directoryId=blog-posts&sort_by=item.additionalFields.createdDate&sort_order=desc&size=50&item.locale=${locale}`, + }); + + const items = response.data.items; + + ctx.state.data = { + title: 'AWS Blog', + link: 'https://aws.amazon.com/blogs/', + description: 'AWS Blog 更新', + item: + items && + items.map((item) => ({ + title: item.item.additionalFields.title, + description: item.item.additionalFields.postExcerpt, + pubDate: parseDate(item.item.dateCreated), + link: item.item.additionalFields.link, + author: item.item.additionalFields.contributors, + })), + }; +}; diff --git a/lib/v2/amazon/maintainer.js b/lib/v2/amazon/maintainer.js index 05066d6510d28d..49f1311dab125a 100644 --- a/lib/v2/amazon/maintainer.js +++ b/lib/v2/amazon/maintainer.js @@ -1,3 +1,4 @@ module.exports = { + '/awsblogs/:locale?': ['HankChow'], '/kindle/software-updates': ['NavePnow'], }; diff --git a/lib/v2/amazon/radar.js b/lib/v2/amazon/radar.js index 5d0b0aff3816fa..a90c7aacca9417 100644 --- a/lib/v2/amazon/radar.js +++ b/lib/v2/amazon/radar.js @@ -14,5 +14,11 @@ module.exports = { }, }, ], + aws: [ + { + title: 'AWS blogs', + docs: 'https://docs.rsshub.app/blogs.html#amazon', + }, + ], }, }; diff --git a/lib/v2/amazon/router.js b/lib/v2/amazon/router.js index 7a77965e8c6294..f139b51139fe3b 100644 --- a/lib/v2/amazon/router.js +++ b/lib/v2/amazon/router.js @@ -1,3 +1,4 @@ module.exports = function (router) { + router.get('/awsblogs/:locale?', require('./awsblogs')); router.get('/kindle/software-updates', require('./kindle-software-updates')); }; diff --git a/lib/v2/coindesk/index.js b/lib/v2/coindesk/index.js index d9b2dacb1eb491..5d44cbd719c250 100644 --- a/lib/v2/coindesk/index.js +++ b/lib/v2/coindesk/index.js @@ -5,9 +5,8 @@ const rootUrl = 'https://www.coindesk.com'; module.exports = async (ctx) => { const channel = ctx.params.channel ?? 'consensus-magazine'; - const response = await got.get(`${rootUrl}/${channel}`); + const response = await got.get(`${rootUrl}/${channel}/`); const $ = cheerio.load(response.data); - const title = $('div.title-holder h2').text(); const content = JSON.parse( $('#fusion-metadata') .text() @@ -16,16 +15,15 @@ module.exports = async (ctx) => { const o1 = content['websked-collections']; // Object key names are different every week - const feature = o1[Object.keys(o1)[2]]; - const opinion = o1[Object.keys(o1)[3]]; + const articles = o1[Object.keys(o1)[2]]; - const list = [...feature.data, ...opinion.data]; + const list = articles.data; const items = list.map((item) => ({ title: item.headlines.basic, link: rootUrl + item.canonical_url, - description: title + '

' + item.subheadlines.basic, - pubDate: item.publish_date, + description: item.subheadlines.basic, + pubDate: item.display_date, })); ctx.state.data = { diff --git a/lib/v2/douban/maintainer.js b/lib/v2/douban/maintainer.js index 83aabf0db8b86f..85a7a6dc4e57b2 100644 --- a/lib/v2/douban/maintainer.js +++ b/lib/v2/douban/maintainer.js @@ -11,6 +11,7 @@ module.exports = { '/explore': ['clarkzsd'], '/explore_column/:id': ['LogicJake'], '/group/:groupid/:type?': ['DIYgod'], + '/list/:type?': ['5upernova-heng'], '/jobs/:type': ['Fatpandac'], '/movie/classification/:sort?/:score?/:tags?': ['zzwab'], '/movie/later': ['DIYgod'], diff --git a/lib/v2/douban/other/list.js b/lib/v2/douban/other/list.js new file mode 100644 index 00000000000000..59cbcd3bf296d0 --- /dev/null +++ b/lib/v2/douban/other/list.js @@ -0,0 +1,40 @@ +const got = require('@/utils/got'); +const path = require('path'); +const { art } = require('@/utils/render'); + +module.exports = async (ctx) => { + const type = ctx.params.type || 'subject_real_time_hotest'; + const url = `https://m.douban.com/rexxar/api/v2/subject_collection/${type}/items`; + const response = await got({ + method: 'get', + url, + headers: { + Referer: `https://m.douban.com/subject_collection/${type}`, + }, + }); + const description = response.data.subject_collection.description; + const items = response.data.subject_collection_items.map((item) => { + const title = item.title; + const link = item.url; + const description = art(path.join(__dirname, '../templates/list_description.art'), { + ranking_value: item.ranking_value, + title, + original_title: item.original_title, + rate: item.rating ? item.rating.value : null, + card_subtitle: item.card_subtitle, + description: item.cards ? item.cards[0].content : item.abstract, + cover: item.cover_url || item.cover.url, + }); + return { + title, + link, + description, + }; + }); + ctx.state.data = { + title: `豆瓣 - ${response.data.subject_collection.name}`, + link: 'https://m.douban.com/subject_collection/subject_real_time_hotest', + item: items, + description, + }; +}; diff --git a/lib/v2/douban/radar.js b/lib/v2/douban/radar.js index ea49ac1c65c393..8fdbdb622f0d19 100644 --- a/lib/v2/douban/radar.js +++ b/lib/v2/douban/radar.js @@ -29,6 +29,12 @@ module.exports = { source: '/group/:groupid', target: '/douban/group/:groupid/elite', }, + { + title: '榜单与集合', + docs: 'https://docs.rsshub.app/social-media.html#douban', + source: ['/subject_collection/:type'], + target: '/douban/list/:type', + }, ], jobs: [ { diff --git a/lib/v2/douban/router.js b/lib/v2/douban/router.js index d40f4432715be5..6c40d1a17b9184 100644 --- a/lib/v2/douban/router.js +++ b/lib/v2/douban/router.js @@ -12,6 +12,7 @@ module.exports = function (router) { router.get('/explore/column/:id', require('./other/explore_column')); router.get('/group/:groupid/:type?', require('./other/group')); router.get('/jobs/:type', require('./other/jobs.js')); + router.get('/list/:type?', require('./other/list')); router.get('/movie/classification/:sort?/:score?/:tags?', require('./other/classification.js')); router.get('/movie/later', require('./other/later')); router.get('/movie/playing', require('./other/playing')); diff --git a/lib/v2/douban/templates/list_description.art b/lib/v2/douban/templates/list_description.art new file mode 100644 index 00000000000000..169b03dd954b30 --- /dev/null +++ b/lib/v2/douban/templates/list_description.art @@ -0,0 +1,25 @@ +{{ if ranking_value }} +

{{ ranking_value }}

+{{ /if }} + +

{{ title }}

+ +{{ if original_title }} +

{{ original_title }}

+{{ /if }} + +{{ if rate }} +

{{ rate }}

+{{ /if }} + +{{ if card_subtitle }} +

{{ card_subtitle }}

+{{ /if }} + +{{ if description }} +

{{ description }}

+{{ /if }} + +{{ if cover }} + +{{ /if }} diff --git a/lib/v2/gamebase/maintainer.js b/lib/v2/gamebase/maintainer.js new file mode 100644 index 00000000000000..aa6c6618a92d09 --- /dev/null +++ b/lib/v2/gamebase/maintainer.js @@ -0,0 +1,3 @@ +module.exports = { + '/news/:type?/:category?': ['nczitzk'], +}; diff --git a/lib/v2/gamebase/news.js b/lib/v2/gamebase/news.js new file mode 100644 index 00000000000000..629d5e4c94acdf --- /dev/null +++ b/lib/v2/gamebase/news.js @@ -0,0 +1,74 @@ +const got = require('@/utils/got'); +const cheerio = require('cheerio'); +const timezone = require('@/utils/timezone'); +const { parseDate } = require('@/utils/parse-date'); + +const types = { + newslist: 'newsList', + r18list: 'newsPornList', +}; + +module.exports = async (ctx) => { + const type = ctx.params.type ?? 'newslist'; + const category = ctx.params.category ?? 'all'; + const limit = ctx.query.limit ? parseInt(ctx.query.limit) : 20; + + const rootUrl = 'https://news.gamebase.com.tw'; + const currentUrl = `${rootUrl}/news/${type}?type=${category}`; + + const apiRootUrl = 'https://api.gamebase.com.tw'; + const apiUrl = `${apiRootUrl}/api/news/getNewsList`; + + const response = await got({ + method: 'post', + url: apiUrl, + json: { + GB_type: types[type], + category, + page: 1, + }, + }); + + const titleResponse = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = cheerio.load(titleResponse.data); + + const items = await Promise.all( + response.data.return_msg.list.slice(0, limit).map((item) => + ctx.cache.tryGet(`gamebase:news:${type}:${category}:${item.news_no}`, async () => { + const i = {}; + + i.author = item.nickname; + i.title = item.news_title; + i.link = `${rootUrl}/news/detail/${item.news_no}`; + i.description = item.news_meta?.meta_des ?? ''; + i.pubDate = timezone(parseDate(item.post_time), +8); + i.category = [item.system]; + + if (i.description) { + return i; + } + + const detailResponse = await got({ + method: 'get', + url: i.link, + }); + + const description = detailResponse.data.match(/(\\u003C.*?)","/)[1].replace(/\\"/g, '"'); + + i.description = description.replace(/\\u[\dA-F]{4}/gi, (match) => String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16))); + + return i; + }) + ) + ); + + ctx.state.data = { + title: $('title').text(), + link: currentUrl, + item: items, + }; +}; diff --git a/lib/v2/gamebase/radar.js b/lib/v2/gamebase/radar.js new file mode 100644 index 00000000000000..17a8eff10614fa --- /dev/null +++ b/lib/v2/gamebase/radar.js @@ -0,0 +1,13 @@ +module.exports = { + 'gamebase.com.tw': { + _name: '遊戲基地 Gamebase', + news: [ + { + title: '新聞', + docs: 'https://docs.rsshub.app/game.html#gamebase-xin-wen', + source: ['/news/:type'], + target: (params, url) => `/gamebase/news/${params.type}/${new URL(url).searchParams.get('type')}`, + }, + ], + }, +}; diff --git a/lib/v2/gamebase/router.js b/lib/v2/gamebase/router.js new file mode 100644 index 00000000000000..8b0471905f12e5 --- /dev/null +++ b/lib/v2/gamebase/router.js @@ -0,0 +1,3 @@ +module.exports = function (router) { + router.get('/news/:type?/:category?', require('./news')); +}; diff --git a/lib/v2/oshwhub/explore.js b/lib/v2/oshwhub/explore.js new file mode 100644 index 00000000000000..d82ec6b8f19328 --- /dev/null +++ b/lib/v2/oshwhub/explore.js @@ -0,0 +1,116 @@ +const got = require('@/utils/got'); +const cheerio = require('cheerio'); +const path = require('path'); +const { art } = require('@/utils/render'); +const timezone = require('@/utils/timezone'); +const { parseDate } = require('@/utils/parse-date'); +const md = require('markdown-it')({ + html: true, + linkify: true, +}); + +const requestImages = (url, tryGet) => + tryGet(url, async () => { + const response = await got({ + method: 'get', + url, + }); + + const images = {}; + const boms = []; + + const imgData = response.data; + imgData?.result?.boards?.forEach((board) => { + const name = board.name; + if (images[name] === undefined) { + images[name] = []; + } + + board.schematic?.documents?.forEach((val) => { + if (val.thumb) { + images[name].push(val.thumb); + } + }); + + if (board.pcb_info?.thumb) { + images[name].push(board.pcb_info.thumb); + } + if (board.pcb_info?.boms?.boms) { + boms.push(...JSON.parse(board.pcb_info.boms.boms)); + } + }); + return { images, boms }; + }); + +module.exports = async (ctx) => { + const baseUrl = 'https://oshwhub.com'; + const sortType = ctx.params.sortType ?? 'updatedTime'; + + const url = `${baseUrl}/explore?projectSort=${sortType}`; + const response = await got({ + method: 'get', + url, + }); + + const html = response.data; + const $ = cheerio.load(html); + const title = $('title').text(); + const projects = $('a[class="project-link"]'); + + const items = await Promise.all( + projects.map((id, item) => { + const detailUrl = `${baseUrl}${item.attribs.href}`; + return ctx.cache.tryGet(detailUrl, async () => { + const response = await got({ + method: 'get', + url: detailUrl, + }); + + const html = response.data; + const $$ = cheerio.load(html); + const projectDetail = $$('div[class="projects-left-top"]'); + + const title = $$(projectDetail).find('span[class="title"]').text(); + const author = $$(projectDetail).find('div[class="member-items"] > ul > li > a > span').first().text().trim(); + const publishTime = $$(projectDetail).find('div[class="issue-time"] > span').text(); + const pubDate = timezone(parseDate(publishTime.trim().split('\n')[1].trim(), 'YYYY/MM/DD HH:mm:ss'), 0); + + const coverImg = $$(projectDetail).find('div[class="img-cover"] > img').attr('src').trim(); + const introduction = $$(projectDetail).find('p[class="introduction"]').text().trim(); + const content = $$(projectDetail).find('div[class*="html-content"]').attr('data-markdown'); + + // request images + const dataUuid = $$('div[class="projects-detail"]').attr()['data-uuid']; + const { images, boms } = await requestImages(`${baseUrl}/api/project/${dataUuid}/pro_images`, ctx.cache.tryGet); + + const description = art(path.join(__dirname, 'templates/description.art'), { + title, + author, + publishTime, + coverImg, + introduction, + content: md.render(content) || 'No Content', + images, + boms, + }); + + const single = { + title, + description, + pubDate, + link: detailUrl, + author, + }; + + return single; + }); + }) + ); + + ctx.state.data = { + title, + link: url, + description: title, + item: items, + }; +}; diff --git a/lib/v2/oshwhub/maintainer.js b/lib/v2/oshwhub/maintainer.js new file mode 100644 index 00000000000000..ff8668b06de44e --- /dev/null +++ b/lib/v2/oshwhub/maintainer.js @@ -0,0 +1,3 @@ +module.exports = { + '/:sortType?': ['tylinux'], +}; diff --git a/lib/v2/oshwhub/radar.js b/lib/v2/oshwhub/radar.js new file mode 100644 index 00000000000000..5176c22e2fb67a --- /dev/null +++ b/lib/v2/oshwhub/radar.js @@ -0,0 +1,16 @@ +module.exports = { + 'oshwhub.com': { + _name: '立创开源硬件平台', + '.': [ + { + title: '开源广场', + docs: 'https://docs.rsshub.app/other.html#li-chuang-kai-yuan-ying-jian-ping-tai', + source: ['/explore'], + target: (_, url) => { + const sortType = new URL(url).searchParams.get('projectSort'); + return sortType ? `/oshwhub/${sortType}` : ''; + }, + }, + ], + }, +}; diff --git a/lib/v2/oshwhub/router.js b/lib/v2/oshwhub/router.js new file mode 100644 index 00000000000000..b112bc945e7f29 --- /dev/null +++ b/lib/v2/oshwhub/router.js @@ -0,0 +1,3 @@ +module.exports = function (router) { + router.get('/:sortType?', require('./explore')); +}; diff --git a/lib/v2/oshwhub/templates/description.art b/lib/v2/oshwhub/templates/description.art new file mode 100644 index 00000000000000..cb6027fcaaa005 --- /dev/null +++ b/lib/v2/oshwhub/templates/description.art @@ -0,0 +1,58 @@ +
+
+

+ {{publishTime}}
+ 作者:{{author}}
+ {{introduction}}
+
+ +
+

描述

+ {{@ content}} +
+ + {{ if images }} +
+

PCB && schematic

+ {{each images $imgs $key}} + {{each $imgs $img}} +
+ {{/each}} + {{/each}} +
+ {{/if}} + + {{ if boms }} +
+

BOM

+ + + + + + + + + + + + + + {{each boms}} + + + + + + + + + + + + + {{/each}} +
NoQuantityDeviceDesignatorFootprintValueManufacturer PartManufacturerSupplier PartSupplier
{{$value.No}}{{$value.Quantity}}{{$value.Device}}{{$value.Designator}}{{$value.Footprint}}{{$value.Value}}{{$value['Manufacturer Part']}}{{$value.Manufacturer}}{{$value['Supplier Part']}}{{$value.Supplier}}
+
+ {{/if}} +
\ No newline at end of file diff --git a/lib/v2/swjtu/jwc.js b/lib/v2/swjtu/jwc.js new file mode 100644 index 00000000000000..e29d90bdf70a45 --- /dev/null +++ b/lib/v2/swjtu/jwc.js @@ -0,0 +1,67 @@ +const got = require('@/utils/got'); +const cheerio = require('cheerio'); +const { parseDate } = require('@/utils/parse-date'); + +const rootURL = 'http://jwc.swjtu.edu.cn'; +const pageURL = `${rootURL}/vatuu/WebAction?setAction=newsList`; + +const getItem = (item, cache) => { + const newsInfo = item.find('h3').find('a'); + const newsTime = item.find('p').find('span').text().slice(0, 19); + const newsTitle = newsInfo.text(); + const link = `${rootURL}${newsInfo.attr('href').slice(2)}`; + return cache.tryGet(link, async () => { + try { + const resp = await got({ + method: 'get', + url: link, + }); + const $$ = cheerio.load(resp.data); + let newsText = $$('.content-main').html(); + if (!newsText) { + newsText = '转发通知'; + } + return { + title: newsTitle, + pubDate: parseDate(String(newsTime)), + link, + description: newsText, + }; + } catch (error) { + if (error.response && error.response.status === 404) { + return { + title: newsTitle, + pubDate: parseDate(String(newsTime)), + link, + description: '', + }; + } else { + throw error; + } + } + }); +}; + +module.exports = async (ctx) => { + const resp = await got({ + method: 'get', + url: pageURL, + }); + + const $ = cheerio.load(resp.data); + const list = $("[class='littleResultDiv']"); + + const items = await Promise.all( + list.toArray().map((i) => { + const item = $(i); + return getItem(item, ctx.cache); + }) + ); + + ctx.state.data = { + title: '西南交大-教务网通知', + link: pageURL, + item: items, + allowEmpty: true, + }; +}; diff --git a/lib/v2/swjtu/maintainer.js b/lib/v2/swjtu/maintainer.js index 47c795c3e3181f..71a35bf3468576 100644 --- a/lib/v2/swjtu/maintainer.js +++ b/lib/v2/swjtu/maintainer.js @@ -1,4 +1,6 @@ module.exports = { '/jtys/yjs': ['qizidog'], '/jyzpxx': ['qizidog'], + '/jwc': ['mobyw'], + '/xg/:code?': ['mobyw'], }; diff --git a/lib/v2/swjtu/radar.js b/lib/v2/swjtu/radar.js index 6717d179f97133..de334d2d452603 100644 --- a/lib/v2/swjtu/radar.js +++ b/lib/v2/swjtu/radar.js @@ -17,5 +17,21 @@ module.exports = { target: '/swjtu/jyzpxx', }, ], + jwc: [ + { + title: '教务处通知', + docs: 'https://docs.rsshub.app/university.html#xi-nan-jiao-tong-da-xue', + source: ['/vatuu/WebAction', '/'], + target: '/swjtu/jwc', + }, + ], + xg: [ + { + title: '扬华素质网', + docs: 'https://docs.rsshub.app/university.html#xi-nan-jiao-tong-da-xue', + source: ['/web/Home/PushNewsList', '/web/Home/NewsList', '/web/Home/ColourfulCollegeNewsList', '/web/Publicity/List', '/'], + target: '/swjtu/xg', + }, + ], }, }; diff --git a/lib/v2/swjtu/router.js b/lib/v2/swjtu/router.js index 632f5957b851e0..8b3854dfa1c545 100644 --- a/lib/v2/swjtu/router.js +++ b/lib/v2/swjtu/router.js @@ -1,4 +1,6 @@ module.exports = function (router) { router.get('/jtys/yjs', require('./jtys/yjs')); router.get('/jyzpxx', require('./jyzpxx')); + router.get('/jwc', require('./jwc')); + router.get('/xg/:code?', require('./xg')); }; diff --git a/lib/v2/swjtu/xg.js b/lib/v2/swjtu/xg.js new file mode 100644 index 00000000000000..1c755d82cc2827 --- /dev/null +++ b/lib/v2/swjtu/xg.js @@ -0,0 +1,79 @@ +const got = require('@/utils/got'); +const cheerio = require('cheerio'); +const { parseDate } = require('@/utils/parse-date'); + +const rootURL = 'http://xg.swjtu.edu.cn'; +const listURL = { + tzgg: `${rootURL}/web/Home/PushNewsList?Lmk7LJw34Jmu=010j.shtml`, + yhxw: `${rootURL}/web/Home/NewsList?LJw34Jmu=011e.shtml`, + dcxy: `${rootURL}/web/Home/ColourfulCollegeNewsList`, + xgzj: `${rootURL}/web/Home/NewsList?xvw34vmu=010e.shtml`, +}; + +const getItem = (item, cache) => { + const newsInfo = item.find('h4').find('a'); + const newsTime = item.find('span.ctxlist-time').text(); + const newsTitle = newsInfo.text(); + const link = `${rootURL}${newsInfo.attr('href')}`; + return cache.tryGet(link, async () => { + try { + const resp = await got({ + method: 'get', + url: link, + }); + const $$ = cheerio.load(resp.data); + let newsText = $$('.detail-content-text').html(); + if (!newsText) { + newsText = '转发通知'; + } + return { + title: newsTitle, + pubDate: parseDate(String(newsTime)), + link, + description: newsText, + }; + } catch (error) { + if (error.response && error.response.status === 404) { + return { + title: newsTitle, + pubDate: parseDate(String(newsTime)), + link, + description: '', + }; + } else { + throw error; + } + } + }); +}; + +module.exports = async (ctx) => { + const code = ctx.params.code ?? 'tzgg'; + const pageURL = listURL[code]; + + if (!pageURL) { + throw new Error('code not supported'); + } + + const resp = await got({ + method: 'get', + url: pageURL, + }); + + const $ = cheerio.load(resp.data); + const list = $('div.right-side ul.block-ctxlist li'); + + const items = await Promise.all( + list.toArray().map((i) => { + const item = $(i); + return getItem(item, ctx.cache); + }) + ); + + ctx.state.data = { + title: '西南交大-扬华素质网', + link: pageURL, + item: items, + allowEmpty: true, + }; +}; diff --git a/lib/v2/theatlantic/maintainer.js b/lib/v2/theatlantic/maintainer.js new file mode 100644 index 00000000000000..1b6cbaac98db1a --- /dev/null +++ b/lib/v2/theatlantic/maintainer.js @@ -0,0 +1,3 @@ +module.exports = { + '/:category': ['NavePnow'], +}; diff --git a/lib/v2/theatlantic/news.js b/lib/v2/theatlantic/news.js new file mode 100644 index 00000000000000..85f29deea6ce54 --- /dev/null +++ b/lib/v2/theatlantic/news.js @@ -0,0 +1,33 @@ +const got = require('@/utils/got'); +const cheerio = require('cheerio'); +const { getArticleDetails } = require('./utils'); +module.exports = async (ctx) => { + const host = 'https://www.theatlantic.com'; + const category = ctx.params.category; + const url = `${host}/${category}/`; + const response = await got({ + method: 'get', + url, + }); + const $ = cheerio.load(response.data); + const contents = JSON.parse($('script#__NEXT_DATA__').text()).props.pageProps.urqlState; + const firstKey = Object.keys(contents)[0]; + const data = JSON.parse(contents[firstKey].data); + let list = Object.values(data)[0].river.edges; + list = list.filter((item) => !item.node.url.startsWith('https://www.theatlantic.com/photo')); + list = list.map((item) => { + const data = {}; + data.title = item.node.title; + data.link = item.node.url; + data.pubDate = item.node.datePublished; + return data; + }); + const items = await getArticleDetails(list, ctx); + + ctx.state.data = { + title: `The Atlantic - ${category.toUpperCase()}`, + link: url, + description: `The Atlantic - ${category.toUpperCase()}`, + item: items, + }; +}; diff --git a/lib/v2/theatlantic/radar.js b/lib/v2/theatlantic/radar.js new file mode 100644 index 00000000000000..a8288a387b5075 --- /dev/null +++ b/lib/v2/theatlantic/radar.js @@ -0,0 +1,13 @@ +module.exports = { + 'theatlantic.com': { + _name: 'The Atlantic', + www: [ + { + title: '新闻', + docs: 'https://docs.rsshub.app/traditional-media.html#the-atlantic', + source: '/:category', + target: '/theatlantic/:category', + }, + ], + }, +}; diff --git a/lib/v2/theatlantic/router.js b/lib/v2/theatlantic/router.js new file mode 100644 index 00000000000000..d146629a4d88de --- /dev/null +++ b/lib/v2/theatlantic/router.js @@ -0,0 +1,3 @@ +module.exports = (router) => { + router.get('/:category', require('./news')); +}; diff --git a/lib/v2/theatlantic/templates/article-description.art b/lib/v2/theatlantic/templates/article-description.art new file mode 100644 index 00000000000000..c64df2e2050866 --- /dev/null +++ b/lib/v2/theatlantic/templates/article-description.art @@ -0,0 +1,19 @@ +
+ {{@ item.caption}} +
+ {{ if item.imgUrl }} +
+ {{item.imgAlt}} +
{{item.imgCaption}}
+
+ {{ /if }} + {{each item.content}} + {{@ $value.innerHtml}} + {{if !$value.tagName}} +
+
+ {{/if}} + {{/each}} + +
+ diff --git a/lib/v2/theatlantic/utils.js b/lib/v2/theatlantic/utils.js new file mode 100644 index 00000000000000..feb3f55e71f945 --- /dev/null +++ b/lib/v2/theatlantic/utils.js @@ -0,0 +1,53 @@ +const cheerio = require('cheerio'); +const got = require('@/utils/got'); +const { parseDate } = require('@/utils/parse-date'); +const { art } = require('@/utils/render'); +const path = require('path'); +const UA = require('@/utils/rand-user-agent')({ browser: 'chrome', os: 'android', device: 'mobile' }); + +const getArticleDetails = async (items, ctx) => { + const list = await Promise.all( + items.map((item) => + ctx.cache.tryGet(item.link, async () => { + const url = item.link; + const response = await got({ + url, + method: 'get', + headers: { + 'User-Agent': UA, + }, + }); + const html = response.data; + const $ = cheerio.load(html); + let data = JSON.parse($('script#__NEXT_DATA__').text()); + + const list = data.props.pageProps.urqlState; + const keyWithContent = Object.keys(list).filter((key) => list[key].data.includes('content')); + data = JSON.parse(list[keyWithContent].data).article; + item.category = data.categories.map((category) => category.slug); + data.channels.forEach((channel) => { + item.category.push(channel.slug); + }); + item.content = data.content.filter((item) => item.innerHtml !== undefined && item.innerHtml !== ''); + item.caption = data.dek; + item.imgUrl = data.leadArt.image?.url; + item.imgAlt = data.leadArt.image?.altText; + item.imgCaption = data.leadArt.image?.attributionText; + item.description = art(path.join(__dirname, 'templates/article-description.art'), { + item, + }); + return { + title: item.title, + pubDate: parseDate(item.pubDate), + link: item.link, + description: item.description, + }; + }) + ) + ); + return list; +}; + +module.exports = { + getArticleDetails, +}; diff --git a/lib/v2/tingshuitz/xiaoshan.js b/lib/v2/tingshuitz/xiaoshan.js index 2577e57b0434a6..c98c9f2ce5e5ca 100644 --- a/lib/v2/tingshuitz/xiaoshan.js +++ b/lib/v2/tingshuitz/xiaoshan.js @@ -3,7 +3,7 @@ const cheerio = require('cheerio'); module.exports = async (ctx) => { // const area = ctx.params.area; - const url = 'http://www.xswater.com/gongshui/channels/227.html'; + const url = 'https://www.xswater.com/gongshui/channels/227.html'; const response = await got({ method: 'get', url, @@ -15,7 +15,7 @@ module.exports = async (ctx) => { ctx.state.data = { title: $('title').text(), - link: 'http://www.xswater.com/gongshui/channels/227.html', + link: 'https://www.xswater.com/gongshui/channels/227.html', description: $('meta[name="description"]').attr('content') || $('title').text(), item: list && @@ -26,7 +26,7 @@ module.exports = async (ctx) => { title: item.find('a').text(), description: `萧山区停水通知:${item.find('a').text()}`, pubDate: new Date(item.find('span').text().slice(1, 11)).toUTCString(), - link: `http://www.xswater.com${item.find('a').attr('href')}`, + link: `https://www.xswater.com${item.find('a').attr('href')}`, }; }) .get(), diff --git a/lib/v2/twitter/utils.js b/lib/v2/twitter/utils.js index 26783dccf8fbac..9064e665406f40 100644 --- a/lib/v2/twitter/utils.js +++ b/lib/v2/twitter/utils.js @@ -48,6 +48,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { showQuotedAuthorAvatarInDesc: fallback(params.showQuotedAuthorAvatarInDesc, queryToBoolean(routeParams.get('showQuotedAuthorAvatarInDesc')), false), showAuthorAvatarInDesc: fallback(params.showAuthorAvatarInDesc, queryToBoolean(routeParams.get('showAuthorAvatarInDesc')), false), showEmojiForRetweetAndReply: fallback(params.showEmojiForRetweetAndReply, queryToBoolean(routeParams.get('showEmojiForRetweetAndReply')), false), + showSymbolForRetweetAndReply: fallback(params.showSymbolForRetweetAndReply, queryToBoolean(routeParams.get('showSymbolForRetweetAndReply')), true), showRetweetTextInTitle: fallback(params.showRetweetTextInTitle, queryToBoolean(routeParams.get('showRetweetTextInTitle')), true), addLinkForPics: fallback(params.addLinkForPics, queryToBoolean(routeParams.get('addLinkForPics')), false), showTimestampInDescription: fallback(params.showTimestampInDescription, queryToBoolean(routeParams.get('showTimestampInDescription')), false), @@ -69,6 +70,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { showQuotedAuthorAvatarInDesc, showAuthorAvatarInDesc, showEmojiForRetweetAndReply, + showSymbolForRetweetAndReply, showRetweetTextInTitle, addLinkForPics, showTimestampInDescription, @@ -228,7 +230,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { } quote += formatMedia(quoteData); picsPrefix += generatePicsPrefix(quoteData); - quoteInTitle += showEmojiForRetweetAndReply ? ' 💬 ' : ' RT '; + quoteInTitle += showEmojiForRetweetAndReply ? ' 💬 ' : showSymbolForRetweetAndReply ? ' RT ' : ''; quoteInTitle += `${author.name}: ${formatText(quoteData.full_text, quoteData.entities.urls)}`; if (readable) { @@ -258,15 +260,15 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { const isQuote = item.is_quote_status; if (!isRetweet && (!isQuote || showRetweetTextInTitle)) { if (item.in_reply_to_screen_name) { - title += showEmojiForRetweetAndReply ? '↩️ ' : 'Re '; + title += showEmojiForRetweetAndReply ? '↩️ ' : showSymbolForRetweetAndReply ? 'Re ' : ''; } title += replaceBreak(originalItem.full_text); } if (isRetweet) { - title += showEmojiForRetweetAndReply ? '🔁 ' : 'RT '; + title += showEmojiForRetweetAndReply ? '🔁 ' : showSymbolForRetweetAndReply ? 'RT ' : ''; title += item.user.name + ': '; if (item.in_reply_to_screen_name) { - title += showEmojiForRetweetAndReply ? ' ↩️ ' : ' Re '; + title += showEmojiForRetweetAndReply ? ' ↩️ ' : showSymbolForRetweetAndReply ? ' Re ' : ''; } title += replaceBreak(item.full_text); } @@ -298,7 +300,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { } description += ' '; } - description += showEmojiForRetweetAndReply ? '🔁' : 'RT'; + description += showEmojiForRetweetAndReply ? '🔁' : showSymbolForRetweetAndReply ? 'RT' : ''; if (!showAuthorInDesc) { description += ' '; if (readable) { @@ -341,7 +343,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { description += `: `; } if (item.in_reply_to_screen_name) { - description += showEmojiForRetweetAndReply ? '↩️ ' : 'Re '; + description += showEmojiForRetweetAndReply ? '↩️ ' : showSymbolForRetweetAndReply ? 'Re ' : ''; } description += item.full_text; diff --git a/lib/v2/wsj/index.js b/lib/v2/wsj/index.js deleted file mode 100644 index 4c7b82c0cf3c3a..00000000000000 --- a/lib/v2/wsj/index.js +++ /dev/null @@ -1,141 +0,0 @@ -const parser = require('@/utils/rss-parser'); -const cheerio = require('cheerio'); -const got = require('@/utils/got'); -const { parseDate } = require('@/utils/parse-date'); - -const categoryToXMLFileName = { - opinion: 'RSSOpinion.xml', - world_news: 'RSSWorldNews.xml', - us_bussiness: 'WSJcomUSBusiness.xml', - market_news: 'RSSMarketsMain.xml', - technology: 'RSSWSJD.xml', - lifestyle: 'RSSLifestyle.xml', -}; - -const categoryToName = { - opinion: 'Opinion', - world_news: 'World News', - us_bussiness: 'U.S. Business', - market_news: 'Markets News', - technology: "Technology: What's News", - lifestyle: 'Lifestyle', -}; - -module.exports = async (ctx) => { - const language = ctx.params.lang; - const category = ctx.params.category; - let rssUrl; - switch (language) { - case 'en-us': - rssUrl = `https://feeds.a.dj.com/rss/${categoryToXMLFileName[category]}`; - break; - case 'zh-cn': - // Doesn't support categorical subscribtion - rssUrl = `https://cn.wsj.com/zh-hans/rss/`; - break; - case 'zh-tw': - rssUrl = `https://cn.wsj.com/zh-hant/rss/`; - break; - default: - // Doesn't support other languages (e.g. ja) for now - throw Error(`Language ${language} is not supported`); - } - - const feed = await parser.parseURL(rssUrl); - const chromeMobileUserAgent = 'Mozilla/5.0 (Linux; Android 7.0; SM-G892A Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/67.0.3396.87 Mobile Safari/537.36'; - - const items = await Promise.all( - feed.items.map((item) => - ctx.cache.tryGet(item.link, async () => { - // Fetch the AMP version - const url = item.link.replace(/(?<=^https:\/\/\w+\.wsj\.com)/, '/amp'); - const response = await got({ - url, - method: 'get', - headers: { - 'User-Agent': chromeMobileUserAgent, - }, - }); - const html = response.body; - const $ = cheerio.load(html); - const content = $('.articleBody > section'); - - // Cover - const cover = $('.articleLead > div.is-lead-inset > div.header > .img-header > div.image-container > amp-img > img'); - - if (cover.length > 0) { - $(``).insertBefore(content[0].childNodes[0]); - $(cover).remove(); - } - - // Summary - const summary = $('head > meta[name="description"]').attr('content'); - - // Metadata (categories & updatedAt) - const updatedAt = $('meta[itemprop="dateModified"]').attr('content'); - const publishedAt = $('meta[itemprop="datePublished"]').attr('content'); - const author = $('.author > a[rel="author"]').html(); - - const categories = $('meta[name="keywords"]') - .attr('content') - .split(',') - .map((c) => c.trim()); - - // Images - content.find('amp-img').each((i, e) => { - const img = $(`${e.attribs.alt}`); - - // Caption follows, no need to handle caption - $(img).insertBefore(e); - $(e).remove(); - }); - - // iframes (youtube videos and interactive elements) - content.find('amp-iframe').each((i, e) => { - const iframe = $(`