diff --git a/web_server.py b/web_server.py index f079d05d..851afef8 100644 --- a/web_server.py +++ b/web_server.py @@ -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)): diff --git a/webui/index.html b/webui/index.html index ec77d111..5a5fb157 100644 --- a/webui/index.html +++ b/webui/index.html @@ -4350,103 +4350,189 @@ - +
-
Metadata Services
- Choose which services embed IDs, genres, and metadata into your files during download +
Tag Embedding
+ Toggle a service to enable/disable all its tags, or expand to control individual tags. -
- - - - - - - + +
+
+ + + 3 tags +
+
-
- -
-
Tags to Embed
- Choose which tag categories are written into your audio files. All enabled by default. - -
- - - - - - - - - - + +
+
+ + + 3 tags +
+ +
+ + +
+
+ + + 18 tags +
+ +
+ + +
+
+ + + 4 tags +
+ +
+ + +
+
+ + + 4 tags +
+ +
+ + +
+
+ + + 4 tags +
+ +
+ + +
+
+ + + 5 tags +
+ +
+ + +
+
+ + + 2 tags +
+ +
+ + +
+
+ + + 2 tags +
+ +
+ + +
+
+ + + 2 tags +
+
- - MusicBrainz IDs and Release Info require MusicBrainz service enabled above. -
diff --git a/webui/static/script.js b/webui/static/script.js index e2a12a0e..5478745f 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -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, diff --git a/webui/static/style.css b/webui/static/style.css index f0d6ca79..4cd55304 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -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;