The reader polled /api/state every 15 s; each call rebuilt the scraper's library summary when it was over 60 s old, reading every chapter.json (on the NAS about 5,300 opens / 16,000 NFS calls and 1.3 s CPU per walk, once a minute, plus 115 KB per poll). The reader now polls GET /api/revision (falls back to /api/state on an older scraper), and the summary is rebuilt after a library change or after 15 minutes. Release-date writes invalidate it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
66 lines
4.0 KiB
JavaScript
66 lines
4.0 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
const http = require('http');
|
|
const path = require('path');
|
|
const { ScraperService } = require('./service');
|
|
const { secret } = require('../runtime/config');
|
|
|
|
const HOST = process.env.SCRAPER_HOST || '127.0.0.1';
|
|
const PORT = Number(process.env.SCRAPER_PORT || 8091);
|
|
const TOKEN = secret('SCRAPER_TOKEN');
|
|
const LIBRARY = path.resolve(process.env.LIBRARY || 'library');
|
|
const DATA = path.resolve(process.env.SCRAPER_DATA || path.join(process.env.DATA || 'data', 'scraper'));
|
|
|
|
if (!['127.0.0.1', 'localhost', '::1'].includes(HOST) && !TOKEN) {
|
|
throw new Error('SCRAPER_TOKEN is required when SCRAPER_HOST is not loopback');
|
|
}
|
|
|
|
function json(res, code, value) {
|
|
res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
res.end(JSON.stringify(value));
|
|
}
|
|
|
|
async function body(req) {
|
|
let text = '';
|
|
for await (const chunk of req) { text += chunk; if (text.length > 1000000) throw Object.assign(new Error('Request too large'), { code: 413 }); }
|
|
try { return text ? JSON.parse(text) : {}; } catch { throw Object.assign(new Error('Invalid JSON'), { code: 400 }); }
|
|
}
|
|
|
|
(async () => {
|
|
const service = await new ScraperService({ libraryRoot: LIBRARY, dataRoot: DATA }).init();
|
|
const server = http.createServer(async (req, res) => {
|
|
try {
|
|
const url = new URL(req.url, 'http://localhost');
|
|
if (url.pathname === '/health') return json(res, 200, { ok: true, status: service.state.status });
|
|
if (TOKEN && req.headers.authorization !== `Bearer ${TOKEN}`) return json(res, 401, { error: 'Unauthorized' });
|
|
if (req.method === 'GET' && url.pathname === '/api/state') return json(res, 200, await service.publicState());
|
|
if (req.method === 'GET' && url.pathname === '/api/revision') return json(res, 200, service.revision());
|
|
if (req.method !== 'POST') return json(res, 404, { error: 'Not found' });
|
|
const input = await body(req);
|
|
if (url.pathname === '/api/queue') return json(res, 200, { item: await service.enqueue(input), state: await service.publicState() });
|
|
if (url.pathname === '/api/queue/remove') { await service.remove(String(input.id || '')); return json(res, 200, await service.publicState()); }
|
|
if (url.pathname === '/api/queue/clear-completed') { const result = await service.clearCompleted(String(input.source || '')); return json(res, 200, { ...result, state: await service.publicState() }); }
|
|
if (url.pathname === '/api/queue/reorder') { await service.reorder(input.ids); return json(res, 200, await service.publicState()); }
|
|
if (url.pathname === '/api/control') { await service.control(String(input.action || ''), input.id); return json(res, 200, await service.publicState()); }
|
|
if (url.pathname === '/api/settings') { await service.updateSettings(input); return json(res, 200, await service.publicState()); }
|
|
if (url.pathname === '/api/discovery/run') { await service.enqueueDiscoveryChecks('manual'); return json(res, 200, await service.publicState()); }
|
|
if (url.pathname === '/api/library/auto-update') { await service.setMangaAutoUpdate(String(input.id || ''), !!input.enabled); return json(res, 200, await service.publicState()); }
|
|
if (url.pathname === '/api/legacy/publish-ready') return json(res, 200, { result: await service.publishLegacyReady(), state: await service.publicState() });
|
|
return json(res, 404, { error: 'Not found' });
|
|
} catch (error) {
|
|
console.error('[scraper-daemon]', error.message);
|
|
json(res, error.code || (/not found/i.test(error.message) ? 404 : 400), { error: error.message });
|
|
}
|
|
});
|
|
server.listen(PORT, HOST, () => console.log(`MangaCircus scraper on http://${HOST}:${PORT} (library: ${LIBRARY}, data: ${DATA})`));
|
|
|
|
let closing = false;
|
|
const close = async () => {
|
|
if (closing) return; closing = true;
|
|
server.close(); await service.close(); process.exit(0);
|
|
};
|
|
process.on('SIGINT', close);
|
|
process.on('SIGTERM', close);
|
|
})().catch(error => { console.error(`[scraper-daemon] ${error.message}`); process.exitCode = 1; });
|