Files
lnreader-plugins/scripts/find-novel.js
T
masi fba9a35075
Publish Plugins / Publish Plugins (push) Has been cancelled
feat: fetch-novel builds an EPUB from any plugin; Kavita TOC fix
- scripts/load-plugins.js: shared plugin loader for the novel CLI scripts
- scripts/fetch-novel.js: download a novel via its plugin into a clean EPUB 3
- find-novel prints the plugin id/path for each match
- kavita 1.0.1: innermost TOC entry names the chapter

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:40:23 -03:00

80 lines
2.4 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 { loadPlugins, print } from './load-plugins.js';
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 plugins = await loadPlugins(
langArgs.length ? langArgs : ['english', 'multi'],
);
process.stderr.write(`Searching ${plugins.length} sources for "${term}"...\n`);
const hits = [];
let failed = 0;
const queue = [...plugins];
const worker = async () => {
for (let plugin; (plugin = queue.shift());) {
try {
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,
id: plugin.id,
name: novel.name,
path: novel.path,
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) {
print(
`${rank(h) === 0 ? '*' : ' '} ${h.source.padEnd(28)} ${h.name}\n` +
` ${''.padEnd(28)} ${h.url}\n` +
` ${''.padEnd(28)} novel fetch ${h.id} ${h.path}`,
);
}
process.stderr.write(
`${matches.length} matches (* = exact title), ${hits.length - matches.length} loose results hidden; ` +
`${plugins.length - failed}/${plugins.length} sources answered.\n`,
);
// Keep-alive sockets can hold the event loop open; results are already printed.
process.exit(0);