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:
parent
d3bff90fd6
commit
b9732156cd
4 changed files with 421 additions and 200 deletions
183
web_server.py
183
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)):
|
||||
|
|
|
|||
270
webui/index.html
270
webui/index.html
|
|
@ -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">▶</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">▶</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">▶</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">▶</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">▶</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">▶</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">▶</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">▶</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">▶</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">▶</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">
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue