Consolidate MangaCircus app, scraper, and Compose baseline

This commit is contained in:
Maashkinoozhe committed 2026-10-07 23:28:20 +02:00
1 parent cfb2d79103
commit eef4ffa07c
103 files changed
+9316 -172

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env node
'use strict';
// Reviewed, one-series migration for ambiguous pre-provenance metadata. Dry run
// by default. Apply only during a stopped-writer cutover with a state snapshot.
const fs = require('node:fs').promises;
const path = require('node:path');
const { imageSize } = require('../library');
const { detectVerticalStrip, KEY } = require('../reading-direction');
const { writeJsonAtomic } = require('../scraper/lib/publisher');
const [folder, expectedId, ...flags] = process.argv.slice(2);
const apply = flags.includes('--apply');
const mustInclude = flags.find(flag => flag.startsWith('--chapter='))?.slice('--chapter='.length);
if (!folder || !/^[A-Z0-9]{20,40}$/.test(expectedId || '') ||
(mustInclude && !/^c\d{1,6}$/i.test(mustInclude)) ||
flags.some(flag => !['--apply', '--writers-stopped'].includes(flag) && !/^--chapter=c\d{1,6}$/i.test(flag)) ||
(apply && !flags.includes('--writers-stopped'))) {
console.error('Usage: node tools/review-reading-direction.js <library-manga-folder> <weebcentral-id> [--chapter=c0056] [--apply --writers-stopped]');
process.exit(2);
}
async function main() {
const libraryRoot = path.resolve(process.env.LIBRARY || path.join(__dirname, '..', 'library'));
const mangaDir = path.resolve(folder);
if (path.dirname(mangaDir) !== libraryRoot) throw new Error('Manga folder must be directly inside the configured library');
const file = path.join(mangaDir, 'manga.json');
const original = await fs.readFile(file, 'utf8');
const meta = JSON.parse(original);
if (meta['x-scraper']?.ids?.weebcentral !== expectedId) throw new Error('Source identity did not match');
if (meta.direction !== 'rtl' || meta.directionOverride || meta[KEY]) throw new Error('Only an unmarked legacy RTL value can be reviewed');
const chapters = [];
for (const book of await fs.readdir(mangaDir, { withFileTypes: true })) {
if (!book.isDirectory() || !/^v\d/i.test(book.name)) continue;
for (const chapter of await fs.readdir(path.join(mangaDir, book.name), { withFileTypes: true })) {
if (chapter.isDirectory() && /^c\d/i.test(chapter.name)) chapters.push(path.join(mangaDir, book.name, chapter.name));
}
}
chapters.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
if (chapters.length < 3) throw new Error('Too few chapters for reviewed migration');
const requiredChapter = mustInclude && chapters.find(item => path.basename(item).toLowerCase().startsWith(mustInclude.toLowerCase() + ' '));
if (mustInclude && !requiredChapter) throw new Error('Required chapter was not found');
const selected = [...new Set([chapters[0], chapters[Math.floor(chapters.length / 2)], chapters.at(-1),
requiredChapter].filter(Boolean))];
const results = [];
for (const dir of selected) {
const pages = [];
for (const entry of (await fs.readdir(dir)).sort()) {
if (!/\.(?:jpe?g|png|webp|gif|avif|bmp)$/i.test(entry)) continue;
const size = await imageSize(path.join(dir, entry)).catch(() => null);
const rotation = /_rot(90|180|270)(?:_|\.)/i.exec(entry);
pages.push({ w: size?.w || 0, h: size?.h || 0, r: rotation ? Number(rotation[1]) : 0 });
}
const detected = detectVerticalStrip(pages);
results.push({ chapter: path.basename(dir), detected: detected?.value || null,
evidence: detected?.evidence || { pages: pages.length } });
}
const supporting = results.filter(item => item.detected === 'ttb').length;
const reviewed = supporting >= Math.max(3, Math.ceil(results.length * .75)) &&
(!requiredChapter || results.some(item => item.chapter === path.basename(requiredChapter) && item.detected === 'ttb'));
if (apply) {
if (!reviewed) throw new Error('Insufficient consistent strip evidence; metadata unchanged');
if (await fs.readFile(file, 'utf8') !== original) throw new Error('Metadata changed during review');
await writeJsonAtomic(file, { ...meta, direction: 'ttb',
[KEY]: { version: 1, value: 'ttb', source: 'detected',
evidence: { reviewedChapters: results.map(item => item.chapter), supportingChapters: supporting } } });
}
console.log(JSON.stringify({ manga: path.basename(mangaDir), reviewed, applied: apply, samples: results }, null, 2));
}
main().catch(error => { console.error(error.message); process.exitCode = 1; });