Add per-track artist to filename: 09_Titel_-_Kuenstler.flac
- Track model: add optional artist field (None = fall back to album artist) - organizer: append _-_<artist> to each filename - tagger: use track.artist over album.artist for the 'artist' tag - playlist: widen glob pattern to match new _-_<artist> suffix - tests: update assertions + add test for track-artist override Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
775f274d02
commit
255496bd1b
6 changed files with 41 additions and 18 deletions
|
|
@ -10,6 +10,7 @@ from pydantic import BaseModel, field_validator
|
||||||
class Track(BaseModel):
|
class Track(BaseModel):
|
||||||
track_number: int
|
track_number: int
|
||||||
title: str
|
title: str
|
||||||
|
artist: str | None = None # Track-Künstler (z.B. bei Samplern); None = Album-Künstler
|
||||||
|
|
||||||
|
|
||||||
class Disc(BaseModel):
|
class Disc(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,8 @@ def build_mapping(
|
||||||
|
|
||||||
for audio_file, track in zip(audio_files, disc.tracks):
|
for audio_file, track in zip(audio_files, disc.tracks):
|
||||||
safe_title = _sanitize_filename(track.title)
|
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
|
mapping[audio_file] = target_dir / new_name
|
||||||
|
|
||||||
return mapping
|
return mapping
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ def generate_playlist(album: Album, album_dir: Path) -> Path:
|
||||||
for track in disc.tracks:
|
for track in disc.tracks:
|
||||||
safe_title = _sanitize_filename(track.title)
|
safe_title = _sanitize_filename(track.title)
|
||||||
# Audiodatei im Zielverzeichnis finden
|
# Audiodatei im Zielverzeichnis finden
|
||||||
pattern = f"{track.track_number:02d}_{safe_title}.*"
|
pattern = f"{track.track_number:02d}_{safe_title}*"
|
||||||
if multi_disc:
|
if multi_disc:
|
||||||
search_dir = album_dir / f"CD{disc.disc_number}"
|
search_dir = album_dir / f"CD{disc.disc_number}"
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ def tag_file(
|
||||||
logger.warning("Kann Datei nicht öffnen: %s", path)
|
logger.warning("Kann Datei nicht öffnen: %s", path)
|
||||||
return
|
return
|
||||||
|
|
||||||
audio["artist"] = album.artist
|
audio["artist"] = track.artist or album.artist
|
||||||
audio["album"] = album.album
|
audio["album"] = album.album
|
||||||
audio["albumartist"] = album.artist
|
audio["albumartist"] = album.artist
|
||||||
audio["title"] = track.title
|
audio["title"] = track.title
|
||||||
|
|
|
||||||
|
|
@ -167,8 +167,8 @@ class TestBuildMapping:
|
||||||
mapping = build_mapping(album, tmp_path, tmp_path / "output")
|
mapping = build_mapping(album, tmp_path, tmp_path / "output")
|
||||||
|
|
||||||
targets = list(mapping.values())
|
targets = list(mapping.values())
|
||||||
assert targets[0].name == "01_Erster_Song.flac"
|
assert targets[0].name == "01_Erster_Song_-_TestArtist.flac"
|
||||||
assert targets[1].name == "02_Zweiter_Song.flac"
|
assert targets[1].name == "02_Zweiter_Song_-_TestArtist.flac"
|
||||||
# Single-Disc: kein CD1-Unterordner
|
# Single-Disc: kein CD1-Unterordner
|
||||||
assert "CD1" not in str(targets[0])
|
assert "CD1" not in str(targets[0])
|
||||||
|
|
||||||
|
|
@ -207,7 +207,28 @@ class TestBuildMapping:
|
||||||
# Source and target are in the same directory
|
# Source and target are in the same directory
|
||||||
assert src.parent == tmp_path
|
assert src.parent == tmp_path
|
||||||
assert dst.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:
|
def test_output_dir_structure(self, tmp_path: Path) -> None:
|
||||||
(tmp_path / "track01.flac").touch()
|
(tmp_path / "track01.flac").touch()
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,17 @@ def test_generate_playlist_single_disc(tmp_path: Path):
|
||||||
"""M3U für Single-CD: keine CD-Prefix, korrekte Dateinamen."""
|
"""M3U für Single-CD: keine CD-Prefix, korrekte Dateinamen."""
|
||||||
album = _make_album(n_tracks=2, n_discs=1)
|
album = _make_album(n_tracks=2, n_discs=1)
|
||||||
|
|
||||||
# Dummy-Audiodateien im neuen Unterstrich-Schema
|
# Dummy-Audiodateien mit Künstler-Suffix
|
||||||
(tmp_path / "01_Disc1_Song_1.flac").touch()
|
(tmp_path / "01_Disc1_Song_1_-_Artist.flac").touch()
|
||||||
(tmp_path / "02_Disc1_Song_2.flac").touch()
|
(tmp_path / "02_Disc1_Song_2_-_Artist.flac").touch()
|
||||||
|
|
||||||
playlist_path = generate_playlist(album, tmp_path)
|
playlist_path = generate_playlist(album, tmp_path)
|
||||||
assert playlist_path.exists()
|
assert playlist_path.exists()
|
||||||
|
|
||||||
content = playlist_path.read_text()
|
content = playlist_path.read_text()
|
||||||
assert "#EXTM3U" in content
|
assert "#EXTM3U" in content
|
||||||
assert "01_Disc1_Song_1.flac" in content
|
assert "01_Disc1_Song_1_-_Artist.flac" in content
|
||||||
assert "02_Disc1_Song_2.flac" in content
|
assert "02_Disc1_Song_2_-_Artist.flac" in content
|
||||||
# Kein CD-Prefix bei Single-Disc
|
# Kein CD-Prefix bei Single-Disc
|
||||||
assert "CD1/" not in content
|
assert "CD1/" not in content
|
||||||
|
|
||||||
|
|
@ -44,16 +44,16 @@ def test_generate_playlist_multi_disc(tmp_path: Path):
|
||||||
cd2 = tmp_path / "CD2"
|
cd2 = tmp_path / "CD2"
|
||||||
cd1.mkdir()
|
cd1.mkdir()
|
||||||
cd2.mkdir()
|
cd2.mkdir()
|
||||||
(cd1 / "01_Disc1_Song_1.flac").touch()
|
(cd1 / "01_Disc1_Song_1_-_Artist.flac").touch()
|
||||||
(cd1 / "02_Disc1_Song_2.flac").touch()
|
(cd1 / "02_Disc1_Song_2_-_Artist.flac").touch()
|
||||||
(cd2 / "01_Disc2_Song_1.flac").touch()
|
(cd2 / "01_Disc2_Song_1_-_Artist.flac").touch()
|
||||||
(cd2 / "02_Disc2_Song_2.flac").touch()
|
(cd2 / "02_Disc2_Song_2_-_Artist.flac").touch()
|
||||||
|
|
||||||
playlist_path = generate_playlist(album, tmp_path)
|
playlist_path = generate_playlist(album, tmp_path)
|
||||||
content = playlist_path.read_text()
|
content = playlist_path.read_text()
|
||||||
|
|
||||||
assert "CD1/01_Disc1_Song_1.flac" in content
|
assert "CD1/01_Disc1_Song_1_-_Artist.flac" in content
|
||||||
assert "CD2/01_Disc2_Song_1.flac" in content
|
assert "CD2/01_Disc2_Song_1_-_Artist.flac" in content
|
||||||
|
|
||||||
|
|
||||||
def test_generate_playlist_filename(tmp_path: Path):
|
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)
|
playlist_path = generate_playlist(album, tmp_path)
|
||||||
content = playlist_path.read_text()
|
content = playlist_path.read_text()
|
||||||
assert "#EXTINF:0,Für Elise" in content
|
assert "#EXTINF:0,Für Elise" in content
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue