feat: redirect /picnob to /picnob.info

This commit is contained in:
Tony
2026-03-11 02:03:34 +08:00
parent e2c991a0ed
commit 8637e63dec
4 changed files with 140 additions and 147 deletions

View File

@@ -12,7 +12,6 @@ services:
CACHE_TYPE: redis
REDIS_URL: 'redis://redis:6379/'
PUPPETEER_WS_ENDPOINT: 'ws://browserless:3000' # marked
PUPPETEER_REAL_BROWSER_SERVICE: 'http://real-browser:3000' # marked
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:1200/healthz']
interval: 30s
@@ -22,17 +21,6 @@ services:
- redis
- browserless # marked
real-browser:
image: ghcr.io/hyoban/puppeteer-real-browser-hono
restart: always
ports:
- '3001:3000'
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000']
interval: 30s
timeout: 10s
retries: 3
browserless: # marked
image: browserless/chrome # marked
restart: always # marked

View File

@@ -25,7 +25,7 @@ export const route: Route = {
target: '/manga/:id',
},
],
name: 'カドコミ(Kadocomi)漫画详情',
name: '漫画详情',
maintainers: ['xiaobailoves'],
handler: async (ctx) => {

View File

@@ -6,6 +6,7 @@ import { ViewType } from '@/types';
import cache from '@/utils/cache';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
import wait from '@/utils/wait';
import type { Post, Profile, Pull, Status, Story } from './types';
@@ -105,7 +106,7 @@ async function handler(ctx) {
}
if (attempt < 9) {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => setTimeout(resolve, 3000));
await wait(3000);
}
}

View File

@@ -1,51 +1,51 @@
import { load } from 'cheerio';
import type { ConnectResult, Options } from 'puppeteer-real-browser';
import { connect } from 'puppeteer-real-browser';
// import { load } from 'cheerio';
// import type { ConnectResult, Options } from 'puppeteer-real-browser';
// import { connect } from 'puppeteer-real-browser';
import { config } from '@/config';
// import { config } from '@/config';
import type { Route } from '@/types';
import { ViewType } from '@/types';
import cache from '@/utils/cache';
import { parseRelativeDate } from '@/utils/parse-date';
// import cache from '@/utils/cache';
// import { parseRelativeDate } from '@/utils/parse-date';
const realBrowserOption: Options = {
args: ['--start-maximized'],
turnstile: true,
headless: false,
// disableXvfb: true,
// ignoreAllFlags:true,
customConfig: {
chromePath: config.chromiumExecutablePath,
},
connectOption: {
defaultViewport: null,
},
plugins: [],
};
// const realBrowserOption: Options = {
// args: ['--start-maximized'],
// turnstile: true,
// headless: false,
// // disableXvfb: true,
// // ignoreAllFlags:true,
// customConfig: {
// chromePath: config.chromiumExecutablePath,
// },
// connectOption: {
// defaultViewport: null,
// },
// plugins: [],
// };
async function getPageWithRealBrowser(url: string, selector: string, conn: ConnectResult | null) {
try {
if (conn) {
const page = conn.page;
await page.goto(url, { timeout: 30000 });
let verify: boolean | null = null;
const startDate = Date.now();
while (!verify && Date.now() - startDate < 30000) {
// eslint-disable-next-line no-await-in-loop, no-restricted-syntax
verify = await page.evaluate((sel) => (document.querySelector(sel) ? true : null), selector).catch(() => null);
// eslint-disable-next-line no-await-in-loop
await new Promise((r) => setTimeout(r, 1000));
}
return await page.content();
} else {
const res = await fetch(`${config.puppeteerRealBrowserService}?url=${encodeURIComponent(url)}&selector=${encodeURIComponent(selector)}`);
const json = await res.json();
return (json.data?.at(0) || '') as string;
}
} catch {
return '';
}
}
// async function getPageWithRealBrowser(url: string, selector: string, conn: ConnectResult | null) {
// try {
// if (conn) {
// const page = conn.page;
// await page.goto(url, { timeout: 30000 });
// let verify: boolean | null = null;
// const startDate = Date.now();
// while (!verify && Date.now() - startDate < 30000) {
// // eslint-disable-next-line no-await-in-loop, no-restricted-syntax
// verify = await page.evaluate((sel) => (document.querySelector(sel) ? true : null), selector).catch(() => null);
// // eslint-disable-next-line no-await-in-loop
// await new Promise((r) => setTimeout(r, 1000));
// }
// return await page.content();
// } else {
// const res = await fetch(`${config.puppeteerRealBrowserService}?url=${encodeURIComponent(url)}&selector=${encodeURIComponent(selector)}`);
// const json = await res.json();
// return (json.data?.at(0) || '') as string;
// }
// } catch {
// return '';
// }
// }
export const route: Route = {
path: '/user/:id/:type?',
@@ -57,8 +57,8 @@ export const route: Route = {
},
features: {
requireConfig: false,
requirePuppeteer: true,
antiCrawler: true,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
@@ -79,104 +79,108 @@ export const route: Route = {
view: ViewType.Pictures,
};
async function handler(ctx) {
if (!config.puppeteerRealBrowserService && !config.chromiumExecutablePath) {
throw new Error('PUPPETEER_REAL_BROWSER_SERVICE or CHROMIUM_EXECUTABLE_PATH is required to use this route.');
}
// NOTE: 'picnob' is still available, but all requests to 'picnob' will be redirected to 'pixnoy' eventually
const baseUrl = 'https://www.pixnoy.com';
function handler(ctx) {
const id = ctx.req.param('id');
const type = ctx.req.param('type') ?? 'profile';
const profileUrl = `${baseUrl}/profile/${id}/${type === 'tagged' ? 'tagged/' : ''}`;
return ctx.set('redirect', `/picnob.info/user/${id}`);
let conn: ConnectResult | null = null;
// // Original puppeteer-real-browser implementation (deprecated)
// if (!config.puppeteerRealBrowserService && !config.chromiumExecutablePath) {
// throw new Error('PUPPETEER_REAL_BROWSER_SERVICE or CHROMIUM_EXECUTABLE_PATH is required to use this route.');
// }
if (!config.puppeteerRealBrowserService) {
conn = await connect(realBrowserOption);
// // NOTE: 'picnob' is still available, but all requests to 'picnob' will be redirected to 'picnob.info' eventually
// const baseUrl = 'https://www.pixnoy.com';
// const id = ctx.req.param('id');
// const type = ctx.req.param('type') ?? 'profile';
// const profileUrl = `${baseUrl}/profile/${id}/${type === 'tagged' ? 'tagged/' : ''}`;
setTimeout(async () => {
if (conn) {
await conn.browser.close();
}
}, 60000);
}
// let conn: ConnectResult | null = null;
const html = await getPageWithRealBrowser(profileUrl, '.post_box', conn);
if (!html) {
if (conn) {
await conn.browser.close();
conn = null;
}
throw new Error('Failed to fetch user profile page. User may not exist or there are no posts available.');
}
// if (!config.puppeteerRealBrowserService) {
// conn = await connect(realBrowserOption);
const $ = load(html);
// setTimeout(async () => {
// if (conn) {
// await conn.browser.close();
// }
// }, 60000);
// }
const list = $('.post_box')
.toArray()
.map((item) => {
const $item = $(item);
const coverLink = $item.find('.cover_link').attr('href');
const shortcode = coverLink?.split('/')?.[2];
const image = $item.find('.cover .cover_link img');
const title = image.attr('alt') || '';
// const html = await getPageWithRealBrowser(profileUrl, '.post_box', conn);
// if (!html) {
// if (conn) {
// await conn.browser.close();
// conn = null;
// }
// throw new Error('Failed to fetch user profile page. User may not exist or there are no posts available.');
// }
return {
title,
description: `<img src="${image.attr('data-src')}" /><br />${title}`,
link: `${baseUrl}${coverLink}`,
guid: shortcode,
pubDate: parseRelativeDate($item.find('.time .txt').text()),
};
});
// const $ = load(html);
const jobs = list.map((item) => cache.tryGet(`picnob:user:${id}:${item.guid}:html`, async () => await getPageWithRealBrowser(item.link, '.view', conn)));
// const list = $('.post_box')
// .toArray()
// .map((item) => {
// const $item = $(item);
// const coverLink = $item.find('.cover_link').attr('href');
// const shortcode = coverLink?.split('/')?.[2];
// const image = $item.find('.cover .cover_link img');
// const title = image.attr('alt') || '';
let htmlList: string[] = [];
if (conn) {
try {
for (const job of jobs) {
// eslint-disable-next-line no-await-in-loop
const html = await job;
htmlList.push(html);
}
} finally {
await conn.browser.close();
conn = null;
}
} else {
htmlList = await Promise.all(jobs);
}
// return {
// title,
// description: `<img src="${image.attr('data-src')}" /><br />${title}`,
// link: `${baseUrl}${coverLink}`,
// guid: shortcode,
// pubDate: parseRelativeDate($item.find('.time .txt').text()),
// };
// });
const newDescription = htmlList.map((html) => {
if (!html) {
return '';
}
const $ = load(html);
if ($('.video_img').length > 0) {
return `<video src="${$('.video_img a').attr('href')}" poster="${$('.video_img img').attr('data-src')}"></video><br />${$('.sum_full').text()}`;
} else {
let description = '';
for (const pic of $('.pic img').toArray()) {
const dataSrc = $(pic).attr('data-src');
if (dataSrc) {
description += `<img src="${dataSrc}" /><br />`;
}
}
description += $('.sum_full').text();
return description;
}
});
// const jobs = list.map((item) => cache.tryGet(`picnob:user:${id}:${item.guid}:html`, async () => await getPageWithRealBrowser(item.link, '.view', conn)));
return {
title: `${$('h1.fullname').text()} (@${id}) ${type === 'tagged' ? 'tagged' : 'public'} posts - Picnob`,
description: $('.info .sum').text(),
link: profileUrl,
image: $('.ava .pic img').attr('src'),
item: list.map((item, index) => ({
...item,
description: newDescription[index] || item.description,
})),
};
// let htmlList: string[] = [];
// if (conn) {
// try {
// for (const job of jobs) {
// // eslint-disable-next-line no-await-in-loop
// const html = await job;
// htmlList.push(html);
// }
// } finally {
// await conn.browser.close();
// conn = null;
// }
// } else {
// htmlList = await Promise.all(jobs);
// }
// const newDescription = htmlList.map((html) => {
// if (!html) {
// return '';
// }
// const $ = load(html);
// if ($('.video_img').length > 0) {
// return `<video src="${$('.video_img a').attr('href')}" poster="${$('.video_img img').attr('data-src')}"></video><br />${$('.sum_full').text()}`;
// } else {
// let description = '';
// for (const pic of $('.pic img').toArray()) {
// const dataSrc = $(pic).attr('data-src');
// if (dataSrc) {
// description += `<img src="${dataSrc}" /><br />`;
// }
// }
// description += $('.sum_full').text();
// return description;
// }
// });
// return {
// title: `${$('h1.fullname').text()} (@${id}) ${type === 'tagged' ? 'tagged' : 'public'} posts - Picnob`,
// description: $('.info .sum').text(),
// link: profileUrl,
// image: $('.ava .pic img').attr('src'),
// item: list.map((item, index) => ({
// ...item,
// description: newDescription[index] || item.description,
// })),
// };
}