From de93618f1838df9c8bcf09cf21d237499f0444d2 Mon Sep 17 00:00:00 2001 From: Sryvkver <20717854+Sryvkver@users.noreply.github.com> Date: Tue, 21 Jul 2026 04:03:19 +0200 Subject: [PATCH] fix(route/fanbox): refactor to use playwright to load posts (#22681) * fix(route/fanbox): refactor to use playwright to load posts * fix(route/fanbox) added missing await * fix(route/fanbox) closing context after page close * fix(route/fanbox) Update model to conform to new API response --------- Co-authored-by: Sryvkver --- lib/routes/fanbox/index.ts | 38 ++++++++++++++++++++++++++++++++++--- lib/routes/fanbox/types.ts | 4 +++- lib/routes/fanbox/utils.tsx | 38 ++++++++++++++++++++++++++++++------- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/lib/routes/fanbox/index.ts b/lib/routes/fanbox/index.ts index 487b5bc98..4e8a0d485 100644 --- a/lib/routes/fanbox/index.ts +++ b/lib/routes/fanbox/index.ts @@ -1,12 +1,14 @@ import type { Context } from 'hono'; import InvalidParameterError from '@/errors/types/invalid-parameter'; -import type { Data, Route } from '@/types'; +import type { Data, DataItem, Route } from '@/types'; import ofetch from '@/utils/ofetch'; +import playwright from '@/utils/playwright'; +import { setCookies } from '@/utils/playwright-utils'; import { isValidHost } from '@/utils/valid-host'; import type { PostListResponse, UserInfoResponse } from './types'; -import { getHeaders, parseItem } from './utils'; +import { getCookieString, getHeaders, parseItem } from './utils'; export const route: Route = { path: '/:creator', @@ -24,6 +26,7 @@ export const route: Route = { optional: true, }, ], + requirePuppeteer: true, nsfw: true, }, }; @@ -53,7 +56,36 @@ async function handler(ctx: Context): Promise { } const postListResponse = (await ofetch(`https://api.fanbox.cc/post.listCreator?creatorId=${creator}&limit=20&withPinned=true`, { headers: getHeaders() })) as PostListResponse; - const items = await Promise.all(postListResponse.body.map((i) => parseItem(i))); + + const context = await playwright(); + const page = await context.newPage(); + + const cookieString = getCookieString(); + if (cookieString) { + await setCookies(page, cookieString, '.fanbox.cc'); + } + + await page.route('**/*', (route) => { + const request = route.request(); + + if (request.url().startsWith('https://api.fanbox.cc/post.info')) { + route.continue(); + return; + } + + request.resourceType() === 'document' ? route.continue() : route.abort(); + }); + await page.goto('https://www.fanbox.cc/', { + waitUntil: 'domcontentloaded', + }); + + let items: DataItem[]; + try { + items = await Promise.all(postListResponse.body.map((i) => parseItem(page, i))); + } finally { + await page.close(); + await context.close(); + } return { title, diff --git a/lib/routes/fanbox/types.ts b/lib/routes/fanbox/types.ts index cb447ef06..774843c39 100644 --- a/lib/routes/fanbox/types.ts +++ b/lib/routes/fanbox/types.ts @@ -29,7 +29,9 @@ export interface PostListResponse { } export interface PostDetailResponse { - body: PostDetail; + body: { + post: PostDetail; + }; } export interface PostItem { diff --git a/lib/routes/fanbox/utils.tsx b/lib/routes/fanbox/utils.tsx index f0d6d6713..2abc076a4 100644 --- a/lib/routes/fanbox/utils.tsx +++ b/lib/routes/fanbox/utils.tsx @@ -5,18 +5,22 @@ import type { DataItem } from '@/types'; import cache from '@/utils/cache'; import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; +import type { Page } from '@/utils/playwright'; import type { ArticlePost, FilePost, ImagePost, PostDetailResponse, PostItem, TextPost, VideoPost } from './types'; export function getHeaders() { - const sessionid = config.fanbox.session; - const cookie = sessionid ? `FANBOXSESSID=${sessionid}` : ''; return { origin: 'https://fanbox.cc', - cookie, + cookie: getCookieString(), }; } +export function getCookieString() { + const sessionid = config.fanbox.session; + return sessionid ? `FANBOXSESSID=${sessionid}` : ''; +} + function embedUrlMap(urlEmbed: ArticlePost['body']['urlEmbedMap'][string]) { switch (urlEmbed.type) { case 'html': @@ -131,7 +135,7 @@ async function parseArtile(body: ArticlePost['body']) { return ret.join(''); } -async function parseDetail(i: PostDetailResponse['body']) { +async function parseDetail(i: PostDetailResponse['body']['post']) { let ret = ''; if (i.feeRequired !== 0) { ret += `Fee Required: ${i.feeRequired} JPY/month
`; @@ -167,12 +171,32 @@ async function parseDetail(i: PostDetailResponse['body']) { return ret; } -export function parseItem(item: PostItem) { +export function parseItem(page: Page, item: PostItem) { return cache.tryGet(`fanbox-${item.id}-${item.updatedDatetime}`, async () => { - const postDetail = (await ofetch(`https://api.fanbox.cc/post.info?postId=${item.id}`, { headers: { ...getHeaders(), 'User-Agent': config.trueUA } })) as PostDetailResponse; + const postDetail: PostDetailResponse = await page.evaluate( + async ({ url }) => { + const res = await fetch(url, { + method: 'GET', + credentials: 'include', + headers: { + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-site', + }, + }); + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + + return res.json(); + }, + { url: `https://api.fanbox.cc/post.info?postId=${item.id}` } + ); + return { title: item.title || 'No title', - description: await parseDetail(postDetail.body), + description: await parseDetail(postDetail.body.post), pubDate: parseDate(item.updatedDatetime), link: `https://${item.creatorId}.fanbox.cc/posts/${item.id}`, category: item.tags,