Files
chitai/backend/tests/unit/test_filesystem_library.py
patrick 55e00ba960 feat: import a Calibre library
Reads metadata.db and copies the books into a library — from a zip uploaded on
the library settings page, or from a path with `litestar calibre-import`. The
source is never touched, and re-running only picks up what is new.

Also names the formats mimetypes does not know: a Calibre library is full of
MOBI and AZW3, and a null content type used to fail the book endpoint.
2026-08-17 13:38:44 -04:00

70 lines
2.4 KiB
Python

"""Tests for BookPathGenerator."""
from pathlib import Path
from chitai.services.filesystem_library import BookPathGenerator, sanitize_path_component
ROOT = Path("/library")
def path_for(**book) -> Path:
return BookPathGenerator(ROOT).generate_path(book)
def test_author_and_title() -> None:
assert path_for(title="Dune", authors=["Frank Herbert"]) == (
ROOT / "Frank Herbert" / "Dune"
)
def test_a_book_with_no_authors() -> None:
assert path_for(title="Beowulf", authors=[]) == ROOT / "Unknown" / "Beowulf"
def test_a_series_adds_a_level_and_pads_the_position() -> None:
assert path_for(
title="Persepolis Rising",
authors=["James S. A. Corey"],
series="The Expanse",
series_position="7",
) == ROOT / "James S. A. Corey" / "The Expanse" / "07 - Persepolis Rising"
def test_a_slash_in_a_title_does_not_add_a_directory() -> None:
"""
The separators in the path come from the template, never from the metadata.
A title with a slash in it — "AC/DC", "Him/Her" — would otherwise put the book one
level below where `book.path` says it is, which is what deletes, moves and file
lookups all act on. Calibre keeps the real title in its database and strips this
from its own directory names, so an import is where they surface.
"""
generated = path_for(title="Back in Black: AC/DC", authors=["Murray Engleheart"])
assert generated == ROOT / "Murray Engleheart" / "Back in Black: AC_DC"
assert generated.relative_to(ROOT).parts == ("Murray Engleheart", "Back in Black: AC_DC")
def test_a_slash_in_an_author_or_series_is_handled_too() -> None:
assert path_for(title="Split", authors=["A/B Collective"]) == (
ROOT / "A_B Collective" / "Split"
)
assert path_for(
title="Volume One", authors=["Someone"], series="Either/Or", series_position="1"
) == ROOT / "Someone" / "Either_Or" / "01 - Volume One"
def test_control_characters_are_removed() -> None:
assert path_for(title="Line\nBreak", authors=["Someone"]) == (
ROOT / "Someone" / "Line_Break"
)
def test_sanitize_path_component() -> None:
assert sanitize_path_component("AC/DC") == "AC_DC"
assert sanitize_path_component("back\\slash") == "back_slash"
assert sanitize_path_component(" padded ") == "padded"
# Colons and other punctuation are legal in a path and are left alone.
assert sanitize_path_component("Title: Subtitle") == "Title: Subtitle"