fix(route/picnob): bring back support for diygod/rsshub:chromium-bundled (#20504)

* ci(docker-release): enable ZRAM to reduce memory pressure

This should hopefully help reduce stuck build jobs. Note that this can't
completely eliminate stuck builds since this is usually caused by
synchronization issues in QEMU.

Signed-off-by: Rongrong <i@rong.moe>

* build(Dockerfile): Add support for puppeteer-real-browser

Sets CHROMIUM_EXECUTABLE_PATH on AMD64 and installs essential
dependencies.

Signed-off-by: Rongrong <i@rong.moe>

* fix(route/picnob): bring back support for diygod/rsshub:chromium-bundled

The support for puppeteer-real-browser on diygod/rsshub:chromium-bundled
was improperly implemented in 400fb1fa49 ("fix(route/picnob): use
puppeteer-real-browser to pass cf check (#20478)") and was then removed
in 2cb11d7c7e ("fix(route/picnob): support img not in slide").

With proper support for puppeteer-real-browser added to Dockerfile, this
patch brings back the support for diygod/rsshub:chromium-bundled.

This partially reverts commit 2cb11d7c7e ("fix(route/picnob): support
img not in slide").

Signed-off-by: Rongrong <i@rong.moe>

* chore(route/picnob): add myself as maintainer

This route explicitly consult CHROMIUM_EXECUTABLE_PATH from the docker
image variant `diygod/rsshub:chromium-bundled`. Since I am the author of
the image variant, add myself as one of route maintainers so that I am
mentioned when the route is broken.

Signed-off-by: Rongrong <i@rong.moe>

# Conflicts:
#	lib/routes/picnob/user.ts

---------

Signed-off-by: Rongrong <i@rong.moe>
This commit is contained in:
Rongrong 2025-11-21 01:23:22 +08:00 committed by GitHub
parent 28bc65d0be
commit 3ede128c55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 93 additions and 12 deletions

View File

@ -34,6 +34,16 @@ jobs:
packages: write
id-token: write
steps:
- name: Enable ZRAM
# Reduce memory pressure
# PERCENT=100 is safe: https://fedoraproject.org/wiki/Changes/Scale_ZRAM_to_full_memory_size
run: |
sudo apt-get update -yq
sudo apt-get install -yq "linux-modules-extra-$(uname -r)" zram-tools
echo -e 'ALGO=lz4\nPERCENT=100' | sudo tee -a /etc/default/zramswap
sudo systemctl restart zramswap
swapon
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1

View File

@ -132,6 +132,7 @@ ARG PUPPETEER_SKIP_DOWNLOAD=1
# https://www.debian.org/releases/bookworm/amd64/release-notes/ch-information.en.html#noteworthy-obsolete-packages
# The official recommended way to use Puppeteer on arm/arm64 is to install Chromium from the distribution repositories:
# https://github.com/puppeteer/puppeteer/blob/07391bbf5feaf85c191e1aa8aa78138dce84008d/packages/puppeteer-core/src/node/BrowserFetcher.ts#L128-L131
# Dependencies of puppeteer-real-browser: xvfb, procps
RUN \
set -ex && \
apt-get update && \
@ -148,10 +149,13 @@ RUN \
; \
else \
apt-get install -yq --no-install-recommends \
chromium xvfb \
chromium \
&& \
echo "CHROMIUM_EXECUTABLE_PATH=$(which chromium)" | tee /app/.env ; \
fi; \
apt-get install -yq --no-install-recommends \
xvfb procps \
; \
fi; \
rm -rf /var/lib/apt/lists/*
@ -161,7 +165,9 @@ RUN \
set -ex && \
if [ "$PUPPETEER_SKIP_DOWNLOAD" = 0 ] && [ "$TARGETPLATFORM" = 'linux/amd64' ]; then \
echo 'Verifying Chromium installation...' && \
if ldd $(find /app/node_modules/.cache/puppeteer/ -name chrome -type f) | grep "not found"; then \
_chrome_path=$(find /app/node_modules/.cache/puppeteer/chrome/ -name chrome -xtype f -executable | head -n1) && \
echo "CHROMIUM_EXECUTABLE_PATH=$_chrome_path" | tee /app/.env && \
if ldd "$_chrome_path" | grep "not found"; then \
echo "!!! Chromium has unmet shared libs !!!" && \
exit 1 ; \
else \

View File

@ -3,12 +3,42 @@ import { Route, ViewType } from '@/types';
import cache from '@/utils/cache';
import { parseRelativeDate } from '@/utils/parse-date';
import { load } from 'cheerio';
import { connect, Options, ConnectResult } from 'puppeteer-real-browser';
async function getPageWithRealBrowser(url: string, selector: string) {
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 {
const res = await fetch(`${config.puppeteerRealBrowserService}?url=${encodeURIComponent(url)}&selector=${encodeURIComponent(selector)}`);
const json = await res.json();
return (json.data?.at(0) || '') as string;
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 '';
}
@ -41,14 +71,14 @@ export const route: Route = {
},
],
name: 'User Profile - Pixnoy',
maintainers: ['TonyRL', 'micheal-death', 'AiraNadih', 'DIYgod', 'hyoban'],
maintainers: ['TonyRL', 'micheal-death', 'AiraNadih', 'DIYgod', 'hyoban', 'Rongronggg9'],
handler,
view: ViewType.Pictures,
};
async function handler(ctx) {
if (!config.puppeteerRealBrowserService) {
throw new Error('PUPPETEER_REAL_BROWSER_SERVICE is required to use this route.');
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
@ -57,8 +87,24 @@ async function handler(ctx) {
const type = ctx.req.param('type') ?? 'profile';
const profileUrl = `${baseUrl}/profile/${id}/${type === 'tagged' ? 'tagged/' : ''}`;
const html = await getPageWithRealBrowser(profileUrl, '.post_box');
let conn: ConnectResult | null = null;
if (!config.puppeteerRealBrowserService) {
conn = await connect(realBrowserOption);
setTimeout(async () => {
if (conn) {
await conn.browser.close();
}
}, 60000);
}
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.');
}
@ -82,7 +128,23 @@ async function handler(ctx) {
};
});
const htmlList = (await Promise.all(list.map((item) => cache.tryGet(`picnob:user:${id}:${item.guid}:html`, async () => await getPageWithRealBrowser(item.link, '.view'))))) as string[];
const jobs = list.map((item) => cache.tryGet(`picnob:user:${id}:${item.guid}:html`, async () => await getPageWithRealBrowser(item.link, '.view', conn)));
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) {
@ -94,7 +156,10 @@ async function handler(ctx) {
} else {
let description = '';
for (const pic of $('.pic img').toArray()) {
description += `<img src="${$(pic).attr('data-src')}" /><br />`;
const dataSrc = $(pic).attr('data-src');
if (dataSrc) {
description += `<img src="${dataSrc}" /><br />`;
}
}
description += $('.sum_full').text();
return description;