Consolidate MangaCircus app, scraper, and Compose baseline
This commit is contained in:
1 parent
cfb2d79103
commit
eef4ffa07c
103 files changed
+9316
-172
No files matched your search
@@ -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; });
|
||||
Reference in new issue
Block a user