Add per-tag granular toggle settings for all 47 embedded tags

Replace category-based tag settings (10 toggles) with per-tag controls
grouped by source service in an accordion UI. Each of the 11 service
groups (Spotify, iTunes, MusicBrainz, Deezer, AudioDB, Tidal, Qobuz,
Last.fm, Genius, General) has a master toggle that disables all child
tags, with individual toggles for fine-grained control. ISRC and
copyright fallback chains are now per-source toggleable. Genre merge
contributions from each source are independently controllable. All
tags default to enabled for backward compatibility.
This commit is contained in:
Broque Thomas 2026-03-18 16:17:20 -07:00
parent d3bff90fd6
commit b9732156cd
4 changed files with 421 additions and 200 deletions

View file

@ -14718,7 +14718,7 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in
# ── Embed audio quality tag ──
quality = context.get('_audio_quality', '')
if quality and config_manager.get('metadata_enhancement.tags.quality', True) is not False:
if quality and config_manager.get('metadata_enhancement.tags.quality_tag', True) is not False:
if isinstance(audio_file.tags, ID3):
audio_file.tags.add(TXXX(encoding=3, desc='QUALITY', text=[quality]))
elif isinstance(audio_file, (FLAC, OggVorbis)):
@ -14967,20 +14967,54 @@ def _embed_source_ids(audio_file, metadata: dict):
Operates on a non-easy-mode MutagenFile object (caller must save).
"""
try:
# ── Tag category config (all default to True) ──
tag_cfg = {
'musicbrainz_ids': config_manager.get('metadata_enhancement.tags.musicbrainz_ids', True) is not False,
'release_info': config_manager.get('metadata_enhancement.tags.release_info', True) is not False,
'source_ids': config_manager.get('metadata_enhancement.tags.source_ids', True) is not False,
'isrc': config_manager.get('metadata_enhancement.tags.isrc', True) is not False,
'bpm': config_manager.get('metadata_enhancement.tags.bpm', True) is not False,
'mood_style': config_manager.get('metadata_enhancement.tags.mood_style', True) is not False,
'copyright_label': config_manager.get('metadata_enhancement.tags.copyright_label', True) is not False,
'genre_merge': config_manager.get('metadata_enhancement.tags.genre_merge', True) is not False,
'urls': config_manager.get('metadata_enhancement.tags.urls', True) is not False,
'quality': config_manager.get('metadata_enhancement.tags.quality', True) is not False,
# ── Per-tag config: maps internal tag name → config path ──
# Each tag can be individually toggled via {service}.tags.{tag_name}
_TAG_CONFIG = {
# Spotify (from metadata, no API call)
'SPOTIFY_TRACK_ID': 'spotify.tags.track_id',
'SPOTIFY_ARTIST_ID': 'spotify.tags.artist_id',
'SPOTIFY_ALBUM_ID': 'spotify.tags.album_id',
# iTunes (from metadata, no API call)
'ITUNES_TRACK_ID': 'itunes.tags.track_id',
'ITUNES_ARTIST_ID': 'itunes.tags.artist_id',
'ITUNES_ALBUM_ID': 'itunes.tags.album_id',
# MusicBrainz IDs
'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',
# MusicBrainz Release Info
'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
'DEEZER_TRACK_ID': 'deezer.tags.track_id',
'DEEZER_ARTIST_ID': 'deezer.tags.artist_id',
# AudioDB
'AUDIODB_TRACK_ID': 'audiodb.tags.track_id',
# Tidal
'TIDAL_TRACK_ID': 'tidal.tags.track_id',
'TIDAL_ARTIST_ID': 'tidal.tags.artist_id',
# Qobuz
'QOBUZ_TRACK_ID': 'qobuz.tags.track_id',
'QOBUZ_ARTIST_ID': 'qobuz.tags.artist_id',
# Genius
'GENIUS_TRACK_ID': 'genius.tags.track_id',
}
def _tag_enabled(config_path):
"""Check if an individual tag is enabled (defaults to True)."""
return config_manager.get(config_path, True) is not False
# ── Helper: normalize + compare names (same logic as enrichment workers) ──
from difflib import SequenceMatcher
def _names_match(a: str, b: str, threshold: float = 0.75) -> bool:
@ -14991,18 +15025,20 @@ def _embed_source_ids(audio_file, metadata: dict):
# ── 1. Collect Spotify / iTunes IDs already in metadata ──
id_tags = {}
if metadata.get('spotify_track_id'):
id_tags['SPOTIFY_TRACK_ID'] = metadata['spotify_track_id']
if metadata.get('spotify_artist_id'):
id_tags['SPOTIFY_ARTIST_ID'] = metadata['spotify_artist_id']
if metadata.get('spotify_album_id'):
id_tags['SPOTIFY_ALBUM_ID'] = metadata['spotify_album_id']
if metadata.get('itunes_track_id'):
id_tags['ITUNES_TRACK_ID'] = metadata['itunes_track_id']
if metadata.get('itunes_artist_id'):
id_tags['ITUNES_ARTIST_ID'] = metadata['itunes_artist_id']
if metadata.get('itunes_album_id'):
id_tags['ITUNES_ALBUM_ID'] = metadata['itunes_album_id']
if config_manager.get('spotify.embed_tags', True) is not False:
if metadata.get('spotify_track_id'):
id_tags['SPOTIFY_TRACK_ID'] = metadata['spotify_track_id']
if metadata.get('spotify_artist_id'):
id_tags['SPOTIFY_ARTIST_ID'] = metadata['spotify_artist_id']
if metadata.get('spotify_album_id'):
id_tags['SPOTIFY_ALBUM_ID'] = metadata['spotify_album_id']
if config_manager.get('itunes.embed_tags', True) is not False:
if metadata.get('itunes_track_id'):
id_tags['ITUNES_TRACK_ID'] = metadata['itunes_track_id']
if metadata.get('itunes_artist_id'):
id_tags['ITUNES_ARTIST_ID'] = metadata['itunes_artist_id']
if metadata.get('itunes_album_id'):
id_tags['ITUNES_ALBUM_ID'] = metadata['itunes_album_id']
# ── 2a. MusicBrainz lookup for MBID, genres, and ISRC ──
# The global rate limiter in musicbrainz_client.py serializes all API
@ -15082,7 +15118,7 @@ def _embed_source_ids(audio_file, metadata: dict):
# ── 2a-2. MusicBrainz release details (release group, barcode, media, etc.) ──
# One API call per release, cached across all tracks on the same album.
if _rc_mbid and (tag_cfg['release_info'] or tag_cfg['musicbrainz_ids']):
if _rc_mbid:
try:
mb_service_for_detail = mb_worker.mb_service if mb_worker else None
if mb_service_for_detail:
@ -15361,26 +15397,11 @@ def _embed_source_ids(audio_file, metadata: dict):
if not id_tags and not deezer_bpm and not deezer_isrc and not audiodb_mood and not audiodb_style:
return
# ── 3. Filter tags by category config, then write ──
_MB_ID_TAGS = {'MUSICBRAINZ_RECORDING_ID', 'MUSICBRAINZ_ARTIST_ID', 'MUSICBRAINZ_RELEASE_ID',
'MUSICBRAINZ_RELEASEGROUPID', 'MUSICBRAINZ_ALBUMARTISTID',
'MUSICBRAINZ_RELEASETRACKID'}
_SOURCE_ID_TAGS = {'SPOTIFY_TRACK_ID', 'SPOTIFY_ARTIST_ID', 'SPOTIFY_ALBUM_ID',
'ITUNES_TRACK_ID', 'ITUNES_ARTIST_ID', 'ITUNES_ALBUM_ID',
'DEEZER_TRACK_ID', 'DEEZER_ARTIST_ID', 'AUDIODB_TRACK_ID',
'TIDAL_TRACK_ID', 'TIDAL_ARTIST_ID', 'QOBUZ_TRACK_ID',
'QOBUZ_ARTIST_ID', 'GENIUS_TRACK_ID'}
_RELEASE_INFO_TAGS = {'RELEASETYPE', 'RELEASESTATUS', 'RELEASECOUNTRY', 'MEDIA',
'BARCODE', 'CATALOGNUMBER', 'ORIGINALDATE', 'TOTALDISCS', 'SCRIPT',
'ASIN'}
# ── 3. Filter tags by per-tag config, then write ──
filtered_tags = {}
for tag_name, value in id_tags.items():
if tag_name in _MB_ID_TAGS and not tag_cfg['musicbrainz_ids']:
continue
if tag_name in _SOURCE_ID_TAGS and not tag_cfg['source_ids']:
continue
if tag_name in _RELEASE_INFO_TAGS and not tag_cfg['release_info']:
config_path = _TAG_CONFIG.get(tag_name)
if config_path and not _tag_enabled(config_path):
continue
filtered_tags[tag_name] = value
@ -15462,7 +15483,7 @@ def _embed_source_ids(audio_file, metadata: dict):
print(f"🔗 Embedded IDs: {', '.join(written)}")
# ── 3b. Write BPM tag (from Deezer) ──
if tag_cfg['bpm'] and deezer_bpm and deezer_bpm > 0:
if _tag_enabled('deezer.tags.bpm') and deezer_bpm and deezer_bpm > 0:
bpm_int = int(deezer_bpm)
if isinstance(audio_file.tags, ID3):
audio_file.tags.add(TBPM(encoding=3, text=[str(bpm_int)]))
@ -15473,7 +15494,7 @@ def _embed_source_ids(audio_file, metadata: dict):
print(f"🥁 BPM: {bpm_int}")
# ── 3c. Write mood tag (from AudioDB) ──
if tag_cfg['mood_style'] and audiodb_mood:
if _tag_enabled('audiodb.tags.mood') and audiodb_mood:
if isinstance(audio_file.tags, ID3):
audio_file.tags.add(TXXX(encoding=3, desc='MOOD', text=[audiodb_mood]))
elif isinstance(audio_file, (FLAC, OggVorbis)):
@ -15483,7 +15504,7 @@ def _embed_source_ids(audio_file, metadata: dict):
print(f"🎭 Mood: {audiodb_mood}")
# ── 3d. Write style tag (from AudioDB) ──
if tag_cfg['mood_style'] and audiodb_style:
if _tag_enabled('audiodb.tags.style') and audiodb_style:
if isinstance(audio_file.tags, ID3):
audio_file.tags.add(TXXX(encoding=3, desc='STYLE', text=[audiodb_style]))
elif isinstance(audio_file, (FLAC, OggVorbis)):
@ -15493,8 +15514,10 @@ def _embed_source_ids(audio_file, metadata: dict):
print(f"🎨 Style: {audiodb_style}")
# ── 4. Merge genres (Spotify + MusicBrainz + AudioDB + Last.fm) and overwrite tag ──
if tag_cfg['genre_merge']:
enrichment_genres = mb_genres + ([audiodb_genre] if audiodb_genre else []) + lastfm_tags
if _tag_enabled('metadata_enhancement.tags.genre_merge'):
enrichment_genres = (mb_genres if _tag_enabled('musicbrainz.tags.genres') else []) + \
([audiodb_genre] if audiodb_genre and _tag_enabled('audiodb.tags.genre') else []) + \
(lastfm_tags if _tag_enabled('lastfm.tags.genres') else [])
if enrichment_genres:
spotify_genres = [g.strip() for g in metadata.get('genre', '').split(',') if g.strip()]
seen = set()
@ -15517,34 +15540,44 @@ def _embed_source_ids(audio_file, metadata: dict):
audio_file['\xa9gen'] = [genre_string]
print(f"🎶 Genres merged: {genre_string}")
# ── 5. Write ISRC if available (MusicBrainz → Deezer → Tidal → Qobuz fallback) ──
if tag_cfg['isrc']:
final_isrc = isrc or deezer_isrc or tidal_isrc or qobuz_isrc
if final_isrc:
if isinstance(audio_file.tags, ID3):
audio_file.tags.add(TSRC(encoding=3, text=[final_isrc]))
elif isinstance(audio_file, (FLAC, OggVorbis)):
audio_file['ISRC'] = [final_isrc]
elif isinstance(audio_file, MP4):
audio_file['----:com.apple.iTunes:ISRC'] = [MP4FreeForm(final_isrc.encode('utf-8'))]
source = "MusicBrainz" if isrc else "Deezer" if deezer_isrc else "Tidal" if tidal_isrc else "Qobuz"
print(f"🔖 ISRC ({source}): {final_isrc}")
# ── 5. Write ISRC if available (per-source fallback chain) ──
_isrc_candidates = []
if isrc and _tag_enabled('musicbrainz.tags.isrc'):
_isrc_candidates.append(('MusicBrainz', isrc))
if deezer_isrc and _tag_enabled('deezer.tags.isrc'):
_isrc_candidates.append(('Deezer', deezer_isrc))
if tidal_isrc and _tag_enabled('tidal.tags.isrc'):
_isrc_candidates.append(('Tidal', tidal_isrc))
if qobuz_isrc and _tag_enabled('qobuz.tags.isrc'):
_isrc_candidates.append(('Qobuz', qobuz_isrc))
if _isrc_candidates:
source, final_isrc = _isrc_candidates[0]
if isinstance(audio_file.tags, ID3):
audio_file.tags.add(TSRC(encoding=3, text=[final_isrc]))
elif isinstance(audio_file, (FLAC, OggVorbis)):
audio_file['ISRC'] = [final_isrc]
elif isinstance(audio_file, MP4):
audio_file['----:com.apple.iTunes:ISRC'] = [MP4FreeForm(final_isrc.encode('utf-8'))]
print(f"🔖 ISRC ({source}): {final_isrc}")
# ── 6. Write copyright tag (Tidal → Qobuz fallback) ──
if tag_cfg['copyright_label']:
final_copyright = tidal_copyright or qobuz_copyright
if final_copyright:
if isinstance(audio_file.tags, ID3):
audio_file.tags.add(TCOP(encoding=3, text=[final_copyright]))
elif isinstance(audio_file, (FLAC, OggVorbis)):
audio_file['COPYRIGHT'] = [final_copyright]
elif isinstance(audio_file, MP4):
audio_file['cprt'] = [final_copyright]
source = "Tidal" if tidal_copyright else "Qobuz"
print(f"©️ Copyright ({source}): {final_copyright[:60]}")
_copyright_candidates = []
if tidal_copyright and _tag_enabled('tidal.tags.copyright'):
_copyright_candidates.append(('Tidal', tidal_copyright))
if qobuz_copyright and _tag_enabled('qobuz.tags.copyright'):
_copyright_candidates.append(('Qobuz', qobuz_copyright))
if _copyright_candidates:
source, final_copyright = _copyright_candidates[0]
if isinstance(audio_file.tags, ID3):
audio_file.tags.add(TCOP(encoding=3, text=[final_copyright]))
elif isinstance(audio_file, (FLAC, OggVorbis)):
audio_file['COPYRIGHT'] = [final_copyright]
elif isinstance(audio_file, MP4):
audio_file['cprt'] = [final_copyright]
print(f"©️ Copyright ({source}): {final_copyright[:60]}")
# ── 7. Write label/publisher tag (from Qobuz) ──
if tag_cfg['copyright_label'] and qobuz_label:
if _tag_enabled('qobuz.tags.label') and qobuz_label:
if isinstance(audio_file.tags, ID3):
audio_file.tags.add(TPUB(encoding=3, text=[qobuz_label]))
elif isinstance(audio_file, (FLAC, OggVorbis)):
@ -15554,7 +15587,7 @@ def _embed_source_ids(audio_file, metadata: dict):
print(f"🏷️ Label (Qobuz): {qobuz_label}")
# ── 8. Write Last.fm and Genius URLs as custom tags ──
if tag_cfg['urls'] and lastfm_url:
if _tag_enabled('lastfm.tags.url') and lastfm_url:
if isinstance(audio_file.tags, ID3):
audio_file.tags.add(TXXX(encoding=3, desc='LASTFM_URL', text=[lastfm_url]))
elif isinstance(audio_file, (FLAC, OggVorbis)):
@ -15562,7 +15595,7 @@ def _embed_source_ids(audio_file, metadata: dict):
elif isinstance(audio_file, MP4):
audio_file['----:com.apple.iTunes:LASTFM_URL'] = [MP4FreeForm(lastfm_url.encode('utf-8'))]
if tag_cfg['urls'] and genius_url:
if _tag_enabled('genius.tags.url') and genius_url:
if isinstance(audio_file.tags, ID3):
audio_file.tags.add(TXXX(encoding=3, desc='GENIUS_URL', text=[genius_url]))
elif isinstance(audio_file, (FLAC, OggVorbis)):

View file

@ -4350,103 +4350,189 @@
</div>
</div>
<!-- Metadata Services -->
<!-- Tag Embedding — per-tag toggles grouped by source -->
<div class="post-processing-section">
<div class="post-processing-section-title">Metadata Services</div>
<small class="settings-hint" style="margin-bottom: 8px; display: block;">Choose which services embed IDs, genres, and metadata into your files during download</small>
<div class="post-processing-section-title">Tag Embedding</div>
<small class="settings-hint" style="margin-bottom: 8px; display: block;">Toggle a service to enable/disable all its tags, or expand to control individual tags.</small>
<div class="post-processing-grid">
<label class="checkbox-label">
<input type="checkbox" id="embed-musicbrainz" checked>
MusicBrainz
</label>
<label class="checkbox-label">
<input type="checkbox" id="embed-deezer" checked>
Deezer
</label>
<label class="checkbox-label">
<input type="checkbox" id="embed-audiodb" checked>
AudioDB
</label>
<label class="checkbox-label">
<input type="checkbox" id="embed-tidal" checked>
Tidal
</label>
<label class="checkbox-label">
<input type="checkbox" id="embed-qobuz" checked>
Qobuz
</label>
<label class="checkbox-label">
<input type="checkbox" id="embed-lastfm" checked>
Last.fm
</label>
<label class="checkbox-label">
<input type="checkbox" id="embed-genius" checked>
Genius
</label>
<!-- Spotify -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-spotify" checked onchange="toggleServiceTags(this, 'spotify')"> Spotify
</label>
<span class="tag-service-count">3 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="spotify.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="spotify.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="spotify.tags.album_id" checked> Album ID</label>
</div>
</div>
</div>
<!-- Tags to Embed -->
<div class="post-processing-section">
<div class="post-processing-section-title">Tags to Embed</div>
<small class="settings-hint" style="margin-bottom: 8px; display: block;">Choose which tag categories are written into your audio files. All enabled by default.</small>
<div class="post-processing-grid tag-embed-grid">
<label class="checkbox-label tag-embed-label">
<input type="checkbox" id="tag-musicbrainz-ids" checked>
MusicBrainz IDs
<small class="tag-embed-desc">Recording, Artist, Album, Release Group, Album Artist, Release Track</small>
</label>
<label class="checkbox-label tag-embed-label">
<input type="checkbox" id="tag-release-info" checked>
Release Info
<small class="tag-embed-desc">Original Date, Type, Status, Country, Media, Barcode, Catalog #, ASIN</small>
</label>
<label class="checkbox-label tag-embed-label">
<input type="checkbox" id="tag-source-ids" checked>
Source IDs
<small class="tag-embed-desc">Spotify, iTunes, Deezer, AudioDB, Tidal, Qobuz, Genius</small>
</label>
<label class="checkbox-label tag-embed-label">
<input type="checkbox" id="tag-isrc" checked>
ISRC
<small class="tag-embed-desc">International Standard Recording Code</small>
</label>
<label class="checkbox-label tag-embed-label">
<input type="checkbox" id="tag-bpm" checked>
BPM
<small class="tag-embed-desc">Beats per minute from Deezer</small>
</label>
<label class="checkbox-label tag-embed-label">
<input type="checkbox" id="tag-mood-style" checked>
Mood & Style
<small class="tag-embed-desc">Mood and Style from AudioDB</small>
</label>
<label class="checkbox-label tag-embed-label">
<input type="checkbox" id="tag-copyright-label" checked>
Copyright & Label
<small class="tag-embed-desc">Copyright from Tidal/Qobuz, Label from Qobuz</small>
</label>
<label class="checkbox-label tag-embed-label">
<input type="checkbox" id="tag-genre-merge" checked>
Genre Merging
<small class="tag-embed-desc">Merge genres from Spotify + MB + AudioDB + Last.fm</small>
</label>
<label class="checkbox-label tag-embed-label">
<input type="checkbox" id="tag-urls" checked>
URLs
<small class="tag-embed-desc">Last.fm and Genius URLs</small>
</label>
<label class="checkbox-label tag-embed-label">
<input type="checkbox" id="tag-quality" checked>
Quality
<small class="tag-embed-desc">Audio quality (e.g. FLAC, MP3-320)</small>
</label>
<!-- iTunes -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-itunes" checked onchange="toggleServiceTags(this, 'itunes')"> iTunes
</label>
<span class="tag-service-count">3 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="itunes.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="itunes.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="itunes.tags.album_id" checked> Album ID</label>
</div>
</div>
<!-- MusicBrainz -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-musicbrainz" checked onchange="toggleServiceTags(this, 'musicbrainz')"> MusicBrainz
</label>
<span class="tag-service-count">18 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.recording_id" checked> Recording ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_id" checked> Release ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_group_id" checked> Release Group ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.album_artist_id" checked> Album Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_track_id" checked> Release Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_type" checked> Release Type</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.original_date" checked> Original Date</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_status" checked> Release Status</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_country" checked> Release Country</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.barcode" checked> Barcode</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.media" checked> Media</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.total_discs" checked> Total Discs</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.catalog_number" checked> Catalog Number</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.script" checked> Script</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.asin" checked> ASIN</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.isrc" checked> ISRC</label>
<label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.genres" checked> Genres</label>
</div>
</div>
<!-- Deezer -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-deezer" checked onchange="toggleServiceTags(this, 'deezer')"> Deezer
</label>
<span class="tag-service-count">4 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="deezer.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="deezer.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="deezer.tags.bpm" checked> BPM</label>
<label class="checkbox-label"><input type="checkbox" data-config="deezer.tags.isrc" checked> ISRC</label>
</div>
</div>
<!-- AudioDB -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-audiodb" checked onchange="toggleServiceTags(this, 'audiodb')"> AudioDB
</label>
<span class="tag-service-count">4 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="audiodb.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="audiodb.tags.mood" checked> Mood</label>
<label class="checkbox-label"><input type="checkbox" data-config="audiodb.tags.style" checked> Style</label>
<label class="checkbox-label"><input type="checkbox" data-config="audiodb.tags.genre" checked> Genre</label>
</div>
</div>
<!-- Tidal -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-tidal" checked onchange="toggleServiceTags(this, 'tidal')"> Tidal
</label>
<span class="tag-service-count">4 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.isrc" checked> ISRC</label>
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.copyright" checked> Copyright</label>
</div>
</div>
<!-- Qobuz -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-qobuz" checked onchange="toggleServiceTags(this, 'qobuz')"> Qobuz
</label>
<span class="tag-service-count">5 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="qobuz.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="qobuz.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="qobuz.tags.isrc" checked> ISRC</label>
<label class="checkbox-label"><input type="checkbox" data-config="qobuz.tags.copyright" checked> Copyright</label>
<label class="checkbox-label"><input type="checkbox" data-config="qobuz.tags.label" checked> Label</label>
</div>
</div>
<!-- Last.fm -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-lastfm" checked onchange="toggleServiceTags(this, 'lastfm')"> Last.fm
</label>
<span class="tag-service-count">2 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="lastfm.tags.genres" checked> Genres</label>
<label class="checkbox-label"><input type="checkbox" data-config="lastfm.tags.url" checked> URL</label>
</div>
</div>
<!-- Genius -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-genius" checked onchange="toggleServiceTags(this, 'genius')"> Genius
</label>
<span class="tag-service-count">2 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="genius.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="genius.tags.url" checked> URL</label>
</div>
</div>
<!-- General -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" style="pointer-events:none;">General</label>
<span class="tag-service-count">2 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="metadata_enhancement.tags.quality_tag" checked> Quality
<small class="tag-embed-desc">Audio quality (e.g. FLAC, MP3-320)</small>
</label>
<label class="checkbox-label"><input type="checkbox" data-config="metadata_enhancement.tags.genre_merge" checked> Genre Merging
<small class="tag-embed-desc">Merge genres from Spotify + enabled sources</small>
</label>
</div>
</div>
<small class="settings-hint" style="margin-top: 6px; display: block;">
MusicBrainz IDs and Release Info require MusicBrainz service enabled above.
</small>
</div>
<div class="form-group">

View file

@ -4880,6 +4880,9 @@ async function loadSettingsData() {
document.getElementById('embed-album-art').checked = settings.metadata_enhancement?.embed_album_art !== false;
document.getElementById('cover-art-download').checked = settings.metadata_enhancement?.cover_art_download !== false;
document.getElementById('lrclib-enabled').checked = settings.metadata_enhancement?.lrclib_enabled !== false;
// Load service master toggles
document.getElementById('embed-spotify').checked = settings.spotify?.embed_tags !== false;
document.getElementById('embed-itunes').checked = settings.itunes?.embed_tags !== false;
document.getElementById('embed-musicbrainz').checked = settings.musicbrainz?.embed_tags !== false;
document.getElementById('embed-deezer').checked = settings.deezer?.embed_tags !== false;
document.getElementById('embed-audiodb').checked = settings.audiodb?.embed_tags !== false;
@ -4887,16 +4890,18 @@ async function loadSettingsData() {
document.getElementById('embed-qobuz').checked = settings.qobuz?.embed_tags !== false;
document.getElementById('embed-lastfm').checked = settings.lastfm?.embed_tags !== false;
document.getElementById('embed-genius').checked = settings.genius?.embed_tags !== false;
document.getElementById('tag-musicbrainz-ids').checked = settings.metadata_enhancement?.tags?.musicbrainz_ids !== false;
document.getElementById('tag-release-info').checked = settings.metadata_enhancement?.tags?.release_info !== false;
document.getElementById('tag-source-ids').checked = settings.metadata_enhancement?.tags?.source_ids !== false;
document.getElementById('tag-isrc').checked = settings.metadata_enhancement?.tags?.isrc !== false;
document.getElementById('tag-bpm').checked = settings.metadata_enhancement?.tags?.bpm !== false;
document.getElementById('tag-mood-style').checked = settings.metadata_enhancement?.tags?.mood_style !== false;
document.getElementById('tag-copyright-label').checked = settings.metadata_enhancement?.tags?.copyright_label !== false;
document.getElementById('tag-genre-merge').checked = settings.metadata_enhancement?.tags?.genre_merge !== false;
document.getElementById('tag-urls').checked = settings.metadata_enhancement?.tags?.urls !== false;
document.getElementById('tag-quality').checked = settings.metadata_enhancement?.tags?.quality !== false;
// Load per-tag toggles from data-config attributes
document.querySelectorAll('[data-config]').forEach(cb => {
const path = cb.dataset.config.split('.');
let val = settings;
for (const key of path) { val = val?.[key]; }
cb.checked = val !== false;
});
// Apply service disabled state to child tags
['spotify','itunes','musicbrainz','deezer','audiodb','tidal','qobuz','lastfm','genius'].forEach(svc => {
const master = document.getElementById('embed-' + svc);
if (master) toggleServiceTags(master, svc);
});
document.getElementById('post-processing-options').style.display = settings.metadata_enhancement?.enabled !== false ? 'block' : 'none';
// Populate File Organization settings
@ -5637,6 +5642,51 @@ async function hydrabaseSendRaw(textareaId) {
}
}
// ── Tag embedding accordion helpers ──
function toggleTagGroup(header) {
const body = header.nextElementSibling;
const arrow = header.querySelector('.tag-group-arrow');
if (body.style.display === 'none') {
body.style.display = 'block';
arrow.classList.add('open');
} else {
body.style.display = 'none';
arrow.classList.remove('open');
}
}
function toggleServiceTags(masterCheckbox, serviceName) {
const group = masterCheckbox.closest('.tag-service-group');
if (!group) return;
const body = group.querySelector('.tag-service-body');
if (!body) return;
const childCheckboxes = body.querySelectorAll('input[type="checkbox"]');
childCheckboxes.forEach(cb => {
const label = cb.closest('.checkbox-label');
if (masterCheckbox.checked) {
if (label) label.classList.remove('disabled-tag');
cb.disabled = false;
} else {
if (label) label.classList.add('disabled-tag');
cb.disabled = true;
}
});
}
function _collectServiceTags(serviceName) {
const tags = {};
document.querySelectorAll(`[data-config^="${serviceName}.tags."]`).forEach(cb => {
const key = cb.dataset.config.split('.').pop();
tags[key] = cb.checked;
});
return tags;
}
function _getTagConfig(path) {
const el = document.querySelector(`[data-config="${path}"]`);
return el ? el.checked : true;
}
async function saveSettings(quiet = false) {
// Validate file organization templates before saving
const validationErrors = validateFileOrganizationTemplates();
@ -5664,7 +5714,8 @@ async function saveSettings(quiet = false) {
client_id: document.getElementById('tidal-client-id').value,
client_secret: document.getElementById('tidal-client-secret').value,
redirect_uri: document.getElementById('tidal-redirect-uri').value,
embed_tags: document.getElementById('embed-tidal').checked
embed_tags: document.getElementById('embed-tidal').checked,
tags: _collectServiceTags('tidal')
},
plex: {
base_url: document.getElementById('plex-url').value,
@ -5699,14 +5750,18 @@ async function saveSettings(quiet = false) {
},
lastfm: {
api_key: document.getElementById('lastfm-api-key').value,
embed_tags: document.getElementById('embed-lastfm').checked
embed_tags: document.getElementById('embed-lastfm').checked,
tags: _collectServiceTags('lastfm')
},
genius: {
access_token: document.getElementById('genius-access-token').value,
embed_tags: document.getElementById('embed-genius').checked
embed_tags: document.getElementById('embed-genius').checked,
tags: _collectServiceTags('genius')
},
itunes: {
country: document.getElementById('itunes-country').value || 'US'
country: document.getElementById('itunes-country').value || 'US',
embed_tags: document.getElementById('embed-itunes').checked,
tags: _collectServiceTags('itunes')
},
metadata: {
fallback_source: document.getElementById('metadata-fallback-source').value || 'itunes'
@ -5724,7 +5779,8 @@ async function saveSettings(quiet = false) {
},
qobuz: {
quality: document.getElementById('qobuz-quality').value || 'lossless',
embed_tags: document.getElementById('embed-qobuz').checked
embed_tags: document.getElementById('embed-qobuz').checked,
tags: _collectServiceTags('qobuz')
},
database: {
max_workers: parseInt(document.getElementById('max-workers').value)
@ -5735,26 +5791,25 @@ async function saveSettings(quiet = false) {
cover_art_download: document.getElementById('cover-art-download').checked,
lrclib_enabled: document.getElementById('lrclib-enabled').checked,
tags: {
musicbrainz_ids: document.getElementById('tag-musicbrainz-ids').checked,
release_info: document.getElementById('tag-release-info').checked,
source_ids: document.getElementById('tag-source-ids').checked,
isrc: document.getElementById('tag-isrc').checked,
bpm: document.getElementById('tag-bpm').checked,
mood_style: document.getElementById('tag-mood-style').checked,
copyright_label: document.getElementById('tag-copyright-label').checked,
genre_merge: document.getElementById('tag-genre-merge').checked,
urls: document.getElementById('tag-urls').checked,
quality: document.getElementById('tag-quality').checked
quality_tag: _getTagConfig('metadata_enhancement.tags.quality_tag'),
genre_merge: _getTagConfig('metadata_enhancement.tags.genre_merge')
}
},
spotify: {
embed_tags: document.getElementById('embed-spotify').checked,
tags: _collectServiceTags('spotify')
},
musicbrainz: {
embed_tags: document.getElementById('embed-musicbrainz').checked
embed_tags: document.getElementById('embed-musicbrainz').checked,
tags: _collectServiceTags('musicbrainz')
},
deezer: {
embed_tags: document.getElementById('embed-deezer').checked
embed_tags: document.getElementById('embed-deezer').checked,
tags: _collectServiceTags('deezer')
},
audiodb: {
embed_tags: document.getElementById('embed-audiodb').checked
embed_tags: document.getElementById('embed-audiodb').checked,
tags: _collectServiceTags('audiodb')
},
file_organization: {
enabled: document.getElementById('file-organization-enabled').checked,

View file

@ -2090,13 +2090,60 @@ body {
grid-template-columns: 1fr 1fr;
gap: 4px 16px;
}
.tag-embed-grid {
gap: 8px 16px;
/* Tag service group accordion */
.tag-service-group {
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 8px;
margin-bottom: 6px;
overflow: hidden;
}
.tag-embed-label {
.tag-service-header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
background: rgba(255, 255, 255, 0.02);
transition: background 0.15s;
user-select: none;
}
.tag-service-header:hover {
background: rgba(255, 255, 255, 0.04);
}
.tag-group-arrow {
font-size: 10px;
color: rgba(255, 255, 255, 0.4);
transition: transform 0.15s;
flex-shrink: 0;
width: 12px;
}
.tag-group-arrow.open {
transform: rotate(90deg);
}
.tag-service-header .checkbox-label {
margin: 0;
font-weight: 500;
font-size: 13px;
}
.tag-service-count {
margin-left: auto;
font-size: 11px;
opacity: 0.4;
}
.tag-service-body {
padding: 6px 12px 10px 32px;
border-top: 1px solid rgba(255, 255, 255, 0.04);
}
.tag-service-body .checkbox-label {
font-size: 12px;
margin-bottom: 3px;
}
.tag-service-body .checkbox-label input[type="checkbox"] {
margin-right: 6px;
}
.tag-service-body .checkbox-label.disabled-tag {
opacity: 0.35;
pointer-events: none;
}
.tag-embed-desc {
display: block;