diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b4a0ae..5be0cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,8 @@ All notable changes to Tome are documented here. Format loosely follows so books of that type are stored under it, e.g. `Manga/Berserk/…` or `Comics/Saga/…`. Empty by default, so nothing moves on upgrade. Existing books move only when you run Reorganize in Library Health; a library scan - takes a matching top-level folder as a hint for the book type. (#270) + takes a matching top-level folder as a hint for the book type, so a plain + CBZ under your own `Manga/` stays manga. (#270) - **A layout per book type.** Each book type picks how its books are laid out (Admin → Types): Series first (the layout so far, still the default), Author, then series (`Harlan Coben/Myron Bolitar/Deal Breaker v01.epub`), or diff --git a/backend/services/book_types.py b/backend/services/book_types.py index 54fd481..282d78d 100644 --- a/backend/services/book_types.py +++ b/backend/services/book_types.py @@ -118,16 +118,19 @@ def detect_book_type( fmt: Optional[str], is_manga: bool = False, in_chapters_dir: bool = False, + by_extension: bool = True, ) -> Optional[BookType]: """The type an incoming file gets automatically, before it is placed. Same rule the importers have always used: a chapters/ drop or a ComicInfo manga flag → manga, a cbz/cbr → comic, anything else none. + ``by_extension=False`` keeps only the explicit manga signal, so the scan + hint can decide before the bare cbz/cbr default does. """ slug: Optional[str] = None if in_chapters_dir or is_manga: slug = "manga" - elif (fmt or "").lower() in ("cbz", "cbr"): + elif by_extension and (fmt or "").lower() in ("cbz", "cbr"): slug = "comic" if slug is None: return None diff --git a/backend/services/scanner.py b/backend/services/scanner.py index 8017c50..e660b0d 100644 --- a/backend/services/scanner.py +++ b/backend/services/scanner.py @@ -312,9 +312,10 @@ def _create_book_entry( """Create the Book + BookFile rows for a file already in place. ``book_type`` is the type the importer already decided (and placed the - file by). Without it the automatic rule applies, and failing that the - scan hint: a file under a top-level folder that matches a type's folder - or label gets that type (#270). + file by). Without it: a manga flag makes it manga; else the scan hint, a + top-level folder matching a type's folder or label, gives that type + (#270); else the extension rule (cbz/cbr → comic). The hint beats the + bare extension so a plain cbz in a user's own ``Manga/`` stays manga. """ book = Book( title=meta.get("title", file_path.stem), @@ -351,12 +352,14 @@ def _create_book_entry( )) record_ko_hash(db, book.id, ko_partial_md5(file_path), "raw") - # Book type: as decided by the importer, else the automatic rule, else - # the scan hint from the file's top-level folder. + # Book type: as decided by the importer, else the manga flag, else the + # scan hint from the file's top-level folder, else the extension rule. + if book_type is None and folder_hint: + book_type = (detect_book_type(db, fmt=fmt, is_manga=bool(meta.get("_is_manga")), + by_extension=False) + or type_for_top_folder(db, folder_hint)) if book_type is None: book_type = detect_book_type(db, fmt=fmt, is_manga=bool(meta.get("_is_manga"))) - if book_type is None and folder_hint: - book_type = type_for_top_folder(db, folder_hint) if book_type is not None: book.book_type_id = book_type.id diff --git a/tests/test_type_folders.py b/tests/test_type_folders.py index 8952d52..f0458f9 100644 --- a/tests/test_type_folders.py +++ b/tests/test_type_folders.py @@ -10,7 +10,8 @@ Library Health lists them, reorganize dry run lists the move, the real run performs it and updates the stored path - scan hint: a file under a top-level folder matching a type's folder or - label gets that type, unless the cbz/manga rule already decided + label gets that type; it beats the bare cbz → comic default but not a + manga flag - two types sharing one folder """ from pathlib import Path @@ -329,13 +330,31 @@ def test_folder_matching_type_assigns_type(self, db, dirs, types, monkeypatch): # The scan never moves files assert (lib / "manga/Frieren/Frieren v01.epub").is_file() - def test_rule_wins_over_hint(self, db, dirs, types, monkeypatch): + def test_hint_beats_cbz_default(self, db, dirs, types, monkeypatch): lib = dirs["lib"] _fake_meta(monkeypatch, "backend.services.scanner.extract_metadata", {}) (lib / "Manga/Saga").mkdir(parents=True) (lib / "Manga/Saga/Saga v01.cbz").write_bytes(b"cbz") scan_library(lib, dirs["covers"], db, workers=1) - assert db.query(Book).one().book_type_id == types["comic"].id + assert db.query(Book).one().book_type_id == types["manga"].id + + def test_manga_flag_beats_hint(self, db, dirs, types, monkeypatch): + lib = dirs["lib"] + _fake_meta(monkeypatch, "backend.services.scanner.extract_metadata", + {"Akira v01.cbz": {"title": "Akira v01", "_is_manga": True}}) + (lib / "Comics/Akira").mkdir(parents=True) + (lib / "Comics/Akira/Akira v01.cbz").write_bytes(b"cbz") + scan_library(lib, dirs["covers"], db, workers=1) + assert db.query(Book).one().book_type_id == types["manga"].id + + def test_cbz_without_matching_folder_stays_comic(self, db, dirs, types, monkeypatch): + lib = dirs["lib"] + _fake_meta(monkeypatch, "backend.services.scanner.extract_metadata", {}) + for rel in ("Downloads/Saga/Saga v01.cbz", "Loose.cbz"): + (lib / rel).parent.mkdir(parents=True, exist_ok=True) + (lib / rel).write_bytes(rel.encode()) + scan_library(lib, dirs["covers"], db, workers=1) + assert {b.book_type_id for b in db.query(Book).all()} == {types["comic"].id} def test_folder_match_beats_label_match(self, db, dirs, types, monkeypatch): # "Comics" is the comic type's label but the book type's folder. diff --git a/website/src/pages/docs/admin.astro b/website/src/pages/docs/admin.astro index 9fd0f48..3bc01e3 100644 --- a/website/src/pages/docs/admin.astro +++ b/website/src/pages/docs/admin.astro @@ -195,7 +195,8 @@ import { withBase } from '../../lib/path'
New install: set the folders before the first scan. A scan never moves
files, but it uses them as a hint: a file whose top-level folder matches a type's folder or
- label (case-insensitive) gets that type, unless the CBZ/manga rule already decided it. So a
+ label (case-insensitive) gets that type, even a plain CBZ; only a manga flag in the file
+ itself overrides the folder. So a
library that already has a Manga/ folder keeps those books as manga.