Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion backend/services/book_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 10 additions & 7 deletions backend/services/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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

Expand Down
25 changes: 22 additions & 3 deletions tests/test_type_folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion website/src/pages/docs/admin.astro
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ import { withBase } from '../../lib/path'
<p>
<strong>New install:</strong> 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 <code>Manga/</code> folder keeps those books as manga.
</p>
<p>
Expand Down
Loading