fix(route): famitsu (#15761)
This commit is contained in:
parent
86dfc3e1d8
commit
2d1a0b7319
|
|
@ -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 `<b>${c.content}</b>`;
|
||||
case 'HEAD':
|
||||
return `<h2>${c.content}</h2>`;
|
||||
case 'SHEAD':
|
||||
return `<h3>${c.content}</h3>`;
|
||||
case 'LINK_B':
|
||||
case 'LINK_B_TAB':
|
||||
return `<a href="${c.url}"><b>${c.content}</b></a><br>`;
|
||||
case 'IMAGE':
|
||||
return `<img src="${c.path}">`;
|
||||
case 'NEWS':
|
||||
return `<a href="${c.url}">${c.content}<br>${c.description}</a><br>`;
|
||||
case 'HTML':
|
||||
return c.content;
|
||||
case 'ANNOTATION':
|
||||
case 'CAPTION':
|
||||
case 'ITEMIZATION':
|
||||
case 'ITEMIZATION_NUM':
|
||||
case 'NOLINK':
|
||||
case 'STRING':
|
||||
case 'TWITTER':
|
||||
case 'YOUTUBE':
|
||||
return `<div><span>${c.content}</span></div>`;
|
||||
case 'BUTTON':
|
||||
case 'BUTTON_ANDROID':
|
||||
case 'BUTTON_EC':
|
||||
case 'BUTTON_IOS':
|
||||
case 'BUTTON_TAB':
|
||||
case 'LINK':
|
||||
case 'LINK_TAB':
|
||||
return `<a href="${c.url}">${c.content}</a><br>`;
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
{{ if bannerImage }}
|
||||
<img src="{{ bannerImage }}"><br>
|
||||
{{ /if }}
|
||||
|
||||
{{ if content }}
|
||||
{{@ content }}
|
||||
{{ /if }}
|
||||
|
|
@ -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;
|
||||
}[];
|
||||
}
|
||||
Loading…
Reference in New Issue