Thread runtime through metadata enrichment
- Pass the live runtime bundle into the shared metadata facade so worker-backed source enrichment can actually run. - Forward runtime from the import pipeline and web-server wrapper into embed_source_ids. - Add a regression test that verifies the runtime object reaches the source-ID embedding path.
This commit is contained in:
parent
8319c6679f
commit
9656dbd46a
5 changed files with 781 additions and 652 deletions
|
|
@ -364,7 +364,7 @@ def post_process_matched_download(context_key, context, file_path, runtime):
|
||||||
f"[Metadata Input] Playlist mode - artist: '{artist_context.get('name', 'MISSING')}' "
|
f"[Metadata Input] Playlist mode - artist: '{artist_context.get('name', 'MISSING')}' "
|
||||||
f"(id: {artist_context.get('id', 'MISSING')})"
|
f"(id: {artist_context.get('id', 'MISSING')})"
|
||||||
)
|
)
|
||||||
enhance_file_metadata(file_path, context, artist_context, None)
|
enhance_file_metadata(file_path, context, artist_context, None, runtime=runtime)
|
||||||
except Exception as meta_err:
|
except Exception as meta_err:
|
||||||
import traceback
|
import traceback
|
||||||
pp_logger.info(f"[inner] Metadata enhancement FAILED for {context_key}: {meta_err}\n{traceback.format_exc()}")
|
pp_logger.info(f"[inner] Metadata enhancement FAILED for {context_key}: {meta_err}\n{traceback.format_exc()}")
|
||||||
|
|
@ -529,7 +529,7 @@ def post_process_matched_download(context_key, context, file_path, runtime):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.info("[Metadata Input] album_info: None (single track)")
|
logger.info("[Metadata Input] album_info: None (single track)")
|
||||||
enhance_file_metadata(file_path, context, artist_context, album_info)
|
enhance_file_metadata(file_path, context, artist_context, album_info, runtime=runtime)
|
||||||
except Exception as meta_err:
|
except Exception as meta_err:
|
||||||
import traceback
|
import traceback
|
||||||
pp_logger.info(f"[inner] Metadata enhancement FAILED for {context_key}: {meta_err}\n{traceback.format_exc()}")
|
pp_logger.info(f"[inner] Metadata enhancement FAILED for {context_key}: {meta_err}\n{traceback.format_exc()}")
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ __all__ = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def enhance_file_metadata(file_path: str, context: dict, artist: dict, album_info: dict) -> bool:
|
def enhance_file_metadata(file_path: str, context: dict, artist: dict, album_info: dict, runtime=None) -> bool:
|
||||||
cfg = get_config_manager()
|
cfg = get_config_manager()
|
||||||
logger_ = get_logger()
|
logger_ = get_logger()
|
||||||
if cfg.get("metadata_enhancement.enabled", True) is False:
|
if cfg.get("metadata_enhancement.enabled", True) is False:
|
||||||
|
|
@ -126,7 +126,7 @@ def enhance_file_metadata(file_path: str, context: dict, artist: dict, album_inf
|
||||||
if metadata.get("disc_number"):
|
if metadata.get("disc_number"):
|
||||||
audio_file["disk"] = [(metadata["disc_number"], 0)]
|
audio_file["disk"] = [(metadata["disc_number"], 0)]
|
||||||
|
|
||||||
embed_source_ids(audio_file, metadata, context)
|
embed_source_ids(audio_file, metadata, context, runtime=runtime)
|
||||||
|
|
||||||
if album_info is not None and metadata.get("musicbrainz_release_id"):
|
if album_info is not None and metadata.get("musicbrainz_release_id"):
|
||||||
album_info["musicbrainz_release_id"] = metadata["musicbrainz_release_id"]
|
album_info["musicbrainz_release_id"] = metadata["musicbrainz_release_id"]
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,10 @@ from core.metadata_service import get_itunes_client
|
||||||
from database.music_database import get_database
|
from database.music_database import get_database
|
||||||
from core.metadata_common import (
|
from core.metadata_common import (
|
||||||
get_config_manager,
|
get_config_manager,
|
||||||
get_logger,
|
|
||||||
get_mutagen_symbols,
|
get_mutagen_symbols,
|
||||||
is_vorbis_like,
|
is_vorbis_like,
|
||||||
)
|
)
|
||||||
|
from utils.logging_config import get_logger as _create_logger
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"extract_source_metadata",
|
"extract_source_metadata",
|
||||||
|
|
@ -42,6 +42,7 @@ mb_release_cache: Dict[tuple, str] = {}
|
||||||
mb_release_cache_lock = threading.RLock()
|
mb_release_cache_lock = threading.RLock()
|
||||||
mb_release_detail_cache: Dict[str, Dict[str, Any]] = {}
|
mb_release_detail_cache: Dict[str, Dict[str, Any]] = {}
|
||||||
mb_release_detail_cache_lock = threading.RLock()
|
mb_release_detail_cache_lock = threading.RLock()
|
||||||
|
logger = _create_logger("metadata.source")
|
||||||
|
|
||||||
_EDITION_PAREN_RE = re.compile(
|
_EDITION_PAREN_RE = re.compile(
|
||||||
r'\s*[\(\[]\s*(?:deluxe|expanded|remaster(?:ed)?|anniversary|special|collector|'
|
r'\s*[\(\[]\s*(?:deluxe|expanded|remaster(?:ed)?|anniversary|special|collector|'
|
||||||
|
|
@ -63,159 +64,7 @@ def normalize_album_cache_key(album_name: str) -> str:
|
||||||
return result.lower().strip()
|
return result.lower().strip()
|
||||||
|
|
||||||
|
|
||||||
def extract_source_metadata(context: dict, artist: dict, album_info: dict) -> dict:
|
SOURCE_TAG_CONFIG = {
|
||||||
if album_info is None:
|
|
||||||
album_info = {}
|
|
||||||
|
|
||||||
cfg = get_config_manager()
|
|
||||||
logger_ = get_logger()
|
|
||||||
context = normalize_import_context(context)
|
|
||||||
original_search = get_import_original_search(context)
|
|
||||||
album_ctx = get_import_context_album(context)
|
|
||||||
track_info = get_import_track_info(context)
|
|
||||||
source = get_import_source(context)
|
|
||||||
source_ids = get_import_source_ids(context)
|
|
||||||
|
|
||||||
artist_dict = artist if isinstance(artist, dict) else {
|
|
||||||
"name": extract_artist_name(artist),
|
|
||||||
"id": getattr(artist, "id", ""),
|
|
||||||
"genres": list(getattr(artist, "genres", []) or []),
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata: Dict[str, Any] = {
|
|
||||||
"source": source,
|
|
||||||
"source_track_id": source_ids["track_id"],
|
|
||||||
"source_artist_id": source_ids["artist_id"],
|
|
||||||
"source_album_id": source_ids["album_id"],
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata["title"] = get_import_clean_title(context, album_info=album_info, default=original_search.get("title", ""))
|
|
||||||
if original_search.get("clean_title"):
|
|
||||||
logger_.info("Metadata: Using clean title: '%s'", metadata["title"])
|
|
||||||
elif album_info.get("clean_track_name"):
|
|
||||||
logger_.info("Metadata: Using album info clean name: '%s'", metadata["title"])
|
|
||||||
else:
|
|
||||||
logger_.warning("Metadata: Using original title as fallback: '%s'", metadata["title"])
|
|
||||||
|
|
||||||
artists = original_search.get("artists")
|
|
||||||
if isinstance(artists, list) and artists:
|
|
||||||
all_artists = []
|
|
||||||
for artist_item in artists:
|
|
||||||
if isinstance(artist_item, dict) and artist_item.get("name"):
|
|
||||||
all_artists.append(artist_item["name"])
|
|
||||||
elif isinstance(artist_item, str):
|
|
||||||
all_artists.append(artist_item)
|
|
||||||
else:
|
|
||||||
all_artists.append(str(artist_item))
|
|
||||||
metadata["artist"] = ", ".join(all_artists)
|
|
||||||
logger_.info("Metadata: Using all artists: '%s'", metadata["artist"])
|
|
||||||
else:
|
|
||||||
metadata["artist"] = artist_dict.get("name", "") or get_import_clean_artist(context)
|
|
||||||
logger_.info("Metadata: Using primary artist: '%s'", metadata["artist"])
|
|
||||||
|
|
||||||
raw_album_artist = artist_dict.get("name", "") or metadata["artist"]
|
|
||||||
track_info_ctx = track_info or {}
|
|
||||||
explicit_artist = track_info_ctx.get("_explicit_artist_context") if isinstance(track_info_ctx, dict) else None
|
|
||||||
album_artists_for_collab = None
|
|
||||||
|
|
||||||
if isinstance(explicit_artist, dict) and explicit_artist.get("name"):
|
|
||||||
raw_album_artist = explicit_artist["name"]
|
|
||||||
album_artists_for_collab = [explicit_artist]
|
|
||||||
elif isinstance(explicit_artist, str) and explicit_artist:
|
|
||||||
raw_album_artist = explicit_artist
|
|
||||||
album_artists_for_collab = [{"name": explicit_artist}]
|
|
||||||
elif album_ctx and isinstance(album_ctx, dict):
|
|
||||||
album_artists = album_ctx.get("artists", [])
|
|
||||||
if album_artists:
|
|
||||||
first_album_artist = album_artists[0]
|
|
||||||
if isinstance(first_album_artist, dict) and first_album_artist.get("name"):
|
|
||||||
raw_album_artist = first_album_artist["name"]
|
|
||||||
elif isinstance(first_album_artist, str) and first_album_artist:
|
|
||||||
raw_album_artist = first_album_artist
|
|
||||||
album_artists_for_collab = album_artists
|
|
||||||
|
|
||||||
collab_mode = cfg.get("file_organization.collab_artist_mode", "first")
|
|
||||||
if collab_mode == "first" and raw_album_artist:
|
|
||||||
context_artists = album_artists_for_collab or original_search.get("artists") or track_info_ctx.get("artists") or []
|
|
||||||
if len(context_artists) > 1:
|
|
||||||
first = context_artists[0]
|
|
||||||
raw_album_artist = first.get("name", first) if isinstance(first, dict) else str(first)
|
|
||||||
elif len(context_artists) == 1 and ("," in raw_album_artist or " & " in raw_album_artist):
|
|
||||||
artist_id = str(artist_dict.get("id", ""))
|
|
||||||
if source == "itunes" and artist_id.isdigit():
|
|
||||||
try:
|
|
||||||
itunes_client = get_itunes_client()
|
|
||||||
if itunes_client and hasattr(itunes_client, "resolve_primary_artist"):
|
|
||||||
resolved = itunes_client.resolve_primary_artist(artist_id)
|
|
||||||
if resolved and resolved != raw_album_artist:
|
|
||||||
raw_album_artist = resolved
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
metadata["album_artist"] = raw_album_artist
|
|
||||||
|
|
||||||
if album_info.get("is_album"):
|
|
||||||
metadata["album"] = album_info.get("album_name", "Unknown Album")
|
|
||||||
metadata["track_number"] = album_info.get("track_number", 1)
|
|
||||||
metadata["total_tracks"] = album_ctx.get("total_tracks", 1) if album_ctx else 1
|
|
||||||
logger_.info("[METADATA] Album track - track_number: %s, album: %s", metadata["track_number"], metadata["album"])
|
|
||||||
else:
|
|
||||||
if album_ctx and album_ctx.get("name"):
|
|
||||||
logger_.info("[SAFEGUARD] Using album context name instead of track title for album metadata")
|
|
||||||
metadata["album"] = album_ctx["name"]
|
|
||||||
metadata["track_number"] = album_info.get("track_number", 1) if album_info else 1
|
|
||||||
metadata["total_tracks"] = album_ctx.get("total_tracks", 1)
|
|
||||||
else:
|
|
||||||
metadata["album"] = metadata["title"]
|
|
||||||
metadata["track_number"] = 1
|
|
||||||
metadata["total_tracks"] = 1
|
|
||||||
|
|
||||||
disc_num = original_search.get("disc_number")
|
|
||||||
if disc_num is None and album_info:
|
|
||||||
disc_num = album_info.get("disc_number")
|
|
||||||
metadata["disc_number"] = disc_num if disc_num is not None else 1
|
|
||||||
|
|
||||||
if album_ctx and album_ctx.get("release_date"):
|
|
||||||
metadata["date"] = album_ctx["release_date"][:4]
|
|
||||||
|
|
||||||
genres = artist_dict.get("genres") or []
|
|
||||||
if genres:
|
|
||||||
from core.genre_filter import filter_genres
|
|
||||||
|
|
||||||
filtered = filter_genres(list(genres[:2]), cfg)
|
|
||||||
if filtered:
|
|
||||||
metadata["genre"] = ", ".join(filtered)
|
|
||||||
|
|
||||||
metadata["album_art_url"] = album_info.get("album_image_url") if album_info else None
|
|
||||||
if not metadata["album_art_url"] and album_ctx:
|
|
||||||
album_image = album_ctx.get("image_url")
|
|
||||||
if not album_image and album_ctx.get("images"):
|
|
||||||
first_image = album_ctx["images"][0]
|
|
||||||
album_image = first_image.get("url") if isinstance(first_image, dict) else None
|
|
||||||
metadata["album_art_url"] = album_image
|
|
||||||
|
|
||||||
logger_.info(
|
|
||||||
"[Metadata Summary] title='%s' | artist='%s' | album_artist='%s' | album='%s' | track=%s/%s | disc=%s",
|
|
||||||
metadata.get("title"),
|
|
||||||
metadata.get("artist"),
|
|
||||||
metadata.get("album_artist"),
|
|
||||||
metadata.get("album"),
|
|
||||||
metadata.get("track_number"),
|
|
||||||
metadata.get("total_tracks"),
|
|
||||||
metadata.get("disc_number"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
|
|
||||||
def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=None):
|
|
||||||
cfg = get_config_manager()
|
|
||||||
logger_ = get_logger()
|
|
||||||
symbols = get_mutagen_symbols()
|
|
||||||
if not symbols:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
tag_config = {
|
|
||||||
"SPOTIFY_TRACK_ID": "spotify.tags.track_id",
|
"SPOTIFY_TRACK_ID": "spotify.tags.track_id",
|
||||||
"SPOTIFY_ARTIST_ID": "spotify.tags.artist_id",
|
"SPOTIFY_ARTIST_ID": "spotify.tags.artist_id",
|
||||||
"SPOTIFY_ALBUM_ID": "spotify.tags.album_id",
|
"SPOTIFY_ALBUM_ID": "spotify.tags.album_id",
|
||||||
|
|
@ -248,9 +97,48 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
"GENIUS_TRACK_ID": "genius.tags.track_id",
|
"GENIUS_TRACK_ID": "genius.tags.track_id",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _tag_enabled(path: str) -> bool:
|
DEFAULT_SOURCE_ORDER = ["musicbrainz", "deezer", "audiodb", "tidal", "qobuz", "lastfm", "genius"]
|
||||||
|
|
||||||
|
ID3_TAG_MAP = {
|
||||||
|
"MUSICBRAINZ_RECORDING_ID": ("UFID", "http://musicbrainz.org"),
|
||||||
|
"MUSICBRAINZ_ARTIST_ID": ("TXXX", "MusicBrainz Artist Id"),
|
||||||
|
"MUSICBRAINZ_RELEASE_ID": ("TXXX", "MusicBrainz Album Id"),
|
||||||
|
"MUSICBRAINZ_RELEASEGROUPID": ("TXXX", "MusicBrainz Release Group Id"),
|
||||||
|
"MUSICBRAINZ_ALBUMARTISTID": ("TXXX", "MusicBrainz Album Artist Id"),
|
||||||
|
"MUSICBRAINZ_RELEASETRACKID": ("TXXX", "MusicBrainz Release Track Id"),
|
||||||
|
"RELEASETYPE": ("TXXX", "MusicBrainz Album Type"),
|
||||||
|
"RELEASESTATUS": ("TXXX", "MusicBrainz Album Status"),
|
||||||
|
"RELEASECOUNTRY": ("TXXX", "MusicBrainz Album Release Country"),
|
||||||
|
"ORIGINALDATE": ("TDOR", None),
|
||||||
|
"MEDIA": ("TMED", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
VORBIS_TAG_MAP = {
|
||||||
|
"MUSICBRAINZ_RECORDING_ID": "MUSICBRAINZ_TRACKID",
|
||||||
|
"MUSICBRAINZ_ARTIST_ID": "MUSICBRAINZ_ARTISTID",
|
||||||
|
"MUSICBRAINZ_RELEASE_ID": "MUSICBRAINZ_ALBUMID",
|
||||||
|
"MUSICBRAINZ_RELEASEGROUPID": "MUSICBRAINZ_RELEASEGROUPID",
|
||||||
|
"MUSICBRAINZ_ALBUMARTISTID": "MUSICBRAINZ_ALBUMARTISTID",
|
||||||
|
"MUSICBRAINZ_RELEASETRACKID": "MUSICBRAINZ_RELEASETRACKID",
|
||||||
|
}
|
||||||
|
|
||||||
|
MP4_TAG_MAP = {
|
||||||
|
"MUSICBRAINZ_RECORDING_ID": "MusicBrainz Track Id",
|
||||||
|
"MUSICBRAINZ_ARTIST_ID": "MusicBrainz Artist Id",
|
||||||
|
"MUSICBRAINZ_RELEASE_ID": "MusicBrainz Album Id",
|
||||||
|
"MUSICBRAINZ_RELEASEGROUPID": "MusicBrainz Release Group Id",
|
||||||
|
"MUSICBRAINZ_ALBUMARTISTID": "MusicBrainz Album Artist Id",
|
||||||
|
"MUSICBRAINZ_RELEASETRACKID": "MusicBrainz Release Track Id",
|
||||||
|
"RELEASETYPE": "MusicBrainz Album Type",
|
||||||
|
"RELEASESTATUS": "MusicBrainz Album Status",
|
||||||
|
"RELEASECOUNTRY": "MusicBrainz Album Release Country",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _tag_enabled(cfg, path: str) -> bool:
|
||||||
return cfg.get(path, True) is not False
|
return cfg.get(path, True) is not False
|
||||||
|
|
||||||
|
|
||||||
def _names_match(a: str, b: str, threshold: float = 0.75) -> bool:
|
def _names_match(a: str, b: str, threshold: float = 0.75) -> bool:
|
||||||
if not a or not b:
|
if not a or not b:
|
||||||
return False
|
return False
|
||||||
|
|
@ -259,9 +147,10 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
norm = lambda s: re.sub(r"[^a-z0-9 ]", "", re.sub(r"\(.*?\)", "", s).lower()).strip()
|
norm = lambda s: re.sub(r"[^a-z0-9 ]", "", re.sub(r"\(.*?\)", "", s).lower()).strip()
|
||||||
return SequenceMatcher(None, norm(a), norm(b)).ratio() >= threshold
|
return SequenceMatcher(None, norm(a), norm(b)).ratio() >= threshold
|
||||||
|
|
||||||
context = normalize_import_context(context)
|
|
||||||
source = (metadata.get("source") or "").strip().lower()
|
def _collect_source_ids(metadata: dict, cfg) -> dict:
|
||||||
source_ids = {}
|
source_ids = {}
|
||||||
|
source = (metadata.get("source") or "").strip().lower()
|
||||||
if source:
|
if source:
|
||||||
source_tag_names = get_source_tag_names(source)
|
source_tag_names = get_source_tag_names(source)
|
||||||
source_track_id = metadata.get("source_track_id")
|
source_track_id = metadata.get("source_track_id")
|
||||||
|
|
@ -291,59 +180,20 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
if metadata.get("itunes_album_id"):
|
if metadata.get("itunes_album_id"):
|
||||||
source_ids["ITUNES_ALBUM_ID"] = metadata["itunes_album_id"]
|
source_ids["ITUNES_ALBUM_ID"] = metadata["itunes_album_id"]
|
||||||
|
|
||||||
track_title = metadata.get("title", "")
|
return source_ids
|
||||||
artist_name = metadata.get("album_artist", "") or metadata.get("artist", "")
|
|
||||||
track_info = get_import_track_info(context)
|
|
||||||
explicit_artist = (track_info or {}).get("_explicit_artist_context") if isinstance(track_info, dict) else None
|
|
||||||
batch_artist_name = None
|
|
||||||
if isinstance(explicit_artist, dict) and explicit_artist.get("name"):
|
|
||||||
batch_artist_name = explicit_artist["name"]
|
|
||||||
elif isinstance(explicit_artist, str) and explicit_artist:
|
|
||||||
batch_artist_name = explicit_artist
|
|
||||||
|
|
||||||
pp = {
|
|
||||||
"id_tags": source_ids,
|
|
||||||
"track_title": track_title,
|
|
||||||
"artist_name": artist_name,
|
|
||||||
"batch_artist_name": batch_artist_name,
|
|
||||||
"metadata": metadata,
|
|
||||||
"recording_mbid": None,
|
|
||||||
"artist_mbid": None,
|
|
||||||
"release_mbid": "",
|
|
||||||
"mb_genres": [],
|
|
||||||
"isrc": None,
|
|
||||||
"deezer_bpm": None,
|
|
||||||
"deezer_isrc": None,
|
|
||||||
"audiodb_mood": None,
|
|
||||||
"audiodb_style": None,
|
|
||||||
"audiodb_genre": None,
|
|
||||||
"tidal_isrc": None,
|
|
||||||
"tidal_copyright": None,
|
|
||||||
"qobuz_isrc": None,
|
|
||||||
"qobuz_copyright": None,
|
|
||||||
"qobuz_label": None,
|
|
||||||
"lastfm_tags": [],
|
|
||||||
"lastfm_url": None,
|
|
||||||
"genius_url": None,
|
|
||||||
"release_year": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
source_order = cfg.get("metadata_enhancement.post_process_order", None)
|
def _process_musicbrainz_source(pp: dict, metadata: dict, cfg, runtime, track_title: str, artist_name: str) -> None:
|
||||||
if not isinstance(source_order, list) or not source_order:
|
|
||||||
source_order = ["musicbrainz", "deezer", "audiodb", "tidal", "qobuz", "lastfm", "genius"]
|
|
||||||
|
|
||||||
db = get_database()
|
|
||||||
|
|
||||||
for source_name in source_order:
|
|
||||||
if source_name == "musicbrainz":
|
|
||||||
if cfg.get("musicbrainz.embed_tags", True) is False:
|
if cfg.get("musicbrainz.embed_tags", True) is False:
|
||||||
continue
|
return
|
||||||
if not track_title or not artist_name:
|
if not track_title or not artist_name:
|
||||||
continue
|
return
|
||||||
|
|
||||||
mb_worker = getattr(runtime, "mb_worker", None)
|
mb_worker = getattr(runtime, "mb_worker", None)
|
||||||
mb_service = mb_worker.mb_service if mb_worker else None
|
mb_service = mb_worker.mb_service if mb_worker else None
|
||||||
if not mb_service:
|
if not mb_service:
|
||||||
continue
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = mb_service.match_recording(track_title, artist_name)
|
result = mb_service.match_recording(track_title, artist_name)
|
||||||
if result and result.get("mbid"):
|
if result and result.get("mbid"):
|
||||||
|
|
@ -367,7 +217,7 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
album_name_for_mb = metadata.get("album", "")
|
album_name_for_mb = metadata.get("album", "")
|
||||||
if album_name_for_mb:
|
if album_name_for_mb:
|
||||||
artist_key = (pp.get("batch_artist_name") or artist_name).lower().strip()
|
artist_key = (pp.get("batch_artist_name") or artist_name).lower().strip()
|
||||||
rc_key_norm = (_normalize_album_cache_key(album_name_for_mb), artist_key)
|
rc_key_norm = (normalize_album_cache_key(album_name_for_mb), artist_key)
|
||||||
rc_key_exact = (album_name_for_mb.lower().strip(), artist_key)
|
rc_key_exact = (album_name_for_mb.lower().strip(), artist_key)
|
||||||
with mb_release_cache_lock:
|
with mb_release_cache_lock:
|
||||||
cached = mb_release_cache.get(rc_key_norm)
|
cached = mb_release_cache.get(rc_key_norm)
|
||||||
|
|
@ -456,19 +306,20 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger_.error("MusicBrainz lookup failed (non-fatal): %s", exc)
|
logger.error("MusicBrainz lookup failed (non-fatal): %s", exc)
|
||||||
continue
|
|
||||||
|
|
||||||
if source_name == "deezer":
|
|
||||||
|
def _process_deezer_source(pp: dict, metadata: dict, cfg, runtime, track_title: str, artist_name: str) -> None:
|
||||||
if cfg.get("deezer.embed_tags", True) is False:
|
if cfg.get("deezer.embed_tags", True) is False:
|
||||||
continue
|
return
|
||||||
if not track_title or not artist_name:
|
if not track_title or not artist_name:
|
||||||
continue
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
deezer_worker = getattr(runtime, "deezer_worker", None)
|
deezer_worker = getattr(runtime, "deezer_worker", None)
|
||||||
dz_client = deezer_worker.client if deezer_worker else None
|
dz_client = deezer_worker.client if deezer_worker else None
|
||||||
if not dz_client:
|
if not dz_client:
|
||||||
continue
|
return
|
||||||
dz_result = dz_client.search_track(artist_name, track_title)
|
dz_result = dz_client.search_track(artist_name, track_title)
|
||||||
if dz_result and _names_match(dz_result.get("title", ""), track_title) and _names_match(dz_result.get("artist", {}).get("name", ""), artist_name):
|
if dz_result and _names_match(dz_result.get("title", ""), track_title) and _names_match(dz_result.get("artist", {}).get("name", ""), artist_name):
|
||||||
dz_track_id = dz_result["id"]
|
dz_track_id = dz_result["id"]
|
||||||
|
|
@ -490,19 +341,20 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
if len(dz_release) >= 4 and dz_release[:4].isdigit():
|
if len(dz_release) >= 4 and dz_release[:4].isdigit():
|
||||||
pp["release_year"] = dz_release[:4]
|
pp["release_year"] = dz_release[:4]
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger_.error("Deezer lookup failed (non-fatal): %s", exc)
|
logger.error("Deezer lookup failed (non-fatal): %s", exc)
|
||||||
continue
|
|
||||||
|
|
||||||
if source_name == "audiodb":
|
|
||||||
|
def _process_audiodb_source(pp: dict, metadata: dict, cfg, runtime, track_title: str, artist_name: str) -> None:
|
||||||
if cfg.get("audiodb.embed_tags", True) is False:
|
if cfg.get("audiodb.embed_tags", True) is False:
|
||||||
continue
|
return
|
||||||
if not track_title or not artist_name:
|
if not track_title or not artist_name:
|
||||||
continue
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
audiodb_worker = getattr(runtime, "audiodb_worker", None)
|
audiodb_worker = getattr(runtime, "audiodb_worker", None)
|
||||||
adb_client = audiodb_worker.client if audiodb_worker else None
|
adb_client = audiodb_worker.client if audiodb_worker else None
|
||||||
if not adb_client:
|
if not adb_client:
|
||||||
continue
|
return
|
||||||
adb_result = adb_client.search_track(artist_name, track_title)
|
adb_result = adb_client.search_track(artist_name, track_title)
|
||||||
if adb_result and _names_match(adb_result.get("strTrack", ""), track_title) and _names_match(adb_result.get("strArtist", ""), artist_name):
|
if adb_result and _names_match(adb_result.get("strTrack", ""), track_title) and _names_match(adb_result.get("strArtist", ""), artist_name):
|
||||||
adb_track_id = adb_result.get("idTrack")
|
adb_track_id = adb_result.get("idTrack")
|
||||||
|
|
@ -520,18 +372,19 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
pp["audiodb_style"] = adb_result.get("strStyle") or None
|
pp["audiodb_style"] = adb_result.get("strStyle") or None
|
||||||
pp["audiodb_genre"] = adb_result.get("strGenre") or None
|
pp["audiodb_genre"] = adb_result.get("strGenre") or None
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger_.error("AudioDB lookup failed (non-fatal): %s", exc)
|
logger.error("AudioDB lookup failed (non-fatal): %s", exc)
|
||||||
continue
|
|
||||||
|
|
||||||
if source_name == "tidal":
|
|
||||||
|
def _process_tidal_source(pp: dict, metadata: dict, cfg, runtime, track_title: str, artist_name: str) -> None:
|
||||||
if cfg.get("tidal.embed_tags", True) is False:
|
if cfg.get("tidal.embed_tags", True) is False:
|
||||||
continue
|
return
|
||||||
if not track_title or not artist_name:
|
if not track_title or not artist_name:
|
||||||
continue
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tidal_client = getattr(runtime, "tidal_client", None)
|
tidal_client = getattr(runtime, "tidal_client", None)
|
||||||
if not (tidal_client and tidal_client.is_authenticated()):
|
if not (tidal_client and tidal_client.is_authenticated()):
|
||||||
continue
|
return
|
||||||
td_result = tidal_client.search_track(artist_name, track_title)
|
td_result = tidal_client.search_track(artist_name, track_title)
|
||||||
if td_result and _names_match(td_result.get("title", ""), track_title):
|
if td_result and _names_match(td_result.get("title", ""), track_title):
|
||||||
td_track_id = td_result.get("id")
|
td_track_id = td_result.get("id")
|
||||||
|
|
@ -556,19 +409,20 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
if len(td_release) >= 4 and td_release[:4].isdigit():
|
if len(td_release) >= 4 and td_release[:4].isdigit():
|
||||||
pp["release_year"] = td_release[:4]
|
pp["release_year"] = td_release[:4]
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger_.error("Tidal lookup failed (non-fatal): %s", exc)
|
logger.error("Tidal lookup failed (non-fatal): %s", exc)
|
||||||
continue
|
|
||||||
|
|
||||||
if source_name == "qobuz":
|
|
||||||
|
def _process_qobuz_source(pp: dict, metadata: dict, cfg, runtime, track_title: str, artist_name: str) -> None:
|
||||||
if cfg.get("qobuz.embed_tags", True) is False:
|
if cfg.get("qobuz.embed_tags", True) is False:
|
||||||
continue
|
return
|
||||||
if not track_title or not artist_name:
|
if not track_title or not artist_name:
|
||||||
continue
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
qobuz_worker = getattr(runtime, "qobuz_enrichment_worker", None)
|
qobuz_worker = getattr(runtime, "qobuz_enrichment_worker", None)
|
||||||
qz_client = qobuz_worker.client if qobuz_worker else None
|
qz_client = qobuz_worker.client if qobuz_worker else None
|
||||||
if not (qz_client and qz_client.is_authenticated()):
|
if not (qz_client and qz_client.is_authenticated()):
|
||||||
continue
|
return
|
||||||
qz_result = qz_client.search_track(artist_name, track_title)
|
qz_result = qz_client.search_track(artist_name, track_title)
|
||||||
if qz_result:
|
if qz_result:
|
||||||
qz_performer = qz_result.get("performer") or {}
|
qz_performer = qz_result.get("performer") or {}
|
||||||
|
|
@ -602,23 +456,25 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
qz_ts = qz_album.get("released_at")
|
qz_ts = qz_album.get("released_at")
|
||||||
if qz_ts and isinstance(qz_ts, (int, float)) and qz_ts > 0:
|
if qz_ts and isinstance(qz_ts, (int, float)) and qz_ts > 0:
|
||||||
import datetime as _dt
|
import datetime as _dt
|
||||||
|
|
||||||
qz_release = str(_dt.datetime.utcfromtimestamp(qz_ts).year)
|
qz_release = str(_dt.datetime.utcfromtimestamp(qz_ts).year)
|
||||||
if len(qz_release) >= 4 and qz_release[:4].isdigit():
|
if len(qz_release) >= 4 and qz_release[:4].isdigit():
|
||||||
pp["release_year"] = qz_release[:4]
|
pp["release_year"] = qz_release[:4]
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger_.error("Qobuz lookup failed (non-fatal): %s", exc)
|
logger.error("Qobuz lookup failed (non-fatal): %s", exc)
|
||||||
continue
|
|
||||||
|
|
||||||
if source_name == "lastfm":
|
|
||||||
|
def _process_lastfm_source(pp: dict, metadata: dict, cfg, runtime, track_title: str, artist_name: str) -> None:
|
||||||
if cfg.get("lastfm.embed_tags", True) is False:
|
if cfg.get("lastfm.embed_tags", True) is False:
|
||||||
continue
|
return
|
||||||
if not track_title or not artist_name:
|
if not track_title or not artist_name:
|
||||||
continue
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
lastfm_worker = getattr(runtime, "lastfm_worker", None)
|
lastfm_worker = getattr(runtime, "lastfm_worker", None)
|
||||||
lf_client = lastfm_worker.client if lastfm_worker else None
|
lf_client = lastfm_worker.client if lastfm_worker else None
|
||||||
if not lf_client:
|
if not lf_client:
|
||||||
continue
|
return
|
||||||
lf_result = lf_client.get_track_info(artist_name, track_title)
|
lf_result = lf_client.get_track_info(artist_name, track_title)
|
||||||
if lf_result:
|
if lf_result:
|
||||||
lf_url = lf_result.get("url")
|
lf_url = lf_result.get("url")
|
||||||
|
|
@ -632,24 +488,25 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
elif isinstance(tag_list, dict) and tag_list.get("name"):
|
elif isinstance(tag_list, dict) and tag_list.get("name"):
|
||||||
pp["lastfm_tags"] = [tag_list["name"]]
|
pp["lastfm_tags"] = [tag_list["name"]]
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger_.error("Last.fm lookup failed (non-fatal): %s", exc)
|
logger.error("Last.fm lookup failed (non-fatal): %s", exc)
|
||||||
continue
|
|
||||||
|
|
||||||
if source_name == "genius":
|
|
||||||
|
def _process_genius_source(pp: dict, metadata: dict, cfg, runtime, track_title: str, artist_name: str) -> None:
|
||||||
if cfg.get("genius.embed_tags", True) is False:
|
if cfg.get("genius.embed_tags", True) is False:
|
||||||
continue
|
return
|
||||||
if not track_title or not artist_name:
|
if not track_title or not artist_name:
|
||||||
continue
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import core.genius_client as _genius_module
|
import core.genius_client as _genius_module
|
||||||
|
|
||||||
if time.time() < _genius_module._rate_limit_until:
|
if time.time() < _genius_module._rate_limit_until:
|
||||||
logger_.info("Genius rate-limited, skipping (non-blocking)")
|
logger.info("Genius rate-limited, skipping (non-blocking)")
|
||||||
continue
|
return
|
||||||
genius_worker = getattr(runtime, "genius_worker", None)
|
genius_worker = getattr(runtime, "genius_worker", None)
|
||||||
g_client = genius_worker.client if genius_worker else None
|
g_client = genius_worker.client if genius_worker else None
|
||||||
if not g_client:
|
if not g_client:
|
||||||
continue
|
return
|
||||||
g_result = g_client.search_song(artist_name, track_title)
|
g_result = g_client.search_song(artist_name, track_title)
|
||||||
if g_result:
|
if g_result:
|
||||||
g_id = g_result.get("id")
|
g_id = g_result.get("id")
|
||||||
|
|
@ -659,56 +516,40 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
if g_url:
|
if g_url:
|
||||||
pp["genius_url"] = g_url
|
pp["genius_url"] = g_url
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger_.error("Genius lookup failed (non-fatal): %s", exc)
|
logger.error("Genius lookup failed (non-fatal): %s", exc)
|
||||||
continue
|
|
||||||
|
|
||||||
if not pp["id_tags"] and not pp["deezer_bpm"] and not pp["deezer_isrc"] and not pp["audiodb_mood"] and not pp["audiodb_style"]:
|
|
||||||
return
|
|
||||||
|
|
||||||
|
def _process_source_enrichment(source_name: str, pp: dict, metadata: dict, cfg, runtime, track_title: str, artist_name: str) -> None:
|
||||||
|
if source_name == "musicbrainz":
|
||||||
|
_process_musicbrainz_source(pp, metadata, cfg, runtime, track_title, artist_name)
|
||||||
|
elif source_name == "deezer":
|
||||||
|
_process_deezer_source(pp, metadata, cfg, runtime, track_title, artist_name)
|
||||||
|
elif source_name == "audiodb":
|
||||||
|
_process_audiodb_source(pp, metadata, cfg, runtime, track_title, artist_name)
|
||||||
|
elif source_name == "tidal":
|
||||||
|
_process_tidal_source(pp, metadata, cfg, runtime, track_title, artist_name)
|
||||||
|
elif source_name == "qobuz":
|
||||||
|
_process_qobuz_source(pp, metadata, cfg, runtime, track_title, artist_name)
|
||||||
|
elif source_name == "lastfm":
|
||||||
|
_process_lastfm_source(pp, metadata, cfg, runtime, track_title, artist_name)
|
||||||
|
elif source_name == "genius":
|
||||||
|
_process_genius_source(pp, metadata, cfg, runtime, track_title, artist_name)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_embedded_metadata(audio_file, metadata: dict, pp: dict, cfg, symbols):
|
||||||
filtered_tags: Dict[str, str] = {}
|
filtered_tags: Dict[str, str] = {}
|
||||||
for tag_name, value in pp["id_tags"].items():
|
for tag_name, value in pp["id_tags"].items():
|
||||||
config_path = tag_config.get(tag_name)
|
config_path = SOURCE_TAG_CONFIG.get(tag_name)
|
||||||
if config_path and not _tag_enabled(config_path):
|
if config_path and not _tag_enabled(cfg, config_path):
|
||||||
continue
|
continue
|
||||||
filtered_tags[tag_name] = value
|
filtered_tags[tag_name] = value
|
||||||
|
|
||||||
written = []
|
written = []
|
||||||
id3_tag_map = {
|
release_year = pp["release_year"]
|
||||||
"MUSICBRAINZ_RECORDING_ID": ("UFID", "http://musicbrainz.org"),
|
|
||||||
"MUSICBRAINZ_ARTIST_ID": ("TXXX", "MusicBrainz Artist Id"),
|
|
||||||
"MUSICBRAINZ_RELEASE_ID": ("TXXX", "MusicBrainz Album Id"),
|
|
||||||
"MUSICBRAINZ_RELEASEGROUPID": ("TXXX", "MusicBrainz Release Group Id"),
|
|
||||||
"MUSICBRAINZ_ALBUMARTISTID": ("TXXX", "MusicBrainz Album Artist Id"),
|
|
||||||
"MUSICBRAINZ_RELEASETRACKID": ("TXXX", "MusicBrainz Release Track Id"),
|
|
||||||
"RELEASETYPE": ("TXXX", "MusicBrainz Album Type"),
|
|
||||||
"RELEASESTATUS": ("TXXX", "MusicBrainz Album Status"),
|
|
||||||
"RELEASECOUNTRY": ("TXXX", "MusicBrainz Album Release Country"),
|
|
||||||
"ORIGINALDATE": ("TDOR", None),
|
|
||||||
"MEDIA": ("TMED", None),
|
|
||||||
}
|
|
||||||
vorbis_tag_map = {
|
|
||||||
"MUSICBRAINZ_RECORDING_ID": "MUSICBRAINZ_TRACKID",
|
|
||||||
"MUSICBRAINZ_ARTIST_ID": "MUSICBRAINZ_ARTISTID",
|
|
||||||
"MUSICBRAINZ_RELEASE_ID": "MUSICBRAINZ_ALBUMID",
|
|
||||||
"MUSICBRAINZ_RELEASEGROUPID": "MUSICBRAINZ_RELEASEGROUPID",
|
|
||||||
"MUSICBRAINZ_ALBUMARTISTID": "MUSICBRAINZ_ALBUMARTISTID",
|
|
||||||
"MUSICBRAINZ_RELEASETRACKID": "MUSICBRAINZ_RELEASETRACKID",
|
|
||||||
}
|
|
||||||
mp4_tag_map = {
|
|
||||||
"MUSICBRAINZ_RECORDING_ID": "MusicBrainz Track Id",
|
|
||||||
"MUSICBRAINZ_ARTIST_ID": "MusicBrainz Artist Id",
|
|
||||||
"MUSICBRAINZ_RELEASE_ID": "MusicBrainz Album Id",
|
|
||||||
"MUSICBRAINZ_RELEASEGROUPID": "MusicBrainz Release Group Id",
|
|
||||||
"MUSICBRAINZ_ALBUMARTISTID": "MusicBrainz Album Artist Id",
|
|
||||||
"MUSICBRAINZ_RELEASETRACKID": "MusicBrainz Release Track Id",
|
|
||||||
"RELEASETYPE": "MusicBrainz Album Type",
|
|
||||||
"RELEASESTATUS": "MusicBrainz Album Status",
|
|
||||||
"RELEASECOUNTRY": "MusicBrainz Album Release Country",
|
|
||||||
}
|
|
||||||
|
|
||||||
if isinstance(audio_file.tags, symbols.ID3):
|
if isinstance(audio_file.tags, symbols.ID3):
|
||||||
for tag_name, value in filtered_tags.items():
|
for tag_name, value in filtered_tags.items():
|
||||||
spec = id3_tag_map.get(tag_name)
|
spec = ID3_TAG_MAP.get(tag_name)
|
||||||
if spec:
|
if spec:
|
||||||
frame_type, desc = spec
|
frame_type, desc = spec
|
||||||
if frame_type == "UFID":
|
if frame_type == "UFID":
|
||||||
|
|
@ -727,22 +568,19 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
audio_file.tags.add(symbols.TXXX(encoding=3, desc=tag_name, text=[str(value)]))
|
audio_file.tags.add(symbols.TXXX(encoding=3, desc=tag_name, text=[str(value)]))
|
||||||
written.append(f"TXXX:{tag_name}")
|
written.append(f"TXXX:{tag_name}")
|
||||||
elif isinstance(audio_file, symbols.MP4):
|
elif isinstance(audio_file, symbols.MP4):
|
||||||
# Keep the dedicated MP4 path last so the same tag maps can be reused.
|
|
||||||
for tag_name, value in filtered_tags.items():
|
for tag_name, value in filtered_tags.items():
|
||||||
key = f"----:com.apple.iTunes:{mp4_tag_map.get(tag_name, tag_name)}"
|
key = f"----:com.apple.iTunes:{MP4_TAG_MAP.get(tag_name, tag_name)}"
|
||||||
audio_file[key] = [symbols.MP4FreeForm(str(value).encode("utf-8"))]
|
audio_file[key] = [symbols.MP4FreeForm(str(value).encode("utf-8"))]
|
||||||
written.append(key)
|
written.append(key)
|
||||||
elif is_vorbis_like(audio_file, symbols):
|
elif is_vorbis_like(audio_file, symbols):
|
||||||
for tag_name, value in filtered_tags.items():
|
for tag_name, value in filtered_tags.items():
|
||||||
audio_file[vorbis_tag_map.get(tag_name, tag_name)] = [str(value)]
|
audio_file[VORBIS_TAG_MAP.get(tag_name, tag_name)] = [str(value)]
|
||||||
written.append(vorbis_tag_map.get(tag_name, tag_name))
|
written.append(VORBIS_TAG_MAP.get(tag_name, tag_name))
|
||||||
|
|
||||||
if written:
|
if written:
|
||||||
logger_.info("Embedded IDs: %s", ", ".join(written))
|
logger.info("Embedded IDs: %s", ", ".join(written))
|
||||||
|
|
||||||
release_year = pp["release_year"]
|
if release_year and not metadata.get("date"):
|
||||||
needs_date_tag = bool(release_year and not metadata.get("date"))
|
|
||||||
if needs_date_tag:
|
|
||||||
metadata["date"] = release_year
|
metadata["date"] = release_year
|
||||||
if isinstance(audio_file.tags, symbols.ID3):
|
if isinstance(audio_file.tags, symbols.ID3):
|
||||||
audio_file.tags.add(symbols.TDRC(encoding=3, text=[release_year]))
|
audio_file.tags.add(symbols.TDRC(encoding=3, text=[release_year]))
|
||||||
|
|
@ -750,9 +588,9 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
audio_file["date"] = [release_year]
|
audio_file["date"] = [release_year]
|
||||||
elif isinstance(audio_file, symbols.MP4):
|
elif isinstance(audio_file, symbols.MP4):
|
||||||
audio_file["\xa9day"] = [release_year]
|
audio_file["\xa9day"] = [release_year]
|
||||||
logger_.info("Date tag: %s", release_year)
|
logger.info("Date tag: %s", release_year)
|
||||||
|
|
||||||
if _tag_enabled("deezer.tags.bpm") and pp["deezer_bpm"] and pp["deezer_bpm"] > 0:
|
if _tag_enabled(cfg, "deezer.tags.bpm") and pp["deezer_bpm"] and pp["deezer_bpm"] > 0:
|
||||||
bpm_int = int(pp["deezer_bpm"])
|
bpm_int = int(pp["deezer_bpm"])
|
||||||
if isinstance(audio_file.tags, symbols.ID3):
|
if isinstance(audio_file.tags, symbols.ID3):
|
||||||
audio_file.tags.add(symbols.TBPM(encoding=3, text=[str(bpm_int)]))
|
audio_file.tags.add(symbols.TBPM(encoding=3, text=[str(bpm_int)]))
|
||||||
|
|
@ -760,9 +598,9 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
audio_file["BPM"] = [str(bpm_int)]
|
audio_file["BPM"] = [str(bpm_int)]
|
||||||
elif isinstance(audio_file, symbols.MP4):
|
elif isinstance(audio_file, symbols.MP4):
|
||||||
audio_file["tmpo"] = [bpm_int]
|
audio_file["tmpo"] = [bpm_int]
|
||||||
logger_.info("BPM: %s", bpm_int)
|
logger.info("BPM: %s", bpm_int)
|
||||||
|
|
||||||
if _tag_enabled("audiodb.tags.mood") and pp["audiodb_mood"]:
|
if _tag_enabled(cfg, "audiodb.tags.mood") and pp["audiodb_mood"]:
|
||||||
if isinstance(audio_file.tags, symbols.ID3):
|
if isinstance(audio_file.tags, symbols.ID3):
|
||||||
audio_file.tags.add(symbols.TXXX(encoding=3, desc="MOOD", text=[pp["audiodb_mood"]]))
|
audio_file.tags.add(symbols.TXXX(encoding=3, desc="MOOD", text=[pp["audiodb_mood"]]))
|
||||||
elif is_vorbis_like(audio_file, symbols):
|
elif is_vorbis_like(audio_file, symbols):
|
||||||
|
|
@ -770,7 +608,7 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
elif isinstance(audio_file, symbols.MP4):
|
elif isinstance(audio_file, symbols.MP4):
|
||||||
audio_file["----:com.apple.iTunes:MOOD"] = [symbols.MP4FreeForm(pp["audiodb_mood"].encode("utf-8"))]
|
audio_file["----:com.apple.iTunes:MOOD"] = [symbols.MP4FreeForm(pp["audiodb_mood"].encode("utf-8"))]
|
||||||
|
|
||||||
if _tag_enabled("audiodb.tags.style") and pp["audiodb_style"]:
|
if _tag_enabled(cfg, "audiodb.tags.style") and pp["audiodb_style"]:
|
||||||
if isinstance(audio_file.tags, symbols.ID3):
|
if isinstance(audio_file.tags, symbols.ID3):
|
||||||
audio_file.tags.add(symbols.TXXX(encoding=3, desc="STYLE", text=[pp["audiodb_style"]]))
|
audio_file.tags.add(symbols.TXXX(encoding=3, desc="STYLE", text=[pp["audiodb_style"]]))
|
||||||
elif is_vorbis_like(audio_file, symbols):
|
elif is_vorbis_like(audio_file, symbols):
|
||||||
|
|
@ -778,13 +616,13 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
elif isinstance(audio_file, symbols.MP4):
|
elif isinstance(audio_file, symbols.MP4):
|
||||||
audio_file["----:com.apple.iTunes:STYLE"] = [symbols.MP4FreeForm(pp["audiodb_style"].encode("utf-8"))]
|
audio_file["----:com.apple.iTunes:STYLE"] = [symbols.MP4FreeForm(pp["audiodb_style"].encode("utf-8"))]
|
||||||
|
|
||||||
if _tag_enabled("metadata_enhancement.tags.genre_merge"):
|
if _tag_enabled(cfg, "metadata_enhancement.tags.genre_merge"):
|
||||||
enrichment_genres = []
|
enrichment_genres = []
|
||||||
if _tag_enabled("musicbrainz.tags.genres"):
|
if _tag_enabled(cfg, "musicbrainz.tags.genres"):
|
||||||
enrichment_genres += pp["mb_genres"]
|
enrichment_genres += pp["mb_genres"]
|
||||||
if pp["audiodb_genre"] and _tag_enabled("audiodb.tags.genre"):
|
if pp["audiodb_genre"] and _tag_enabled(cfg, "audiodb.tags.genre"):
|
||||||
enrichment_genres.append(pp["audiodb_genre"])
|
enrichment_genres.append(pp["audiodb_genre"])
|
||||||
if _tag_enabled("lastfm.tags.genres"):
|
if _tag_enabled(cfg, "lastfm.tags.genres"):
|
||||||
enrichment_genres += pp["lastfm_tags"]
|
enrichment_genres += pp["lastfm_tags"]
|
||||||
if enrichment_genres:
|
if enrichment_genres:
|
||||||
from core.genre_filter import filter_genres as _filter_genres
|
from core.genre_filter import filter_genres as _filter_genres
|
||||||
|
|
@ -808,16 +646,16 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
audio_file["GENRE"] = [genre_string]
|
audio_file["GENRE"] = [genre_string]
|
||||||
elif isinstance(audio_file, symbols.MP4):
|
elif isinstance(audio_file, symbols.MP4):
|
||||||
audio_file["\xa9gen"] = [genre_string]
|
audio_file["\xa9gen"] = [genre_string]
|
||||||
logger_.info("Genres merged: %s", genre_string)
|
logger.info("Genres merged: %s", genre_string)
|
||||||
|
|
||||||
isrc_candidates = []
|
isrc_candidates = []
|
||||||
if pp["isrc"] and _tag_enabled("musicbrainz.tags.isrc"):
|
if pp["isrc"] and _tag_enabled(cfg, "musicbrainz.tags.isrc"):
|
||||||
isrc_candidates.append(("MusicBrainz", pp["isrc"]))
|
isrc_candidates.append(("MusicBrainz", pp["isrc"]))
|
||||||
if pp["deezer_isrc"] and _tag_enabled("deezer.tags.isrc"):
|
if pp["deezer_isrc"] and _tag_enabled(cfg, "deezer.tags.isrc"):
|
||||||
isrc_candidates.append(("Deezer", pp["deezer_isrc"]))
|
isrc_candidates.append(("Deezer", pp["deezer_isrc"]))
|
||||||
if pp["tidal_isrc"] and _tag_enabled("tidal.tags.isrc"):
|
if pp["tidal_isrc"] and _tag_enabled(cfg, "tidal.tags.isrc"):
|
||||||
isrc_candidates.append(("Tidal", pp["tidal_isrc"]))
|
isrc_candidates.append(("Tidal", pp["tidal_isrc"]))
|
||||||
if pp["qobuz_isrc"] and _tag_enabled("qobuz.tags.isrc"):
|
if pp["qobuz_isrc"] and _tag_enabled(cfg, "qobuz.tags.isrc"):
|
||||||
isrc_candidates.append(("Qobuz", pp["qobuz_isrc"]))
|
isrc_candidates.append(("Qobuz", pp["qobuz_isrc"]))
|
||||||
if isrc_candidates:
|
if isrc_candidates:
|
||||||
isrc_source, final_isrc = isrc_candidates[0]
|
isrc_source, final_isrc = isrc_candidates[0]
|
||||||
|
|
@ -827,12 +665,12 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
audio_file["ISRC"] = [final_isrc]
|
audio_file["ISRC"] = [final_isrc]
|
||||||
elif isinstance(audio_file, symbols.MP4):
|
elif isinstance(audio_file, symbols.MP4):
|
||||||
audio_file["----:com.apple.iTunes:ISRC"] = [symbols.MP4FreeForm(final_isrc.encode("utf-8"))]
|
audio_file["----:com.apple.iTunes:ISRC"] = [symbols.MP4FreeForm(final_isrc.encode("utf-8"))]
|
||||||
logger_.info("ISRC (%s): %s", isrc_source, final_isrc)
|
logger.info("ISRC (%s): %s", isrc_source, final_isrc)
|
||||||
|
|
||||||
copyright_candidates = []
|
copyright_candidates = []
|
||||||
if pp["tidal_copyright"] and _tag_enabled("tidal.tags.copyright"):
|
if pp["tidal_copyright"] and _tag_enabled(cfg, "tidal.tags.copyright"):
|
||||||
copyright_candidates.append(("Tidal", pp["tidal_copyright"]))
|
copyright_candidates.append(("Tidal", pp["tidal_copyright"]))
|
||||||
if pp["qobuz_copyright"] and _tag_enabled("qobuz.tags.copyright"):
|
if pp["qobuz_copyright"] and _tag_enabled(cfg, "qobuz.tags.copyright"):
|
||||||
copyright_candidates.append(("Qobuz", pp["qobuz_copyright"]))
|
copyright_candidates.append(("Qobuz", pp["qobuz_copyright"]))
|
||||||
if copyright_candidates:
|
if copyright_candidates:
|
||||||
copyright_source, final_copyright = copyright_candidates[0]
|
copyright_source, final_copyright = copyright_candidates[0]
|
||||||
|
|
@ -842,9 +680,9 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
audio_file["COPYRIGHT"] = [final_copyright]
|
audio_file["COPYRIGHT"] = [final_copyright]
|
||||||
elif isinstance(audio_file, symbols.MP4):
|
elif isinstance(audio_file, symbols.MP4):
|
||||||
audio_file["cprt"] = [final_copyright]
|
audio_file["cprt"] = [final_copyright]
|
||||||
logger_.info("Copyright (%s): %s", copyright_source, final_copyright[:60])
|
logger.info("Copyright (%s): %s", copyright_source, final_copyright[:60])
|
||||||
|
|
||||||
if _tag_enabled("qobuz.tags.label") and pp["qobuz_label"]:
|
if _tag_enabled(cfg, "qobuz.tags.label") and pp["qobuz_label"]:
|
||||||
if isinstance(audio_file.tags, symbols.ID3):
|
if isinstance(audio_file.tags, symbols.ID3):
|
||||||
audio_file.tags.add(symbols.TPUB(encoding=3, text=[pp["qobuz_label"]]))
|
audio_file.tags.add(symbols.TPUB(encoding=3, text=[pp["qobuz_label"]]))
|
||||||
elif is_vorbis_like(audio_file, symbols):
|
elif is_vorbis_like(audio_file, symbols):
|
||||||
|
|
@ -852,7 +690,7 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
elif isinstance(audio_file, symbols.MP4):
|
elif isinstance(audio_file, symbols.MP4):
|
||||||
audio_file["----:com.apple.iTunes:LABEL"] = [symbols.MP4FreeForm(pp["qobuz_label"].encode("utf-8"))]
|
audio_file["----:com.apple.iTunes:LABEL"] = [symbols.MP4FreeForm(pp["qobuz_label"].encode("utf-8"))]
|
||||||
|
|
||||||
if _tag_enabled("lastfm.tags.url") and pp["lastfm_url"]:
|
if _tag_enabled(cfg, "lastfm.tags.url") and pp["lastfm_url"]:
|
||||||
if isinstance(audio_file.tags, symbols.ID3):
|
if isinstance(audio_file.tags, symbols.ID3):
|
||||||
audio_file.tags.add(symbols.TXXX(encoding=3, desc="LASTFM_URL", text=[pp["lastfm_url"]]))
|
audio_file.tags.add(symbols.TXXX(encoding=3, desc="LASTFM_URL", text=[pp["lastfm_url"]]))
|
||||||
elif is_vorbis_like(audio_file, symbols):
|
elif is_vorbis_like(audio_file, symbols):
|
||||||
|
|
@ -860,7 +698,7 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
elif isinstance(audio_file, symbols.MP4):
|
elif isinstance(audio_file, symbols.MP4):
|
||||||
audio_file["----:com.apple.iTunes:LASTFM_URL"] = [symbols.MP4FreeForm(pp["lastfm_url"].encode("utf-8"))]
|
audio_file["----:com.apple.iTunes:LASTFM_URL"] = [symbols.MP4FreeForm(pp["lastfm_url"].encode("utf-8"))]
|
||||||
|
|
||||||
if _tag_enabled("genius.tags.url") and pp["genius_url"]:
|
if _tag_enabled(cfg, "genius.tags.url") and pp["genius_url"]:
|
||||||
if isinstance(audio_file.tags, symbols.ID3):
|
if isinstance(audio_file.tags, symbols.ID3):
|
||||||
audio_file.tags.add(symbols.TXXX(encoding=3, desc="GENIUS_URL", text=[pp["genius_url"]]))
|
audio_file.tags.add(symbols.TXXX(encoding=3, desc="GENIUS_URL", text=[pp["genius_url"]]))
|
||||||
elif is_vorbis_like(audio_file, symbols):
|
elif is_vorbis_like(audio_file, symbols):
|
||||||
|
|
@ -868,10 +706,13 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
elif isinstance(audio_file, symbols.MP4):
|
elif isinstance(audio_file, symbols.MP4):
|
||||||
audio_file["----:com.apple.iTunes:GENIUS_URL"] = [symbols.MP4FreeForm(pp["genius_url"].encode("utf-8"))]
|
audio_file["----:com.apple.iTunes:GENIUS_URL"] = [symbols.MP4FreeForm(pp["genius_url"].encode("utf-8"))]
|
||||||
|
|
||||||
release_id = pp["release_mbid"]
|
return release_year
|
||||||
if release_id:
|
|
||||||
metadata["musicbrainz_release_id"] = release_id
|
|
||||||
if db is not None:
|
def _update_album_year_in_database(db, metadata: dict, release_year) -> None:
|
||||||
|
if db is None:
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
album_name_for_db = metadata.get("album", "")
|
album_name_for_db = metadata.get("album", "")
|
||||||
album_artist_for_db = metadata.get("album_artist", "") or metadata.get("artist", "")
|
album_artist_for_db = metadata.get("album_artist", "") or metadata.get("artist", "")
|
||||||
|
|
@ -893,13 +734,222 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N
|
||||||
)
|
)
|
||||||
if cursor.rowcount > 0:
|
if cursor.rowcount > 0:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger_.info("Updated album year to %s in database", release_year)
|
logger.info("Updated album year to %s in database", release_year)
|
||||||
else:
|
else:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger_.error("Could not update album year in DB: %s", exc)
|
logger.error("Could not update album year in DB: %s", exc)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_source_metadata(context: dict, artist: dict, album_info: dict) -> dict:
|
||||||
|
if album_info is None:
|
||||||
|
album_info = {}
|
||||||
|
|
||||||
|
cfg = get_config_manager()
|
||||||
|
context = normalize_import_context(context)
|
||||||
|
original_search = get_import_original_search(context)
|
||||||
|
album_ctx = get_import_context_album(context)
|
||||||
|
track_info = get_import_track_info(context)
|
||||||
|
source = get_import_source(context)
|
||||||
|
source_ids = get_import_source_ids(context)
|
||||||
|
|
||||||
|
artist_dict = artist if isinstance(artist, dict) else {
|
||||||
|
"name": extract_artist_name(artist),
|
||||||
|
"id": getattr(artist, "id", ""),
|
||||||
|
"genres": list(getattr(artist, "genres", []) or []),
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata: Dict[str, Any] = {
|
||||||
|
"source": source,
|
||||||
|
"source_track_id": source_ids["track_id"],
|
||||||
|
"source_artist_id": source_ids["artist_id"],
|
||||||
|
"source_album_id": source_ids["album_id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata["title"] = get_import_clean_title(context, album_info=album_info, default=original_search.get("title", ""))
|
||||||
|
if original_search.get("clean_title"):
|
||||||
|
logger.info("Metadata: Using clean title: '%s'", metadata["title"])
|
||||||
|
elif album_info.get("clean_track_name"):
|
||||||
|
logger.info("Metadata: Using album info clean name: '%s'", metadata["title"])
|
||||||
|
else:
|
||||||
|
logger.warning("Metadata: Using original title as fallback: '%s'", metadata["title"])
|
||||||
|
|
||||||
|
artists = original_search.get("artists")
|
||||||
|
if isinstance(artists, list) and artists:
|
||||||
|
all_artists = []
|
||||||
|
for artist_item in artists:
|
||||||
|
if isinstance(artist_item, dict) and artist_item.get("name"):
|
||||||
|
all_artists.append(artist_item["name"])
|
||||||
|
elif isinstance(artist_item, str):
|
||||||
|
all_artists.append(artist_item)
|
||||||
|
else:
|
||||||
|
all_artists.append(str(artist_item))
|
||||||
|
metadata["artist"] = ", ".join(all_artists)
|
||||||
|
logger.info("Metadata: Using all artists: '%s'", metadata["artist"])
|
||||||
|
else:
|
||||||
|
metadata["artist"] = artist_dict.get("name", "") or get_import_clean_artist(context)
|
||||||
|
logger.info("Metadata: Using primary artist: '%s'", metadata["artist"])
|
||||||
|
|
||||||
|
raw_album_artist = artist_dict.get("name", "") or metadata["artist"]
|
||||||
|
track_info_ctx = track_info or {}
|
||||||
|
explicit_artist = track_info_ctx.get("_explicit_artist_context") if isinstance(track_info_ctx, dict) else None
|
||||||
|
album_artists_for_collab = None
|
||||||
|
|
||||||
|
if isinstance(explicit_artist, dict) and explicit_artist.get("name"):
|
||||||
|
raw_album_artist = explicit_artist["name"]
|
||||||
|
album_artists_for_collab = [explicit_artist]
|
||||||
|
elif isinstance(explicit_artist, str) and explicit_artist:
|
||||||
|
raw_album_artist = explicit_artist
|
||||||
|
album_artists_for_collab = [{"name": explicit_artist}]
|
||||||
|
elif album_ctx and isinstance(album_ctx, dict):
|
||||||
|
album_artists = album_ctx.get("artists", [])
|
||||||
|
if album_artists:
|
||||||
|
first_album_artist = album_artists[0]
|
||||||
|
if isinstance(first_album_artist, dict) and first_album_artist.get("name"):
|
||||||
|
raw_album_artist = first_album_artist["name"]
|
||||||
|
elif isinstance(first_album_artist, str) and first_album_artist:
|
||||||
|
raw_album_artist = first_album_artist
|
||||||
|
album_artists_for_collab = album_artists
|
||||||
|
|
||||||
|
collab_mode = cfg.get("file_organization.collab_artist_mode", "first")
|
||||||
|
if collab_mode == "first" and raw_album_artist:
|
||||||
|
context_artists = album_artists_for_collab or original_search.get("artists") or track_info_ctx.get("artists") or []
|
||||||
|
if len(context_artists) > 1:
|
||||||
|
first = context_artists[0]
|
||||||
|
raw_album_artist = first.get("name", first) if isinstance(first, dict) else str(first)
|
||||||
|
elif len(context_artists) == 1 and ("," in raw_album_artist or " & " in raw_album_artist):
|
||||||
|
artist_id = str(artist_dict.get("id", ""))
|
||||||
|
if source == "itunes" and artist_id.isdigit():
|
||||||
|
try:
|
||||||
|
itunes_client = get_itunes_client()
|
||||||
|
if itunes_client and hasattr(itunes_client, "resolve_primary_artist"):
|
||||||
|
resolved = itunes_client.resolve_primary_artist(artist_id)
|
||||||
|
if resolved and resolved != raw_album_artist:
|
||||||
|
raw_album_artist = resolved
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
metadata["album_artist"] = raw_album_artist
|
||||||
|
|
||||||
|
if album_info.get("is_album"):
|
||||||
|
metadata["album"] = album_info.get("album_name", "Unknown Album")
|
||||||
|
metadata["track_number"] = album_info.get("track_number", 1)
|
||||||
|
metadata["total_tracks"] = album_ctx.get("total_tracks", 1) if album_ctx else 1
|
||||||
|
logger.info("[METADATA] Album track - track_number: %s, album: %s", metadata["track_number"], metadata["album"])
|
||||||
|
else:
|
||||||
|
if album_ctx and album_ctx.get("name"):
|
||||||
|
logger.info("[SAFEGUARD] Using album context name instead of track title for album metadata")
|
||||||
|
metadata["album"] = album_ctx["name"]
|
||||||
|
metadata["track_number"] = album_info.get("track_number", 1) if album_info else 1
|
||||||
|
metadata["total_tracks"] = album_ctx.get("total_tracks", 1)
|
||||||
|
else:
|
||||||
|
metadata["album"] = metadata["title"]
|
||||||
|
metadata["track_number"] = 1
|
||||||
|
metadata["total_tracks"] = 1
|
||||||
|
|
||||||
|
disc_num = original_search.get("disc_number")
|
||||||
|
if disc_num is None and album_info:
|
||||||
|
disc_num = album_info.get("disc_number")
|
||||||
|
metadata["disc_number"] = disc_num if disc_num is not None else 1
|
||||||
|
|
||||||
|
if album_ctx and album_ctx.get("release_date"):
|
||||||
|
metadata["date"] = album_ctx["release_date"][:4]
|
||||||
|
|
||||||
|
genres = artist_dict.get("genres") or []
|
||||||
|
if genres:
|
||||||
|
from core.genre_filter import filter_genres
|
||||||
|
|
||||||
|
filtered = filter_genres(list(genres[:2]), cfg)
|
||||||
|
if filtered:
|
||||||
|
metadata["genre"] = ", ".join(filtered)
|
||||||
|
|
||||||
|
metadata["album_art_url"] = album_info.get("album_image_url") if album_info else None
|
||||||
|
if not metadata["album_art_url"] and album_ctx:
|
||||||
|
album_image = album_ctx.get("image_url")
|
||||||
|
if not album_image and album_ctx.get("images"):
|
||||||
|
first_image = album_ctx["images"][0]
|
||||||
|
album_image = first_image.get("url") if isinstance(first_image, dict) else None
|
||||||
|
metadata["album_art_url"] = album_image
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[Metadata Summary] title='%s' | artist='%s' | album_artist='%s' | album='%s' | track=%s/%s | disc=%s",
|
||||||
|
metadata.get("title"),
|
||||||
|
metadata.get("artist"),
|
||||||
|
metadata.get("album_artist"),
|
||||||
|
metadata.get("album"),
|
||||||
|
metadata.get("track_number"),
|
||||||
|
metadata.get("total_tracks"),
|
||||||
|
metadata.get("disc_number"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
|
def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=None):
|
||||||
|
cfg = get_config_manager()
|
||||||
|
symbols = get_mutagen_symbols()
|
||||||
|
if not symbols:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
context = normalize_import_context(context)
|
||||||
|
source_ids = _collect_source_ids(metadata, cfg)
|
||||||
|
|
||||||
|
track_title = metadata.get("title", "")
|
||||||
|
artist_name = metadata.get("album_artist", "") or metadata.get("artist", "")
|
||||||
|
track_info = get_import_track_info(context)
|
||||||
|
explicit_artist = (track_info or {}).get("_explicit_artist_context") if isinstance(track_info, dict) else None
|
||||||
|
batch_artist_name = None
|
||||||
|
if isinstance(explicit_artist, dict) and explicit_artist.get("name"):
|
||||||
|
batch_artist_name = explicit_artist["name"]
|
||||||
|
elif isinstance(explicit_artist, str) and explicit_artist:
|
||||||
|
batch_artist_name = explicit_artist
|
||||||
|
|
||||||
|
pp = {
|
||||||
|
"id_tags": source_ids,
|
||||||
|
"track_title": track_title,
|
||||||
|
"artist_name": artist_name,
|
||||||
|
"batch_artist_name": batch_artist_name,
|
||||||
|
"metadata": metadata,
|
||||||
|
"recording_mbid": None,
|
||||||
|
"artist_mbid": None,
|
||||||
|
"release_mbid": "",
|
||||||
|
"mb_genres": [],
|
||||||
|
"isrc": None,
|
||||||
|
"deezer_bpm": None,
|
||||||
|
"deezer_isrc": None,
|
||||||
|
"audiodb_mood": None,
|
||||||
|
"audiodb_style": None,
|
||||||
|
"audiodb_genre": None,
|
||||||
|
"tidal_isrc": None,
|
||||||
|
"tidal_copyright": None,
|
||||||
|
"qobuz_isrc": None,
|
||||||
|
"qobuz_copyright": None,
|
||||||
|
"qobuz_label": None,
|
||||||
|
"lastfm_tags": [],
|
||||||
|
"lastfm_url": None,
|
||||||
|
"genius_url": None,
|
||||||
|
"release_year": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
source_order = cfg.get("metadata_enhancement.post_process_order", None)
|
||||||
|
if not isinstance(source_order, list) or not source_order:
|
||||||
|
source_order = DEFAULT_SOURCE_ORDER
|
||||||
|
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
for source_name in source_order:
|
||||||
|
_process_source_enrichment(source_name, pp, metadata, cfg, runtime, track_title, artist_name)
|
||||||
|
|
||||||
|
if not pp["id_tags"] and not pp["deezer_bpm"] and not pp["deezer_isrc"] and not pp["audiodb_mood"] and not pp["audiodb_style"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
release_year = _write_embedded_metadata(audio_file, metadata, pp, cfg, symbols)
|
||||||
|
release_id = pp["release_mbid"]
|
||||||
|
if release_id:
|
||||||
|
metadata["musicbrainz_release_id"] = release_id
|
||||||
|
_update_album_year_in_database(db, metadata, release_year)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger_.error("Error embedding source IDs (non-fatal): %s", exc)
|
logger.error("Error embedding source IDs (non-fatal): %s", exc)
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,60 @@ def test_embed_source_ids_uses_current_source_ids_and_legacy_fallback(monkeypatc
|
||||||
assert "ITUNES_ALBUM_ID" in legacy_descs
|
assert "ITUNES_ALBUM_ID" in legacy_descs
|
||||||
|
|
||||||
|
|
||||||
|
def test_enhance_file_metadata_forwards_runtime_to_source_embedding(monkeypatch):
|
||||||
|
audio = _FakeAudio()
|
||||||
|
symbols = _fake_symbols(audio)
|
||||||
|
seen = {}
|
||||||
|
runtime = types.SimpleNamespace(marker="runtime")
|
||||||
|
|
||||||
|
monkeypatch.setattr(me, "get_config_manager", lambda: _Config(
|
||||||
|
{
|
||||||
|
"metadata_enhancement.enabled": True,
|
||||||
|
"metadata_enhancement.embed_album_art": False,
|
||||||
|
"metadata_enhancement.tags.write_multi_artist": False,
|
||||||
|
}
|
||||||
|
))
|
||||||
|
monkeypatch.setattr(me, "get_mutagen_symbols", lambda: symbols)
|
||||||
|
monkeypatch.setattr(me, "strip_all_non_audio_tags", lambda file_path: {"apev2_stripped": False, "apev2_tag_count": 0})
|
||||||
|
monkeypatch.setattr(
|
||||||
|
me,
|
||||||
|
"extract_source_metadata",
|
||||||
|
lambda context, artist, album_info: {
|
||||||
|
"source": "deezer",
|
||||||
|
"source_track_id": "dz-track",
|
||||||
|
"source_artist_id": "dz-artist",
|
||||||
|
"source_album_id": "dz-album",
|
||||||
|
"title": "Song One",
|
||||||
|
"artist": "Artist One",
|
||||||
|
"album_artist": "Artist One",
|
||||||
|
"album": "Album One",
|
||||||
|
"track_number": 3,
|
||||||
|
"total_tracks": 12,
|
||||||
|
"disc_number": 2,
|
||||||
|
"date": "2024",
|
||||||
|
"genre": "Rock",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
me,
|
||||||
|
"embed_source_ids",
|
||||||
|
lambda audio_file, metadata, context, runtime=None: seen.setdefault("runtime", runtime),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(me, "embed_album_art_metadata", lambda *args, **kwargs: None)
|
||||||
|
monkeypatch.setattr(me, "verify_metadata_written", lambda file_path: True)
|
||||||
|
|
||||||
|
result = me.enhance_file_metadata(
|
||||||
|
"song.flac",
|
||||||
|
{"_audio_quality": ""},
|
||||||
|
{"name": "Artist One"},
|
||||||
|
{},
|
||||||
|
runtime=runtime,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert seen["runtime"] is runtime
|
||||||
|
|
||||||
|
|
||||||
def test_enhance_file_metadata_writes_tags_and_propagates_release_id(monkeypatch):
|
def test_enhance_file_metadata_writes_tags_and_propagates_release_id(monkeypatch):
|
||||||
audio = _FakeAudio()
|
audio = _FakeAudio()
|
||||||
symbols = _fake_symbols(audio)
|
symbols = _fake_symbols(audio)
|
||||||
|
|
|
||||||
|
|
@ -19678,8 +19678,14 @@ import urllib.request
|
||||||
def _wipe_source_tags(file_path: str) -> bool:
|
def _wipe_source_tags(file_path: str) -> bool:
|
||||||
return metadata_enrichment.wipe_source_tags(file_path)
|
return metadata_enrichment.wipe_source_tags(file_path)
|
||||||
|
|
||||||
def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_info: dict) -> bool:
|
def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_info: dict, runtime=None) -> bool:
|
||||||
return metadata_enrichment.enhance_file_metadata(file_path, context, artist, album_info)
|
return metadata_enrichment.enhance_file_metadata(
|
||||||
|
file_path,
|
||||||
|
context,
|
||||||
|
artist,
|
||||||
|
album_info,
|
||||||
|
runtime=runtime or _build_import_pipeline_runtime(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _download_cover_art(album_info: dict, target_dir: str, context: dict = None):
|
def _download_cover_art(album_info: dict, target_dir: str, context: dict = None):
|
||||||
|
|
@ -19781,6 +19787,15 @@ def _build_import_pipeline_runtime():
|
||||||
on_download_completed=_on_download_completed,
|
on_download_completed=_on_download_completed,
|
||||||
web_scan_manager=web_scan_manager,
|
web_scan_manager=web_scan_manager,
|
||||||
repair_worker=repair_worker,
|
repair_worker=repair_worker,
|
||||||
|
mb_worker=mb_worker,
|
||||||
|
deezer_worker=deezer_worker,
|
||||||
|
audiodb_worker=audiodb_worker,
|
||||||
|
tidal_client=tidal_client,
|
||||||
|
qobuz_enrichment_worker=qobuz_enrichment_worker,
|
||||||
|
lastfm_worker=lastfm_worker,
|
||||||
|
genius_worker=genius_worker,
|
||||||
|
spotify_enrichment_worker=spotify_enrichment_worker,
|
||||||
|
itunes_enrichment_worker=itunes_enrichment_worker,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -19897,18 +19912,28 @@ def _build_import_pipeline_runtime():
|
||||||
on_download_completed=_on_download_completed,
|
on_download_completed=_on_download_completed,
|
||||||
web_scan_manager=web_scan_manager,
|
web_scan_manager=web_scan_manager,
|
||||||
repair_worker=repair_worker,
|
repair_worker=repair_worker,
|
||||||
|
mb_worker=mb_worker,
|
||||||
|
deezer_worker=deezer_worker,
|
||||||
|
audiodb_worker=audiodb_worker,
|
||||||
|
tidal_client=tidal_client,
|
||||||
|
qobuz_enrichment_worker=qobuz_enrichment_worker,
|
||||||
|
lastfm_worker=lastfm_worker,
|
||||||
|
genius_worker=genius_worker,
|
||||||
|
spotify_enrichment_worker=spotify_enrichment_worker,
|
||||||
|
itunes_enrichment_worker=itunes_enrichment_worker,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _wipe_source_tags(file_path: str) -> bool:
|
def _wipe_source_tags(file_path: str) -> bool:
|
||||||
return metadata_enrichment.wipe_source_tags(file_path)
|
return metadata_enrichment.wipe_source_tags(file_path)
|
||||||
|
|
||||||
|
|
||||||
def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_info: dict) -> bool:
|
def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_info: dict, runtime=None) -> bool:
|
||||||
return metadata_enrichment.enhance_file_metadata(
|
return metadata_enrichment.enhance_file_metadata(
|
||||||
file_path,
|
file_path,
|
||||||
context,
|
context,
|
||||||
artist,
|
artist,
|
||||||
album_info,
|
album_info,
|
||||||
|
runtime=runtime or _build_import_pipeline_runtime(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue