引擎补充
This commit is contained in:
+175
-34
@@ -108,7 +108,14 @@ function normalizeUrl(base: string, href?: string): string {
|
|||||||
const trimmed = href.trim();
|
const trimmed = href.trim();
|
||||||
if (!trimmed) return base;
|
if (!trimmed) return base;
|
||||||
if (/^javascript:/i.test(trimmed)) return '';
|
if (/^javascript:/i.test(trimmed)) return '';
|
||||||
return new URL(trimmed, base).toString();
|
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
||||||
|
try {
|
||||||
|
return new URL(trimmed, base).toString();
|
||||||
|
} catch {
|
||||||
|
// 部分 Legado 源会把 bookSourceUrl 当作名称,真实服务端地址放在变量里。
|
||||||
|
// 无法解析相对地址时保留原值,让上层给出更准确的“非 HTTP URL”错误,而不是直接 Invalid URL。
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeRuleParam(value: string) {
|
function encodeRuleParam(value: string) {
|
||||||
@@ -171,13 +178,51 @@ function renderTemplateExpressions(raw: string, keyword = '', page = 1) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function runJsSnippet(code: string, context: Record<string, any>, timeout = 1000): string {
|
|
||||||
|
function unwrapJsRule(code: string) {
|
||||||
|
const raw = String(code || '').trim();
|
||||||
|
const block = raw.match(/^<js>([\s\S]*?)<\/js>$/i);
|
||||||
|
if (block) return block[1].trim();
|
||||||
|
return raw.replace(/^@js:/, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArguments(value?: string) {
|
||||||
|
const out: Record<string, string> = {};
|
||||||
|
String(value || '').split(/[&;\n]+/).forEach((part) => {
|
||||||
|
const idx = part.indexOf('=');
|
||||||
|
if (idx <= 0) return;
|
||||||
|
const key = part.slice(0, idx).trim();
|
||||||
|
const val = part.slice(idx + 1).trim();
|
||||||
|
try { out[key] = decodeURIComponent(val); } catch { out[key] = val; }
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSourceRuntime(source: any, baseUrl?: string) {
|
||||||
|
const raw = source || {};
|
||||||
|
const variable = raw.variable || raw.loginUrl || raw.bookSourceUrl || baseUrl || '';
|
||||||
|
return {
|
||||||
|
...raw,
|
||||||
|
getVariable: () => variable,
|
||||||
|
getLoginInfoMap: () => ({}),
|
||||||
|
getKey: () => raw.key || raw.bookSourceUrl || baseUrl || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function runJsSnippetRaw(code: string, context: Record<string, any>, timeout = 1000): any {
|
||||||
|
const sourceRuntime = makeSourceRuntime(context.source, context.baseUrl);
|
||||||
const java = {
|
const java = {
|
||||||
base64Encode: (value: unknown) => Buffer.from(String(value ?? ''), 'utf8').toString('base64'),
|
base64Encode: (value: unknown) => Buffer.from(String(value ?? ''), 'utf8').toString('base64'),
|
||||||
base64Decode: (value: unknown) => Buffer.from(String(value ?? ''), 'base64').toString('utf8'),
|
base64Decode: (value: unknown) => Buffer.from(String(value ?? ''), 'base64').toString('utf8'),
|
||||||
md5Encode: (value: unknown) => crypto.createHash('md5').update(String(value ?? '')).digest('hex'),
|
md5Encode: (value: unknown) => crypto.createHash('md5').update(String(value ?? '')).digest('hex'),
|
||||||
strToBytes: (value: unknown) => Buffer.from(String(value ?? ''), 'utf8'),
|
strToBytes: (value: unknown) => Buffer.from(String(value ?? ''), 'utf8'),
|
||||||
base64DecodeToByteArray: (value: unknown) => Buffer.from(String(value ?? ''), 'base64'),
|
base64DecodeToByteArray: (value: unknown) => Buffer.from(String(value ?? ''), 'base64'),
|
||||||
|
hexDecodeToString: (value: unknown) => Buffer.from(String(value ?? ''), 'hex').toString('utf8'),
|
||||||
|
ajax: () => '',
|
||||||
|
getWebViewUA: () => 'Mozilla/5.0 (Linux; Android 10) Mobile Safari/537.36',
|
||||||
|
deviceID: () => '',
|
||||||
|
androidId: () => '',
|
||||||
|
longToast: () => undefined,
|
||||||
encodeURI: (value: unknown) => encodeURIComponent(String(value ?? '')),
|
encodeURI: (value: unknown) => encodeURIComponent(String(value ?? '')),
|
||||||
encodeURIComponent: (value: unknown) => encodeURIComponent(String(value ?? '')),
|
encodeURIComponent: (value: unknown) => encodeURIComponent(String(value ?? '')),
|
||||||
put: (key: string, value: any) => { variableStore.set(key, value); return value; },
|
put: (key: string, value: any) => { variableStore.set(key, value); return value; },
|
||||||
@@ -186,11 +231,14 @@ function runJsSnippet(code: string, context: Record<string, any>, timeout = 1000
|
|||||||
htmlFormat: (value: unknown) => he.decode(String(value ?? '')).replace(/<br\s*\/?/gi, '\n').replace(/<[^>]+>/g, ''),
|
htmlFormat: (value: unknown) => he.decode(String(value ?? '')).replace(/<br\s*\/?/gi, '\n').replace(/<[^>]+>/g, ''),
|
||||||
};
|
};
|
||||||
const cookie = {
|
const cookie = {
|
||||||
|
getCookie: () => '',
|
||||||
removeCookie: () => undefined,
|
removeCookie: () => undefined,
|
||||||
mapToCookie: (input: any) => typeof input === 'string' ? input : Array.isArray(input) ? input.join('; ') : '',
|
mapToCookie: (input: any) => typeof input === 'string' ? input : Array.isArray(input) ? input.join('; ') : '',
|
||||||
};
|
};
|
||||||
const sandbox: Record<string, any> = {
|
const sandbox: Record<string, any> = {
|
||||||
...context,
|
...context,
|
||||||
|
source: sourceRuntime,
|
||||||
|
book: context.book || {},
|
||||||
java,
|
java,
|
||||||
cookie,
|
cookie,
|
||||||
Buffer,
|
Buffer,
|
||||||
@@ -203,26 +251,41 @@ function runJsSnippet(code: string, context: Record<string, any>, timeout = 1000
|
|||||||
encodeURIComponent,
|
encodeURIComponent,
|
||||||
encryptText: encryptTongrenKeyword,
|
encryptText: encryptTongrenKeyword,
|
||||||
openUrl: (encryptedText: string, num: string) => decryptTongrenOpenUrl(encryptedText, num, context.source),
|
openUrl: (encryptedText: string, num: string) => decryptTongrenOpenUrl(encryptedText, num, context.source),
|
||||||
|
getArguments: (value: string, key?: string) => key ? (parseArguments(value)[key] || '') : parseArguments(value),
|
||||||
console: { log: () => undefined },
|
console: { log: () => undefined },
|
||||||
};
|
};
|
||||||
const body = code.replace(/^@js:/, '').trim();
|
const body = unwrapJsRule(code);
|
||||||
try {
|
try {
|
||||||
const script = new vm.Script(`(function(){ ${body}\n})()`);
|
const script = new vm.Script(`(function(){ ${body}\n})()`);
|
||||||
const result = script.runInNewContext(sandbox, { timeout });
|
const result = script.runInNewContext(sandbox, { timeout });
|
||||||
const value = jsonPrimitiveToString(result ?? sandbox.result ?? '');
|
if (result !== undefined && result !== null && result !== '') return result;
|
||||||
if (value) return value;
|
if (sandbox.result !== context.result && sandbox.result !== undefined && sandbox.result !== null && sandbox.result !== '') return sandbox.result;
|
||||||
} catch {
|
} catch {
|
||||||
// 如果 @js 后面是单个表达式(例如 header: @js:JSON.stringify({...})),上面的函数体不会自动返回。
|
// 如果 @js 后面是单个表达式(例如 header: @js:JSON.stringify({...})),上面的函数体不会自动返回。
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const script = new vm.Script(`(function(){ ${body}
|
||||||
|
; if (typeof result !== 'undefined') return result; if (typeof res !== 'undefined') return res; return ''; })()`);
|
||||||
|
const result = script.runInNewContext(sandbox, { timeout });
|
||||||
|
if (result !== undefined && result !== null && result !== '') return result;
|
||||||
|
} catch {
|
||||||
|
// fallback to expression mode below
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const script = new vm.Script(`(function(){ return (${body}); })()`);
|
const script = new vm.Script(`(function(){ return (${body}); })()`);
|
||||||
const result = script.runInNewContext(sandbox, { timeout });
|
const result = script.runInNewContext(sandbox, { timeout });
|
||||||
return jsonPrimitiveToString(result ?? sandbox.result ?? '');
|
if (result !== undefined && result !== null && result !== '') return result;
|
||||||
|
if (sandbox.result !== context.result && sandbox.result !== undefined && sandbox.result !== null && sandbox.result !== '') return sandbox.result;
|
||||||
|
return '';
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runJsSnippet(code: string, context: Record<string, any>, timeout = 1000): string {
|
||||||
|
return jsonPrimitiveToString(runJsSnippetRaw(code, context, timeout));
|
||||||
|
}
|
||||||
|
|
||||||
function applyPutGetRules(rule: string, value: string) {
|
function applyPutGetRules(rule: string, value: string) {
|
||||||
const putMatch = rule.match(/@put:\s*\{([\s\S]*?)\}/);
|
const putMatch = rule.match(/@put:\s*\{([\s\S]*?)\}/);
|
||||||
if (putMatch) {
|
if (putMatch) {
|
||||||
@@ -242,7 +305,7 @@ function applyPutGetRules(rule: string, value: string) {
|
|||||||
function buildUrlFromTemplate(template: string, source: BookSource, keyword?: string, page = 1, baseOverride?: string) {
|
function buildUrlFromTemplate(template: string, source: BookSource, keyword?: string, page = 1, baseOverride?: string) {
|
||||||
const base = baseOverride || sourceBase(source);
|
const base = baseOverride || sourceBase(source);
|
||||||
let raw = template || base;
|
let raw = template || base;
|
||||||
if (raw.trim().startsWith('@js:')) {
|
if (/^(?:@js:|<js>)/i.test(raw.trim())) {
|
||||||
if (/\/k-\{\{encryptText\(key\)\}\}-\{\{page\}\}\.html/.test(raw)) {
|
if (/\/k-\{\{encryptText\(key\)\}\}-\{\{page\}\}\.html/.test(raw)) {
|
||||||
return `https://www.rrssk.com/k-${encryptTongrenKeyword(keyword || '')}-${page}.html`;
|
return `https://www.rrssk.com/k-${encryptTongrenKeyword(keyword || '')}-${page}.html`;
|
||||||
}
|
}
|
||||||
@@ -360,10 +423,31 @@ function ruleIsJson(rule?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectJsonItems(json: any, rule?: string): any[] {
|
function selectJsonItems(json: any, rule?: string): any[] {
|
||||||
const reverse = !!rule?.trim().startsWith('-');
|
const alternatives = splitAlternatives(rule);
|
||||||
const value = readJsonPath(json, rule);
|
for (let index = 0; index < alternatives.length; index += 1) {
|
||||||
const list = Array.isArray(value) ? value : value ? [value] : [];
|
const alternative = alternatives[index];
|
||||||
return reverse ? [...list].reverse() : list;
|
const reverse = alternative.trim().startsWith('-');
|
||||||
|
const value = readJsonPath(json, alternative);
|
||||||
|
if (Array.isArray(value) && value.length > 0) return reverse ? [...value].reverse() : value;
|
||||||
|
// 备用 JSONPath 常见写法:$.data||$.data.data[*]。前者可能是分页包装对象,优先继续尝试后续数组规则。
|
||||||
|
if (value && typeof value === 'object' && index === alternatives.length - 1) return [value];
|
||||||
|
if (value && typeof value !== 'object') return [value];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateJsListRule(rule: string | undefined, context: Record<string, any>): any[] {
|
||||||
|
const trimmed = (rule || '').trim();
|
||||||
|
if (!/^(?:@js:|<js>)/i.test(trimmed)) return [];
|
||||||
|
const value = runJsSnippetRaw(trimmed, context);
|
||||||
|
if (Array.isArray(value)) return value;
|
||||||
|
if (value && typeof value === 'object') return [value];
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const parsed = parseJsonMaybe(value);
|
||||||
|
if (Array.isArray(parsed)) return parsed;
|
||||||
|
if (parsed && typeof parsed === 'object') return [parsed];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTemplateWithJson(template: string, json: any, source: BookSource, baseUrl: string) {
|
function renderTemplateWithJson(template: string, json: any, source: BookSource, baseUrl: string) {
|
||||||
@@ -379,7 +463,7 @@ function readJsonRule(json: any, rule?: string, source?: BookSource, baseUrl?: s
|
|||||||
if (!rule) return '';
|
if (!rule) return '';
|
||||||
const trimmed = rule.trim();
|
const trimmed = rule.trim();
|
||||||
if (trimmed.includes('{{')) return renderTemplateWithJson(trimmed, json, source as BookSource, baseUrl || sourceBase(source as BookSource));
|
if (trimmed.includes('{{')) return renderTemplateWithJson(trimmed, json, source as BookSource, baseUrl || sourceBase(source as BookSource));
|
||||||
if (trimmed.startsWith('@js:')) {
|
if (/^(?:@js:|<js>)/i.test(trimmed)) {
|
||||||
if (/result\s*=\s*['"]([^'"]+)['"]\s*\+\s*result\.([A-Za-z0-9_$-]+)/.test(trimmed)) {
|
if (/result\s*=\s*['"]([^'"]+)['"]\s*\+\s*result\.([A-Za-z0-9_$-]+)/.test(trimmed)) {
|
||||||
const match = trimmed.match(/result\s*=\s*['"]([^'"]+)['"]\s*\+\s*result\.([A-Za-z0-9_$-]+)/);
|
const match = trimmed.match(/result\s*=\s*['"]([^'"]+)['"]\s*\+\s*result\.([A-Za-z0-9_$-]+)/);
|
||||||
return normalizeUrl(baseUrl || sourceBase(source as BookSource), `${match?.[1] || ''}${jsonPrimitiveToString(json?.[match?.[2] || ''])}`);
|
return normalizeUrl(baseUrl || sourceBase(source as BookSource), `${match?.[1] || ''}${jsonPrimitiveToString(json?.[match?.[2] || ''])}`);
|
||||||
@@ -514,11 +598,11 @@ function stripFilters(rule: string) {
|
|||||||
|
|
||||||
function applyLegadoIndexSelector(current: cheerio.Cheerio<any>, selector: string): { current: cheerio.Cheerio<any>; selector: string } {
|
function applyLegadoIndexSelector(current: cheerio.Cheerio<any>, selector: string): { current: cheerio.Cheerio<any>; selector: string } {
|
||||||
let normalized = selector.trim();
|
let normalized = selector.trim();
|
||||||
const exclude = normalized.match(/\[!(-?\d+)\]$/);
|
const exclude = normalized.match(/(?:\[!(-?\d+)\]|!(-?\d+))$/);
|
||||||
if (exclude) {
|
if (exclude) {
|
||||||
normalized = normalized.slice(0, exclude.index).trim();
|
normalized = normalized.slice(0, exclude.index).trim();
|
||||||
current = normalized ? current.find(normalized) : current;
|
current = normalized ? current.find(normalized) : current;
|
||||||
const idx = Number(exclude[1]);
|
const idx = Number(exclude[1] ?? exclude[2]);
|
||||||
const real = idx < 0 ? current.length + idx : idx;
|
const real = idx < 0 ? current.length + idx : idx;
|
||||||
return { current: current.filter((index) => index !== real), selector: '' };
|
return { current: current.filter((index) => index !== real), selector: '' };
|
||||||
}
|
}
|
||||||
@@ -574,24 +658,31 @@ function applyLegadoSelector($: cheerio.CheerioAPI, current: cheerio.Cheerio<any
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectElements($: cheerio.CheerioAPI, root: cheerio.Cheerio<any>, rule?: string): cheerio.Cheerio<any> {
|
function selectElements($: cheerio.CheerioAPI, root: cheerio.Cheerio<any>, rule?: string): cheerio.Cheerio<any> {
|
||||||
const normalized = stripFilters(rule || '');
|
for (const alternative of splitAlternatives(rule)) {
|
||||||
const reverse = normalized.trim().startsWith('-') && !normalized.trim().startsWith('-@');
|
try {
|
||||||
const effective = reverse ? normalized.trim().slice(1).trim() : normalized;
|
const normalized = stripFilters(alternative || '');
|
||||||
if (!effective) return root;
|
const reverse = normalized.trim().startsWith('-') && !normalized.trim().startsWith('-@');
|
||||||
const steps = effective.split(/&&|@css:/).map((item) => item.trim()).filter(Boolean);
|
const effective = reverse ? normalized.trim().slice(1).trim() : normalized;
|
||||||
let current = root;
|
if (!effective) continue;
|
||||||
for (const rawStep of steps) {
|
const steps = effective.split(/&&|@css:/).map((item) => item.trim()).filter(Boolean);
|
||||||
const { selector, attr } = parseStep(rawStep);
|
let current = root;
|
||||||
if (attr) break;
|
for (const rawStep of steps) {
|
||||||
if (!selector) continue;
|
const { selector, attr } = parseStep(rawStep);
|
||||||
if (/^children$/i.test(selector)) {
|
if (attr) break;
|
||||||
current = current.children();
|
if (!selector) continue;
|
||||||
|
if (/^children$/i.test(selector)) {
|
||||||
|
current = current.children();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current = applyLegadoSelector($, current, selector);
|
||||||
|
}
|
||||||
|
if (reverse) current = $(current.toArray().reverse());
|
||||||
|
if (current.length > 0) return current;
|
||||||
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
current = applyLegadoSelector($, current, selector);
|
|
||||||
}
|
}
|
||||||
if (reverse) current = $(current.toArray().reverse());
|
return cheerio.load('')('');
|
||||||
return current;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -752,7 +843,25 @@ function applyBookInfoInit($: cheerio.CheerioAPI, root: cheerio.Cheerio<any>, in
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyContentJsRule(value: string, jsRule: string): string {
|
function applyContentJsRule(value: string, jsRule: string): string {
|
||||||
if (/result\.split\(["']\\n["']\)|<img\s+src=/.test(jsRule)) {
|
if (/window\.comicInfo/.test(value)) {
|
||||||
|
try {
|
||||||
|
const match = value.match(/window\.comicInfo\s*=\s*(.*?)(?:,window\.hideguide|;|<\/script>)/);
|
||||||
|
if (match?.[1]) {
|
||||||
|
const comicInfo = new vm.Script(`(${match[1]})`).runInNewContext({}, { timeout: 1000 });
|
||||||
|
const images = comicInfo?.current_chapter?.chapter_img_list;
|
||||||
|
if (Array.isArray(images)) {
|
||||||
|
return images
|
||||||
|
.map((src) => String(src || '').replace(/^\/\//, 'https://'))
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((src) => `<img src="${src}" style="max-width:100%; display:block;" referrerpolicy="no-referrer">`)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fallback to generic JS execution below
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (/result\.split\(["']\\n["']\)/.test(jsRule)) {
|
||||||
return value
|
return value
|
||||||
.split(/\n+/)
|
.split(/\n+/)
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
@@ -774,8 +883,8 @@ function contentFromRule(raw: string, rule?: string, baseUrl?: string): string {
|
|||||||
const jsRule = jsIndex >= 0 ? rawRule.slice(jsIndex + 4).trim() : '';
|
const jsRule = jsIndex >= 0 ? rawRule.slice(jsIndex + 4).trim() : '';
|
||||||
const $ = cheerio.load(raw);
|
const $ = cheerio.load(raw);
|
||||||
if (jsRule) {
|
if (jsRule) {
|
||||||
const values = readValues($, $.root(), selectorRule, baseUrl);
|
const values = selectorRule ? readValues($, $.root(), selectorRule, baseUrl) : [];
|
||||||
const value = values.length > 0 ? values.join('\n') : readValue($, $.root(), selectorRule, baseUrl);
|
const value = selectorRule ? (values.length > 0 ? values.join('\n') : readValue($, $.root(), selectorRule, baseUrl)) : raw;
|
||||||
return applyContentJsRule(value, jsRule);
|
return applyContentJsRule(value, jsRule);
|
||||||
}
|
}
|
||||||
return readValue($, $.root(), selectorRule, baseUrl);
|
return readValue($, $.root(), selectorRule, baseUrl);
|
||||||
@@ -913,7 +1022,19 @@ function splitUrlOptions(input: string): RequestOptions {
|
|||||||
retry: Number.isFinite(Number(options.retry)) ? Number(options.retry) : undefined,
|
retry: Number.isFinite(Number(options.retry)) ? Number(options.retry) : undefined,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return { url: raw };
|
try {
|
||||||
|
const options = JSON.parse(candidate.replace(/'/g, '"'));
|
||||||
|
return {
|
||||||
|
url: raw.slice(0, comma).trim(),
|
||||||
|
method: options.method,
|
||||||
|
body: options.body,
|
||||||
|
headers: options.headers && typeof options.headers === 'object' ? options.headers : undefined,
|
||||||
|
charset: options.charset,
|
||||||
|
retry: Number.isFinite(Number(options.retry)) ? Number(options.retry) : undefined,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { url: raw.slice(0, comma).trim() };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1086,6 +1207,16 @@ function buildExploreTargetUrl(source: BookSource, target: ExploreTarget) {
|
|||||||
async function resolveExploreCategories(source: BookSource, rule: LegadoBookSourceRule): Promise<Array<{ title: string; template: string }>> {
|
async function resolveExploreCategories(source: BookSource, rule: LegadoBookSourceRule): Promise<Array<{ title: string; template: string }>> {
|
||||||
const raw = (rule.exploreUrl || '').trim();
|
const raw = (rule.exploreUrl || '').trim();
|
||||||
if (!raw.startsWith('@js:')) return parseExploreUrl(raw);
|
if (!raw.startsWith('@js:')) return parseExploreUrl(raw);
|
||||||
|
|
||||||
|
const jsValue = runJsSnippetRaw(raw, { baseUrl: sourceBase(source), source: rule });
|
||||||
|
const parsed = Array.isArray(jsValue) ? jsValue : typeof jsValue === 'string' ? parseJsonMaybe(jsValue) : null;
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
const categories = parsed
|
||||||
|
.map((item) => ({ title: jsonPrimitiveToString(item?.title), template: jsonPrimitiveToString(item?.url) || `__group__:${jsonPrimitiveToString(item?.title)}` }))
|
||||||
|
.filter((item) => !!item.title);
|
||||||
|
if (categories.length > 0) return categories;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const html = await fetchText(source, sourceBase(source));
|
const html = await fetchText(source, sourceBase(source));
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
@@ -1412,7 +1543,17 @@ export class LegadoClient {
|
|||||||
const html = await fetchText(source, targetUrl);
|
const html = await fetchText(source, targetUrl);
|
||||||
const chapters: BookChapter[] = [];
|
const chapters: BookChapter[] = [];
|
||||||
const json = parseJsonMaybe(html);
|
const json = parseJsonMaybe(html);
|
||||||
if (json && ruleIsJson(rule.ruleToc.chapterList)) {
|
const jsItems = evaluateJsListRule(rule.ruleToc.chapterList, { result: html, src: html, baseUrl: targetUrl, source: rule });
|
||||||
|
if (jsItems.length > 0) {
|
||||||
|
jsItems.forEach((item, index) => {
|
||||||
|
const title = readJsonRule(item, rule.ruleToc?.chapterName, source, targetUrl) || `第 ${index + 1} 章`;
|
||||||
|
const href = readJsonRule(item, rule.ruleToc?.chapterUrl, source, targetUrl)
|
||||||
|
|| fallbackChapterHrefFromItem(item, rule.ruleToc?.chapterUrl, targetUrl);
|
||||||
|
if (!href) return;
|
||||||
|
const normalizedHref = normalizeUrl(targetUrl, href);
|
||||||
|
chapters.push({ id: stableId(`${source.id}|${normalizedHref}`), title, href: normalizedHref, order: index });
|
||||||
|
});
|
||||||
|
} else if (json && ruleIsJson(rule.ruleToc.chapterList)) {
|
||||||
const items = selectJsonItems(json, rule.ruleToc.chapterList);
|
const items = selectJsonItems(json, rule.ruleToc.chapterList);
|
||||||
items.forEach((item, index) => {
|
items.forEach((item, index) => {
|
||||||
const title = readJsonRule(item, rule.ruleToc?.chapterName, source, targetUrl) || `第 ${index + 1} 章`;
|
const title = readJsonRule(item, rule.ruleToc?.chapterName, source, targetUrl) || `第 ${index + 1} 章`;
|
||||||
|
|||||||
Reference in New Issue
Block a user