From 255496bd1bda7decb8b51543e55f2ebaf8bd7398 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 18 Feb 2026 00:11:07 +0100 Subject: [PATCH] Add per-track artist to filename: 09_Titel_-_Kuenstler.flac - Track model: add optional artist field (None = fall back to album artist) - organizer: append _-_ to each filename - tagger: use track.artist over album.artist for the 'artist' tag - playlist: widen glob pattern to match new _-_ suffix - tests: update assertions + add test for track-artist override Co-Authored-By: Claude Sonnet 4.5 --- src/musiksammlung/models.py | 1 + src/musiksammlung/organizer.py | 3 ++- src/musiksammlung/playlist.py | 2 +- src/musiksammlung/tagger.py | 2 +- tests/test_organizer.py | 27 ++++++++++++++++++++++++--- tests/test_playlist.py | 24 ++++++++++++------------ 6 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/musiksammlung/models.py b/src/musiksammlung/models.py index f459f2e..f60e891 100644 --- a/src/musiksammlung/models.py +++ b/src/musiksammlung/models.py @@ -10,6 +10,7 @@ from pydantic import BaseModel, field_validator class Track(BaseModel): track_number: int title: str + artist: str | None = None # Track-Künstler (z.B. bei Samplern); None = Album-Künstler class Disc(BaseModel): diff --git a/src/musiksammlung/organizer.py b/src/musiksammlung/organizer.py index d0ab508..be38a13 100644 --- a/src/musiksammlung/organizer.py +++ b/src/musiksammlung/organizer.py @@ -131,7 +131,8 @@ def build_mapping( for audio_file, track in zip(audio_files, disc.tracks): safe_title = _sanitize_filename(track.title) - new_name = f"{track.track_number:02d}_{safe_title}{audio_file.suffix}" + safe_artist = _sanitize_filename(track.artist or album.artist) + new_name = f"{track.track_number:02d}_{safe_title}_-_{safe_artist}{audio_file.suffix}" mapping[audio_file] = target_dir / new_name return mapping diff --git a/src/musiksammlung/playlist.py b/src/musiksammlung/playlist.py index abe46de..abaf808 100644 --- a/src/musiksammlung/playlist.py +++ b/src/musiksammlung/playlist.py @@ -35,7 +35,7 @@ def generate_playlist(album: Album, album_dir: Path) -> Path: for track in disc.tracks: safe_title = _sanitize_filename(track.title) # Audiodatei im Zielverzeichnis finden - pattern = f"{track.track_number:02d}_{safe_title}.*" + pattern = f"{track.track_number:02d}_{safe_title}*" if multi_disc: search_dir = album_dir / f"CD{disc.disc_number}" else: diff --git a/src/musiksammlung/tagger.py b/src/musiksammlung/tagger.py index c162da9..996c465 100644 --- a/src/musiksammlung/tagger.py +++ b/src/musiksammlung/tagger.py @@ -29,7 +29,7 @@ def tag_file( logger.warning("Kann Datei nicht öffnen: %s", path) return - audio["artist"] = album.artist + audio["artist"] = track.artist or album.artist audio["album"] = album.album audio["albumartist"] = album.artist audio["title"] = track.title diff --git a/tests/test_organizer.py b/tests/test_organizer.py index 7446bde..5a5f54d 100644 --- a/tests/test_organizer.py +++ b/tests/test_organizer.py @@ -167,8 +167,8 @@ class TestBuildMapping: mapping = build_mapping(album, tmp_path, tmp_path / "output") targets = list(mapping.values()) - assert targets[0].name == "01_Erster_Song.flac" - assert targets[1].name == "02_Zweiter_Song.flac" + 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]) @@ -207,7 +207,28 @@ class TestBuildMapping: # Source and target are in the same directory assert src.parent == tmp_path assert dst.parent == tmp_path - assert dst.name == "01_Song.flac" + 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() diff --git a/tests/test_playlist.py b/tests/test_playlist.py index a55c51d..2eb3bdf 100644 --- a/tests/test_playlist.py +++ b/tests/test_playlist.py @@ -21,17 +21,17 @@ def test_generate_playlist_single_disc(tmp_path: Path): """M3U für Single-CD: keine CD-Prefix, korrekte Dateinamen.""" album = _make_album(n_tracks=2, n_discs=1) - # Dummy-Audiodateien im neuen Unterstrich-Schema - (tmp_path / "01_Disc1_Song_1.flac").touch() - (tmp_path / "02_Disc1_Song_2.flac").touch() + # Dummy-Audiodateien mit Künstler-Suffix + (tmp_path / "01_Disc1_Song_1_-_Artist.flac").touch() + (tmp_path / "02_Disc1_Song_2_-_Artist.flac").touch() playlist_path = generate_playlist(album, tmp_path) assert playlist_path.exists() content = playlist_path.read_text() assert "#EXTM3U" in content - assert "01_Disc1_Song_1.flac" in content - assert "02_Disc1_Song_2.flac" in content + assert "01_Disc1_Song_1_-_Artist.flac" in content + assert "02_Disc1_Song_2_-_Artist.flac" in content # Kein CD-Prefix bei Single-Disc assert "CD1/" not in content @@ -44,16 +44,16 @@ def test_generate_playlist_multi_disc(tmp_path: Path): cd2 = tmp_path / "CD2" cd1.mkdir() cd2.mkdir() - (cd1 / "01_Disc1_Song_1.flac").touch() - (cd1 / "02_Disc1_Song_2.flac").touch() - (cd2 / "01_Disc2_Song_1.flac").touch() - (cd2 / "02_Disc2_Song_2.flac").touch() + (cd1 / "01_Disc1_Song_1_-_Artist.flac").touch() + (cd1 / "02_Disc1_Song_2_-_Artist.flac").touch() + (cd2 / "01_Disc2_Song_1_-_Artist.flac").touch() + (cd2 / "02_Disc2_Song_2_-_Artist.flac").touch() playlist_path = generate_playlist(album, tmp_path) content = playlist_path.read_text() - assert "CD1/01_Disc1_Song_1.flac" in content - assert "CD2/01_Disc2_Song_1.flac" in content + assert "CD1/01_Disc1_Song_1_-_Artist.flac" in content + assert "CD2/01_Disc2_Song_1_-_Artist.flac" in content def test_generate_playlist_filename(tmp_path: Path): @@ -79,7 +79,7 @@ def test_generate_playlist_extinf_contains_title(tmp_path: Path): ) ], ) - (tmp_path / "01_Für_Elise.flac").touch() + (tmp_path / "01_Für_Elise_-_A.flac").touch() playlist_path = generate_playlist(album, tmp_path) content = playlist_path.read_text() assert "#EXTINF:0,Für Elise" in content