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.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user