Files
MangaCircus/scraper/daemon.js
T

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 !== '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/recovery') return json(res, 200, await service.recover(input));
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; });