diff --git a/lib/routes/famitsu/category.ts b/lib/routes/famitsu/category.ts index ec4fe3bec..e666837a5 100644 --- a/lib/routes/famitsu/category.ts +++ b/lib/routes/famitsu/category.ts @@ -1,10 +1,15 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; -import got from '@/utils/got'; -import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import * as cheerio from 'cheerio'; import { parseDate } from '@/utils/parse-date'; -import timezone from '@/utils/timezone'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; +import { config } from '@/config'; +import { ArticleDetail, Category, CategoryArticle } from './types'; +const __dirname = getCurrentPath(import.meta.url); const baseUrl = 'https://www.famitsu.com'; export const route: Route = { @@ -12,71 +17,133 @@ export const route: Route = { categories: ['game'], example: '/famitsu/category/new-article', parameters: { category: 'Category, see table below, `new-article` by default' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, + radar: [ + { + source: ['www.famitsu.com/category/:category/page/1'], + }, + ], name: 'Category', maintainers: ['TonyRL'], handler, - description: `| 新着 | PS5 | Switch | PS4 | ニュース | ゲームニュース | PR TIMES | 動画 | 特集・企画記事 | インタビュー | 取材・リポート | レビュー | インディーゲーム | - | ----------- | --- | ------ | --- | -------- | -------------- | -------- | ------ | --------------- | ------------ | -------------- | -------- | ---------------- | - | new-article | ps5 | switch | ps4 | news | news-game | prtimes | videos | special-article | interview | event-report | review | indie-game |`, + description: `| 新着 | Switch | PS5 | PS4 | PC ゲーム | ニュース | 動画 | 特集・企画記事 | インタビュー | 取材・リポート | レビュー | インディーゲーム | + | ----------- | ------ | --- | --- | --------- | -------- | ------ | --------------- | ------------ | -------------- | -------- | ---------------- | + | new-article | switch | ps5 | ps4 | pc-game | news | videos | special-article | interview | event-report | review | indie-game |`, }; +function getBuildId() { + return cache.tryGet( + 'famitsu:buildId', + async () => { + const data = await ofetch(baseUrl); + const $ = cheerio.load(data); + const nextData = JSON.parse($('#__NEXT_DATA__').text()); + return nextData.buildId; + }, + config.cache.routeExpire, + false + ); +} + +function render(data) { + return art(path.join(__dirname, 'templates', 'description.art'), data); +} + +function renderJSON(c) { + if (Array.isArray(c.content)) { + return c.content.map((con) => con.type && renderJSON(con)).join(''); + } + + switch (c.type) { + case 'B': + case 'INTERVIEWEE': + case 'STRONG': + return `${c.content}`; + case 'HEAD': + return `

${c.content}

`; + case 'SHEAD': + return `

${c.content}

`; + case 'LINK_B': + case 'LINK_B_TAB': + return `${c.content}
`; + case 'IMAGE': + return ``; + case 'NEWS': + return `${c.content}
${c.description}

`; + case 'HTML': + return c.content; + case 'ANNOTATION': + case 'CAPTION': + case 'ITEMIZATION': + case 'ITEMIZATION_NUM': + case 'NOLINK': + case 'STRING': + case 'TWITTER': + case 'YOUTUBE': + return `
${c.content}
`; + case 'BUTTON': + case 'BUTTON_ANDROID': + case 'BUTTON_EC': + case 'BUTTON_IOS': + case 'BUTTON_TAB': + case 'LINK': + case 'LINK_TAB': + return `${c.content}
`; + default: + throw new Error(`Unhandle type: ${c.type}`); + } +} + async function handler(ctx) { const { category = 'new-article' } = ctx.req.param(); - const url = `${baseUrl}/search/?category=${category}`; - const { data } = await got(url); - const $ = load(data); + const url = `${baseUrl}/category/${category}/page/1`; - const list = $('.col-12 .card__body') - .toArray() + const buildId = await getBuildId(); + + const data = await ofetch(`https://www.famitsu.com/_next/data/${buildId}/category/${category}/page/1.json`, { + query: { + categoryCode: category, + pageNumber: 1, + }, + }); + + const list = (data.pageProps.categoryArticleData as CategoryArticle[]) + .filter((item) => !item.advertiserName) .map((item) => { - item = $(item); + const publicationDate = item.publishedAt?.slice(0, 7).replace('-', ''); return { - title: item.find('.card__title').text(), - link: new URL(item.find('.card__title a').attr('href'), baseUrl).href, - pubDate: timezone(parseDate(item.find('time').attr('datetime'), 'YYYY.MM.DDTHH:mm'), +9), + title: item.title, + link: `https://www.famitsu.com/article/${publicationDate}/${item.id}`, + pubDate: parseDate(item.publishedAt!), + category: [...new Set([item.mainCategory.nameJa, ...(item.subCategories?.map((c) => c.nameJa) ?? [])])], + publicationDate, + articleId: item.id, }; - }) - .filter((item) => item.link.startsWith('https://www.famitsu.com/news/')); + }); const items = await Promise.all( list.map((item) => cache.tryGet(item.link, async () => { - const { data } = await got(item.link); - const $ = load(data); - - // remove ads - $('.article-body__contents-pr-primary').remove(); - - // fix header image - $('.article-body div.media-image').each((_, e) => { - e.tagName = 'img'; - e.attribs.src = e.attribs.style.match(/url\((.+?)\);/)[1]; - delete e.attribs['data-src']; - delete e.attribs.style; + const data = await ofetch(`https://www.famitsu.com/_next/data/${buildId}/article/${item.publicationDate}/${item.articleId}.json`, { + query: { + publicationDate: item.publicationDate, + articleId: item.articleId, + }, }); - // remove white space - $('.article-body__contents-img-block, .article-body__contents-img-common-col').each((_, e) => { - delete e.attribs.style; + const articleDetail = data.pageProps.articleDetailData as ArticleDetail; + item.author = articleDetail.authors?.map((a) => a.name_ja).join(', ') ?? articleDetail.user.name_ja; + item.description = render({ + bannerImage: articleDetail.ogpImageUrl ?? articleDetail.thumbnailUrl, + content: articleDetail.content.flatMap((c) => c.contents.map((con) => renderJSON(con))).join(''), }); - item.description = $('.article-body').html(); return item; }) ) ); return { - title: $('head title').text(), - description: $('head meta[name="description"]').attr('content'), + title: `${(data.pageProps.targetCategory as Category).nameJa}の最新記事 | ゲーム・エンタメ最新情報のファミ通.com`, image: 'https://www.famitsu.com/img/1812/favicons/apple-touch-icon.png', link: url, item: items, diff --git a/lib/routes/famitsu/templates/description.art b/lib/routes/famitsu/templates/description.art new file mode 100644 index 000000000..7285d1d90 --- /dev/null +++ b/lib/routes/famitsu/templates/description.art @@ -0,0 +1,7 @@ +{{ if bannerImage }} +
+{{ /if }} + +{{ if content }} +{{@ content }} +{{ /if }} diff --git a/lib/routes/famitsu/types.ts b/lib/routes/famitsu/types.ts new file mode 100644 index 000000000..234008d7b --- /dev/null +++ b/lib/routes/famitsu/types.ts @@ -0,0 +1,117 @@ +export interface Category { + id: string; + status: string; + isMainCategory: boolean; + code: string; + nameJa: string; + priority: number; +} + +export interface CategoryArticle { + description: string; + id: string; + mainCategory: Category; + publishedAt: string | null; + thumbnailUrl: string; + title: string; + isPr: boolean; + content?: string; + isVideoArticle?: boolean; + subCategories?: Category[]; + redirectUrl?: string; + iconImage?: string; + advertiserName?: string; + linkedUrl?: string; +} + +interface Content { + type: string; + url: string; + content: string | Content[] | string[]; + image_id?: number; + path?: string; +} + +export interface ArticleDetail { + articleId: number; + isR18: boolean; + isDisplayAd: boolean; + dept: { + id: number; + code: string; + name_ja: string; + }; + description: string; + content: { + page_no: number; + contents: Content[]; + text: string; + }[]; + mainCategories: Category; + publishedAt: string; + subCategories: Category[]; + thumbnailUrl: string; + ogpImageUrl: string; + title: string; + updatedAt: string; + user: { + id: number; + name_ja: string; + }; + relatedArticles: { + count: number; + offset: number; + page: number; + limit: number; + results: { + id: string; + article_type: string; + main_category_ids: string[]; + sub_category_ids: string[]; + content_text: string; + creation_time: string; + creation_time_jst: string; + dept_id: string; + description: string; + hide_on_top_page: string; + is_pr: string; + publication_time: string; + publication_time_jst: string; + is_r18: string; + revision: string; + importance_degree: string; + thumbnail_caption: string; + thumbnail_url: string; + title: string; + update_time: string; + update_time_jst: string; + user_id: string; + status: string; + has_video: string; + tweet_id: string; + game_ids: string[]; + author_ids: string[]; + }[]; + }; + authors: { + id: number; + icon_url: string; + name_ja: string; + description: string; + relate_urls: any[]; + }[]; + redirectUrl: string; + copyright: string; + relatedLinks: { + title: string; + description: null; + url: string; + article: any[]; + }[]; + isToc: boolean; + items: { + status: string; + item_id: string; + item_type: string; + }[]; +}