Files
lnreader-plugins/scripts/find-novel.js
T
masi 053aa91b02
Publish Plugins / Publish Plugins (push) Has been cancelled
feat: find-novel search across all sources; fix WeTriedTLS apostrophe search
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:31:47 -03:00

123 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Search every plugin of the given languages for a title and list the sources
// that carry it. Usage: node scripts/find-novel.js "<title>" [lang...]
// Languages are plugin folder names (default: english multi).
import * as esbuild from 'esbuild';
import { execFileSync } from 'child_process';
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import path, { dirname } from 'path';
import fs from 'fs';
import os from 'os';
const require = createRequire(import.meta.url);
const ROOT = path.resolve(dirname(fileURLToPath(import.meta.url)), '..');
const TIMEOUT_MS = 20_000;
const CONCURRENCY = 16;
const PER_SOURCE = 3;
const [term, ...langArgs] = process.argv.slice(2);
if (!term) {
console.error('Usage: node scripts/find-novel.js "<title>" [lang...]');
process.exit(2);
}
const langs = langArgs.length ? langArgs : ['english', 'multi'];
// Multisrc plugins (Madara, NovelFire, ...) are generated, not committed.
execFileSync('node', ['plugins/multisrc/generate-multisrc-plugins.js'], {
cwd: ROOT,
stdio: 'ignore',
});
const entryPoints = langs.flatMap(lang => {
const dir = path.join(ROOT, 'plugins', lang);
if (!fs.existsSync(dir)) throw new Error(`No plugin folder: plugins/${lang}`);
return fs
.readdirSync(dir)
.filter(f => f.endsWith('.ts'))
.map(f => path.join(dir, f));
});
const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'find-novel-'));
try {
const result = await esbuild.build({
entryPoints,
outdir,
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node22',
outExtension: { '.js': '.cjs' },
logLevel: 'silent',
alias: {
'@libs': path.join(ROOT, 'src/libs'),
'@': path.join(ROOT, 'src'),
},
});
if (result.errors.length) throw new Error('Plugin bundling failed');
const bundles = fs
.readdirSync(outdir, { recursive: true })
.filter(f => f.endsWith('.cjs'))
.map(f => path.join(outdir, f));
process.stderr.write(
`Searching ${bundles.length} sources for "${term}"...\n`,
);
const hits = [];
let failed = 0;
const queue = [...bundles];
const worker = async () => {
for (let file; (file = queue.shift());) {
let plugin;
try {
const mod = require(file);
plugin = mod.default ?? mod;
const found = await Promise.race([
plugin.searchNovels(term, 1),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), TIMEOUT_MS),
),
]);
for (const novel of (found || []).slice(0, PER_SOURCE)) {
const url = plugin.resolveUrl
? plugin.resolveUrl(novel.path, true)
: new URL(novel.path, plugin.site).href;
hits.push({ source: plugin.name, name: novel.name, url });
}
} catch {
failed++;
}
}
};
await Promise.all(Array.from({ length: CONCURRENCY }, worker));
// Keep titles containing every word of the query (quotes/punctuation ignored).
const norm = t =>
t
.toLowerCase()
.replace(/[’']/g, '')
.replace(/[^a-z0-9]+/g, ' ')
.trim();
const words = norm(term).split(' ');
const exact = norm(term);
const matches = hits.filter(h =>
words.every(w => norm(h.name).split(' ').includes(w)),
);
const rank = h => (norm(h.name) === exact ? 0 : 1);
matches.sort((a, b) => rank(a) - rank(b) || a.source.localeCompare(b.source));
for (const h of matches) {
console.log(
`${rank(h) === 0 ? '*' : ' '} ${h.source.padEnd(28)} ${h.name}\n ${''.padEnd(28)} ${h.url}`,
);
}
process.stderr.write(
`${matches.length} matches (* = exact title), ${hits.length - matches.length} loose results hidden; ` +
`${bundles.length - failed}/${bundles.length} sources answered.\n`,
);
} finally {
fs.rmSync(outdir, { recursive: true, force: true });
}
// Keep-alive sockets can hold the event loop open; results are already printed.
process.exit(0);