Add first-party MangaPill source and distinct volume releases

This commit is contained in:
Codex committed 2026-10-14 09:05:34 +02:00
1 parent a8df57f165
commit a08e847932
20 files changed
+364 -34

No files matched your search

+7
View File
@@ -26,6 +26,7 @@ function sourceKey(url, name = '') {
if (parsed.protocol === 'peer:' && /^p[0-9a-f]{24}$/.test(host)) return `peer-${host}`;
if (host === 'weebcentral.com') return 'weebcentral';
if (host === 'mangadex.org') return 'mangadex';
if (host === 'mangapill.com') return 'mangapill';
return host;
} catch {}
const label = String(name).toLowerCase();
@@ -59,6 +60,7 @@ async function scanLibrary(libraryRoot) {
book, bookFolder: bookEntry.name, number, version: versionEntry.name,
sourceName: String(source.name || ''), sourceUrl: String(source.url || ''),
sourceKey: sourceKey(source.url, source.name),
releaseKind: chapterMeta.releaseKind || null,
});
}
}
@@ -113,11 +115,16 @@ function existingVersions(manga, ref) {
if (!manga) return [];
const book = chapterBook(ref, manga);
const number = isSpecialKey(ref.number) ? String(ref.number).toLowerCase() : chapterNumber(ref.number);
if (ref.releaseKind === 'volume') return manga.chapters.filter(item => item.book === book && item.releaseKind === 'volume');
return manga.chapters.filter(item => item.book === book && item.number === number);
}
function shouldDownload(manga, ref, source, existingPolicy = 'skip-any') {
const versions = existingVersions(manga, ref);
// A whole-volume archive can overlap every numbered chapter in that book.
// Require an explicit add-source choice before storing both representations.
if (ref.releaseKind === 'volume' && existingPolicy === 'skip-any' && manga?.chapters.some(item =>
item.book === chapterBook(ref, manga) && item.releaseKind !== 'volume')) return false;
if (!versions.length) return true;
if (existingPolicy === 'add-source') return !versions.some(item => item.sourceKey === source);
return false;
+15 -3
View File
@@ -11,6 +11,7 @@ const { sizeFromBuffer, imageSize } = require('../../library');
const { directionFields } = require('../../reading-direction');
const { mergeImportedMetadata } = require('./metadata-merge');
const { normalizeWorkIdentity } = require('./work-identity');
const { parseKey } = require('../../chapter-identity');
const token = () => crypto.randomBytes(5).toString('hex');
// Staging is a recovery asset, not disposable garbage. Legacy stages without an
@@ -91,6 +92,15 @@ async function findManga(libraryRoot, sourceUrls) {
return null;
}
async function bookDirectory(mangaDir, bookNumber, bookTitle) {
const names = (await fsp.readdir(mangaDir, { withFileTypes: true }))
.filter(entry => entry.isDirectory() && parseKey(entry.name)?.key === String(bookNumber))
.map(entry => entry.name);
if (names.length > 1) throw new Error(`Ambiguous library book ${bookNumber}: ${names.join(', ')}`);
const defaultName = `v${String(bookNumber).padStart(2, '0')}`;
return path.join(mangaDir, names[0] || (bookTitle ? `${defaultName} ${cleanPart(bookTitle)}` : defaultName));
}
function validateManifest(manifest) {
if (!manifest || manifest.version !== 1) throw new Error('Manifest version must be 1');
if (!manifest.series?.title) throw new Error('series.title is required');
@@ -172,7 +182,10 @@ async function stageChapter(bookDir, chapter, http, onPage, priorMeta = {}) {
title: chapter.title || '', source: chapter.source,
added: chapter.added || new Date().toISOString(),
priority: Number(chapter.priority || 0), extra: !!chapter.extra,
...(chapter.special ? { special: true, label: chapter.label || 'Special', sourceId: chapter.sourceId, sourceNumber: chapter.sourceNumber ?? null } : {}),
...(chapter.special ? { special: true, label: chapter.label || 'Special', sourceId: chapter.sourceId,
sourceNumber: chapter.sourceNumber ?? null,
...(chapter.releaseKind ? { releaseKind: chapter.releaseKind } : {}),
...(chapter.volume ? { volume: chapter.volume } : {}) } : {}),
...directionFields([priorMeta, chapter], dimensions),
}, null, 2) + '\n', { mode: 0o644 });
// A process interruption during a write can leave a hidden pending file.
@@ -230,8 +243,7 @@ async function publishManifest(manifest, options = {}) {
for (const chapter of manifest.chapters) {
const bookNumber = Number(chapter.book?.number || 1);
if (!Number.isInteger(bookNumber) || bookNumber < 1) throw new Error(`Invalid book number for chapter ${chapter.number}`);
const bookName = `v${String(bookNumber).padStart(2, '0')}${chapter.book?.title ? ` ${cleanPart(chapter.book.title)}` : ''}`;
const bookDir = path.join(mangaDir, bookName);
const bookDir = await bookDirectory(mangaDir, bookNumber, chapter.book?.title);
await fsp.mkdir(bookDir, { recursive: true, mode: 0o755 });
await cleanupOwnTemps(bookDir);
if (chapter.book?.title) {