diff --git a/lib/routes/gamebase/news.ts b/lib/routes/gamebase/news.ts index ab030741090d36..3970034c6a1f58 100644 --- a/lib/routes/gamebase/news.ts +++ b/lib/routes/gamebase/news.ts @@ -1,98 +1,181 @@ -import { Route } from '@/types'; +import { type Data, type DataItem, type Route, ViewType } from '@/types'; + +import { art } from '@/utils/render'; import cache from '@/utils/cache'; -import got from '@/utils/got'; -import { load } from 'cheerio'; -import timezone from '@/utils/timezone'; +import { getCurrentPath } from '@/utils/helpers'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +import { type CheerioAPI, load } from 'cheerio'; +import { type Context } from 'hono'; +import path from 'node:path'; + +const __dirname = getCurrentPath(import.meta.url); const types = { newslist: 'newsList', r18list: 'newsPornList', }; -export const route: Route = { - path: '/news/:type?/:category?', - categories: ['game'], - example: '/gamebase/news', - parameters: { type: '类型,见下表,默认为 newslist', category: '分类,可在对应分类页 URL 中找到,默认为 `all` 即全部' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - name: '新聞', - maintainers: ['nczitzk'], - handler, - description: `类型 - - | newslist | r18list | - | -------- | ------- |`, -}; +export const handler = async (ctx: Context): Promise => { + const { type = 'newslist', category = 'all' } = ctx.req.param(); -async function handler(ctx) { - const type = ctx.req.param('type') ?? 'newslist'; - const category = ctx.req.param('category') ?? 'all'; - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 20; + if (!types.hasOwnProperty(type)) { + throw new InvalidParameterError(`Invalid type: ${type}`); + } - const rootUrl = 'https://news.gamebase.com.tw'; - const currentUrl = `${rootUrl}/news/${type}?type=${category}`; + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); - const apiRootUrl = 'https://api.gamebase.com.tw'; - const apiUrl = `${apiRootUrl}/api/news/getNewsList`; + const baseUrl: string = 'https://news.gamebase.com.tw'; + const targetUrl: string = new URL(`news${category === 'all' ? '' : `/newslist?type=${category}`}`, baseUrl).href; + const apiBaseUrl: string = 'https://api.gamebase.com.tw'; + const apiUrl: string = new URL('api/news/getNewsList', apiBaseUrl).href; - const response = await got({ + const response = await ofetch(apiUrl, { method: 'post', - url: apiUrl, - json: { + body: { GB_type: types[type], category, page: 1, }, }); - const titleResponse = await got({ - method: 'get', - url: currentUrl, - }); + const targetResponse = await ofetch(targetUrl); + const $: CheerioAPI = load(targetResponse); + const language = $('html').attr('lang') ?? 'zh-TW'; - const $ = load(titleResponse.data); + const items: DataItem[] = await Promise.all( + response.return_msg?.list?.slice(0, limit).map((item) => + cache.tryGet(`gamebase-news-${item.news_no}`, async (): Promise => { + const title: string = item.news_title; + const pubDate: number | string = item.post_time; + const linkUrl: string | undefined = item.news_no ? `news/detail/${item.news_no}` : undefined; + const categories: string[] = [item.system]; + const authors: DataItem['author'] = item.nickname; + const guid: string = `gamebase-news-${item.news_no}`; + const image: string | undefined = item.news_img; + const updated: number | string = item.updated ?? pubDate; - const items = await Promise.all( - response.data.return_msg.list.slice(0, limit).map((item) => - cache.tryGet(`gamebase:news:${type}:${category}:${item.news_no}`, async () => { - const i = {}; + let metaDesc = item.news_meta?.meta_des; - 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 (!metaDesc) { + const detailResponse = await ofetch(item.link); - if (i.description) { - return i; + metaDesc = (detailResponse.match(/(\\u003C.*?)","/)?.[1] ?? '').replaceAll(String.raw`\"`, '"').replaceAll(/\\u([\da-f]{4})/gi, (match, hex) => String.fromCodePoint(Number.parseInt(hex, 16))); } - const detailResponse = await got({ - method: 'get', - url: i.link, + const description: string = art(path.join(__dirname, 'templates/description.art'), { + images: + image && !metaDesc + ? [ + { + src: image, + alt: title, + }, + ] + : undefined, + intro: item.news_short_desc, + description: metaDesc, }); - const description = detailResponse.data.match(/(\\u003C.*?)","/)[1].replaceAll(String.raw`\"`, '"'); - - i.description = description.replaceAll(/\\u[\da-f]{4}/gi, (match) => String.fromCharCode(Number.parseInt(match.replaceAll(String.raw`\u`, ''), 16))); - - return i; + const processedItem: DataItem = { + title, + description, + pubDate: pubDate ? timezone(parseDate(pubDate), +8) : undefined, + link: linkUrl ? new URL(linkUrl, baseUrl).href : undefined, + category: categories, + author: authors, + guid, + id: guid, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: updated ? timezone(parseDate(updated), +8) : undefined, + language, + }; + + return processedItem; }) - ) + ) ?? [] ); return { title: $('title').text(), - link: currentUrl, + description: $('meta[property="og:description"]').attr('content'), + link: targetUrl, item: items, + allowEmpty: true, + image: $('meta[property="og:image"]').attr('content'), + author: $('meta[property="og:title"]').attr('content')?.split(/\|/).pop()?.trim(), + language, + id: $('meta[property="og:url"]').attr('content'), }; -} +}; + +export const route: Route = { + path: '/news/:type?/:category?', + name: '新聞', + url: 'news.gamebase.com.tw', + maintainers: ['nczitzk'], + handler, + example: '/gamebase/news', + parameters: { + type: '類型,見下表,預設為 newslist', + category: '分類,預設為 `all`,即全部,可在對應分類頁 URL 中找到', + }, + description: `:::tip +若訂閱 [手機遊戲新聞](https://news.gamebase.com.tw/news/newslist?type=mobile),網址為 \`https://news.gamebase.com.tw/news/newslist?type=mobile\`,請截取 \`https://news.gamebase.com.tw/news/\` 到末尾的部分 \`newslist\` 作為 \`type\` 參數填入,\`mobile\` 作為 \`category\` 參數填入,此時目標路由為 [\`/gamebase/news/newslist/mobile\`](https://rsshub.app/gamebase/news/newslist/mobile)。 +::: + +| newslist | r18list | +| -------- | ------- | +`, + categories: ['game'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['news.gamebase.com.tw/news', 'news.gamebase.com.tw/news/:type'], + target: (params, url) => { + const type: string = params.type; + const urlObj: URL = new URL(url); + const category: string | undefined = urlObj.searchParams.get('type') ?? undefined; + + return `/gamebase/news${type ? `/${type}${category ? `/${category}` : ''}` : ''}`; + }, + }, + ], + view: ViewType.Articles, + + zh: { + path: '/news/:type?/:category?', + name: '新闻', + url: 'news.gamebase.com.tw', + maintainers: ['nczitzk'], + handler, + example: '/gamebase/news', + parameters: { + type: '类型,见下表,默认为 newslist', + category: '分类,默认为 `all`,即全部,可在对应分类页 URL 中找到', + }, + description: `:::tip +若订阅 [手机游戏新闻](https://news.gamebase.com.tw/news/newslist?type=mobile),网址为 \`https://news.gamebase.com.tw/news/newslist?type=mobile\`,请截取 \`https://news.gamebase.com.tw/news/\` 到末尾的部分 \`newslist\` 作为 \`type\` 参数填入,\`mobile\` 作为 \`category\` 参数填入,此时目标路由为 [\`/gamebase/news/newslist/mobile\`](https://rsshub.app/gamebase/news/newslist/mobile)。 +::: + +| newslist | r18list | +| -------- | ------- | +`, + }, +}; diff --git a/lib/routes/gamebase/templates/description.art b/lib/routes/gamebase/templates/description.art new file mode 100644 index 00000000000000..249654e7e618a4 --- /dev/null +++ b/lib/routes/gamebase/templates/description.art @@ -0,0 +1,21 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} +
+ {{ image.alt }} +
+ {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} +
{{ intro }}
+{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file