#!/usr/bin/env node 'use strict'; const fs = require('fs').promises; const path = require('path'); const { publishManifest } = require('./lib/publisher'); const { discoverWeebCentral } = require('./adapters/weebcentral'); const { lookupAniList } = require('./metadata/anilist'); const { runArchive } = require('./jobs/weebcentral-archive'); const { runQueue } = require('./jobs/weebcentral-queue'); function flag(args, name, fallback) { const index = args.indexOf(name); return index < 0 ? fallback : args[index + 1]; } async function main() { const args = process.argv.slice(2); const command = args.shift(); if (command === 'publish') { const file = args[0]; if (!file) throw new Error('Usage: publish [--library path] [--metadata-search title] [--replace]'); const manifest = JSON.parse(await fs.readFile(path.resolve(file), 'utf8')); const metadataSearch = flag(args, '--metadata-search'); if (metadataSearch) { const metadata = await lookupAniList(metadataSearch); manifest.series.metadata = { ...metadata, title: manifest.series.title }; manifest.series.scraper = { ...(manifest.series.scraper || {}), ids: { ...(manifest.series.scraper?.ids || {}), anilist: metadata['x-anilist']?.id } }; } const result = await publishManifest(manifest, { libraryRoot: flag(args, '--library'), replace: args.includes('--replace') }); console.log(JSON.stringify(result, null, 2)); return; } if (command === 'discover-weebcentral') { const url = args[0]; if (!url) throw new Error('Usage: discover-weebcentral --out manifest.json [--limit 3]'); const manifest = await discoverWeebCentral(url, { limit: Number(flag(args, '--limit', 3)) }); const out = flag(args, '--out'); if (out) await fs.writeFile(path.resolve(out), JSON.stringify(manifest, null, 2) + '\n'); else console.log(JSON.stringify(manifest, null, 2)); return; } if (command === 'metadata-anilist') { const search = args.join(' '); if (!search) throw new Error('Usage: metadata-anilist '); console.log(JSON.stringify(await lookupAniList(search), null, 2)); return; } if (command === 'archive-weebcentral') { const url = args[0]; if (!url) throw new Error('Usage: archive-weebcentral <series-url> [--library path] [--metadata-search title]'); await runArchive({ seriesUrl: url, libraryRoot: flag(args, '--library', 'library'), metadataSearch: flag(args, '--metadata-search'), minMs: Number(flag(args, '--min-ms', 8000)), maxMs: Number(flag(args, '--max-ms', 15000)), chapterMinMs: Number(flag(args, '--chapter-min-ms', 30000)), chapterMaxMs: Number(flag(args, '--chapter-max-ms', 60000)), }); return; } if (command === 'status-weebcentral') { const url = args[0]; if (!url) throw new Error('Usage: status-weebcentral <series-url> [--library path]'); const sourceId = url.split('/series/')[1]?.split('/')[0]; if (!sourceId) throw new Error('Could not read WeebCentral series id'); const workRoot = path.resolve(flag(args, '--library', 'library'), `.mcscraper-work-${sourceId}`); const state = JSON.parse(await fs.readFile(path.join(workRoot, 'job.json'), 'utf8')); console.log(JSON.stringify({ status: state.status || 'unknown', completed: state.completed?.length || 0, total: state.total || null, percent: state.total ? Math.round((state.completed?.length || 0) * 1000 / state.total) / 10 : null, lastChapter: state.lastChapter || null, current: state.current || null, lastProblem: state.lastProblem || null, lastError: state.lastError || null, problemCount: state.problemCount || 0, rate: state.rate || null, updated: state.updated || null, log: path.join(workRoot, 'job.log'), }, null, 2)); return; } if (command === 'run-wishlist') { const entries = [ ['The Gamer', '01J76XY9DYH4MH2HZG2PPF74A6', 'The-Gamer'], ['Kanojo Okarishimasu', '01J76XYC7KP5M48JAY8S4ZNDX5', 'Kanojo-Okarishimasu'], ['Toradora', '01J76XY7J4Z4G6E42GED6QEQXZ', 'Toradora'], ['Solo Leveling', '01J76XYCPSY3C4BNPBRY8JMCBE', 'Solo-Leveling'], ['Gantz', '01J76XY7EREQ4B1Q5Z5RJ749V6', 'Gantz'], ['One Piece', '01J76XY7E9FNDZ1DBBM6PBJPFK', 'One-Piece'], ].map(([title, id, slug]) => ({ id, title, url: `https://weebcentral.com/series/${id}/${slug}` })); await runQueue({ entries, waitForFolder: flag(args, '--wait-for', 'Berserk'), libraryRoot: flag(args, '--library', 'library'), minMs: Number(flag(args, '--min-ms', 3000)), maxMs: Number(flag(args, '--max-ms', 7000)), chapterMinMs: Number(flag(args, '--chapter-min-ms', 12000)), chapterMaxMs: Number(flag(args, '--chapter-max-ms', 25000)), }); return; } if (command === 'status-queue') { const queueRoot = path.resolve(flag(args, '--library', 'library'), '.mcscraper-queue'); const state = JSON.parse(await fs.readFile(path.join(queueRoot, 'queue.json'), 'utf8')); console.log(JSON.stringify({ ...state, log: path.join(queueRoot, 'queue.log') }, null, 2)); return; } console.log('Commands:\n publish <manifest.json> [--library path] [--metadata-search title] [--replace]\n discover-weebcentral <url> [--limit 3] [--out file]\n archive-weebcentral <url> [--library path] [--metadata-search title]\n status-weebcentral <url> [--library path]\n run-wishlist [--library path] [--wait-for Berserk]\n status-queue [--library path]\n metadata-anilist <title>'); } main().catch(error => { console.error(`[scraper] ${error.message}`); process.exitCode = 1; });