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

+111
View File
@@ -0,0 +1,111 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs').promises;
const os = require('node:os');
const path = require('node:path');
const { detectVerticalStrip, resolveReadingDirection, fallbackDirection } = require('../../reading-direction');
const { publishManifest } = require('../lib/publisher');
const { scanLibrary } = require('../../library');
const strip = (w = 800) => ({ w, h: 12000 });
test('strip detection tolerates a landscape title card and rejects weak evidence', () => {
const pages = [{ w: 1200, h: 800 }, strip(), strip(), strip()];
assert.equal(detectVerticalStrip(pages).value, 'ttb');
assert.equal(detectVerticalStrip(pages).evidence.tallPages, 3);
assert.equal(detectVerticalStrip([strip()]), null);
assert.equal(detectVerticalStrip([strip(), strip(), { w: 800, h: 1200 }, { w: 800, h: 1200 }]), null);
assert.equal(detectVerticalStrip([strip(), strip(), strip(1400), { w: 1200, h: 800 }]), null);
assert.equal(detectVerticalStrip([{ w: 800, h: 12000, r: 90 }, strip(), strip()]), null);
assert.equal(detectVerticalStrip([{ w: 12000, h: 800, r: 90 }, strip(), strip()]).value, 'ttb');
});
test('manual choices at chapter, book or manga level beat detection and fallback', () => {
const pages = [strip(), strip(), strip()];
const automatic = fallbackDirection();
assert.equal(resolveReadingDirection([automatic], pages).value, 'ttb');
assert.equal(resolveReadingDirection([automatic]).source, 'fallback');
assert.equal(resolveReadingDirection([{ direction: 'ltr' }, automatic], pages).value, 'ltr');
assert.equal(resolveReadingDirection([{}, { direction: 'rtl' }, automatic], pages).value, 'rtl');
assert.equal(resolveReadingDirection([{ directionOverride: 'rtl', ...automatic }], pages).value, 'rtl');
assert.equal(resolveReadingDirection([{ ...automatic, direction: 'ltr' }], pages).value, 'ltr');
assert.equal(resolveReadingDirection([{ direction: 'rtl' }], pages).source, 'manual');
assert.equal(resolveReadingDirection([], pages).value, 'ttb');
});
function png(w, h) {
const b = Buffer.alloc(24);
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(b);
b.writeUInt32BE(w, 16); b.writeUInt32BE(h, 20);
return b;
}
async function json(file) { return JSON.parse(await fs.readFile(file, 'utf8')); }
test('publisher detects new strips, preserves updates and manual metadata, and scanner agrees', async t => {
const temp = await fs.mkdtemp(path.join(os.tmpdir(), 'mcscraper-direction-'));
t.after(() => fs.rm(temp, { recursive: true, force: true }));
const libraryRoot = path.join(temp, 'library');
const sizes = [[1200, 800], [800, 12000], [800, 12500], [800, 14210]];
const files = [];
for (const [i, size] of sizes.entries()) {
const file = path.join(temp, `page${i}.png`);
await fs.writeFile(file, png(...size)); files.push(file);
}
const series = { title: 'Strip Work', ...fallbackDirection(),
sources: [{ name: 'Test', url: 'https://example.test/series' }] };
const chapter = number => ({ number, source: { name: 'Test', url: `https://example.test/${number}` },
book: { number: 1, title: 'Season 1' }, pages: files.map(file => ({ path: file })) });
const first = await publishManifest({ version: 1, series, chapters: [chapter('1')] }, { libraryRoot });
const mangaDir = first.mangaDir, bookDir = path.join(mangaDir, 'v01 Season 1');
const mangaFile = path.join(mangaDir, 'manga.json');
assert.equal((await json(mangaFile)).direction, 'ttb');
assert.equal((await json(mangaFile))['x-reading-direction'].source, 'detected');
const scan = (await scanLibrary(libraryRoot)).index.mangas[0];
assert.equal(scan.chapters[0].versions[0].direction, 'ttb');
assert.equal(scan.chapters[0].versions[0].pages[0].w, 1200);
const same = await publishManifest({ version: 1, series, chapters: [chapter('2')] }, { libraryRoot });
assert.equal(same.mangaDir, mangaDir);
assert.equal((await json(mangaFile)).direction, 'ttb');
const bookFile = path.join(bookDir, 'book.json');
await fs.writeFile(bookFile, JSON.stringify({ title: 'Season 1', direction: 'ltr', note: 'human' }));
const chapterDir = path.join(bookDir, first.results[0].folder);
const chapterFile = path.join(chapterDir, 'chapter.json');
await fs.writeFile(chapterFile, JSON.stringify({ ...await json(chapterFile), directionOverride: 'rtl', note: 'human' }));
await publishManifest({ version: 1, series, chapters: [chapter('1')] }, { libraryRoot, replace: true });
assert.equal((await json(bookFile)).direction, 'ltr');
assert.equal((await json(bookFile)).note, 'human');
assert.equal((await json(chapterFile)).direction, 'rtl');
assert.equal((await json(chapterFile)).directionOverride, 'rtl');
assert.equal((await json(chapterFile)).note, 'human');
const rescanned = (await scanLibrary(libraryRoot)).index.mangas[0];
assert.equal(rescanned.chapters[0].versions[0].direction, 'rtl');
assert.equal(rescanned.chapters[1].versions[0].direction, 'ltr');
await fs.writeFile(mangaFile, JSON.stringify({ ...await json(mangaFile), direction: 'rtl' }));
await publishManifest({ version: 1, series, chapters: [chapter('3')] }, { libraryRoot });
assert.equal((await json(mangaFile)).direction, 'rtl');
assert.equal((await json(mangaFile))['x-reading-direction'].source, 'manual');
assert.equal((await scanLibrary(libraryRoot)).index.mangas[0].chapters[2].versions[0].direction, 'ltr');
});
test('legacy plain RTL stays protected across automatic imports', async t => {
const temp = await fs.mkdtemp(path.join(os.tmpdir(), 'mcscraper-legacy-direction-'));
t.after(() => fs.rm(temp, { recursive: true, force: true }));
const libraryRoot = path.join(temp, 'library');
const page = path.join(temp, 'tall.png'); await fs.writeFile(page, png(800, 12000));
const series = { title: 'Legacy', ...fallbackDirection(), sources: [{ name: 'Test', url: 'https://example.test/legacy' }] };
const chapter = { number: '1', source: { name: 'Test', url: 'https://example.test/legacy/1' },
pages: [1, 2, 3].map(() => ({ path: page })) };
const first = await publishManifest({ version: 1, series, chapters: [chapter] }, { libraryRoot });
const file = path.join(first.mangaDir, 'manga.json');
const meta = await json(file); delete meta['x-reading-direction']; meta.direction = 'rtl';
await fs.writeFile(file, JSON.stringify(meta));
await publishManifest({ version: 1, series, chapters: [{ ...chapter, number: '2', source: { name: 'Test', url: 'https://example.test/legacy/2' } }] }, { libraryRoot });
assert.equal((await json(file)).direction, 'rtl');
assert.equal((await json(file))['x-reading-direction'].source, 'manual');
});