soulsync/core/metadata/source.py
Broque Thomas 34ba26f5c8 Persist source IDs at download time + backfill onto tracks on sync
Followup to fix/watchlist-external-id-match. The companion PR closed
the demand side — the watchlist scanner asks for tracks by external IDs
before falling back to fuzzy. But for users on Plex / Jellyfin /
Navidrome the supply side was still broken: tracks.spotify_track_id
(and the other ID columns) only got populated by the asynchronous
enrichment workers, sometimes hours after the file was actually
written. During that window the ID match fell through to fuzzy and
the bug returned.

We were already collecting every ID during post-processing — they
live in the `pp` dict in core/metadata/source.py:embed_source_ids and
get embedded into file tags. We just dropped the in-memory copy
afterwards.

This PR persists them and uses them:

- Schema migration adds spotify_track_id / itunes_track_id /
  deezer_track_id / tidal_track_id / qobuz_track_id /
  musicbrainz_recording_id / audiodb_id / soul_id / isrc columns +
  indexes to the existing track_downloads table (already keyed by
  file_path).
- core/metadata/source.py:embed_source_ids exposes pp["id_tags"] and
  the resolved ISRC back to the import context as _embedded_id_tags
  / _isrc.
- core/imports/side_effects.py:record_download_provenance reads those
  context fields and passes them to db.record_track_download, which
  now accepts the new ID kwargs and persists them.
- New db.get_provenance_by_file_path with exact + basename-suffix
  fallback (handles container mount-root differences between
  download-time path and media-server-reported path).
- New db.backfill_track_external_ids_from_provenance copies IDs
  from track_downloads onto a tracks row idempotently — COALESCE on
  every column preserves any value the enrichment worker already
  wrote (enrichment is more authoritative for late binding).
- database/music_database.py:insert_or_update_media_track (the
  single insertion point used by every Plex / Jellyfin / Navidrome
  sync) calls the backfill immediately after each INSERT/UPDATE.
- New core/library/track_identity.py:find_provenance_by_external_id
  used as a second-tier fallback in watchlist_scanner.is_track_missing
  _from_library — catches the window between download and media-server
  sync. Caller checks os.path.exists on the provenance file_path
  before treating it as "already in library" so a deleted file
  doesn't prevent re-download.

Effect: freshly downloaded files become ID-recognizable to the
watchlist on the very next scan, no enrichment-wait window.

19 regression tests in tests/test_provenance_id_persistence.py:
- Schema migration adds expected columns + indexes
- record_track_download persists every ID kwarg
- record_track_download backward-compat (old kwargs still work)
- get_provenance_by_file_path: exact match, basename fallback for
  mount-root differences, multi-record latest-wins, defensive None
- backfill: copies all IDs, preserves existing via COALESCE,
  no-op when no provenance exists
- find_provenance_by_external_id: per-ID lookup, ISRC cross-bridge,
  OR semantics, latest-wins on multiple matches

Out of scope: backfilling provenance for files downloaded BEFORE
this PR (their track_downloads rows don't carry the new IDs). Those
continue to wait for enrichment. Acceptable — only affects historical
files; new downloads benefit immediately.

Full pytest 1625 passed; ruff clean.
2026-05-02 17:44:10 -07:00

1116 lines
52 KiB
Python

"""Source metadata extraction and source-ID embedding helpers."""
from __future__ import annotations
import re
import socket
import threading
import time
from collections import OrderedDict
from typing import Any, Dict
import requests
from core.imports.context import (
extract_artist_name,
get_import_clean_artist,
get_import_clean_title,
get_import_context_album,
get_import_original_search,
get_import_source,
get_import_source_ids,
get_import_track_info,
get_source_tag_names,
normalize_import_context,
)
from core.metadata.registry import get_itunes_client
from database.music_database import get_database
from core.metadata.common import (
get_config_manager,
get_mutagen_symbols,
is_vorbis_like,
)
from utils.logging_config import get_logger as _create_logger
__all__ = [
"extract_source_metadata",
"embed_source_ids",
"normalize_album_cache_key",
"mb_release_cache",
"mb_release_cache_lock",
"mb_release_detail_cache",
"mb_release_detail_cache_lock",
]
_MB_RELEASE_CACHE_MAX_ENTRIES = 4096
_MB_RELEASE_DETAIL_CACHE_MAX_ENTRIES = 4096
mb_release_cache: "OrderedDict[tuple, str]" = OrderedDict()
mb_release_cache_lock = threading.RLock()
mb_release_detail_cache: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
mb_release_detail_cache_lock = threading.RLock()
logger = _create_logger("metadata.source")
_SOURCE_NETWORK_EXCEPTIONS = (requests.RequestException, socket.timeout, TimeoutError)
_EDITION_PAREN_RE = re.compile(
r'\s*[\(\[]\s*(?:deluxe|expanded|remaster(?:ed)?|anniversary|special|collector|'
r'limited|bonus|platinum|gold|super\s*deluxe|standard)'
r'(?:\s+(?:edition|version))?[^)\]]*[\)\]]',
re.IGNORECASE,
)
_EDITION_BARE_RE = re.compile(
r'\s+(?:-\s+)?(?:deluxe|expanded|remaster(?:ed)?|anniversary|special|collector|'
r'limited|bonus|platinum|gold|super\s*deluxe|standard)'
r'(?:\s+(?:edition|version))?\s*$',
re.IGNORECASE,
)
def normalize_album_cache_key(album_name: str) -> str:
result = _EDITION_PAREN_RE.sub("", album_name or "")
result = _EDITION_BARE_RE.sub("", result)
return result.lower().strip()
def _bounded_cache_get(cache, key):
value = cache.get(key)
if value is not None and hasattr(cache, "move_to_end"):
cache.move_to_end(key)
return value
def _bounded_cache_set(cache, key, value, max_entries: int) -> None:
cache[key] = value
if hasattr(cache, "move_to_end"):
cache.move_to_end(key)
while len(cache) > max_entries:
cache.popitem(last=False)
def _call_source_lookup(label: str, func, *args, **kwargs):
try:
return func(*args, **kwargs)
except _SOURCE_NETWORK_EXCEPTIONS as exc:
logger.warning("%s lookup failed (network): %s", label, exc)
return None
SOURCE_TAG_CONFIG = {
"SPOTIFY_TRACK_ID": "spotify.tags.track_id",
"SPOTIFY_ARTIST_ID": "spotify.tags.artist_id",
"SPOTIFY_ALBUM_ID": "spotify.tags.album_id",
"ITUNES_TRACK_ID": "itunes.tags.track_id",
"ITUNES_ARTIST_ID": "itunes.tags.artist_id",
"ITUNES_ALBUM_ID": "itunes.tags.album_id",
"MUSICBRAINZ_RECORDING_ID": "musicbrainz.tags.recording_id",
"MUSICBRAINZ_ARTIST_ID": "musicbrainz.tags.artist_id",
"MUSICBRAINZ_RELEASE_ID": "musicbrainz.tags.release_id",
"MUSICBRAINZ_RELEASEGROUPID": "musicbrainz.tags.release_group_id",
"MUSICBRAINZ_ALBUMARTISTID": "musicbrainz.tags.album_artist_id",
"MUSICBRAINZ_RELEASETRACKID": "musicbrainz.tags.release_track_id",
"RELEASETYPE": "musicbrainz.tags.release_type",
"ORIGINALDATE": "musicbrainz.tags.original_date",
"RELEASESTATUS": "musicbrainz.tags.release_status",
"RELEASECOUNTRY": "musicbrainz.tags.release_country",
"BARCODE": "musicbrainz.tags.barcode",
"MEDIA": "musicbrainz.tags.media",
"TOTALDISCS": "musicbrainz.tags.total_discs",
"CATALOGNUMBER": "musicbrainz.tags.catalog_number",
"SCRIPT": "musicbrainz.tags.script",
"ASIN": "musicbrainz.tags.asin",
"DEEZER_TRACK_ID": "deezer.tags.track_id",
"DEEZER_ARTIST_ID": "deezer.tags.artist_id",
"AUDIODB_TRACK_ID": "audiodb.tags.track_id",
"TIDAL_TRACK_ID": "tidal.tags.track_id",
"TIDAL_ARTIST_ID": "tidal.tags.artist_id",
"HIFI_TRACK_ID": "hifi.tags.track_id",
"HIFI_ARTIST_ID": "hifi.tags.artist_id",
"QOBUZ_TRACK_ID": "qobuz.tags.track_id",
"QOBUZ_ARTIST_ID": "qobuz.tags.artist_id",
"GENIUS_TRACK_ID": "genius.tags.track_id",
}
DEFAULT_SOURCE_ORDER = ["musicbrainz", "deezer", "audiodb", "tidal", "hifi", "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
def _names_match(a: str, b: str, threshold: float = 0.75) -> bool:
if not a or not b:
return False
from difflib import SequenceMatcher
norm = lambda s: re.sub(r"[^a-z0-9 ]", "", re.sub(r"\(.*?\)", "", s).lower()).strip()
return SequenceMatcher(None, norm(a), norm(b)).ratio() >= threshold
def _collect_source_ids(metadata: dict, cfg) -> dict:
source_ids = {}
source = (metadata.get("source") or "").strip().lower()
if source:
source_tag_names = get_source_tag_names(source)
source_track_id = metadata.get("source_track_id")
source_artist_id = metadata.get("source_artist_id")
source_album_id = metadata.get("source_album_id")
if cfg.get(f"{source}.embed_tags", True) is not False:
if source_tag_names.get("track") and source_track_id:
source_ids[source_tag_names["track"]] = source_track_id
if source_tag_names.get("artist") and source_artist_id:
source_ids[source_tag_names["artist"]] = source_artist_id
if source_tag_names.get("album") and source_album_id:
source_ids[source_tag_names["album"]] = source_album_id
if not source_ids:
if cfg.get("spotify.embed_tags", True) is not False:
if metadata.get("spotify_track_id"):
source_ids["SPOTIFY_TRACK_ID"] = metadata["spotify_track_id"]
if metadata.get("spotify_artist_id"):
source_ids["SPOTIFY_ARTIST_ID"] = metadata["spotify_artist_id"]
if metadata.get("spotify_album_id"):
source_ids["SPOTIFY_ALBUM_ID"] = metadata["spotify_album_id"]
if cfg.get("itunes.embed_tags", True) is not False:
if metadata.get("itunes_track_id"):
source_ids["ITUNES_TRACK_ID"] = metadata["itunes_track_id"]
if metadata.get("itunes_artist_id"):
source_ids["ITUNES_ARTIST_ID"] = metadata["itunes_artist_id"]
if metadata.get("itunes_album_id"):
source_ids["ITUNES_ALBUM_ID"] = metadata["itunes_album_id"]
return source_ids
def _process_musicbrainz_source(pp: dict, metadata: dict, cfg, runtime, track_title: str, artist_name: str) -> None:
if cfg.get("musicbrainz.embed_tags", True) is False:
return
if not track_title or not artist_name:
return
mb_worker = getattr(runtime, "mb_worker", None)
mb_service = mb_worker.mb_service if mb_worker else None
if not mb_service:
return
result = _call_source_lookup("MusicBrainz recording", mb_service.match_recording, track_title, artist_name)
if result and result.get("mbid"):
pp["recording_mbid"] = result["mbid"]
pp["id_tags"]["MUSICBRAINZ_RECORDING_ID"] = pp["recording_mbid"]
details = _call_source_lookup(
"MusicBrainz recording details",
mb_service.mb_client.get_recording,
pp["recording_mbid"],
includes=["isrcs", "genres"],
)
if details:
isrcs = details.get("isrcs", [])
if isrcs:
pp["isrc"] = isrcs[0]
pp["mb_genres"] = [g["name"] for g in sorted(details.get("genres", []), key=lambda x: x.get("count", 0), reverse=True)]
track_artist_name = metadata.get("artist", "") or artist_name
if ", " in track_artist_name:
track_artist_name = track_artist_name.split(", ")[0]
artist_result = _call_source_lookup("MusicBrainz artist", mb_service.match_artist, track_artist_name)
if artist_result and artist_result.get("mbid"):
pp["artist_mbid"] = artist_result["mbid"]
pp["id_tags"]["MUSICBRAINZ_ARTIST_ID"] = pp["artist_mbid"]
album_name_for_mb = metadata.get("album", "")
if album_name_for_mb:
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_exact = (album_name_for_mb.lower().strip(), artist_key)
release_mbid = None
with mb_release_cache_lock:
cached = _bounded_cache_get(mb_release_cache, rc_key_norm)
if cached is None:
cached = _bounded_cache_get(mb_release_cache, rc_key_exact)
if cached:
release_mbid = cached
else:
rc_result = _call_source_lookup("MusicBrainz release", mb_service.match_release, album_name_for_mb, artist_name)
if rc_result and rc_result.get("mbid"):
release_mbid = rc_result["mbid"]
if release_mbid:
_bounded_cache_set(mb_release_cache, rc_key_norm, release_mbid, _MB_RELEASE_CACHE_MAX_ENTRIES)
_bounded_cache_set(mb_release_cache, rc_key_exact, release_mbid, _MB_RELEASE_CACHE_MAX_ENTRIES)
pp["release_mbid"] = release_mbid or ""
if pp["release_mbid"]:
pp["id_tags"]["MUSICBRAINZ_RELEASE_ID"] = pp["release_mbid"]
if pp["release_mbid"]:
with mb_release_detail_cache_lock:
release_detail = _bounded_cache_get(mb_release_detail_cache, pp["release_mbid"])
if release_detail is None:
release_detail = _call_source_lookup(
"MusicBrainz release details",
mb_service.mb_client.get_release,
pp["release_mbid"],
includes=["release-groups", "labels", "media", "artist-credits", "recordings", "genres"],
) or {}
with mb_release_detail_cache_lock:
_bounded_cache_set(mb_release_detail_cache, pp["release_mbid"], release_detail, _MB_RELEASE_DETAIL_CACHE_MAX_ENTRIES)
if release_detail:
rg = release_detail.get("release-group", {})
if rg.get("id"):
pp["id_tags"]["MUSICBRAINZ_RELEASEGROUPID"] = rg["id"]
ac = release_detail.get("artist-credit", [])
if ac and isinstance(ac[0], dict):
aa = ac[0].get("artist", {})
if aa.get("id"):
pp["id_tags"]["MUSICBRAINZ_ALBUMARTISTID"] = aa["id"]
if rg.get("primary-type"):
pp["id_tags"]["RELEASETYPE"] = rg["primary-type"]
if rg.get("first-release-date"):
pp["id_tags"]["ORIGINALDATE"] = rg["first-release-date"]
if not pp["release_year"] and len(rg["first-release-date"]) >= 4:
year = rg["first-release-date"][:4]
if year.isdigit():
pp["release_year"] = year
if release_detail.get("status"):
pp["id_tags"]["RELEASESTATUS"] = release_detail["status"]
if release_detail.get("country"):
pp["id_tags"]["RELEASECOUNTRY"] = release_detail["country"]
if release_detail.get("barcode"):
pp["id_tags"]["BARCODE"] = release_detail["barcode"]
media_list = release_detail.get("media", [])
if media_list:
fmt = media_list[0].get("format", "")
if fmt:
pp["id_tags"]["MEDIA"] = fmt
pp["id_tags"]["TOTALDISCS"] = str(len(media_list))
label_info = release_detail.get("label-info", [])
if label_info and isinstance(label_info[0], dict):
cat = label_info[0].get("catalog-number", "")
if cat:
pp["id_tags"]["CATALOGNUMBER"] = cat
text_rep = release_detail.get("text-representation", {})
if isinstance(text_rep, dict) and text_rep.get("script"):
pp["id_tags"]["SCRIPT"] = text_rep["script"]
if release_detail.get("asin"):
pp["id_tags"]["ASIN"] = release_detail["asin"]
track_num = metadata.get("track_number")
disc_num = metadata.get("disc_number") or 1
if track_num and media_list:
try:
track_num_int = int(track_num)
disc_num_int = int(disc_num)
for medium in media_list:
if medium.get("position", 1) == disc_num_int:
for mtrack in (medium.get("tracks") or medium.get("track-list", [])):
if mtrack.get("position") == track_num_int:
if mtrack.get("id"):
pp["id_tags"]["MUSICBRAINZ_RELEASETRACKID"] = mtrack["id"]
release_recording = mtrack.get("recording", {})
if release_recording.get("id"):
pp["recording_mbid"] = release_recording["id"]
pp["id_tags"]["MUSICBRAINZ_RECORDING_ID"] = release_recording["id"]
break
break
except (ValueError, TypeError):
pass
# Genre fallback chain: most MusicBrainz recordings don't carry genres at
# the track level, but releases and artists usually do. If the recording
# came back empty, try the release; if that's empty too, fetch the artist
# with `includes=['genres']` and use that.
_release_detail_for_genres = locals().get("release_detail")
if not pp["mb_genres"] and _release_detail_for_genres:
pp["mb_genres"] = [
g["name"] for g in sorted(
_release_detail_for_genres.get("genres", []), key=lambda x: x.get("count", 0), reverse=True,
)
]
if not pp["mb_genres"] and pp.get("artist_mbid"):
artist_detail = _call_source_lookup(
"MusicBrainz artist details",
mb_service.mb_client.get_artist,
pp["artist_mbid"],
includes=["genres"],
)
if artist_detail:
pp["mb_genres"] = [
g["name"] for g in sorted(
artist_detail.get("genres", []), key=lambda x: x.get("count", 0), reverse=True,
)
]
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:
return
if not track_title or not artist_name:
return
deezer_worker = getattr(runtime, "deezer_worker", None)
dz_client = deezer_worker.client if deezer_worker else None
if not dz_client:
return
dz_result = _call_source_lookup("Deezer track", 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):
dz_track_id = dz_result["id"]
pp["id_tags"]["DEEZER_TRACK_ID"] = str(dz_track_id)
dz_artist_id = dz_result.get("artist", {}).get("id")
if dz_artist_id:
pp["id_tags"]["DEEZER_ARTIST_ID"] = str(dz_artist_id)
dz_details = _call_source_lookup("Deezer track details", dz_client.get_track_details, dz_track_id)
if dz_details:
bpm_val = dz_details.get("bpm")
if bpm_val and bpm_val > 0:
pp["deezer_bpm"] = bpm_val
dz_isrc = dz_details.get("isrc")
if dz_isrc:
pp["deezer_isrc"] = dz_isrc
if not pp["release_year"]:
dz_album = dz_result.get("album", {})
dz_release = (dz_album.get("release_date", "") if isinstance(dz_album, dict) else "") or ""
if len(dz_release) >= 4 and dz_release[:4].isdigit():
pp["release_year"] = dz_release[:4]
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:
return
if not track_title or not artist_name:
return
audiodb_worker = getattr(runtime, "audiodb_worker", None)
adb_client = audiodb_worker.client if audiodb_worker else None
if not adb_client:
return
adb_result = _call_source_lookup("AudioDB track", 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):
adb_track_id = adb_result.get("idTrack")
if adb_track_id:
pp["id_tags"]["AUDIODB_TRACK_ID"] = str(adb_track_id)
adb_mb_track = adb_result.get("strMusicBrainzID")
if adb_mb_track and "MUSICBRAINZ_RECORDING_ID" not in pp["id_tags"]:
pp["id_tags"]["MUSICBRAINZ_RECORDING_ID"] = adb_mb_track
pp["recording_mbid"] = adb_mb_track
adb_mb_artist = adb_result.get("strMusicBrainzArtistID")
if adb_mb_artist and "MUSICBRAINZ_ARTIST_ID" not in pp["id_tags"]:
pp["id_tags"]["MUSICBRAINZ_ARTIST_ID"] = adb_mb_artist
pp["artist_mbid"] = adb_mb_artist
pp["audiodb_mood"] = adb_result.get("strMood") or None
pp["audiodb_style"] = adb_result.get("strStyle") or None
pp["audiodb_genre"] = adb_result.get("strGenre") or None
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:
return
if not track_title or not artist_name:
return
tidal_client = getattr(runtime, "tidal_client", None)
if not (tidal_client and tidal_client.is_authenticated()):
return
td_result = _call_source_lookup("Tidal track", tidal_client.search_track, artist_name, track_title)
if td_result and _names_match(td_result.get("title", ""), track_title):
td_track_id = td_result.get("id")
if td_track_id:
pp["id_tags"]["TIDAL_TRACK_ID"] = str(td_track_id)
td_artist = td_result.get("artist", {})
if isinstance(td_artist, dict) and td_artist.get("id"):
pp["id_tags"]["TIDAL_ARTIST_ID"] = str(td_artist["id"])
if td_track_id:
td_details = _call_source_lookup("Tidal track details", tidal_client.get_track, str(td_track_id))
if td_details:
pp["tidal_isrc"] = td_details.get("isrc")
td_bpm = td_details.get("bpm")
if td_bpm and td_bpm > 0:
pp["tidal_bpm"] = td_bpm
td_copyright = td_details.get("copyright")
if isinstance(td_copyright, dict):
td_copyright = td_copyright.get("text", td_copyright.get("name", ""))
pp["tidal_copyright"] = td_copyright or None
if not pp["release_year"]:
td_album = td_result.get("album", {})
td_release = ""
if isinstance(td_album, dict):
td_release = str(td_album.get("release_date", "") or td_album.get("releaseDate", "") or "")
if len(td_release) >= 4 and td_release[:4].isdigit():
pp["release_year"] = td_release[:4]
def _process_hifi_source(pp: dict, metadata: dict, cfg, runtime, track_title: str, artist_name: str) -> None:
if cfg.get("hifi.embed_tags", True) is False:
return
if not track_title or not artist_name:
return
hifi_client = getattr(runtime, "hifi_client", None)
if not hifi_client:
return
hifi_results = _call_source_lookup("HiFi track", hifi_client.search_tracks, track_title, artist_name)
if hifi_results and len(hifi_results) > 0:
hifi_track = hifi_results[0]
if _names_match(hifi_track.get("title", ""), track_title):
hifi_track_id = hifi_track.get("id")
if hifi_track_id:
pp["id_tags"]["HIFI_TRACK_ID"] = str(hifi_track_id)
hifi_artist_id = hifi_track.get("artist_id")
if hifi_artist_id:
pp["id_tags"]["HIFI_ARTIST_ID"] = str(hifi_artist_id)
if hifi_track_id:
hifi_details = _call_source_lookup("HiFi track details", hifi_client.get_track_info, hifi_track_id)
if hifi_details:
hifi_isrc = hifi_details.get("isrc")
if hifi_isrc:
pp["hifi_isrc"] = hifi_isrc
hifi_bpm = hifi_details.get("bpm")
if hifi_bpm and hifi_bpm > 0:
pp["hifi_bpm"] = hifi_bpm
hifi_copyright = hifi_details.get("copyright")
if hifi_copyright:
pp["hifi_copyright"] = hifi_copyright
if not pp["release_year"]:
hifi_album_id = hifi_track.get("album_id")
if hifi_album_id:
hifi_album = _call_source_lookup("HiFi album", hifi_client.get_album, hifi_album_id)
if hifi_album:
hifi_release = str(hifi_album.get("release_date", "") or "")
if len(hifi_release) >= 4 and hifi_release[:4].isdigit():
pp["release_year"] = hifi_release[:4]
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:
return
if not track_title or not artist_name:
return
qobuz_worker = getattr(runtime, "qobuz_enrichment_worker", None)
qz_client = qobuz_worker.client if qobuz_worker else None
if not (qz_client and qz_client.is_authenticated()):
return
qz_result = _call_source_lookup("Qobuz track", qz_client.search_track, artist_name, track_title)
if qz_result:
qz_performer = qz_result.get("performer") or {}
if not isinstance(qz_performer, dict):
qz_performer = {}
qz_artist_name = qz_performer.get("name", "")
if _names_match(qz_result.get("title", ""), track_title) and _names_match(qz_artist_name, artist_name):
qz_track_id = qz_result.get("id")
if qz_track_id:
pp["id_tags"]["QOBUZ_TRACK_ID"] = str(qz_track_id)
if qz_performer.get("id"):
pp["id_tags"]["QOBUZ_ARTIST_ID"] = str(qz_performer["id"])
qz_isrc = qz_result.get("isrc")
if isinstance(qz_isrc, dict):
qz_isrc = qz_isrc.get("value", qz_isrc.get("id", ""))
if qz_isrc:
pp["qobuz_isrc"] = qz_isrc
qz_copyright = qz_result.get("copyright")
if isinstance(qz_copyright, dict):
qz_copyright = qz_copyright.get("text", qz_copyright.get("name", ""))
if isinstance(qz_copyright, str):
pp["qobuz_copyright"] = qz_copyright
qz_album = qz_result.get("album", {})
if isinstance(qz_album, dict):
qz_label_info = qz_album.get("label", {})
if isinstance(qz_label_info, dict) and qz_label_info.get("name"):
pp["qobuz_label"] = qz_label_info["name"]
if not pp["release_year"]:
qz_release = str(qz_album.get("release_date_original", "") or "")
if not qz_release:
qz_ts = qz_album.get("released_at")
if qz_ts and isinstance(qz_ts, (int, float)) and qz_ts > 0:
import datetime as _dt
qz_release = str(_dt.datetime.utcfromtimestamp(qz_ts).year)
if len(qz_release) >= 4 and qz_release[:4].isdigit():
pp["release_year"] = qz_release[:4]
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:
return
if not track_title or not artist_name:
return
lastfm_worker = getattr(runtime, "lastfm_worker", None)
lf_client = lastfm_worker.client if lastfm_worker else None
if not lf_client:
return
lf_result = _call_source_lookup("Last.fm track", lf_client.get_track_info, artist_name, track_title)
if lf_result:
lf_url = lf_result.get("url")
if lf_url:
pp["lastfm_url"] = lf_url
lf_toptags = lf_result.get("toptags", {})
if isinstance(lf_toptags, dict):
tag_list = lf_toptags.get("tag", [])
if isinstance(tag_list, list):
pp["lastfm_tags"] = [tag.get("name", "") for tag in tag_list if isinstance(tag, dict) and tag.get("name")]
elif isinstance(tag_list, dict) and tag_list.get("name"):
pp["lastfm_tags"] = [tag_list["name"]]
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:
return
if not track_title or not artist_name:
return
import core.genius_client as _genius_module
if time.time() < _genius_module._rate_limit_until:
logger.info("Genius rate-limited, skipping (non-blocking)")
return
genius_worker = getattr(runtime, "genius_worker", None)
g_client = genius_worker.client if genius_worker else None
if not g_client:
return
g_result = _call_source_lookup("Genius track", g_client.search_song, artist_name, track_title)
if g_result:
g_id = g_result.get("id")
if g_id:
pp["id_tags"]["GENIUS_TRACK_ID"] = str(g_id)
g_url = g_result.get("url")
if g_url:
pp["genius_url"] = g_url
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 == "hifi":
_process_hifi_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] = {}
for tag_name, value in pp["id_tags"].items():
config_path = SOURCE_TAG_CONFIG.get(tag_name)
if config_path and not _tag_enabled(cfg, config_path):
continue
filtered_tags[tag_name] = value
written = []
release_year = pp["release_year"]
if isinstance(audio_file.tags, symbols.ID3):
for tag_name, value in filtered_tags.items():
spec = ID3_TAG_MAP.get(tag_name)
if spec:
frame_type, desc = spec
if frame_type == "UFID":
audio_file.tags.add(symbols.UFID(owner=desc, data=str(value).encode("ascii")))
written.append(f"UFID:{desc}")
elif frame_type == "TDOR":
audio_file.tags.add(symbols.TDOR(encoding=3, text=[value]))
written.append("TDOR")
elif frame_type == "TMED":
audio_file.tags.add(symbols.TMED(encoding=3, text=[value]))
written.append("TMED")
else:
audio_file.tags.add(symbols.TXXX(encoding=3, desc=desc, text=[value]))
written.append(f"TXXX:{desc}")
else:
audio_file.tags.add(symbols.TXXX(encoding=3, desc=tag_name, text=[str(value)]))
written.append(f"TXXX:{tag_name}")
elif isinstance(audio_file, symbols.MP4):
for tag_name, value in filtered_tags.items():
key = f"----:com.apple.iTunes:{MP4_TAG_MAP.get(tag_name, tag_name)}"
audio_file[key] = [symbols.MP4FreeForm(str(value).encode("utf-8"))]
written.append(key)
elif is_vorbis_like(audio_file, symbols):
for tag_name, value in filtered_tags.items():
audio_file[VORBIS_TAG_MAP.get(tag_name, tag_name)] = [str(value)]
written.append(VORBIS_TAG_MAP.get(tag_name, tag_name))
if written:
logger.info("Embedded IDs: %s", ", ".join(written))
if release_year and not metadata.get("date"):
metadata["date"] = release_year
if isinstance(audio_file.tags, symbols.ID3):
audio_file.tags.add(symbols.TDRC(encoding=3, text=[release_year]))
elif is_vorbis_like(audio_file, symbols):
audio_file["date"] = [release_year]
elif isinstance(audio_file, symbols.MP4):
audio_file["\xa9day"] = [release_year]
logger.info("Date tag: %s", release_year)
bpm_candidates = []
if pp["deezer_bpm"] and pp["deezer_bpm"] > 0 and _tag_enabled(cfg, "deezer.tags.bpm"):
bpm_candidates.append(("Deezer", pp["deezer_bpm"]))
if pp["tidal_bpm"] and pp["tidal_bpm"] > 0 and _tag_enabled(cfg, "tidal.tags.bpm"):
bpm_candidates.append(("Tidal", pp["tidal_bpm"]))
if pp["hifi_bpm"] and pp["hifi_bpm"] > 0 and _tag_enabled(cfg, "hifi.tags.bpm"):
bpm_candidates.append(("HiFi", pp["hifi_bpm"]))
if bpm_candidates:
bpm_source, bpm_val = bpm_candidates[0]
bpm_int = int(bpm_val)
if isinstance(audio_file.tags, symbols.ID3):
audio_file.tags.add(symbols.TBPM(encoding=3, text=[str(bpm_int)]))
elif is_vorbis_like(audio_file, symbols):
audio_file["BPM"] = [str(bpm_int)]
elif isinstance(audio_file, symbols.MP4):
audio_file["tmpo"] = [bpm_int]
logger.info("BPM (%s): %s", bpm_source, bpm_int)
if _tag_enabled(cfg, "audiodb.tags.mood") and pp["audiodb_mood"]:
if isinstance(audio_file.tags, symbols.ID3):
audio_file.tags.add(symbols.TXXX(encoding=3, desc="MOOD", text=[pp["audiodb_mood"]]))
elif is_vorbis_like(audio_file, symbols):
audio_file["MOOD"] = [pp["audiodb_mood"]]
elif isinstance(audio_file, symbols.MP4):
audio_file["----:com.apple.iTunes:MOOD"] = [symbols.MP4FreeForm(pp["audiodb_mood"].encode("utf-8"))]
if _tag_enabled(cfg, "audiodb.tags.style") and pp["audiodb_style"]:
if isinstance(audio_file.tags, symbols.ID3):
audio_file.tags.add(symbols.TXXX(encoding=3, desc="STYLE", text=[pp["audiodb_style"]]))
elif is_vorbis_like(audio_file, symbols):
audio_file["STYLE"] = [pp["audiodb_style"]]
elif isinstance(audio_file, symbols.MP4):
audio_file["----:com.apple.iTunes:STYLE"] = [symbols.MP4FreeForm(pp["audiodb_style"].encode("utf-8"))]
if _tag_enabled(cfg, "metadata_enhancement.tags.genre_merge"):
enrichment_genres = []
if _tag_enabled(cfg, "musicbrainz.tags.genres"):
enrichment_genres += pp["mb_genres"]
if pp["audiodb_genre"] and _tag_enabled(cfg, "audiodb.tags.genre"):
enrichment_genres.append(pp["audiodb_genre"])
if _tag_enabled(cfg, "lastfm.tags.genres"):
enrichment_genres += pp["lastfm_tags"]
if enrichment_genres:
from core.genre_filter import filter_genres as _filter_genres
enrichment_genres = _filter_genres(enrichment_genres, cfg)
source_genres = [g.strip() for g in str(metadata.get("genre", "")).split(",") if g.strip()]
seen = set()
merged = []
for genre in source_genres + enrichment_genres:
key = genre.strip().lower()
if key and key not in seen:
seen.add(key)
merged.append(genre.strip().title())
if len(merged) >= 5:
break
if merged:
genre_string = ", ".join(merged)
if isinstance(audio_file.tags, symbols.ID3):
audio_file.tags.add(symbols.TCON(encoding=3, text=[genre_string]))
elif is_vorbis_like(audio_file, symbols):
audio_file["GENRE"] = [genre_string]
elif isinstance(audio_file, symbols.MP4):
audio_file["\xa9gen"] = [genre_string]
logger.info("Genres merged: %s", genre_string)
isrc_candidates = []
if pp["isrc"] and _tag_enabled(cfg, "musicbrainz.tags.isrc"):
isrc_candidates.append(("MusicBrainz", pp["isrc"]))
if pp["deezer_isrc"] and _tag_enabled(cfg, "deezer.tags.isrc"):
isrc_candidates.append(("Deezer", pp["deezer_isrc"]))
if pp["tidal_isrc"] and _tag_enabled(cfg, "tidal.tags.isrc"):
isrc_candidates.append(("Tidal", pp["tidal_isrc"]))
if pp["hifi_isrc"] and _tag_enabled(cfg, "hifi.tags.isrc"):
isrc_candidates.append(("HiFi", pp["hifi_isrc"]))
if pp["qobuz_isrc"] and _tag_enabled(cfg, "qobuz.tags.isrc"):
isrc_candidates.append(("Qobuz", pp["qobuz_isrc"]))
if isrc_candidates:
isrc_source, final_isrc = isrc_candidates[0]
if isinstance(audio_file.tags, symbols.ID3):
audio_file.tags.add(symbols.TSRC(encoding=3, text=[final_isrc]))
elif is_vorbis_like(audio_file, symbols):
audio_file["ISRC"] = [final_isrc]
elif isinstance(audio_file, symbols.MP4):
audio_file["----:com.apple.iTunes:ISRC"] = [symbols.MP4FreeForm(final_isrc.encode("utf-8"))]
logger.info("ISRC (%s): %s", isrc_source, final_isrc)
copyright_candidates = []
if pp["tidal_copyright"] and _tag_enabled(cfg, "tidal.tags.copyright"):
copyright_candidates.append(("Tidal", pp["tidal_copyright"]))
if pp["qobuz_copyright"] and _tag_enabled(cfg, "qobuz.tags.copyright"):
copyright_candidates.append(("Qobuz", pp["qobuz_copyright"]))
if pp["hifi_copyright"] and _tag_enabled(cfg, "hifi.tags.copyright"):
copyright_candidates.append(("HiFi", pp["hifi_copyright"]))
if copyright_candidates:
copyright_source, final_copyright = copyright_candidates[0]
if isinstance(audio_file.tags, symbols.ID3):
audio_file.tags.add(symbols.TCOP(encoding=3, text=[final_copyright]))
elif is_vorbis_like(audio_file, symbols):
audio_file["COPYRIGHT"] = [final_copyright]
elif isinstance(audio_file, symbols.MP4):
audio_file["cprt"] = [final_copyright]
logger.info("Copyright (%s): %s", copyright_source, final_copyright[:60])
if _tag_enabled(cfg, "qobuz.tags.label") and pp["qobuz_label"]:
if isinstance(audio_file.tags, symbols.ID3):
audio_file.tags.add(symbols.TPUB(encoding=3, text=[pp["qobuz_label"]]))
elif is_vorbis_like(audio_file, symbols):
audio_file["LABEL"] = [pp["qobuz_label"]]
elif isinstance(audio_file, symbols.MP4):
audio_file["----:com.apple.iTunes:LABEL"] = [symbols.MP4FreeForm(pp["qobuz_label"].encode("utf-8"))]
if _tag_enabled(cfg, "lastfm.tags.url") and pp["lastfm_url"]:
if isinstance(audio_file.tags, symbols.ID3):
audio_file.tags.add(symbols.TXXX(encoding=3, desc="LASTFM_URL", text=[pp["lastfm_url"]]))
elif is_vorbis_like(audio_file, symbols):
audio_file["LASTFM_URL"] = [pp["lastfm_url"]]
elif isinstance(audio_file, symbols.MP4):
audio_file["----:com.apple.iTunes:LASTFM_URL"] = [symbols.MP4FreeForm(pp["lastfm_url"].encode("utf-8"))]
if _tag_enabled(cfg, "genius.tags.url") and pp["genius_url"]:
if isinstance(audio_file.tags, symbols.ID3):
audio_file.tags.add(symbols.TXXX(encoding=3, desc="GENIUS_URL", text=[pp["genius_url"]]))
elif is_vorbis_like(audio_file, symbols):
audio_file["GENIUS_URL"] = [pp["genius_url"]]
elif isinstance(audio_file, symbols.MP4):
audio_file["----:com.apple.iTunes:GENIUS_URL"] = [symbols.MP4FreeForm(pp["genius_url"].encode("utf-8"))]
return release_year
def _update_album_year_in_database(db, metadata: dict, release_year) -> None:
if db is None:
return
try:
album_name_for_db = metadata.get("album", "")
album_artist_for_db = metadata.get("album_artist", "") or metadata.get("artist", "")
if album_name_for_db and album_artist_for_db:
conn = db._get_connection()
try:
cursor = conn.cursor()
cursor.execute(
"""
UPDATE albums SET year = ?
WHERE (year IS NULL OR year = 0)
AND id IN (
SELECT al.id FROM albums al
JOIN artists ar ON ar.id = al.artist_id
WHERE LOWER(al.title) = LOWER(?) AND LOWER(ar.name) = LOWER(?)
)
""",
(int(release_year), album_name_for_db, album_artist_for_db),
)
if cursor.rowcount > 0:
conn.commit()
logger.info("Updated album year to %s in database", release_year)
else:
conn.rollback()
finally:
conn.close()
except Exception as 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,
"tidal_bpm": None,
"hifi_bpm": None,
"hifi_copyright": None,
"audiodb_mood": None,
"audiodb_style": None,
"audiodb_genre": None,
"tidal_isrc": None,
"tidal_copyright": None,
"hifi_isrc": 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
# If this download came from HiFi, use cached metadata from the download
# pipeline instead of re-searching the HiFi API.
original_search = get_import_original_search(context)
cached_meta = original_search.get("_source_metadata") or {}
if cached_meta.get("source") == "hifi":
if _tag_enabled(cfg, "hifi.embed_tags"):
if cfg.get("hifi.tags.track_id", True) and cached_meta.get("track_id"):
pp["id_tags"]["HIFI_TRACK_ID"] = str(cached_meta["track_id"])
if cfg.get("hifi.tags.artist_id", True) and cached_meta.get("artist_id"):
pp["id_tags"]["HIFI_ARTIST_ID"] = str(cached_meta["artist_id"])
if cfg.get("hifi.tags.isrc", True) and cached_meta.get("isrc"):
pp["hifi_isrc"] = cached_meta["isrc"]
if cfg.get("hifi.tags.bpm", True) and cached_meta.get("bpm"):
pp["hifi_bpm"] = cached_meta["bpm"]
if cfg.get("hifi.tags.copyright", True) and cached_meta.get("copyright"):
pp["hifi_copyright"] = cached_meta["copyright"]
source_order = [s for s in source_order if s != "hifi"]
# If this download came from Tidal, use cached metadata from the download
# pipeline instead of re-searching the Tidal API.
if cached_meta.get("source") == "tidal":
if _tag_enabled(cfg, "tidal.embed_tags"):
if cfg.get("tidal.tags.track_id", True) and cached_meta.get("track_id"):
pp["id_tags"]["TIDAL_TRACK_ID"] = str(cached_meta["track_id"])
if cfg.get("tidal.tags.artist_id", True) and cached_meta.get("artist_id"):
pp["id_tags"]["TIDAL_ARTIST_ID"] = str(cached_meta["artist_id"])
if cfg.get("tidal.tags.isrc", True) and cached_meta.get("isrc"):
pp["tidal_isrc"] = cached_meta["isrc"]
if cfg.get("tidal.tags.bpm", True) and cached_meta.get("bpm"):
pp["tidal_bpm"] = cached_meta["bpm"]
if cfg.get("tidal.tags.copyright", True) and cached_meta.get("copyright"):
pp["tidal_copyright"] = cached_meta["copyright"]
source_order = [s for s in source_order if s != "tidal"]
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["tidal_bpm"] and not pp["hifi_bpm"] and not pp["hifi_copyright"] 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)
# Expose the final ID tag set + per-source values back to the
# import context so downstream side-effects (notably
# ``record_download_provenance``) can persist them to the
# ``track_downloads`` table without re-collecting. Without this,
# the watchlist scanner would have to wait for the async
# enrichment workers to backfill ``tracks.spotify_track_id`` etc.
# before recognizing freshly downloaded files.
if isinstance(context, dict):
try:
context["_embedded_id_tags"] = dict(pp.get("id_tags") or {})
isrc_value = (
pp.get("isrc") or pp.get("deezer_isrc") or pp.get("tidal_isrc")
or pp.get("hifi_isrc") or pp.get("qobuz_isrc")
)
if isrc_value:
context["_isrc"] = str(isrc_value)
except Exception:
pass
except Exception as exc:
logger.error("Error embedding source IDs (non-fatal): %s", exc)