Fixed failure message logging - added wait step.
This commit is contained in:
+4
-1
@@ -13,9 +13,12 @@ novels:
|
|||||||
scope: "#chr-content"
|
scope: "#chr-content"
|
||||||
stealth:
|
stealth:
|
||||||
enabled: true
|
enabled: true
|
||||||
waitMs: 2000
|
|
||||||
addons:
|
addons:
|
||||||
- "uBlock0_1.67.0.firefox.signed.xpi"
|
- "uBlock0_1.67.0.firefox.signed.xpi"
|
||||||
|
steps:
|
||||||
|
- type: waitForElement
|
||||||
|
selector: "#list-chapter .chapter-archive-grid"
|
||||||
|
timeoutMs: 15000
|
||||||
download:
|
download:
|
||||||
parallelism: 4
|
parallelism: 4
|
||||||
minContentBytes: 10
|
minContentBytes: 10
|
||||||
|
|||||||
+9
-1
@@ -23,10 +23,18 @@ export interface ContentSelector {
|
|||||||
scope: string;
|
scope: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StealthStep =
|
||||||
|
| { type: 'waitMs'; ms: number }
|
||||||
|
| { type: 'waitForElement'; selector: string; timeoutMs?: number }
|
||||||
|
| { type: 'click'; selector: string }
|
||||||
|
| { type: 'scrollToBottom' };
|
||||||
|
|
||||||
export interface StealthConfig {
|
export interface StealthConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
waitMs: number;
|
/** @deprecated Use `steps` instead. Kept for backward compatibility. */
|
||||||
|
waitMs?: number;
|
||||||
addons: string[];
|
addons: string[];
|
||||||
|
steps?: StealthStep[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadConfig {
|
export interface DownloadConfig {
|
||||||
|
|||||||
@@ -27,4 +27,7 @@ console.log(`[init] Listening on port ${appConfig.server.port}`);
|
|||||||
export default {
|
export default {
|
||||||
port: appConfig.server.port,
|
port: appConfig.server.port,
|
||||||
fetch: app.fetch,
|
fetch: app.fetch,
|
||||||
|
// TOC scraping can take 15-30s; raise above Bun's 10s default so the first
|
||||||
|
// request doesn't time out before the cache is populated.
|
||||||
|
idleTimeout: 120,
|
||||||
};
|
};
|
||||||
|
|||||||
+44
-11
@@ -1,16 +1,41 @@
|
|||||||
import { Builder, type WebDriver } from 'selenium-webdriver';
|
import { Builder, By, until, type WebDriver } from 'selenium-webdriver';
|
||||||
import { Options as FirefoxOptions } from 'selenium-webdriver/firefox.js';
|
import { Options as FirefoxOptions } from 'selenium-webdriver/firefox.js';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import pLimit from 'p-limit';
|
import pLimit from 'p-limit';
|
||||||
import pRetry from 'p-retry';
|
import pRetry from 'p-retry';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { AppConfig, NovelDefinition } from '../config/types.ts';
|
import type { AppConfig, NovelDefinition, StealthConfig } from '../config/types.ts';
|
||||||
import type { Cache } from './cache.ts';
|
import type { Cache } from './cache.ts';
|
||||||
import type { ChapterView, TextFragment, Style } from '../types.ts';
|
import type { ChapterView, TextFragment, Style } from '../types.ts';
|
||||||
import { analyzeTocPage, formatDiagnosis } from './analyzer.ts';
|
import { analyzeTocPage, formatDiagnosis } from './analyzer.ts';
|
||||||
|
|
||||||
export type DriverFactory = () => Promise<WebDriver>;
|
export type DriverFactory = () => Promise<WebDriver>;
|
||||||
|
|
||||||
|
async function executeStealthSteps(driver: WebDriver, stealth: StealthConfig): Promise<void> {
|
||||||
|
if (stealth.steps && stealth.steps.length > 0) {
|
||||||
|
for (const step of stealth.steps) {
|
||||||
|
switch (step.type) {
|
||||||
|
case 'waitMs':
|
||||||
|
await driver.sleep(step.ms);
|
||||||
|
break;
|
||||||
|
case 'waitForElement':
|
||||||
|
await driver.wait(until.elementLocated(By.css(step.selector)), step.timeoutMs ?? 10_000);
|
||||||
|
break;
|
||||||
|
case 'click': {
|
||||||
|
const el = await driver.findElement(By.css(step.selector));
|
||||||
|
await el.click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'scrollToBottom':
|
||||||
|
await driver.executeScript('window.scrollTo(0, document.body.scrollHeight)');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (stealth.waitMs != null) {
|
||||||
|
await driver.sleep(stealth.waitMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildDefaultDriverFactory(def: NovelDefinition, appConfig: AppConfig): DriverFactory {
|
function buildDefaultDriverFactory(def: NovelDefinition, appConfig: AppConfig): DriverFactory {
|
||||||
return async () => {
|
return async () => {
|
||||||
const options = new FirefoxOptions();
|
const options = new FirefoxOptions();
|
||||||
@@ -61,7 +86,7 @@ export async function fetchToc(
|
|||||||
const urls = await pRetry(
|
const urls = await pRetry(
|
||||||
async () => {
|
async () => {
|
||||||
await driver.get(sourceUrl);
|
await driver.get(sourceUrl);
|
||||||
await driver.sleep(def.stealth!.waitMs);
|
await executeStealthSteps(driver, def.stealth!);
|
||||||
|
|
||||||
// Read the live rendered DOM (post-JS, post-uBlock) and the URL we
|
// Read the live rendered DOM (post-JS, post-uBlock) and the URL we
|
||||||
// actually landed on (may differ from sourceUrl after redirects).
|
// actually landed on (may differ from sourceUrl after redirects).
|
||||||
@@ -97,10 +122,16 @@ export async function fetchToc(
|
|||||||
{
|
{
|
||||||
retries: 3,
|
retries: 3,
|
||||||
onFailedAttempt: err => {
|
onFailedAttempt: err => {
|
||||||
// p-retry types this as RetryContext; the original thrown value is at
|
// p-retry passes a plain frozen context {error, attemptNumber, retriesLeft, ...}
|
||||||
// err.error. Error.message is non-enumerable so JSON.stringify shows {}.
|
// NOT an Error subclass, so err.message is always undefined.
|
||||||
const cause = (err as unknown as { error?: unknown }).error;
|
// err.error is the original thrown value.
|
||||||
const msg = cause instanceof Error ? cause.message : String(cause ?? err);
|
const cause: unknown = (err as { error?: unknown }).error;
|
||||||
|
const msg =
|
||||||
|
cause instanceof Error
|
||||||
|
? (cause.message || cause.constructor.name)
|
||||||
|
: cause != null
|
||||||
|
? String(cause)
|
||||||
|
: `attempt ${err.attemptNumber} failed`;
|
||||||
console.warn(
|
console.warn(
|
||||||
`[scraper] TOC fetch attempt ${err.attemptNumber}/${err.attemptNumber + err.retriesLeft} failed: ${msg}`,
|
`[scraper] TOC fetch attempt ${err.attemptNumber}/${err.attemptNumber + err.retriesLeft} failed: ${msg}`,
|
||||||
);
|
);
|
||||||
@@ -184,10 +215,12 @@ export async function fetchChapters(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
retries: 2,
|
retries: 2,
|
||||||
onFailedAttempt: err =>
|
onFailedAttempt: err => {
|
||||||
console.warn(
|
const cause: unknown = (err as { error?: unknown }).error;
|
||||||
`[scraper] Chapter ${index} attempt ${err.attemptNumber} failed: ${err.message}`,
|
const msg =
|
||||||
),
|
cause instanceof Error ? cause.message || cause.constructor.name : String(cause ?? err);
|
||||||
|
console.warn(`[scraper] Chapter ${index} attempt ${err.attemptNumber} failed: ${msg}`);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user