Tagging: write source IDs too (Write Tags button + library re-tag now complete)

write_tags_to_file wrote the core fields + cover but never the source IDs
(Spotify/iTunes/MusicBrainz) the import post-process embeds. Added a focused
source.embed_known_source_ids() that writes ALREADY-KNOWN ids (from db_data)
via the canonical, Picard-compatible frame writer the import uses
(_write_embedded_metadata) — no API re-fetch, frames correct by construction.
write_tags_to_file now calls it whenever db_data carries id keys.

Fed from both paths: the enhanced-library 'Write Tags' button now carries the
track's spotify/itunes/musicbrainz ids, and the Library Re-tag job stamps the
matched album/track source ids onto each track. So both now write the full tag
set, not a subset.
This commit is contained in:
BoulderBadgeDad 2026-06-04 09:20:34 -07:00
parent 50b6876d6b
commit 3ea9da1cba
4 changed files with 102 additions and 1 deletions

View file

@ -1122,6 +1122,60 @@ def extract_source_metadata(context: dict, artist: dict, album_info: dict) -> di
return metadata
def embed_known_source_ids(audio_file, metadata: dict) -> list:
"""Embed ALREADY-KNOWN source IDs into a file's tags, no API re-fetch.
For the library re-tag / "Write Tags" paths: ``metadata`` carries flat id
keys already stored in the DB (``spotify_track_id``, ``spotify_album_id``,
``itunes_track_id``, ``itunes_album_id``, ``musicbrainz_recording_id``,
``musicbrainz_release_id``, ). Reuses the SAME canonical, format-specific,
Picard-compatible frame writer as the import post-process
(``_write_embedded_metadata``) so the frames never diverge it just skips
the heavy multi-source re-search that ``embed_source_ids`` does.
The caller is responsible for saving the file. Returns the canonical tag
names written (for logging/diagnostics).
"""
cfg = get_config_manager()
symbols = get_mutagen_symbols()
if not symbols or audio_file is None:
return []
try:
id_tags = _collect_source_ids(metadata, cfg)
# MusicBrainz ids aren't in _collect_source_ids (they come from the MB
# processor at import time); add them from the DB when present, using
# the canonical names the frame map already knows.
if cfg.get("musicbrainz.embed_tags", True) is not False:
if metadata.get("musicbrainz_recording_id"):
id_tags["MUSICBRAINZ_RECORDING_ID"] = metadata["musicbrainz_recording_id"]
if metadata.get("musicbrainz_release_id"):
id_tags["MUSICBRAINZ_RELEASE_ID"] = metadata["musicbrainz_release_id"]
if not id_tags:
return []
pp = _blank_post_process_state()
pp["id_tags"] = id_tags
_write_embedded_metadata(audio_file, metadata, pp, cfg, symbols)
return list(id_tags.keys())
except Exception as exc:
logger.debug("embed_known_source_ids failed: %s", exc)
return []
def _blank_post_process_state() -> dict:
"""A fully-defaulted post-process state so _write_embedded_metadata can run
with only id_tags set (every enrichment branch no-ops on the blanks)."""
return {
"id_tags": {}, "track_title": "", "artist_name": "", "batch_artist_name": None,
"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,
}
def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=None):
cfg = get_config_manager()
symbols = get_mutagen_symbols()

View file

@ -45,6 +45,26 @@ def _read_current_tags(file_path):
return {}
def _add_source_ids(db_data, source, album_source_id, source_track):
"""Stamp the album/track source IDs onto the write payload so the canonical
writer embeds them too (Spotify / iTunes / MusicBrainz)."""
album_key = {'spotify': 'spotify_album_id', 'itunes': 'itunes_album_id',
'musicbrainz': 'musicbrainz_release_id'}.get(source)
track_key = {'spotify': 'spotify_track_id', 'itunes': 'itunes_track_id',
'musicbrainz': 'musicbrainz_recording_id'}.get(source)
if album_key and album_source_id:
db_data[album_key] = album_source_id
if track_key:
tid = None
for k in ('id', 'track_id', 'source_track_id'):
v = source_track.get(k) if isinstance(source_track, dict) else getattr(source_track, k, None)
if v:
tid = v
break
if tid:
db_data[track_key] = tid
def _track_list(result):
"""Normalize a get_album_tracks result into a plain list of track items."""
if result is None:
@ -222,12 +242,14 @@ class LibraryRetagJob(RepairJob):
# Include a track when its tags change, OR when there's a cover action
# to apply to it (db_data may be empty — apply embeds art either way).
if plan['changes'] or cover_action:
db_data = plan['db_data']
_add_source_ids(db_data, source, album_source_id, src)
track_plans.append({
'file_path': lib['file_path'],
'track_id': lib['id'],
'title': lib['title'],
'changes': plan['changes'],
'db_data': plan['db_data'],
'db_data': db_data,
})
tag_change_tracks = sum(1 for tp in track_plans if tp['changes'])

View file

@ -294,6 +294,24 @@ def write_tags_to_file(file_path: str, db_data: Dict[str, Any],
year, genre_str, track_num, total_tracks,
disc_num, bpm, artists_list=artists_list)
# Embed already-known source IDs (Spotify / iTunes / MusicBrainz) from
# db_data, reusing the canonical import-time frame writer — no API
# re-fetch. Only fires when db_data carries id keys, so the plain
# "write the core tags" callers are unaffected.
_src_meta = {k: db_data[k] for k in (
'source', 'source_track_id', 'source_album_id', 'source_artist_id',
'spotify_track_id', 'spotify_album_id', 'spotify_artist_id',
'itunes_track_id', 'itunes_album_id', 'itunes_artist_id',
'musicbrainz_recording_id', 'musicbrainz_release_id',
) if db_data.get(k)}
if _src_meta:
try:
from core.metadata.source import embed_known_source_ids
if embed_known_source_ids(audio, _src_meta):
written.append('source_ids')
except Exception as e:
logger.debug("source-id embed skipped for %s: %s", file_path, e)
# Embed cover art if requested
if embed_cover:
art_ok = False

View file

@ -9336,6 +9336,13 @@ def _build_library_tag_db_data(track_data, album_genres=None):
if artists_list:
db_data['artists_list'] = artists_list
# Carry the known source IDs through so they get embedded too (the writer
# only acts on the ones present). These come from t.* on the track row.
for _k in ('spotify_track_id', 'itunes_track_id', 'musicbrainz_recording_id'):
_v = track_data.get(_k)
if _v:
db_data[_k] = _v
return db_data