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

View file

@ -4350,103 +4350,189 @@
</div> </div>
</div> </div>
<!-- Metadata Services --> <!-- Tag Embedding — per-tag toggles grouped by source -->
<div class="post-processing-section"> <div class="post-processing-section">
<div class="post-processing-section-title">Metadata Services</div> <div class="post-processing-section-title">Tag Embedding</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> <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"> <!-- Spotify -->
<label class="checkbox-label"> <div class="tag-service-group">
<input type="checkbox" id="embed-musicbrainz" checked> <div class="tag-service-header" onclick="toggleTagGroup(this)">
MusicBrainz <span class="tag-group-arrow">&#9654;</span>
</label> <label class="checkbox-label" onclick="event.stopPropagation()">
<label class="checkbox-label"> <input type="checkbox" id="embed-spotify" checked onchange="toggleServiceTags(this, 'spotify')"> Spotify
<input type="checkbox" id="embed-deezer" checked> </label>
Deezer <span class="tag-service-count">3 tags</span>
</label> </div>
<label class="checkbox-label"> <div class="tag-service-body" style="display:none;">
<input type="checkbox" id="embed-audiodb" checked> <label class="checkbox-label"><input type="checkbox" data-config="spotify.tags.track_id" checked> Track ID</label>
AudioDB <label class="checkbox-label"><input type="checkbox" data-config="spotify.tags.artist_id" checked> Artist ID</label>
</label> <label class="checkbox-label"><input type="checkbox" data-config="spotify.tags.album_id" checked> Album ID</label>
<label class="checkbox-label"> </div>
<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>
</div> </div>
</div>
<!-- Tags to Embed --> <!-- iTunes -->
<div class="post-processing-section"> <div class="tag-service-group">
<div class="post-processing-section-title">Tags to Embed</div> <div class="tag-service-header" onclick="toggleTagGroup(this)">
<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> <span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<div class="post-processing-grid tag-embed-grid"> <input type="checkbox" id="embed-itunes" checked onchange="toggleServiceTags(this, 'itunes')"> iTunes
<label class="checkbox-label tag-embed-label"> </label>
<input type="checkbox" id="tag-musicbrainz-ids" checked> <span class="tag-service-count">3 tags</span>
MusicBrainz IDs </div>
<small class="tag-embed-desc">Recording, Artist, Album, Release Group, Album Artist, Release Track</small> <div class="tag-service-body" style="display:none;">
</label> <label class="checkbox-label"><input type="checkbox" data-config="itunes.tags.track_id" checked> Track ID</label>
<label class="checkbox-label tag-embed-label"> <label class="checkbox-label"><input type="checkbox" data-config="itunes.tags.artist_id" checked> Artist ID</label>
<input type="checkbox" id="tag-release-info" checked> <label class="checkbox-label"><input type="checkbox" data-config="itunes.tags.album_id" checked> Album ID</label>
Release Info </div>
<small class="tag-embed-desc">Original Date, Type, Status, Country, Media, Barcode, Catalog #, ASIN</small> </div>
</label>
<label class="checkbox-label tag-embed-label"> <!-- MusicBrainz -->
<input type="checkbox" id="tag-source-ids" checked> <div class="tag-service-group">
Source IDs <div class="tag-service-header" onclick="toggleTagGroup(this)">
<small class="tag-embed-desc">Spotify, iTunes, Deezer, AudioDB, Tidal, Qobuz, Genius</small> <span class="tag-group-arrow">&#9654;</span>
</label> <label class="checkbox-label" onclick="event.stopPropagation()">
<label class="checkbox-label tag-embed-label"> <input type="checkbox" id="embed-musicbrainz" checked onchange="toggleServiceTags(this, 'musicbrainz')"> MusicBrainz
<input type="checkbox" id="tag-isrc" checked> </label>
ISRC <span class="tag-service-count">18 tags</span>
<small class="tag-embed-desc">International Standard Recording Code</small> </div>
</label> <div class="tag-service-body" style="display:none;">
<label class="checkbox-label tag-embed-label"> <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.recording_id" checked> Recording ID</label>
<input type="checkbox" id="tag-bpm" checked> <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.artist_id" checked> Artist ID</label>
BPM <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_id" checked> Release ID</label>
<small class="tag-embed-desc">Beats per minute from Deezer</small> <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_group_id" checked> Release Group ID</label>
</label> <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.album_artist_id" checked> Album Artist ID</label>
<label class="checkbox-label tag-embed-label"> <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_track_id" checked> Release Track ID</label>
<input type="checkbox" id="tag-mood-style" checked> <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_type" checked> Release Type</label>
Mood & Style <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.original_date" checked> Original Date</label>
<small class="tag-embed-desc">Mood and Style from AudioDB</small> <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_status" checked> Release Status</label>
</label> <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.release_country" checked> Release Country</label>
<label class="checkbox-label tag-embed-label"> <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.barcode" checked> Barcode</label>
<input type="checkbox" id="tag-copyright-label" checked> <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.media" checked> Media</label>
Copyright & Label <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.total_discs" checked> Total Discs</label>
<small class="tag-embed-desc">Copyright from Tidal/Qobuz, Label from Qobuz</small> <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.catalog_number" checked> Catalog Number</label>
</label> <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.script" checked> Script</label>
<label class="checkbox-label tag-embed-label"> <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.asin" checked> ASIN</label>
<input type="checkbox" id="tag-genre-merge" checked> <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.isrc" checked> ISRC</label>
Genre Merging <label class="checkbox-label"><input type="checkbox" data-config="musicbrainz.tags.genres" checked> Genres</label>
<small class="tag-embed-desc">Merge genres from Spotify + MB + AudioDB + Last.fm</small> </div>
</label> </div>
<label class="checkbox-label tag-embed-label">
<input type="checkbox" id="tag-urls" checked> <!-- Deezer -->
URLs <div class="tag-service-group">
<small class="tag-embed-desc">Last.fm and Genius URLs</small> <div class="tag-service-header" onclick="toggleTagGroup(this)">
</label> <span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label tag-embed-label"> <label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="tag-quality" checked> <input type="checkbox" id="embed-deezer" checked onchange="toggleServiceTags(this, 'deezer')"> Deezer
Quality </label>
<small class="tag-embed-desc">Audio quality (e.g. FLAC, MP3-320)</small> <span class="tag-service-count">4 tags</span>
</label> </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> </div>
<small class="settings-hint" style="margin-top: 6px; display: block;">
MusicBrainz IDs and Release Info require MusicBrainz service enabled above.
</small>
</div> </div>
<div class="form-group"> <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('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('cover-art-download').checked = settings.metadata_enhancement?.cover_art_download !== false;
document.getElementById('lrclib-enabled').checked = settings.metadata_enhancement?.lrclib_enabled !== 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-musicbrainz').checked = settings.musicbrainz?.embed_tags !== false;
document.getElementById('embed-deezer').checked = settings.deezer?.embed_tags !== false; document.getElementById('embed-deezer').checked = settings.deezer?.embed_tags !== false;
document.getElementById('embed-audiodb').checked = settings.audiodb?.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-qobuz').checked = settings.qobuz?.embed_tags !== false;
document.getElementById('embed-lastfm').checked = settings.lastfm?.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('embed-genius').checked = settings.genius?.embed_tags !== false;
document.getElementById('tag-musicbrainz-ids').checked = settings.metadata_enhancement?.tags?.musicbrainz_ids !== false; // Load per-tag toggles from data-config attributes
document.getElementById('tag-release-info').checked = settings.metadata_enhancement?.tags?.release_info !== false; document.querySelectorAll('[data-config]').forEach(cb => {
document.getElementById('tag-source-ids').checked = settings.metadata_enhancement?.tags?.source_ids !== false; const path = cb.dataset.config.split('.');
document.getElementById('tag-isrc').checked = settings.metadata_enhancement?.tags?.isrc !== false; let val = settings;
document.getElementById('tag-bpm').checked = settings.metadata_enhancement?.tags?.bpm !== false; for (const key of path) { val = val?.[key]; }
document.getElementById('tag-mood-style').checked = settings.metadata_enhancement?.tags?.mood_style !== false; cb.checked = val !== 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; // Apply service disabled state to child tags
document.getElementById('tag-urls').checked = settings.metadata_enhancement?.tags?.urls !== false; ['spotify','itunes','musicbrainz','deezer','audiodb','tidal','qobuz','lastfm','genius'].forEach(svc => {
document.getElementById('tag-quality').checked = settings.metadata_enhancement?.tags?.quality !== false; 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'; document.getElementById('post-processing-options').style.display = settings.metadata_enhancement?.enabled !== false ? 'block' : 'none';
// Populate File Organization settings // 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) { async function saveSettings(quiet = false) {
// Validate file organization templates before saving // Validate file organization templates before saving
const validationErrors = validateFileOrganizationTemplates(); const validationErrors = validateFileOrganizationTemplates();
@ -5664,7 +5714,8 @@ async function saveSettings(quiet = false) {
client_id: document.getElementById('tidal-client-id').value, client_id: document.getElementById('tidal-client-id').value,
client_secret: document.getElementById('tidal-client-secret').value, client_secret: document.getElementById('tidal-client-secret').value,
redirect_uri: document.getElementById('tidal-redirect-uri').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: { plex: {
base_url: document.getElementById('plex-url').value, base_url: document.getElementById('plex-url').value,
@ -5699,14 +5750,18 @@ async function saveSettings(quiet = false) {
}, },
lastfm: { lastfm: {
api_key: document.getElementById('lastfm-api-key').value, 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: { genius: {
access_token: document.getElementById('genius-access-token').value, 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: { 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: { metadata: {
fallback_source: document.getElementById('metadata-fallback-source').value || 'itunes' fallback_source: document.getElementById('metadata-fallback-source').value || 'itunes'
@ -5724,7 +5779,8 @@ async function saveSettings(quiet = false) {
}, },
qobuz: { qobuz: {
quality: document.getElementById('qobuz-quality').value || 'lossless', 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: { database: {
max_workers: parseInt(document.getElementById('max-workers').value) 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, cover_art_download: document.getElementById('cover-art-download').checked,
lrclib_enabled: document.getElementById('lrclib-enabled').checked, lrclib_enabled: document.getElementById('lrclib-enabled').checked,
tags: { tags: {
musicbrainz_ids: document.getElementById('tag-musicbrainz-ids').checked, quality_tag: _getTagConfig('metadata_enhancement.tags.quality_tag'),
release_info: document.getElementById('tag-release-info').checked, genre_merge: _getTagConfig('metadata_enhancement.tags.genre_merge')
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
} }
}, },
spotify: {
embed_tags: document.getElementById('embed-spotify').checked,
tags: _collectServiceTags('spotify')
},
musicbrainz: { musicbrainz: {
embed_tags: document.getElementById('embed-musicbrainz').checked embed_tags: document.getElementById('embed-musicbrainz').checked,
tags: _collectServiceTags('musicbrainz')
}, },
deezer: { deezer: {
embed_tags: document.getElementById('embed-deezer').checked embed_tags: document.getElementById('embed-deezer').checked,
tags: _collectServiceTags('deezer')
}, },
audiodb: { audiodb: {
embed_tags: document.getElementById('embed-audiodb').checked embed_tags: document.getElementById('embed-audiodb').checked,
tags: _collectServiceTags('audiodb')
}, },
file_organization: { file_organization: {
enabled: document.getElementById('file-organization-enabled').checked, enabled: document.getElementById('file-organization-enabled').checked,

View file

@ -2090,13 +2090,60 @@ body {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 4px 16px; gap: 4px 16px;
} }
.tag-embed-grid { /* Tag service group accordion */
gap: 8px 16px; .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; display: flex;
flex-wrap: wrap; align-items: center;
align-items: baseline; 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 { .tag-embed-desc {
display: block; display: block;