'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'); });