From aa713c082d5e3f31031e6ff4856cb3d5546e3cac Mon Sep 17 00:00:00 2001 From: mobyw Date: Sun, 27 Oct 2024 00:08:31 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat(route/qq):=20add=20=E8=85=BE=E8=AE=AF?= =?UTF-8?q?=E9=A2=91=E9=81=93=20(/qq/pd)=20route=20(#17316)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(route/qq): add qq/pd route * chore(format): remove `any` type hints * chore(types): add types for qq/pd * fix(route): fix route example of /qq/pd/guild --- lib/routes/qq/pd/guild.ts | 146 ++++++++++++++++++++++++++++++++++++++ lib/routes/qq/pd/types.ts | 82 +++++++++++++++++++++ lib/routes/qq/pd/utils.ts | 119 +++++++++++++++++++++++++++++++ 3 files changed, 347 insertions(+) create mode 100644 lib/routes/qq/pd/guild.ts create mode 100644 lib/routes/qq/pd/types.ts create mode 100644 lib/routes/qq/pd/utils.ts diff --git a/lib/routes/qq/pd/guild.ts b/lib/routes/qq/pd/guild.ts new file mode 100644 index 00000000000000..91c21736aeab69 --- /dev/null +++ b/lib/routes/qq/pd/guild.ts @@ -0,0 +1,146 @@ +import { Data, DataItem, Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import type { Context } from 'hono'; + +import { Feed } from './types'; +import { parseFeed } from './utils'; +import cache from '@/utils/cache'; + +const baseUrl = 'https://pd.qq.com/g/'; +const baseApiUrl = 'https://pd.qq.com/qunng/guild/gotrpc/noauth/trpc.qchannel.commreader.ComReader/'; +const getGuildFeedsUrl = baseApiUrl + 'GetGuildFeeds'; +const getChannelTimelineFeedsUrl = baseApiUrl + 'GetChannelTimelineFeeds'; +const getFeedDetailUrl = baseApiUrl + 'GetFeedDetail'; + +const sortMap = { + hot: 0, + created: 1, + replied: 2, +}; + +export const route: Route = { + path: ['/pd/guild/:id/:sub?/:sort?'], + categories: ['bbs'], + example: '/qq/pd/guild/qrp4pkq01d/650967831/created', + parameters: { + id: '频道号', + sub: '子频道 ID,网页端 URL `subc` 参数的值,默认为 `hot`(全部)', + sort: '排序方式,`hot`(热门),`created`(最新发布),`replied`(最新回复),默认为 `created`', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['pd.qq.com/'], + }, + ], + name: '腾讯频道', + maintainers: ['mobyw'], + handler, + url: 'pd.qq.com/', +}; + +async function handler(ctx: Context): Promise { + const { id, sub = 'hot', sort = 'created' } = ctx.req.param(); + + if (sort in sortMap === false) { + throw new InvalidParameterError('invalid sort parameter, should be `hot`, `created`, or `replied`'); + } + const sortType = sortMap[sort]; + + let url = ''; + let body = {}; + let headers = {}; + + if (sub === 'hot') { + url = getGuildFeedsUrl; + // notice: do not change the order of the keys in the body + body = { count: 20, from: 7, guild_number: id, get_type: 1, feedAttchInfo: '', sortOption: sortType, need_channel_list: false, need_top_info: false }; + headers = { + cookie: 'p_uin=o09000002', + 'x-oidb': '{"uint32_service_type":12}', + 'x-qq-client-appid': '537246381', + }; + } else { + url = getChannelTimelineFeedsUrl; + // notice: do not change the order of the keys in the body + body = { count: 20, from: 7, guild_number: id, channelSign: { channel_id: sub }, feedAttchInfo: '', sortOption: sortType, need_top_info: false }; + headers = { + cookie: 'p_uin=o09000002', + 'x-oidb': '{"uint32_service_type":11}', + 'x-qq-client-appid': '537246381', + }; + } + + const data = await ofetch(url, { method: 'POST', body, headers }); + const feeds = data.data?.vecFeed || []; + + const items = feeds.map(async (feed: Feed) => { + let subId = sub; + if (sub === 'hot') { + // get real subId for hot feeds + subId = feed.channelInfo?.sign?.channel_id || ''; + } + const feedLink = baseUrl + id + '/post/' + feed.id; + const feedDetail = await cache.tryGet(feedLink, async () => { + // notice: do not change the order of the keys in the body + body = { + feedId: feed.id, + userId: feed.poster?.id, + createTime: feed.createTime, + from: 2, + detail_type: 1, + channelSign: { guild_number: id, channel_id: subId }, + extInfo: { + mapInfo: [ + { key: 'qc-tabid', value: 'ark' }, + { key: 'qc-pageid', value: 'pc' }, + ], + }, + }; + headers = { + cookie: 'p_uin=o09000002', + referer: feedLink, + 'x-oidb': '{"uint32_service_type":5}', + 'x-qq-client-appid': '537246381', + }; + const feedResponse = await ofetch(getFeedDetailUrl, { method: 'POST', body, headers }); + const feedContent: Feed = feedResponse.data?.feed || {}; + return { + title: feed.title?.contents[0]?.text_content?.text || feed.channelInfo?.guild_name || '', + link: feedLink, + description: parseFeed(feedContent), + pubDate: new Date(Number(feed.createTime) * 1000), + author: feed.poster?.nick, + }; + }); + + return feedDetail; + }); + + const feedItems = await Promise.all(items); + + let guildName = ''; + + if (feeds.length > 0 && feeds[0].channelInfo?.guild_name) { + guildName = feeds[0].channelInfo?.guild_name; + if (sub !== 'hot' && feeds[0].channelInfo?.name) { + guildName += ' (' + feeds[0].channelInfo?.name + ')'; + } + guildName += ' - 腾讯频道'; + } + + return { + title: guildName, + link: baseUrl + id, + description: guildName, + item: feedItems as DataItem[], + }; +} diff --git a/lib/routes/qq/pd/types.ts b/lib/routes/qq/pd/types.ts new file mode 100644 index 00000000000000..7164c822d6cba0 --- /dev/null +++ b/lib/routes/qq/pd/types.ts @@ -0,0 +1,82 @@ +// Description: Types for QQ PD API. + +export type Feed = { + id: string; + feed_type: number; // 1-post, 2-article + patternInfo: string; // JSON string + channelInfo: ChannelInfo; + title: { + contents: FeedContent[]; + }; + contents: { + contents: FeedContent[]; + }; + images: FeedImage[]; + poster: { + id: string; + nick: string; + }; + createTime: string; +}; + +export type ChannelInfo = { + name: string; + guild_number: string; + guild_name: string; + sign: { + guild_id: string; + channel_id: string; + }; +}; + +export type FeedContent = { + type: number; + pattern_id: string; + text_content?: { + text: string; + }; + emoji_content?: { + id: string; + type: string; + }; + url_content?: { + url: string; + displayText: string; + type: number; + }; +}; + +export type FeedImage = { + picId: string; + picUrl: string; + width: number; + height: number; + pattern_id?: string; +}; + +export type FeedPattern = { + id: string; + props?: { + textAlignment: number; // 0-left, 1-center, 2-right + }; + data: FeedPatternData[]; +}; + +export type FeedPatternData = { + type: number; // 1-text, 2-emoji, 5-link, 6-image, 9-newline + text?: string; + props?: FeedFontProps; + fileId?: string; + taskId?: string; + url?: string; + width?: number; + height?: number; + desc?: string; + href?: string; +}; + +export type FeedFontProps = { + fontWeight: number; // 400-normal, 700-bold + italic: boolean; + underline: boolean; +}; diff --git a/lib/routes/qq/pd/utils.ts b/lib/routes/qq/pd/utils.ts new file mode 100644 index 00000000000000..7e469d552539d7 --- /dev/null +++ b/lib/routes/qq/pd/utils.ts @@ -0,0 +1,119 @@ +// Description: QQ PD utils + +import { Feed, FeedImage, FeedPattern, FeedFontProps, FeedPatternData } from './types'; + +const patternTypeMap = { + 1: 'text', + 2: 'emoji', + 5: 'link', + 6: 'image', + 9: 'newline', +}; + +const textAlignmentMap = { + 0: 'left', + 1: 'center', + 2: 'right', +}; + +function parseText(text: string, props: FeedFontProps | undefined): string { + if (props === undefined) { + return text; + } + let style = ''; + if (props.fontWeight === 700) { + style += 'font-weight: bold;'; + } + if (props.italic) { + style += 'font-style: italic;'; + } + if (props.underline) { + style += 'text-decoration: underline;'; + } + if (style === '') { + return text; + } + return `${text}`; +} + +function parseDataItem(item: FeedPatternData, texts: string[], images: { [id: string]: FeedImage }): string { + let imageId = ''; + switch (patternTypeMap[item.type] || undefined) { + case 'text': + return parseText(texts.shift() ?? '', item.props); + case 'newline': + texts.shift(); + return '
'; + case 'link': + return `${item.desc ?? ''}`; + case 'image': + imageId = item.fileId || item.taskId || ''; + return `
`; + default: + return ''; + } +} + +function parseArticle(feed: Feed, texts: string[], images: { [id: string]: FeedImage }): string { + let result = ''; + if (feed.patternInfo === undefined || feed.patternInfo === null || feed.patternInfo === '') { + feed.patternInfo = '[]'; + } + const patterns: FeedPattern[] = JSON.parse(feed.patternInfo); + for (const pattern of patterns) { + if (pattern.props === undefined) { + continue; + } + const textAlign = pattern.props.textAlignment || 0; + result += '

'; + for (const item of pattern.data) { + result += parseDataItem(item, texts, images); + } + result += '

'; + } + return result; +} + +function parsePost(feed: Feed, texts: string[], images: { [id: string]: FeedImage }): string { + for (const content of feed.contents.contents) { + if (content.text_content) { + texts.push(content.text_content.text); + } + } + let result = ''; + for (const text of texts) { + result += text; + } + for (const image of Object.values(images)) { + result += '

'; + result += ``; + result += '

'; + } + return result; +} + +export function parseFeed(feed: Feed): string { + const texts: string[] = []; + const images: { [id: string]: FeedImage } = {}; + for (const content of feed.contents.contents) { + if (content.text_content) { + texts.push(content.text_content.text); + } + } + for (const image of feed.images) { + images[image.picId] = { + picId: image.picId, + picUrl: image.picUrl, + width: image.width, + height: image.height, + }; + } + if (feed.feed_type === 1) { + // post: text and attachments + return parsePost(feed, texts, images); + } else if (feed.feed_type === 2) { + // article: pattern info + return parseArticle(feed, texts, images); + } + return ''; +} From c3123cc330ac591558539256e9a6628a20c6aba8 Mon Sep 17 00:00:00 2001 From: Axojhf <20625305+axojhf@users.noreply.github.com> Date: Sun, 27 Oct 2024 01:18:35 +0800 Subject: [PATCH 2/7] fix(route): fix lovelive-anime topics and schedules route (#17313) * fix: lovelive-anime topics and schedules * fix: /lovelive-anime/topics js Check warning * fix , * fix renderDescription * fix: imglink * Update lib/routes/lovelive-anime/topics.ts * Update lib/routes/lovelive-anime/topics.ts --------- --- lib/routes/lovelive-anime/schedules.ts | 9 +- lib/routes/lovelive-anime/topics.ts | 118 ++++++++++++++++++------- 2 files changed, 88 insertions(+), 39 deletions(-) diff --git a/lib/routes/lovelive-anime/schedules.ts b/lib/routes/lovelive-anime/schedules.ts index 16dd15fa339a55..046111b9ab03df 100644 --- a/lib/routes/lovelive-anime/schedules.ts +++ b/lib/routes/lovelive-anime/schedules.ts @@ -2,7 +2,7 @@ import { Route } from '@/types'; import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import path from 'node:path'; import { art } from '@/utils/render'; const renderDescription = (desc) => art(path.join(__dirname, 'templates/scheduleDesc.art'), desc); @@ -33,8 +33,7 @@ export const route: Route = { }; async function handler(ctx) { - const serie = ctx.req.param('serie'); - const category = ctx.req.param('category'); + const { serie = 'all', category = 'all' } = ctx.req.param(); const rootUrl = 'https://www.lovelive-anime.jp/common/api/calendar_list.php'; const nowTime = dayjs(); const dataPara = { @@ -47,9 +46,9 @@ async function handler(ctx) { if (category && 'all' !== category) { dataPara.category = [category]; } - const response = await got(`${rootUrl}?site=jp&ip=lovelive&data=${JSON.stringify(dataPara)}`); + const response = await ofetch(`${rootUrl}?site=jp&ip=lovelive&data=${encodeURIComponent(JSON.stringify(dataPara))}`); - const items = response.data.data.schedule_list.map((item) => { + const items = response.data.schedule_list.map((item) => { const link = item.url.select_url; const title = item.title; const category = item.categories.name; diff --git a/lib/routes/lovelive-anime/topics.ts b/lib/routes/lovelive-anime/topics.ts index 8802c0fc745f20..b327081032fdec 100644 --- a/lib/routes/lovelive-anime/topics.ts +++ b/lib/routes/lovelive-anime/topics.ts @@ -3,11 +3,12 @@ import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); import cache from '@/utils/cache'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import path from 'node:path'; import { art } from '@/utils/render'; import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; const renderDescription = (desc) => art(path.join(__dirname, 'templates/description.art'), desc); export const route: Route = { @@ -40,10 +41,25 @@ export const route: Route = { }; async function handler(ctx) { - const abbr = ctx.req.param('abbr'); - const rootUrl = `https://www.lovelive-anime.jp/${abbr}`; - const topicsUrlPart = 'yuigaoka' === abbr ? 'topics/' : 'topics.php'; - const baseUrl = `${rootUrl}/${'yuigaoka' === abbr ? 'topics/' : ''}`; + const { abbr, category = '', option } = ctx.req.param(); + let rootUrl: string; + switch (abbr) { + case 'musical': + rootUrl = 'https://www.lovelive-anime.jp/special/musical'; + break; + default: + rootUrl = `https://www.lovelive-anime.jp/${abbr}`; + break; + } + const topicsTable = { + otonokizaka: 'topics.php', + uranohoshi: 'topics.php', + nijigasaki: 'topics.php', + yuigaoka: 'topics/', + hasunosora: 'news/', + musical: 'topics.php', + }; + const baseUrl = `${rootUrl}/${topicsTable[abbr]}`; const abbrDetail = { otonokizaka: 'ラブライブ!', uranohoshi: 'サンシャイン!!', @@ -51,50 +67,84 @@ async function handler(ctx) { yuigaoka: 'スーパースター!!', }; - const url = Object.hasOwn(ctx.params, 'category') && ctx.req.param('category') !== 'detail' ? `${rootUrl}/${topicsUrlPart}?cat=${ctx.req.param('category')}` : `${rootUrl}/${topicsUrlPart}`; + const url = category !== '' && category !== 'detail' ? `${baseUrl}?cat=${category}` : baseUrl; - const response = await got(url); + const response = await ofetch(url); - const $ = load(response.data); + const $ = load(response); const categoryName = 'uranohoshi' === abbr ? $('div.llbox > p').text() : $('div.category_title > h2').text(); - let items = $('ul.listbox > li') - .map((_, item) => { - item = $(item); + const newsList = 'hasunosora' === abbr ? $('.list__content > ul > li').toArray() : $('ul.listbox > li').toArray(); + let items; - const link = `${baseUrl}${item.find('div > a').attr('href')}`; - const pubDate = parseDate(item.find('a > p.date').text(), 'YYYY/MM/DD'); - const title = item.find('a > p.title').text(); - const category = item.find('a > p.category').text(); - const imglink = `${baseUrl}${ - item + switch (abbr) { + case 'hasunosora': + items = newsList.map((item) => { + item = $(item); + const link = `${rootUrl}/news/${item.find('a').attr('href')}`; + const pubDate = timezone(parseDate(item.find('.list--date').text(), 'YYYY.MM.DD'), +9); + const title = item.find('.list--text').text(); + const category = item.find('.list--category').text(); + + return { + link, + pubDate, + title, + category, + description: title, + }; + }); + break; + default: + items = newsList.map((item) => { + item = $(item); + let link: string; + switch (abbr) { + case 'yuigaoka': + link = `${baseUrl}${item.find('div > a').attr('href')}`; + break; + default: + link = `${rootUrl}/${item.find('div > a').attr('href')}`; + break; + } + const pubDate = timezone(parseDate(item.find('a > p.date').text(), 'YYYY/MM/DD'), +9); + const title = item.find('a > p.title').text(); + const category = item.find('a > p.category').text(); + const imglink = `${rootUrl}/${item .find('a > img') .attr('style') .match(/background-image:url\((.*)\)/)[1] - }`; + }`; - return { - link, - pubDate, - title, - category, - description: renderDescription({ + return { + link, + pubDate, title, - imglink, - }), - }; - }) - .get(); + category, + description: renderDescription({ + imglink, + }), + }; + }); + break; + } - if (ctx.req.param('option') === 'detail' || ctx.req.param('category') === 'detail') { + if (option === 'detail' || category === 'detail') { items = await Promise.all( items.map((item) => cache.tryGet(item.link, async () => { - const detailResp = await got(item.link); - const $ = load(detailResp.data); - - const content = $('div.p-page__detail.p-article'); + const detailResp = await ofetch(item.link); + const $ = load(detailResp); + let content; + switch (abbr) { + case 'hasunosora': + content = $('div.detail__content'); + break; + default: + content = $('div.p-page__detail.p-article'); + break; + } for (const v of content.find('img')) { v.attribs.src = `${baseUrl}${v.attribs.src}`; } From d79e6cdf56c4586c51ca42d9e4798cf342c95676 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 17:20:34 +0000 Subject: [PATCH 3/7] style: auto format --- lib/routes/lovelive-anime/topics.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/routes/lovelive-anime/topics.ts b/lib/routes/lovelive-anime/topics.ts index b327081032fdec..efc4132da7d59e 100644 --- a/lib/routes/lovelive-anime/topics.ts +++ b/lib/routes/lovelive-anime/topics.ts @@ -111,11 +111,12 @@ async function handler(ctx) { const pubDate = timezone(parseDate(item.find('a > p.date').text(), 'YYYY/MM/DD'), +9); const title = item.find('a > p.title').text(); const category = item.find('a > p.category').text(); - const imglink = `${rootUrl}/${item - .find('a > img') - .attr('style') - .match(/background-image:url\((.*)\)/)[1] - }`; + const imglink = `${rootUrl}/${ + item + .find('a > img') + .attr('style') + .match(/background-image:url\((.*)\)/)[1] + }`; return { link, From 9916c348bb05873dcfe6d33108a40c7dc93bd79e Mon Sep 17 00:00:00 2001 From: XieSC Date: Sun, 27 Oct 2024 01:38:35 +0800 Subject: [PATCH 4/7] fix(route): get correct item link (#17278) When the `href` link inside the element is a relative link, the original handling would obtain an incorrect full link --- lib/routes/rsshub/transform/html.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/routes/rsshub/transform/html.ts b/lib/routes/rsshub/transform/html.ts index 1084bf240df254..c4354c1688219e 100644 --- a/lib/routes/rsshub/transform/html.ts +++ b/lib/routes/rsshub/transform/html.ts @@ -92,10 +92,10 @@ Specify options (in the format of query string) in parameter \`routeParams\` par } else { link = linkEle.is('a') ? linkEle.attr('href') : linkEle.find('a').attr('href'); } - // 补全绝对链接 + // 补全绝对链接或相对链接 link = link.trim(); if (link && !link.startsWith('http')) { - link = `${new URL(url).origin}${link}`; + link = (new URL(link, url)).href; } const descEle = routeParams.get('itemDesc') ? item.find(routeParams.get('itemDesc')) : item; From 91599978b42187074bca74276b6b771ed0938b07 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 17:40:25 +0000 Subject: [PATCH 5/7] style: auto format --- lib/routes/rsshub/transform/html.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routes/rsshub/transform/html.ts b/lib/routes/rsshub/transform/html.ts index c4354c1688219e..e6288d3169cfbd 100644 --- a/lib/routes/rsshub/transform/html.ts +++ b/lib/routes/rsshub/transform/html.ts @@ -95,7 +95,7 @@ Specify options (in the format of query string) in parameter \`routeParams\` par // 补全绝对链接或相对链接 link = link.trim(); if (link && !link.startsWith('http')) { - link = (new URL(link, url)).href; + link = new URL(link, url).href; } const descEle = routeParams.get('itemDesc') ? item.find(routeParams.get('itemDesc')) : item; From b4217bf89212c2cd9e188e18f5e71edcdeb8762b Mon Sep 17 00:00:00 2001 From: Ethan Shen <42264778+nczitzk@users.noreply.github.com> Date: Sun, 27 Oct 2024 02:11:34 +0800 Subject: [PATCH 6/7] =?UTF-8?q?feat(route):=20add=20=E4=B8=AD=E5=9B=BD?= =?UTF-8?q?=E6=9C=89=E8=89=B2=E9=87=91=E5=B1=9E=E5=B7=A5=E4=B8=9A=E7=BD=91?= =?UTF-8?q?=20(#17320)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/routes/chinania/index.ts | 240 +++++++++++++++++++++++++++++++ lib/routes/chinania/namespace.ts | 8 ++ 2 files changed, 248 insertions(+) create mode 100644 lib/routes/chinania/index.ts create mode 100644 lib/routes/chinania/namespace.ts diff --git a/lib/routes/chinania/index.ts b/lib/routes/chinania/index.ts new file mode 100644 index 00000000000000..d3cea7ae41f4f8 --- /dev/null +++ b/lib/routes/chinania/index.ts @@ -0,0 +1,240 @@ +import { Route } from '@/types'; + +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; + +export const handler = async (ctx) => { + const { category = 'xiehuidongtai/xiehuitongzhi' } = ctx.req.param(); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 25; + + const rootUrl = 'https://www.chinania.org.cn'; + const currentUrl = new URL(`html/${category.endsWith('/') ? category : `${category}/`}`, rootUrl).href; + + const { data: response } = await got(currentUrl); + + const $ = load(response); + + const language = $('html').prop('lang'); + + let items = $('ul.notice_list_ul li') + .slice(0, limit) + .toArray() + .map((item) => { + item = $(item); + + return { + title: item.find('p').first().text(), + pubDate: parseDate(item.find('p').last().text()), + link: item.find('a').prop('href'), + language, + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const { data: detailResponse } = await got(item.link); + + const $$ = load(detailResponse); + + const title = $$('div.article_title p').first().text(); + const description = $$('div.article_content').html(); + + item.title = title; + item.description = description; + item.pubDate = parseDate($$('div.article_title p').last().text().split(':')); + item.author = $$("meta[name='keywords']").prop('content'); + item.content = { + html: description, + text: $$('div.article_content').text(), + }; + item.language = language; + + return item; + }) + ) + ); + + const title = $('title').text(); + const image = new URL($('img.logo').prop('src'), rootUrl).href; + + return { + title, + description: title.split(/-/)[0]?.trim(), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author: $("meta[name='keywords']").prop('content'), + language, + }; +}; + +export const route: Route = { + path: '/:category{.+}?', + name: '分类', + url: 'www.chinania.org.cn', + maintainers: ['nczitzk'], + handler, + example: '/chinania/xiehuidongtai/xiehuitongzhi', + parameters: { category: '分类,默认为 `xiehuidongtai/xiehuitongzhi`,即协会通知,可在对应分类页 URL 中找到' }, + description: `:::tip + 若订阅 [协会通知](https://www.chinania.org.cn/html/xiehuidongtai/xiehuitongzhi/),网址为 \`https://www.chinania.org.cn/html/xiehuidongtai/xiehuitongzhi/\`。截取 \`https://www.chinania.org.cn/html\` 到末尾 \`/\` 的部分 \`xiehuidongtai/xiehuitongzhi\` 作为参数填入,此时路由为 [\`/chinania/xiehuidongtai/xiehuitongzhi\`](https://rsshub.app/chinania/xiehuidongtai/xiehuitongzhi)。 + ::: + +
+ 更多分类 + + #### [协会动态](https://www.chinania.org.cn/html/xiehuidongtai/) + + | [协会动态](https://www.chinania.org.cn/html/xiehuidongtai/xiehuidongtai/) | [协会通知](https://www.chinania.org.cn/html/xiehuidongtai/xiehuitongzhi/) | [有色企业50强](https://www.chinania.org.cn/html/xiehuidongtai/youseqiye50qiang/) | + | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | + | [xiehuidongtai/xiehuidongtai](https://rsshub.app/chinania/xiehuidongtai/xiehuidongtai) | [xiehuidongtai/xiehuitongzhi](https://rsshub.app/chinania/xiehuidongtai/xiehuitongzhi) | [xiehuidongtai/youseqiye50qiang](https://rsshub.app/chinania/xiehuidongtai/youseqiye50qiang) | + + #### [党建工作](https://www.chinania.org.cn/html/djgz/) + + | [协会党建](https://www.chinania.org.cn/html/djgz/xiehuidangjian/) | [行业党建](https://www.chinania.org.cn/html/djgz/hangyedangjian/) | + | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | + | [djgz/xiehuidangjian](https://rsshub.app/chinania/djgz/xiehuidangjian) | [djgz/hangyedangjian](https://rsshub.app/chinania/djgz/hangyedangjian) | + + #### [行业新闻](https://www.chinania.org.cn/html/hangyexinwen/) + + | [时政要闻](https://www.chinania.org.cn/html/hangyexinwen/shizhengyaowen/) | [要闻](https://www.chinania.org.cn/html/hangyexinwen/yaowen/) | [行业新闻](https://www.chinania.org.cn/html/hangyexinwen/guoneixinwen/) | [资讯](https://www.chinania.org.cn/html/hangyexinwen/zixun/) | + | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------- | + | [hangyexinwen/shizhengyaowen](https://rsshub.app/chinania/hangyexinwen/shizhengyaowen) | [hangyexinwen/yaowen](https://rsshub.app/chinania/hangyexinwen/yaowen) | [hangyexinwen/guoneixinwen](https://rsshub.app/chinania/hangyexinwen/guoneixinwen) | [hangyexinwen/zixun](https://rsshub.app/chinania/hangyexinwen/zixun) | + + #### [人力资源](https://www.chinania.org.cn/html/renliziyuan/) + + | [相关通知](https://www.chinania.org.cn/html/renliziyuan/xiangguantongzhi/) | [人事招聘](https://www.chinania.org.cn/html/renliziyuan/renshizhaopin/) | + | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | + | [renliziyuan/xiangguantongzhi](https://rsshub.app/chinania/renliziyuan/xiangguantongzhi) | [renliziyuan/renshizhaopin](https://rsshub.app/chinania/renliziyuan/renshizhaopin) | + + #### [行业统计](https://www.chinania.org.cn/html/hangyetongji/jqzs/) + + | [行业分析](https://www.chinania.org.cn/html/hangyetongji/tongji/) | [数据统计](https://www.chinania.org.cn/html/hangyetongji/chanyeshuju/) | [景气指数](https://www.chinania.org.cn/html/hangyetongji/jqzs/) | + | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------ | + | [hangyetongji/tongji](https://rsshub.app/chinania/hangyetongji/tongji) | [hangyetongji/chanyeshuju](https://rsshub.app/chinania/hangyetongji/chanyeshuju) | [hangyetongji/jqzs](https://rsshub.app/chinania/hangyetongji/jqzs) | + + #### [政策法规](https://www.chinania.org.cn/html/zcfg/zhengcefagui/) + + | [政策法规](https://www.chinania.org.cn/html/zcfg/zhengcefagui/) | + | ------------------------------------------------------------------ | + | [zcfg/zhengcefagui](https://rsshub.app/chinania/zcfg/zhengcefagui) | + + #### [会议展览](https://www.chinania.org.cn/html/hyzl/huiyizhanlan/) + + | [会展通知](https://www.chinania.org.cn/html/hyzl/huiyizhanlan/) | [会展报道](https://www.chinania.org.cn/html/hyzl/huizhanbaodao/) | + | ------------------------------------------------------------------ | -------------------------------------------------------------------- | + | [hyzl/huiyizhanlan](https://rsshub.app/chinania/hyzl/huiyizhanlan) | [hyzl/huizhanbaodao](https://rsshub.app/chinania/hyzl/huizhanbaodao) | + +
+ `, + categories: ['new-media'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.chinania.org.cn/html/:category'], + target: (params) => { + const category = params.category; + + return category ? `/${category}` : ''; + }, + }, + { + title: '协会动态 - 协会动态', + source: ['www.chinania.org.cn/html/xiehuidongtai/xiehuidongtai/'], + target: '/xiehuidongtai/xiehuidongtai', + }, + { + title: '协会动态 - 协会通知', + source: ['www.chinania.org.cn/html/xiehuidongtai/xiehuitongzhi/'], + target: '/xiehuidongtai/xiehuitongzhi', + }, + { + title: '协会动态 - 有色企业50强', + source: ['www.chinania.org.cn/html/xiehuidongtai/youseqiye50qiang/'], + target: '/xiehuidongtai/youseqiye50qiang', + }, + { + title: '党建工作 - 协会党建', + source: ['www.chinania.org.cn/html/djgz/xiehuidangjian/'], + target: '/djgz/xiehuidangjian', + }, + { + title: '党建工作 - 行业党建', + source: ['www.chinania.org.cn/html/djgz/hangyedangjian/'], + target: '/djgz/hangyedangjian', + }, + { + title: '会议展览 - 会展通知', + source: ['www.chinania.org.cn/html/hyzl/huiyizhanlan/'], + target: '/hyzl/huiyizhanlan', + }, + { + title: '会议展览 - 会展报道', + source: ['www.chinania.org.cn/html/hyzl/huizhanbaodao/'], + target: '/hyzl/huizhanbaodao', + }, + { + title: '行业新闻 - 时政要闻', + source: ['www.chinania.org.cn/html/hangyexinwen/shizhengyaowen/'], + target: '/hangyexinwen/shizhengyaowen', + }, + { + title: '行业新闻 - 要闻', + source: ['www.chinania.org.cn/html/hangyexinwen/yaowen/'], + target: '/hangyexinwen/yaowen', + }, + { + title: '行业新闻 - 行业新闻', + source: ['www.chinania.org.cn/html/hangyexinwen/guoneixinwen/'], + target: '/hangyexinwen/guoneixinwen', + }, + { + title: '行业新闻 - 资讯', + source: ['www.chinania.org.cn/html/hangyexinwen/zixun/'], + target: '/hangyexinwen/zixun', + }, + { + title: '行业统计 - 行业分析', + source: ['www.chinania.org.cn/html/hangyetongji/tongji/'], + target: '/hangyetongji/tongji', + }, + { + title: '行业统计 - 数据统计', + source: ['www.chinania.org.cn/html/hangyetongji/chanyeshuju/'], + target: '/hangyetongji/chanyeshuju', + }, + { + title: '行业统计 - 景气指数', + source: ['www.chinania.org.cn/html/hangyetongji/jqzs/'], + target: '/hangyetongji/jqzs', + }, + { + title: '人力资源 - 相关通知', + source: ['www.chinania.org.cn/html/renliziyuan/xiangguantongzhi/'], + target: '/renliziyuan/xiangguantongzhi', + }, + { + title: '人力资源 - 人事招聘', + source: ['www.chinania.org.cn/html/renliziyuan/renshizhaopin/'], + target: '/renliziyuan/renshizhaopin', + }, + { + title: '政策法规 - 政策法规', + source: ['www.chinania.org.cn/html/zcfg/zhengcefagui/'], + target: '/zcfg/zhengcefagui', + }, + ], +}; diff --git a/lib/routes/chinania/namespace.ts b/lib/routes/chinania/namespace.ts new file mode 100644 index 00000000000000..09f465dd891478 --- /dev/null +++ b/lib/routes/chinania/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '中国有色金属工业网', + url: 'chinania.org.cn', + categories: ['new-media'], + description: '', +}; From a71410fdd770c5a66a19207ebcef13e4b76859d8 Mon Sep 17 00:00:00 2001 From: Thomas <1688389+Rakambda@users.noreply.github.com> Date: Sat, 26 Oct 2024 20:56:48 +0200 Subject: [PATCH 7/7] feat(twitter): only display tweets that contains a media (#17310) * feat: only display tweets that contains a media * Use lambda to simplify filtering * wording: media is uncountable --------- --- lib/routes/twitter/home-latest.ts | 5 ++++- lib/routes/twitter/home.ts | 5 ++++- lib/routes/twitter/likes.ts | 5 ++++- lib/routes/twitter/list.ts | 5 ++++- lib/routes/twitter/namespace.ts | 1 + lib/routes/twitter/utils.ts | 12 +++++++++--- 6 files changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/routes/twitter/home-latest.ts b/lib/routes/twitter/home-latest.ts index 40e44ef823c523..6c1a7b51bac1c4 100644 --- a/lib/routes/twitter/home-latest.ts +++ b/lib/routes/twitter/home-latest.ts @@ -40,7 +40,7 @@ export const route: Route = { async function handler(ctx) { // For compatibility - const { count, include_rts } = utils.parseRouteParams(ctx.req.param('routeParams')); + const { count, include_rts, only_media } = utils.parseRouteParams(ctx.req.param('routeParams')); const params = count ? { count } : {}; await api.init(); @@ -48,6 +48,9 @@ async function handler(ctx) { if (!include_rts) { data = utils.excludeRetweet(data); } + if (only_media) { + data = utils.keepOnlyMedia(data); + } return { title: `Twitter following timeline`, diff --git a/lib/routes/twitter/home.ts b/lib/routes/twitter/home.ts index 072edab95db0e3..1516c6829f2c69 100644 --- a/lib/routes/twitter/home.ts +++ b/lib/routes/twitter/home.ts @@ -40,7 +40,7 @@ export const route: Route = { async function handler(ctx) { // For compatibility - const { count, include_rts } = utils.parseRouteParams(ctx.req.param('routeParams')); + const { count, include_rts, only_media } = utils.parseRouteParams(ctx.req.param('routeParams')); const params = count ? { count } : {}; await api.init(); @@ -48,6 +48,9 @@ async function handler(ctx) { if (!include_rts) { data = utils.excludeRetweet(data); } + if (only_media) { + data = utils.keepOnlyMedia(data); + } return { title: `Twitter following timeline`, diff --git a/lib/routes/twitter/likes.ts b/lib/routes/twitter/likes.ts index 8a5d7890b73f94..76f3ebe98466c1 100644 --- a/lib/routes/twitter/likes.ts +++ b/lib/routes/twitter/likes.ts @@ -27,7 +27,7 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); - const { count, include_rts } = utils.parseRouteParams(ctx.req.param('routeParams')); + const { count, include_rts, only_media } = utils.parseRouteParams(ctx.req.param('routeParams')); const params = count ? { count } : {}; await api.init(); @@ -35,6 +35,9 @@ async function handler(ctx) { if (!include_rts) { data = utils.excludeRetweet(data); } + if (only_media) { + data = utils.keepOnlyMedia(data); + } return { title: `Twitter Likes - ${id}`, diff --git a/lib/routes/twitter/list.ts b/lib/routes/twitter/list.ts index 96f461458b7ce6..c19e5feec3c6ff 100644 --- a/lib/routes/twitter/list.ts +++ b/lib/routes/twitter/list.ts @@ -33,7 +33,7 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); - const { count, include_rts } = utils.parseRouteParams(ctx.req.param('routeParams')); + const { count, include_rts, only_media } = utils.parseRouteParams(ctx.req.param('routeParams')); const params = count ? { count } : {}; await api.init(); @@ -41,6 +41,9 @@ async function handler(ctx) { if (!include_rts) { data = utils.excludeRetweet(data); } + if (only_media) { + data = utils.keepOnlyMedia(data); + } return { title: `Twitter List - ${id}`, diff --git a/lib/routes/twitter/namespace.ts b/lib/routes/twitter/namespace.ts index e30ec149a1a61c..7bccc310a60793 100644 --- a/lib/routes/twitter/namespace.ts +++ b/lib/routes/twitter/namespace.ts @@ -27,6 +27,7 @@ export const namespace: Namespace = { | \`includeRts\` | Include retweets, only available in \`/twitter/user\` | \`0\`/\`1\`/\`true\`/\`false\` | \`true\` | | \`forceWebApi\` | Force using Web API even if Developer API is configured, only available in \`/twitter/user\` and \`/twitter/keyword\` | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` | | \`count\` | \`count\` parameter passed to Twitter API, only available in \`/twitter/user\` | Unspecified/Integer | Unspecified | +| \`onlyMedia\` | Only get tweets with a media | \`0\`/\`1\`/\`true\`/\`false\` | \`false\` | Specify different option values than default values to improve readability. The URL diff --git a/lib/routes/twitter/utils.ts b/lib/routes/twitter/utils.ts index 90d266b7396c16..387884ca2e73b2 100644 --- a/lib/routes/twitter/utils.ts +++ b/lib/routes/twitter/utils.ts @@ -451,7 +451,7 @@ if (config.twitter.consumer_key && config.twitter.consumer_secret) { } const parseRouteParams = (routeParams) => { - let count, exclude_replies, include_rts; + let count, exclude_replies, include_rts, only_media; let force_web_api = false; switch (routeParams) { case 'exclude_rts_replies': @@ -479,9 +479,10 @@ const parseRouteParams = (routeParams) => { exclude_replies = fallback(undefined, queryToBoolean(parsed.get('excludeReplies')), false); include_rts = fallback(undefined, queryToBoolean(parsed.get('includeRts')), true); force_web_api = fallback(undefined, queryToBoolean(parsed.get('forceWebApi')), false); + only_media = fallback(undefined, queryToBoolean(parsed.get('onlyMedia')), false); } } - return { count, exclude_replies, include_rts, force_web_api }; + return { count, exclude_replies, include_rts, force_web_api, only_media }; }; export const excludeRetweet = function (tweets) { @@ -495,4 +496,9 @@ export const excludeRetweet = function (tweets) { return excluded; }; -export default { ProcessFeed, getAppClient, parseRouteParams, excludeRetweet }; +export const keepOnlyMedia = function (tweets) { + const excluded = tweets.filter((t) => t.extended_entities && t.extended_entities.media); + return excluded; +}; + +export default { ProcessFeed, getAppClient, parseRouteParams, excludeRetweet, keepOnlyMedia };