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 <git@sryvkver.com>
This commit is contained in:
Sryvkver 2026-07-21 04:03:19 +02:00 committed by GitHub
parent abb7a21e28
commit de93618f18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 69 additions and 11 deletions

View File

@ -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<Data> {
}
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,

View File

@ -29,7 +29,9 @@ export interface PostListResponse {
}
export interface PostDetailResponse {
body: PostDetail;
body: {
post: PostDetail;
};
}
export interface PostItem {

View File

@ -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: <b>${i.feeRequired} JPY/month</b><hr>`;
@ -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,