Skip to content

Commit 3ede128

Browse files
authored
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 400fb1f ("fix(route/picnob): use puppeteer-real-browser to pass cf check (#20478)") and was then removed in 2cb11d7 ("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 2cb11d7 ("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>
1 parent 28bc65d commit 3ede128

File tree

3 files changed

+93
-12
lines changed

3 files changed

+93
-12
lines changed

.github/workflows/docker-release.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ jobs:
3434
packages: write
3535
id-token: write
3636
steps:
37+
- name: Enable ZRAM
38+
# Reduce memory pressure
39+
# PERCENT=100 is safe: https://fedoraproject.org/wiki/Changes/Scale_ZRAM_to_full_memory_size
40+
run: |
41+
sudo apt-get update -yq
42+
sudo apt-get install -yq "linux-modules-extra-$(uname -r)" zram-tools
43+
echo -e 'ALGO=lz4\nPERCENT=100' | sudo tee -a /etc/default/zramswap
44+
sudo systemctl restart zramswap
45+
swapon
46+
3747
- name: Checkout
3848
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
3949

Dockerfile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ ARG PUPPETEER_SKIP_DOWNLOAD=1
132132
# https://www.debian.org/releases/bookworm/amd64/release-notes/ch-information.en.html#noteworthy-obsolete-packages
133133
# The official recommended way to use Puppeteer on arm/arm64 is to install Chromium from the distribution repositories:
134134
# https://github.com/puppeteer/puppeteer/blob/07391bbf5feaf85c191e1aa8aa78138dce84008d/packages/puppeteer-core/src/node/BrowserFetcher.ts#L128-L131
135+
# Dependencies of puppeteer-real-browser: xvfb, procps
135136
RUN \
136137
set -ex && \
137138
apt-get update && \
@@ -148,10 +149,13 @@ RUN \
148149
; \
149150
else \
150151
apt-get install -yq --no-install-recommends \
151-
chromium xvfb \
152+
chromium \
152153
&& \
153154
echo "CHROMIUM_EXECUTABLE_PATH=$(which chromium)" | tee /app/.env ; \
154155
fi; \
156+
apt-get install -yq --no-install-recommends \
157+
xvfb procps \
158+
; \
155159
fi; \
156160
rm -rf /var/lib/apt/lists/*
157161

@@ -161,7 +165,9 @@ RUN \
161165
set -ex && \
162166
if [ "$PUPPETEER_SKIP_DOWNLOAD" = 0 ] && [ "$TARGETPLATFORM" = 'linux/amd64' ]; then \
163167
echo 'Verifying Chromium installation...' && \
164-
if ldd $(find /app/node_modules/.cache/puppeteer/ -name chrome -type f) | grep "not found"; then \
168+
_chrome_path=$(find /app/node_modules/.cache/puppeteer/chrome/ -name chrome -xtype f -executable | head -n1) && \
169+
echo "CHROMIUM_EXECUTABLE_PATH=$_chrome_path" | tee /app/.env && \
170+
if ldd "$_chrome_path" | grep "not found"; then \
165171
echo "!!! Chromium has unmet shared libs !!!" && \
166172
exit 1 ; \
167173
else \

lib/routes/picnob/user.ts

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,42 @@ import { Route, ViewType } from '@/types';
33
import cache from '@/utils/cache';
44
import { parseRelativeDate } from '@/utils/parse-date';
55
import { load } from 'cheerio';
6+
import { connect, Options, ConnectResult } from 'puppeteer-real-browser';
67

7-
async function getPageWithRealBrowser(url: string, selector: string) {
8+
const realBrowserOption: Options = {
9+
args: ['--start-maximized'],
10+
turnstile: true,
11+
headless: false,
12+
// disableXvfb: true,
13+
// ignoreAllFlags:true,
14+
customConfig: {
15+
chromePath: config.chromiumExecutablePath,
16+
},
17+
connectOption: {
18+
defaultViewport: null,
19+
},
20+
plugins: [],
21+
};
22+
23+
async function getPageWithRealBrowser(url: string, selector: string, conn: ConnectResult | null) {
824
try {
9-
const res = await fetch(`${config.puppeteerRealBrowserService}?url=${encodeURIComponent(url)}&selector=${encodeURIComponent(selector)}`);
10-
const json = await res.json();
11-
return (json.data?.at(0) || '') as string;
25+
if (conn) {
26+
const page = conn.page;
27+
await page.goto(url, { timeout: 30000 });
28+
let verify: boolean | null = null;
29+
const startDate = Date.now();
30+
while (!verify && Date.now() - startDate < 30000) {
31+
// eslint-disable-next-line no-await-in-loop, no-restricted-syntax
32+
verify = await page.evaluate((sel) => (document.querySelector(sel) ? true : null), selector).catch(() => null);
33+
// eslint-disable-next-line no-await-in-loop
34+
await new Promise((r) => setTimeout(r, 1000));
35+
}
36+
return await page.content();
37+
} else {
38+
const res = await fetch(`${config.puppeteerRealBrowserService}?url=${encodeURIComponent(url)}&selector=${encodeURIComponent(selector)}`);
39+
const json = await res.json();
40+
return (json.data?.at(0) || '') as string;
41+
}
1242
} catch {
1343
return '';
1444
}
@@ -41,14 +71,14 @@ export const route: Route = {
4171
},
4272
],
4373
name: 'User Profile - Pixnoy',
44-
maintainers: ['TonyRL', 'micheal-death', 'AiraNadih', 'DIYgod', 'hyoban'],
74+
maintainers: ['TonyRL', 'micheal-death', 'AiraNadih', 'DIYgod', 'hyoban', 'Rongronggg9'],
4575
handler,
4676
view: ViewType.Pictures,
4777
};
4878

4979
async function handler(ctx) {
50-
if (!config.puppeteerRealBrowserService) {
51-
throw new Error('PUPPETEER_REAL_BROWSER_SERVICE is required to use this route.');
80+
if (!config.puppeteerRealBrowserService && !config.chromiumExecutablePath) {
81+
throw new Error('PUPPETEER_REAL_BROWSER_SERVICE or CHROMIUM_EXECUTABLE_PATH is required to use this route.');
5282
}
5383

5484
// NOTE: 'picnob' is still available, but all requests to 'picnob' will be redirected to 'pixnoy' eventually
@@ -57,8 +87,24 @@ async function handler(ctx) {
5787
const type = ctx.req.param('type') ?? 'profile';
5888
const profileUrl = `${baseUrl}/profile/${id}/${type === 'tagged' ? 'tagged/' : ''}`;
5989

60-
const html = await getPageWithRealBrowser(profileUrl, '.post_box');
90+
let conn: ConnectResult | null = null;
91+
92+
if (!config.puppeteerRealBrowserService) {
93+
conn = await connect(realBrowserOption);
94+
95+
setTimeout(async () => {
96+
if (conn) {
97+
await conn.browser.close();
98+
}
99+
}, 60000);
100+
}
101+
102+
const html = await getPageWithRealBrowser(profileUrl, '.post_box', conn);
61103
if (!html) {
104+
if (conn) {
105+
await conn.browser.close();
106+
conn = null;
107+
}
62108
throw new Error('Failed to fetch user profile page. User may not exist or there are no posts available.');
63109
}
64110

@@ -82,7 +128,23 @@ async function handler(ctx) {
82128
};
83129
});
84130

85-
const htmlList = (await Promise.all(list.map((item) => cache.tryGet(`picnob:user:${id}:${item.guid}:html`, async () => await getPageWithRealBrowser(item.link, '.view'))))) as string[];
131+
const jobs = list.map((item) => cache.tryGet(`picnob:user:${id}:${item.guid}:html`, async () => await getPageWithRealBrowser(item.link, '.view', conn)));
132+
133+
let htmlList: string[] = [];
134+
if (conn) {
135+
try {
136+
for (const job of jobs) {
137+
// eslint-disable-next-line no-await-in-loop
138+
const html = await job;
139+
htmlList.push(html);
140+
}
141+
} finally {
142+
await conn.browser.close();
143+
conn = null;
144+
}
145+
} else {
146+
htmlList = await Promise.all(jobs);
147+
}
86148

87149
const newDescription = htmlList.map((html) => {
88150
if (!html) {
@@ -94,7 +156,10 @@ async function handler(ctx) {
94156
} else {
95157
let description = '';
96158
for (const pic of $('.pic img').toArray()) {
97-
description += `<img src="${$(pic).attr('data-src')}" /><br />`;
159+
const dataSrc = $(pic).attr('data-src');
160+
if (dataSrc) {
161+
description += `<img src="${dataSrc}" /><br />`;
162+
}
98163
}
99164
description += $('.sum_full').text();
100165
return description;

0 commit comments

Comments
 (0)