add program files
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def samples_dir() -> Path:
|
||||
return Path(__file__).parent.parent / "samples"
|
||||
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from audiobooksorter.export import write_sorted
|
||||
from audiobooksorter.ingest import AudiobookRecord
|
||||
|
||||
|
||||
def test_write_sorted_copies_source_directories(tmp_path: Path) -> None:
|
||||
source = tmp_path / "library"
|
||||
book_dir = source / "Brandon" / "Mistborn1"
|
||||
book_dir.mkdir(parents=True)
|
||||
(book_dir / "chapter1.mp3").write_text("audio-data", encoding="utf-8")
|
||||
|
||||
destination = tmp_path / "sorted"
|
||||
record = AudiobookRecord(
|
||||
title="The Final Empire",
|
||||
series="Mistborn",
|
||||
series_index=1,
|
||||
author="Brandon Sanderson",
|
||||
source_path=book_dir,
|
||||
)
|
||||
|
||||
write_sorted([record], destination)
|
||||
|
||||
expected = destination / "Brandon Sanderson" / "Mistborn" / "Book 1 - The Final Empire"
|
||||
assert (expected / "chapter1.mp3").read_text(encoding="utf-8") == "audio-data"
|
||||
|
||||
|
||||
def test_write_sorted_creates_metadata_when_missing_source(tmp_path: Path) -> None:
|
||||
destination = tmp_path / "sorted"
|
||||
record = AudiobookRecord(
|
||||
title="",
|
||||
series=None,
|
||||
series_index=None,
|
||||
author="",
|
||||
source_path=None,
|
||||
)
|
||||
|
||||
write_sorted([record], destination)
|
||||
|
||||
expected = destination / "Unknown Author" / "Untitled"
|
||||
metadata = expected / "metadata.json"
|
||||
assert metadata.exists()
|
||||
assert '"title": ""' in metadata.read_text(encoding="utf-8")
|
||||
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from audiobooksorter.ingest import IngestError, load_library
|
||||
|
||||
|
||||
def test_load_library_from_json(samples_dir: Path) -> None:
|
||||
records = load_library(samples_dir / "library.json")
|
||||
|
||||
assert len(records) == 5
|
||||
assert records[0].title == "The Final Empire"
|
||||
assert records[0].source_path is None
|
||||
|
||||
|
||||
def test_load_library_from_directory(tmp_path: Path) -> None:
|
||||
library = tmp_path / "library"
|
||||
book_one = library / "Mistborn1"
|
||||
book_two = library / "Wayfarers"
|
||||
book_one.mkdir(parents=True)
|
||||
book_two.mkdir(parents=True)
|
||||
|
||||
(book_one / "metadata.json").write_text(
|
||||
json.dumps(
|
||||
{"title": "The Final Empire", "series": "Mistborn", "seriesIndex": 1, "author": "Brandon"}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(book_two / "metadata.json").write_text(
|
||||
json.dumps({"title": "Wayfarers", "author": "Becky Chambers"}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
records = load_library(library)
|
||||
|
||||
sources = {record.title: record.source_path for record in records}
|
||||
assert sources["The Final Empire"] == book_one
|
||||
assert sources["Wayfarers"] == book_two
|
||||
|
||||
|
||||
def test_missing_library_raises(tmp_path: Path) -> None:
|
||||
with pytest.raises(IngestError):
|
||||
load_library(tmp_path / "missing")
|
||||
@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from audiobooksorter.ingest import AudiobookRecord
|
||||
from audiobooksorter.sorter import sort_by_series
|
||||
|
||||
|
||||
def _load_sample(path: Path) -> list[AudiobookRecord]:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
return [
|
||||
AudiobookRecord(
|
||||
title=item["title"],
|
||||
series=item.get("series"),
|
||||
series_index=item.get("seriesIndex"),
|
||||
author=item["author"],
|
||||
)
|
||||
for item in data
|
||||
]
|
||||
|
||||
|
||||
def test_sorting_orders_by_author_then_series(samples_dir: Path) -> None:
|
||||
records = _load_sample(samples_dir / "library.json")
|
||||
|
||||
sorted_records = sort_by_series(records)
|
||||
|
||||
titles = [record.title for record in sorted_records]
|
||||
assert titles == [
|
||||
"Wayfarers", # Becky Chambers
|
||||
"The Final Empire",
|
||||
"The Well of Ascension",
|
||||
"The Way of Kings",
|
||||
"Words of Radiance",
|
||||
]
|
||||
|
||||
|
||||
def test_author_parsing_handles_last_first_pairs() -> None:
|
||||
records = [
|
||||
AudiobookRecord(
|
||||
title="A Solo Standard",
|
||||
series=None,
|
||||
series_index=None,
|
||||
author="Ichiro Kishimi",
|
||||
),
|
||||
AudiobookRecord(
|
||||
title="B Solo LastFirst",
|
||||
series=None,
|
||||
series_index=None,
|
||||
author="Kishimi, Ichiro",
|
||||
),
|
||||
AudiobookRecord(
|
||||
title="C Ampersand",
|
||||
series=None,
|
||||
series_index=None,
|
||||
author="Ichiro Kishimi & Fumitake Koga",
|
||||
),
|
||||
AudiobookRecord(
|
||||
title="D Comma List",
|
||||
series=None,
|
||||
series_index=None,
|
||||
author="Kishimi, Ichiro, Koga, Fumitake",
|
||||
),
|
||||
]
|
||||
|
||||
sorted_records = sort_by_series(records)
|
||||
|
||||
titles = [record.title for record in sorted_records]
|
||||
assert titles == [
|
||||
"A Solo Standard",
|
||||
"B Solo LastFirst",
|
||||
"C Ampersand",
|
||||
"D Comma List",
|
||||
]
|
||||
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from audiobooksorter.cli import main
|
||||
|
||||
|
||||
def _make_book(folder: Path, title: str, author: str, series: str | None, index: int | None) -> None:
|
||||
folder.mkdir(parents=True)
|
||||
(folder / "metadata.json").write_text(
|
||||
json.dumps(
|
||||
{"title": title, "author": author, "series": series, "seriesIndex": index},
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(folder / f"{title}.txt").write_text(title, encoding="utf-8")
|
||||
|
||||
|
||||
def test_cli_sorts_library(tmp_path: Path) -> None:
|
||||
library = tmp_path / "input"
|
||||
_make_book(library / "BookA", "Wayfarers", "Becky Chambers", None, None)
|
||||
_make_book(library / "BookB", "The Final Empire", "Brandon Sanderson", "Mistborn", 1)
|
||||
|
||||
output = tmp_path / "output"
|
||||
|
||||
exit_code = main(["--library", str(library), "--output", str(output)])
|
||||
|
||||
assert exit_code == 0
|
||||
assert (output / "Becky Chambers" / "Wayfarers" / "Wayfarers.txt").read_text(encoding="utf-8") == "Wayfarers"
|
||||
assert (
|
||||
output / "Brandon Sanderson" / "Mistborn" / "Book 1 - The Final Empire" / "The Final Empire.txt"
|
||||
).read_text(encoding="utf-8") == "The Final Empire"
|
||||
|
||||
|
||||
def test_cli_dry_run_shows_directory_structure(tmp_path: Path, capsys: object) -> None:
|
||||
library = tmp_path / "input"
|
||||
_make_book(library / "BookA", "All the Skills", "Honour Rae", "All the Skills", 1)
|
||||
_make_book(library / "BookB", "All the Skills", "Honour Rae", "All the Skills", 2)
|
||||
_make_book(library / "BookC", "The Wandering Inn", "PirateAba", "The Wandering Inn", 1)
|
||||
_make_book(library / "BookD", "Fae and Fare", "PirateAba", "The Wandering Inn", 2)
|
||||
_make_book(library / "BookE", "Domestication", "Seth Ring", "Battle Mage Farmer", 1)
|
||||
|
||||
exit_code = main(["--library", str(library), "--dry-run"])
|
||||
|
||||
assert exit_code == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "Honour Rae / All the Skills" in captured.out
|
||||
assert "Honour Rae / All the Skills / Book 1 - All the Skills" in captured.out
|
||||
assert "Honour Rae / All the Skills / Book 2 - All the Skills" in captured.out
|
||||
assert "PirateAba / The Wandering Inn" in captured.out
|
||||
assert "PirateAba / The Wandering Inn / Book 1 - The Wandering Inn" in captured.out
|
||||
assert "PirateAba / The Wandering Inn / Book 2 - Fae and Fare" in captured.out
|
||||
assert "Seth Ring / Battle Mage Farmer" in captured.out
|
||||
assert "Seth Ring / Battle Mage Farmer / Book 1 - Domestication" in captured.out
|
||||
@@ -0,0 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from audiobooksorter import constants
|
||||
|
||||
|
||||
def test_constants_exposed() -> None:
|
||||
assert constants.PACKAGE_NAME == "audiobooksorter"
|
||||
assert constants.SUPPORTED_VERSION == "1"
|
||||
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from audiobooksorter.cli import _format_dry_run
|
||||
from audiobooksorter.ingest import load_library
|
||||
from audiobooksorter.sorter import sort_by_series
|
||||
|
||||
|
||||
def test_loads_directory_without_metadata_files() -> None:
|
||||
records = load_library(Path("testdata"))
|
||||
|
||||
assert len(records) == 14
|
||||
series_names = {record.series for record in records}
|
||||
assert {"All the Skills", "Battle Mage Farmer", "The Wandering Inn"} <= series_names
|
||||
|
||||
battle_indexes = sorted(
|
||||
record.series_index for record in records if record.series == "Battle Mage Farmer"
|
||||
)
|
||||
assert battle_indexes == [1.0, 2.0, 3.0, 4.0]
|
||||
|
||||
|
||||
def test_dry_run_formats_preview() -> None:
|
||||
records = sort_by_series(load_library(Path("testdata")))
|
||||
|
||||
preview = _format_dry_run(records)
|
||||
|
||||
assert "Dry run (no files written)" in preview
|
||||
assert "Seth Ring / Battle Mage Farmer" in preview
|
||||
assert "Seth Ring / Battle Mage Farmer / Book 1" in preview
|
||||
assert "PirateAba / The Wandering Inn" in preview
|
||||
assert "PirateAba / The Wandering Inn / Book 1" in preview
|
||||
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from audiobooksorter.logging import configure_logging
|
||||
|
||||
|
||||
def test_configure_logging_reuses_logger() -> None:
|
||||
logger = configure_logging("audiobooksorter-tests")
|
||||
logger.info("hello")
|
||||
# calling again should not attach additional handlers
|
||||
logger_again = configure_logging("audiobooksorter-tests")
|
||||
assert logger is logger_again
|
||||
assert len(logger.handlers) == 1
|
||||
Reference in New Issue
Block a user