"""Tests für den Organizer.""" from pathlib import Path import pytest from musiksammlung.models import Album, Disc, Track from musiksammlung.organizer import ( _sanitize_filename, build_mapping, check_disc_counts, discover_audio_files, ) class TestSanitizeFilename: """Tests für _sanitize_filename.""" def test_replaces_spaces(self) -> None: assert _sanitize_filename("Hello World") == "Hello_World" def test_replaces_punctuation(self) -> None: assert _sanitize_filename("Song (Live)") == "Song_Live" assert _sanitize_filename("Artist: Name") == "Artist_Name" def test_collapses_multiple_underscores(self) -> None: assert _sanitize_filename("A B") == "A_B" assert _sanitize_filename("A--B") == "A_B" def test_strips_leading_trailing_underscores(self) -> None: 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)