59 lines
1.4 KiB
Python
59 lines
1.4 KiB
Python
"""Read audio file tags via mutagen. Always safe — never raises."""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
|
|
@dataclass
|
|
class TrackMetadata:
|
|
title: Optional[str] = None
|
|
artist: Optional[str] = None
|
|
album: Optional[str] = None
|
|
duration: Optional[float] = None
|
|
has_identity: bool = False
|
|
|
|
|
|
def read_metadata(path: Path) -> TrackMetadata:
|
|
"""Extract title/artist/album/duration from tags. Never raises."""
|
|
try:
|
|
from mutagen import File
|
|
except ImportError:
|
|
return TrackMetadata()
|
|
|
|
try:
|
|
audio = File(str(path), easy=True)
|
|
except Exception:
|
|
return TrackMetadata()
|
|
|
|
if audio is None:
|
|
return TrackMetadata()
|
|
|
|
def _first(key: str) -> Optional[str]:
|
|
v = audio.get(key)
|
|
if isinstance(v, list):
|
|
return v[0] if v else None
|
|
if isinstance(v, str):
|
|
return v
|
|
return None
|
|
|
|
title = _first("title")
|
|
artist = _first("artist")
|
|
album = _first("album")
|
|
|
|
duration: Optional[float] = None
|
|
try:
|
|
if audio.info is not None:
|
|
duration = float(audio.info.length)
|
|
except Exception:
|
|
pass
|
|
|
|
return TrackMetadata(
|
|
title=title,
|
|
artist=artist,
|
|
album=album,
|
|
duration=duration,
|
|
has_identity=bool(title and artist),
|
|
)
|