108 lines
4.1 KiB
TypeScript
108 lines
4.1 KiB
TypeScript
import { load } from 'cheerio';
|
|
|
|
import { config } from '@/config';
|
|
import type { DataItem, Route } from '@/types';
|
|
import ofetch from '@/utils/ofetch';
|
|
import { parseDate } from '@/utils/parse-date';
|
|
|
|
import { processContent } from './utils';
|
|
|
|
export const route: Route = {
|
|
path: '/thread/:thread',
|
|
name: 'Thread',
|
|
maintainers: ['wsmbsbbz'],
|
|
example: '/f95zone/thread/ubermation-collection-2026-01-19-uebermation-uebermation.231247',
|
|
categories: ['game'],
|
|
description: `Track replies in a thread. Fetches the first page and the last page.
|
|
|
|
URL format: \`https://f95zone.to/threads/{thread}/\` → use \`{thread}\` as the parameter.
|
|
|
|
Example: \`https://f95zone.to/threads/ubermation-collection-2026-01-19-uebermation-uebermation.231247/\` → \`/f95zone/thread/ubermation-collection-2026-01-19-uebermation-uebermation.231247\`
|
|
|
|
Note: If you want to track a specific post's content changes (e.g., first post with download links), use the \`/f95zone/post\` route instead.`,
|
|
parameters: {
|
|
thread: 'Thread slug with ID, copy from browser URL after `/threads/`',
|
|
},
|
|
features: {
|
|
requireConfig: [
|
|
{
|
|
name: 'F95ZONE_COOKIE',
|
|
optional: true,
|
|
description: 'F95zone cookie for accessing restricted content.',
|
|
},
|
|
],
|
|
requirePuppeteer: false,
|
|
antiCrawler: false,
|
|
supportBT: false,
|
|
supportPodcast: false,
|
|
supportScihub: false,
|
|
nsfw: true,
|
|
},
|
|
radar: [
|
|
{
|
|
source: ['f95zone.to/threads/:thread/*'],
|
|
target: '/thread/:thread',
|
|
},
|
|
],
|
|
handler: async (ctx) => {
|
|
const { thread } = ctx.req.param();
|
|
const baseUrl = 'https://f95zone.to';
|
|
const threadLink = `${baseUrl}/threads/${thread}/`;
|
|
|
|
const headers = {
|
|
referer: baseUrl,
|
|
...(config.f95zone.cookie ? { cookie: config.f95zone.cookie } : {}),
|
|
};
|
|
|
|
const firstPageResponse = await ofetch(threadLink, { headers });
|
|
const $firstPage = load(firstPageResponse);
|
|
const title = $firstPage('h1.p-title-value').text().trim();
|
|
|
|
const lastPageLink = $firstPage('ul.pageNav-main li.pageNav-page:last-child a').attr('href');
|
|
const totalPages = lastPageLink ? Number(lastPageLink.match(/page-(\d+)/)?.[1] || '1') : 1;
|
|
|
|
const extractPosts = ($: ReturnType<typeof load>): DataItem[] =>
|
|
$('article.message')
|
|
.toArray()
|
|
.flatMap((article) => {
|
|
const $article = $(article);
|
|
const postId = $article.attr('data-content')?.replace('post-', '');
|
|
if (!postId) {
|
|
return [];
|
|
}
|
|
|
|
const author = $article.find('.message-name a').text().trim();
|
|
const postDate = $article.find('time.u-dt').attr('datetime');
|
|
const content = $article.find('.bbWrapper').html() || '';
|
|
const postLink = `${threadLink}post-${postId}`;
|
|
|
|
// Get post number from the attribution list (e.g., "#717")
|
|
const postNumber = $article.find('.message-attribution-opposite--list li:last-child a').text().trim().replace('#', '') || postId;
|
|
|
|
return {
|
|
title: `#${postNumber} by ${author}`,
|
|
link: postLink,
|
|
guid: postLink,
|
|
description: processContent(content),
|
|
pubDate: postDate ? parseDate(postDate) : undefined,
|
|
author,
|
|
};
|
|
});
|
|
|
|
// Extract posts from the first page
|
|
const allPosts: DataItem[] = [...extractPosts($firstPage)];
|
|
|
|
// Fetch the last page if there are multiple pages
|
|
if (totalPages > 1) {
|
|
const lastPageResponse = await ofetch(`${threadLink}page-${totalPages}`, { headers });
|
|
allPosts.push(...extractPosts(load(lastPageResponse)));
|
|
}
|
|
|
|
return {
|
|
title: `[F95zone] ${title}`,
|
|
link: threadLink,
|
|
item: allPosts,
|
|
};
|
|
},
|
|
};
|