- Unify sanitize_filename (organizer) and _sanitize_name (ripper): both now use whitelist approach — spaces→underscore, keep \w and hyphens, remove everything else (brackets, punctuation, commas, dots, …). _sanitize_name removed from ripper.py; ripper now imports sanitize_filename from organizer directly. - Add tests/test_scanner_server.py: 15 tests covering HTTP GET/POST handlers, image upload queue, 404/400 error paths, _get_local_ip fallback, print_qr graceful degradation without qrcode installed. - Delete empty stray file '3' from repo root. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
254 lines
8.7 KiB
Python
254 lines
8.7 KiB
Python
"""Tests für den Organizer."""
|
|
|
|
from pathlib import Path
|
|
|
|
from musiksammlung.models import Album, Disc, Track
|
|
from musiksammlung.organizer import (
|
|
build_mapping,
|
|
check_disc_counts,
|
|
discover_audio_files,
|
|
sanitize_filename,
|
|
)
|
|
|
|
|
|
class TestSanitizeFilename:
|
|
"""Tests für sanitize_filename."""
|
|
|
|
def test_replaces_spaces(self) -> None:
|
|
assert sanitize_filename("Hello World") == "Hello_World"
|
|
|
|
def test_removes_punctuation(self) -> None:
|
|
assert sanitize_filename("Song (Live)") == "Song_Live"
|
|
assert sanitize_filename("Artist: Name") == "Artist_Name"
|
|
assert sanitize_filename("Vol. 2") == "Vol_2"
|
|
assert sanitize_filename("Hello, World!") == "Hello_World"
|
|
|
|
def test_keeps_hyphens(self) -> None:
|
|
assert sanitize_filename("AC-DC") == "AC-DC"
|
|
assert sanitize_filename("Sonata - Allegro") == "Sonata_-_Allegro"
|
|
|
|
def test_collapses_multiple_underscores(self) -> None:
|
|
assert sanitize_filename("A B") == "A_B"
|
|
|
|
def test_strips_leading_trailing(self) -> None:
|
|
assert sanitize_filename("_Test_") == "Test"
|
|
assert sanitize_filename("-Test-") == "Test"
|
|
|
|
def test_keeps_umlauts(self) -> None:
|
|
assert sanitize_filename("Für Elise") == "Für_Elise"
|
|
assert sanitize_filename("Über den Wolken") == "Über_den_Wolken"
|
|
|
|
def test_keeps_digits(self) -> None:
|
|
assert sanitize_filename("Track 01") == "Track_01"
|
|
|
|
|
|
class TestDiscoverAudioFiles:
|
|
"""Tests für discover_audio_files."""
|
|
|
|
def test_finds_and_sorts_numerically(self, tmp_path: Path) -> None:
|
|
(tmp_path / "Track_03.flac").touch()
|
|
(tmp_path / "Track_01.flac").touch()
|
|
(tmp_path / "Track_02.flac").touch()
|
|
(tmp_path / "cover.jpg").touch()
|
|
|
|
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_ignores_non_audio_files(self, tmp_path: Path) -> None:
|
|
(tmp_path / "track01.flac").touch()
|
|
(tmp_path / "album.json").touch()
|
|
(tmp_path / "playlist.m3u").touch()
|
|
files = discover_audio_files(tmp_path)
|
|
assert len(files) == 1
|
|
|
|
def test_empty_directory(self, tmp_path: Path) -> None:
|
|
assert discover_audio_files(tmp_path) == []
|
|
|
|
|
|
class TestCheckDiscCounts:
|
|
"""Tests für check_disc_counts."""
|
|
|
|
def _album_with_tracks(self, n: int) -> Album:
|
|
return Album(
|
|
artist="Test",
|
|
album="Album",
|
|
discs=[
|
|
Disc(
|
|
disc_number=1,
|
|
tracks=[Track(track_number=i, title=f"Track {i}") for i in range(1, n + 1)],
|
|
)
|
|
],
|
|
)
|
|
|
|
def test_matching_counts_is_ok(self, tmp_path: Path) -> None:
|
|
(tmp_path / "track01.flac").touch()
|
|
(tmp_path / "track02.flac").touch()
|
|
album = self._album_with_tracks(2)
|
|
|
|
results = check_disc_counts(album, tmp_path)
|
|
assert len(results) == 1
|
|
assert results[0].ok is True
|
|
assert results[0].surplus_files == 0
|
|
assert results[0].surplus_json == 0
|
|
|
|
def test_surplus_files(self, tmp_path: Path) -> None:
|
|
for i in range(1, 4):
|
|
(tmp_path / f"track0{i}.flac").touch()
|
|
album = self._album_with_tracks(2) # JSON has 2, disk has 3
|
|
|
|
results = check_disc_counts(album, tmp_path)
|
|
assert results[0].ok is False
|
|
assert results[0].surplus_files == 1
|
|
assert results[0].surplus_json == 0
|
|
|
|
def test_surplus_json(self, tmp_path: Path) -> None:
|
|
(tmp_path / "track01.flac").touch()
|
|
album = self._album_with_tracks(3) # JSON has 3, disk has 1
|
|
|
|
results = check_disc_counts(album, tmp_path)
|
|
assert results[0].ok is False
|
|
assert results[0].surplus_json == 2
|
|
assert results[0].surplus_files == 0
|
|
|
|
def test_multi_disc(self, tmp_path: Path) -> None:
|
|
cd1 = tmp_path / "CD1"
|
|
cd2 = tmp_path / "CD2"
|
|
cd1.mkdir()
|
|
cd2.mkdir()
|
|
(cd1 / "track01.flac").touch()
|
|
(cd2 / "track01.flac").touch()
|
|
(cd2 / "track02.flac").touch()
|
|
|
|
album = Album(
|
|
artist="A",
|
|
album="B",
|
|
discs=[
|
|
Disc(disc_number=1, tracks=[Track(track_number=1, title="T1")]),
|
|
Disc(
|
|
disc_number=2,
|
|
tracks=[Track(track_number=1, title="T2"), Track(track_number=2, title="T3")],
|
|
),
|
|
],
|
|
)
|
|
results = check_disc_counts(album, tmp_path)
|
|
assert all(r.ok for r in results)
|
|
|
|
def test_missing_disc_directory(self, tmp_path: Path) -> None:
|
|
album = Album(
|
|
artist="A",
|
|
album="B",
|
|
discs=[
|
|
Disc(disc_number=1, tracks=[Track(track_number=1, title="T")]),
|
|
Disc(disc_number=2, tracks=[Track(track_number=1, title="T")]),
|
|
],
|
|
)
|
|
# Neither CD1 nor CD2 exists
|
|
results = check_disc_counts(album, tmp_path)
|
|
assert all(r.audio_file_count == 0 for r in results)
|
|
assert all(not r.ok for r in results)
|
|
|
|
|
|
class TestBuildMapping:
|
|
"""Tests für build_mapping."""
|
|
|
|
def test_single_disc(self, tmp_path: Path) -> None:
|
|
(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"),
|
|
],
|
|
)
|
|
],
|
|
)
|
|
mapping = build_mapping(album, tmp_path, tmp_path / "output")
|
|
|
|
targets = list(mapping.values())
|
|
assert targets[0].name == "01_-_Erster_Song_-_TestArtist.flac"
|
|
assert targets[1].name == "02_-_Zweiter_Song_-_TestArtist.flac"
|
|
# Single-Disc: kein CD1-Unterordner
|
|
assert "CD1" not in str(targets[0])
|
|
|
|
def test_multi_disc(self, tmp_path: Path) -> None:
|
|
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")]),
|
|
],
|
|
)
|
|
mapping = build_mapping(album, tmp_path, tmp_path / "output")
|
|
targets = list(mapping.values())
|
|
assert "CD1" in str(targets[0])
|
|
assert "CD2" in str(targets[1])
|
|
|
|
def test_in_place_mode(self, tmp_path: Path) -> None:
|
|
(tmp_path / "track01.flac").touch()
|
|
album = Album(
|
|
artist="Artist",
|
|
album="Album",
|
|
discs=[Disc(disc_number=1, tracks=[Track(track_number=1, title="Song")])],
|
|
)
|
|
mapping = build_mapping(album, tmp_path, in_place=True)
|
|
|
|
src, dst = next(iter(mapping.items()))
|
|
# Source and target are in the same directory
|
|
assert src.parent == tmp_path
|
|
assert dst.parent == tmp_path
|
|
assert dst.name == "01_-_Song_-_Artist.flac"
|
|
|
|
def test_track_artist_overrides_album_artist(self, tmp_path: Path) -> None:
|
|
(tmp_path / "track01.flac").touch()
|
|
(tmp_path / "track02.flac").touch()
|
|
album = Album(
|
|
artist="Various Artists",
|
|
album="Sampler",
|
|
discs=[
|
|
Disc(
|
|
disc_number=1,
|
|
tracks=[
|
|
Track(track_number=1, title="Song A", artist="Artist X"),
|
|
Track(track_number=2, title="Song B"), # kein Track-Künstler → Fallback
|
|
],
|
|
)
|
|
],
|
|
)
|
|
mapping = build_mapping(album, tmp_path, in_place=True)
|
|
targets = list(mapping.values())
|
|
assert targets[0].name == "01_-_Song_A_-_Artist_X.flac"
|
|
assert targets[1].name == "02_-_Song_B_-_Various_Artists.flac"
|
|
|
|
def test_output_dir_structure(self, tmp_path: Path) -> None:
|
|
(tmp_path / "track01.flac").touch()
|
|
album = Album(
|
|
artist="Karl Böhm",
|
|
album="Beethoven Sinfonien",
|
|
year=1970,
|
|
discs=[Disc(disc_number=1, tracks=[Track(track_number=1, title="Allegro")])],
|
|
)
|
|
output = tmp_path / "Musik"
|
|
mapping = build_mapping(album, tmp_path, output)
|
|
|
|
dst = next(iter(mapping.values()))
|
|
# Artist and album dirs are sanitized
|
|
assert "Karl_Böhm" in str(dst)
|
|
assert "Beethoven_Sinfonien" in str(dst)
|