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;
+ }[];
+}