Skip to content

Commit

Permalink
Merge pull request #33 from DIYgod/master
Browse files Browse the repository at this point in the history
[pull] master from diygod:master
  • Loading branch information
pull[bot] authored May 30, 2024
2 parents 5655c62 + abbebfb commit 036cf27
Show file tree
Hide file tree
Showing 17 changed files with 838 additions and 427 deletions.
153 changes: 110 additions & 43 deletions lib/routes/famitsu/category.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,149 @@
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 = {
path: '/category/:category?',
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 buildId = await getBuildId();

const list = $('.col-12 .card__body')
.toArray()
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,
Expand Down
7 changes: 7 additions & 0 deletions lib/routes/famitsu/templates/description.art
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{ if bannerImage }}
<img src="{{ bannerImage }}"><br>
{{ /if }}

{{ if content }}
{{@ content }}
{{ /if }}
117 changes: 117 additions & 0 deletions lib/routes/famitsu/types.ts
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 036cf27

Please sign in to comment.