Add project skeleton: CLI pipeline for CD digitization
Modular Python package with Typer CLI (scan/apply/process commands), Pydantic data models, OCR via Tesseract, LLM-based tracklist parsing, mutagen audio tagging, M3U playlist generation, and cover processing. Includes 8 passing tests and ruff lint config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
225f6b3dbf
commit
3e073250ca
17 changed files with 1027 additions and 0 deletions
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
42
tests/test_models.py
Normal file
42
tests/test_models.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"""Tests für die Datenmodelle."""
|
||||
|
||||
from musiksammlung.models import Album
|
||||
|
||||
|
||||
def test_album_folder_name_with_year():
|
||||
album = Album(artist="Test", album="Mein Album", year=1987, discs=[])
|
||||
assert album.folder_name == "Mein Album (1987)"
|
||||
|
||||
|
||||
def test_album_folder_name_without_year():
|
||||
album = Album(artist="Test", album="Mein Album", year=None, discs=[])
|
||||
assert album.folder_name == "Mein Album"
|
||||
|
||||
|
||||
def test_sanitize_name():
|
||||
album = Album(artist='Art:ist', album='Al/bum?', year=None, discs=[])
|
||||
assert ":" not in album.artist
|
||||
assert "/" not in album.album
|
||||
assert "?" not in album.album
|
||||
|
||||
|
||||
def test_album_from_json():
|
||||
data = {
|
||||
"artist": "Die Toten Hosen",
|
||||
"album": "Opium fürs Volk",
|
||||
"year": 1996,
|
||||
"discs": [
|
||||
{
|
||||
"disc_number": 1,
|
||||
"tracks": [
|
||||
{"track_number": 1, "title": "Bonnie & Clyde"},
|
||||
{"track_number": 2, "title": "Zehn kleine Jägermeister"},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
album = Album.model_validate(data)
|
||||
assert album.artist == "Die Toten Hosen"
|
||||
assert len(album.discs) == 1
|
||||
assert len(album.discs[0].tracks) == 2
|
||||
assert album.discs[0].tracks[1].title == "Zehn kleine Jägermeister"
|
||||
78
tests/test_organizer.py
Normal file
78
tests/test_organizer.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""Tests für den Organizer."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from musiksammlung.models import Album, Disc, Track
|
||||
from musiksammlung.organizer import build_mapping, discover_audio_files
|
||||
|
||||
|
||||
def test_discover_audio_files(tmp_path: Path):
|
||||
"""Findet und sortiert Audiodateien korrekt."""
|
||||
(tmp_path / "Track_03.flac").touch()
|
||||
(tmp_path / "Track_01.flac").touch()
|
||||
(tmp_path / "Track_02.flac").touch()
|
||||
(tmp_path / "cover.jpg").touch() # soll ignoriert werden
|
||||
|
||||
files = discover_audio_files(tmp_path)
|
||||
assert len(files) == 3
|
||||
assert files[0].name == "Track_01.flac"
|
||||
assert files[2].name == "Track_03.flac"
|
||||
|
||||
|
||||
def test_build_mapping_single_disc(tmp_path: Path):
|
||||
"""Mapping für ein Single-CD-Album."""
|
||||
(tmp_path / "Track_01.flac").touch()
|
||||
(tmp_path / "Track_02.flac").touch()
|
||||
|
||||
album = Album(
|
||||
artist="TestArtist",
|
||||
album="TestAlbum",
|
||||
year=2000,
|
||||
discs=[
|
||||
Disc(
|
||||
disc_number=1,
|
||||
tracks=[
|
||||
Track(track_number=1, title="Erster Song"),
|
||||
Track(track_number=2, title="Zweiter Song"),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
output = tmp_path / "output"
|
||||
mapping = build_mapping(album, tmp_path, output)
|
||||
|
||||
assert len(mapping) == 2
|
||||
targets = list(mapping.values())
|
||||
assert targets[0].name == "01 Erster Song.flac"
|
||||
assert targets[1].name == "02 Zweiter Song.flac"
|
||||
# Single-Disc: kein CD1-Unterordner
|
||||
assert "CD1" not in str(targets[0])
|
||||
|
||||
|
||||
def test_build_mapping_multi_disc(tmp_path: Path):
|
||||
"""Mapping für ein Multi-CD-Album."""
|
||||
cd1 = tmp_path / "CD1"
|
||||
cd2 = tmp_path / "CD2"
|
||||
cd1.mkdir()
|
||||
cd2.mkdir()
|
||||
(cd1 / "Track_01.flac").touch()
|
||||
(cd2 / "Track_01.flac").touch()
|
||||
|
||||
album = Album(
|
||||
artist="Artist",
|
||||
album="Box Set",
|
||||
year=1999,
|
||||
discs=[
|
||||
Disc(disc_number=1, tracks=[Track(track_number=1, title="Song A")]),
|
||||
Disc(disc_number=2, tracks=[Track(track_number=1, title="Song B")]),
|
||||
],
|
||||
)
|
||||
|
||||
output = tmp_path / "output"
|
||||
mapping = build_mapping(album, tmp_path, output)
|
||||
|
||||
assert len(mapping) == 2
|
||||
targets = list(mapping.values())
|
||||
assert "CD1" in str(targets[0])
|
||||
assert "CD2" in str(targets[1])
|
||||
37
tests/test_playlist.py
Normal file
37
tests/test_playlist.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"""Tests für die Playlist-Generierung."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from musiksammlung.models import Album, Disc, Track
|
||||
from musiksammlung.playlist import generate_playlist
|
||||
|
||||
|
||||
def test_generate_playlist_single_disc(tmp_path: Path):
|
||||
"""Erzeugt eine M3U-Playlist für ein Single-CD-Album."""
|
||||
album = Album(
|
||||
artist="Artist",
|
||||
album="TestAlbum",
|
||||
year=2000,
|
||||
discs=[
|
||||
Disc(
|
||||
disc_number=1,
|
||||
tracks=[
|
||||
Track(track_number=1, title="Song Eins"),
|
||||
Track(track_number=2, title="Song Zwei"),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Dummy-Audiodateien anlegen
|
||||
(tmp_path / "01 Song Eins.flac").touch()
|
||||
(tmp_path / "02 Song Zwei.flac").touch()
|
||||
|
||||
playlist_path = generate_playlist(album, tmp_path)
|
||||
assert playlist_path.exists()
|
||||
content = playlist_path.read_text()
|
||||
assert "#EXTM3U" in content
|
||||
assert "01 Song Eins.flac" in content
|
||||
assert "02 Song Zwei.flac" in content
|
||||
# Kein CD-Prefix bei Single-Disc
|
||||
assert "CD1/" not in content
|
||||
Loading…
Add table
Add a link
Reference in a new issue