feat: real mutagen tagger and MusicBrainz tracklist
This commit is contained in:
@@ -47,6 +47,7 @@ class MusicBrainzResolver:
|
||||
|
||||
track_count = None
|
||||
total_duration_s = None
|
||||
titles: tuple[str, ...] = ()
|
||||
rgid = rg.get("id")
|
||||
if rgid:
|
||||
rgfull = musicbrainzngs.get_release_group_by_id(rgid, includes=["releases"])
|
||||
@@ -62,6 +63,7 @@ class MusicBrainzResolver:
|
||||
track_count = len(tracks)
|
||||
total_ms = sum(int((t.get("recording") or {}).get("length") or 0) for t in tracks)
|
||||
total_duration_s = total_ms // 1000 if total_ms else None
|
||||
titles = tuple((t.get("recording") or {}).get("title", "") for t in tracks)
|
||||
|
||||
return MBTarget(
|
||||
artist=canonical_artist,
|
||||
@@ -69,4 +71,5 @@ class MusicBrainzResolver:
|
||||
track_count=track_count,
|
||||
total_duration_s=total_duration_s,
|
||||
year=year,
|
||||
tracklist=titles,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import os
|
||||
|
||||
from lyra_worker.library import safe_name
|
||||
|
||||
_AUDIO_EXT = {".flac", ".mp3", ".m4a", ".opus", ".ogg", ".aac", ".wav"}
|
||||
|
||||
|
||||
def plan_track_names(names: list[str], tracklist: tuple[str, ...]) -> list[tuple[str, str, str, int]]:
|
||||
"""Map audio filenames -> (old_name, new_name, title, track_number).
|
||||
|
||||
Files are sorted; titles come from `tracklist` by position when available,
|
||||
else the filename stem. Pure — no filesystem or tag I/O.
|
||||
"""
|
||||
plan: list[tuple[str, str, str, int]] = []
|
||||
audio = sorted(n for n in names if os.path.splitext(n)[1].lower() in _AUDIO_EXT)
|
||||
for i, name in enumerate(audio):
|
||||
ext = os.path.splitext(name)[1].lower()
|
||||
title = tracklist[i] if i < len(tracklist) else os.path.splitext(name)[0]
|
||||
new_name = f"{i + 1:02d} {safe_name(title)}{ext}"
|
||||
plan.append((name, new_name, title, i + 1))
|
||||
return plan
|
||||
|
||||
|
||||
class MutagenTagger:
|
||||
"""Real Tagger using mutagen (lazily imported). The tag-write is not unit-tested
|
||||
offline (needs real audio); the naming logic (`plan_track_names`) is."""
|
||||
|
||||
def tag_album(self, album_dir: str, target) -> None:
|
||||
import mutagen
|
||||
|
||||
names = os.listdir(album_dir)
|
||||
for old_name, new_name, title, number in plan_track_names(names, target.tracklist):
|
||||
path = os.path.join(album_dir, old_name)
|
||||
audio = mutagen.File(path, easy=True)
|
||||
if audio is None: # unreadable/unsupported file — leave it untouched
|
||||
continue
|
||||
audio["artist"] = target.artist
|
||||
audio["albumartist"] = target.artist
|
||||
audio["album"] = target.album
|
||||
audio["title"] = title
|
||||
audio["tracknumber"] = str(number)
|
||||
if target.year:
|
||||
audio["date"] = str(target.year)
|
||||
audio.save()
|
||||
|
||||
new_path = os.path.join(album_dir, new_name)
|
||||
if os.path.abspath(new_path) != os.path.abspath(path) and not os.path.exists(new_path):
|
||||
os.rename(path, new_path)
|
||||
Reference in New Issue
Block a user