Add Picard-compatible MusicBrainz tags and per-category tag settings
Post-processing now writes all 18 MusicBrainz tags that Picard writes: Release Group ID, Album Artist ID, Release Track ID, Release Type, Status, Country, Original Date, Media, Barcode, Catalog #, ASIN, Script, Total Discs (plus the 5 already supported). One cached API call per album via get_release with recordings include. New "Tags to Embed" settings section with 10 category toggles (all enabled by default): MusicBrainz IDs, Release Info, Source IDs, ISRC, BPM, Mood & Style, Copyright & Label, Genre Merging, URLs, Quality. Each shows inline description of what it includes.
This commit is contained in:
parent
5adfdad9a3
commit
d3bff90fd6
4 changed files with 327 additions and 83 deletions
308
web_server.py
308
web_server.py
|
|
@ -2867,6 +2867,8 @@ album_name_cache = {} # album_key -> cached_final_name
|
|||
# to split one album into multiple entries.
|
||||
_mb_release_cache = {} # (album_lower, artist_lower) -> mbid_string_or_empty
|
||||
_mb_release_cache_lock = threading.Lock()
|
||||
_mb_release_detail_cache = {} # mbid -> release detail dict from get_release()
|
||||
_mb_release_detail_cache_lock = threading.Lock()
|
||||
|
||||
def _prepare_stream_task(track_data):
|
||||
"""
|
||||
|
|
@ -14528,7 +14530,7 @@ def _get_file_path_from_template(context: dict, template_type: str = 'album_path
|
|||
# METADATA & COVER ART HELPERS (Ported from downloads.py)
|
||||
# ===================================================================
|
||||
from mutagen import File as MutagenFile
|
||||
from mutagen.id3 import ID3, TIT2, TPE1, TALB, TDRC, TRCK, TCON, TPE2, TPOS, TXXX, APIC, UFID, TSRC, TBPM, TCOP, TPUB
|
||||
from mutagen.id3 import ID3, TIT2, TPE1, TALB, TDRC, TRCK, TCON, TPE2, TPOS, TXXX, APIC, UFID, TSRC, TBPM, TCOP, TPUB, TMED, TDOR
|
||||
from mutagen.flac import FLAC, Picture
|
||||
from mutagen.mp4 import MP4, MP4Cover, MP4FreeForm
|
||||
from mutagen.oggvorbis import OggVorbis
|
||||
|
|
@ -14716,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:
|
||||
if quality and config_manager.get('metadata_enhancement.tags.quality', 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)):
|
||||
|
|
@ -14965,6 +14967,20 @@ 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,
|
||||
}
|
||||
|
||||
# ── 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:
|
||||
|
|
@ -14994,6 +15010,7 @@ def _embed_source_ids(audio_file, metadata: dict):
|
|||
# via _api_call_lock, so no pause/resume needed.
|
||||
recording_mbid = None
|
||||
artist_mbid = None
|
||||
_rc_mbid = ''
|
||||
mb_genres = []
|
||||
isrc = None
|
||||
track_title = metadata.get('title', '')
|
||||
|
|
@ -15063,6 +15080,82 @@ def _embed_source_ids(audio_file, metadata: dict):
|
|||
except Exception as e:
|
||||
print(f"⚠️ MusicBrainz lookup failed (non-fatal): {e}")
|
||||
|
||||
# ── 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']):
|
||||
try:
|
||||
mb_service_for_detail = mb_worker.mb_service if mb_worker else None
|
||||
if mb_service_for_detail:
|
||||
with _mb_release_detail_cache_lock:
|
||||
release_detail = _mb_release_detail_cache.get(_rc_mbid)
|
||||
if release_detail is None:
|
||||
release_detail = mb_service_for_detail.mb_client.get_release(
|
||||
_rc_mbid, includes=['release-groups', 'labels', 'media', 'artist-credits', 'recordings']
|
||||
) or {}
|
||||
with _mb_release_detail_cache_lock:
|
||||
_mb_release_detail_cache[_rc_mbid] = release_detail
|
||||
if release_detail:
|
||||
rg = release_detail.get('release-group', {})
|
||||
if rg.get('id'):
|
||||
id_tags['MUSICBRAINZ_RELEASEGROUPID'] = rg['id']
|
||||
ac = release_detail.get('artist-credit', [])
|
||||
if ac and isinstance(ac[0], dict):
|
||||
aa_artist = ac[0].get('artist', {})
|
||||
if aa_artist.get('id'):
|
||||
id_tags['MUSICBRAINZ_ALBUMARTISTID'] = aa_artist['id']
|
||||
primary_type = rg.get('primary-type', '')
|
||||
if primary_type:
|
||||
id_tags['RELEASETYPE'] = primary_type
|
||||
orig_date = rg.get('first-release-date', '')
|
||||
if orig_date:
|
||||
id_tags['ORIGINALDATE'] = orig_date
|
||||
status = release_detail.get('status', '')
|
||||
if status:
|
||||
id_tags['RELEASESTATUS'] = status
|
||||
country = release_detail.get('country', '')
|
||||
if country:
|
||||
id_tags['RELEASECOUNTRY'] = country
|
||||
barcode = release_detail.get('barcode', '')
|
||||
if barcode:
|
||||
id_tags['BARCODE'] = barcode
|
||||
media_list = release_detail.get('media', [])
|
||||
if media_list:
|
||||
media_format = media_list[0].get('format', '')
|
||||
if media_format:
|
||||
id_tags['MEDIA'] = media_format
|
||||
id_tags['TOTALDISCS'] = str(len(media_list))
|
||||
label_info = release_detail.get('label-info', [])
|
||||
if label_info and isinstance(label_info[0], dict):
|
||||
cat_num = label_info[0].get('catalog-number', '')
|
||||
if cat_num:
|
||||
id_tags['CATALOGNUMBER'] = cat_num
|
||||
text_rep = release_detail.get('text-representation', {})
|
||||
if isinstance(text_rep, dict) and text_rep.get('script'):
|
||||
id_tags['SCRIPT'] = text_rep['script']
|
||||
asin = release_detail.get('asin', '')
|
||||
if asin:
|
||||
id_tags['ASIN'] = asin
|
||||
# Release Track ID — match by disc + track position
|
||||
_trk_num = metadata.get('track_number')
|
||||
_disc_num = metadata.get('disc_number') or 1
|
||||
if _trk_num and media_list:
|
||||
try:
|
||||
_trk_num_int = int(_trk_num)
|
||||
_disc_num_int = int(_disc_num)
|
||||
for medium in media_list:
|
||||
if medium.get('position', 1) == _disc_num_int:
|
||||
for mtrack in (medium.get('tracks') or medium.get('track-list', [])):
|
||||
if mtrack.get('position') == _trk_num_int and mtrack.get('id'):
|
||||
id_tags['MUSICBRAINZ_RELEASETRACKID'] = mtrack['id']
|
||||
break
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
print(f"🎵 MusicBrainz release details: type={primary_type or '?'}, "
|
||||
f"country={country or '?'}, media={id_tags.get('MEDIA', '?')}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ MusicBrainz release detail lookup failed (non-fatal): {e}")
|
||||
|
||||
# ── 2b. Deezer lookup for BPM, ISRC fallback, and source IDs ──
|
||||
deezer_bpm = None
|
||||
deezer_isrc = None
|
||||
|
|
@ -15268,52 +15361,100 @@ 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. Write all tags into the file ──
|
||||
# ── 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'}
|
||||
|
||||
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']:
|
||||
continue
|
||||
filtered_tags[tag_name] = value
|
||||
|
||||
# ── 3a. Write ID tags (MusicBrainz, source IDs, release info) ──
|
||||
written = []
|
||||
|
||||
# Format-specific tag name mappings for Picard-compatible output
|
||||
_ID3_TAG_MAP = {
|
||||
'MUSICBRAINZ_RECORDING_ID': ('UFID', 'http://musicbrainz.org'),
|
||||
'MUSICBRAINZ_ARTIST_ID': ('TXXX', 'MusicBrainz Artist Id'),
|
||||
'MUSICBRAINZ_RELEASE_ID': ('TXXX', 'MusicBrainz Album Id'),
|
||||
'MUSICBRAINZ_RELEASEGROUPID': ('TXXX', 'MusicBrainz Release Group Id'),
|
||||
'MUSICBRAINZ_ALBUMARTISTID': ('TXXX', 'MusicBrainz Album Artist Id'),
|
||||
'MUSICBRAINZ_RELEASETRACKID': ('TXXX', 'MusicBrainz Release Track Id'),
|
||||
'RELEASETYPE': ('TXXX', 'MusicBrainz Album Type'),
|
||||
'RELEASESTATUS': ('TXXX', 'MusicBrainz Album Status'),
|
||||
'RELEASECOUNTRY': ('TXXX', 'MusicBrainz Album Release Country'),
|
||||
'ORIGINALDATE': ('TDOR', None),
|
||||
'MEDIA': ('TMED', None),
|
||||
}
|
||||
_VORBIS_TAG_MAP = {
|
||||
'MUSICBRAINZ_RECORDING_ID': 'MUSICBRAINZ_TRACKID',
|
||||
'MUSICBRAINZ_ARTIST_ID': 'MUSICBRAINZ_ARTISTID',
|
||||
'MUSICBRAINZ_RELEASE_ID': 'MUSICBRAINZ_ALBUMID',
|
||||
'MUSICBRAINZ_RELEASEGROUPID': 'MUSICBRAINZ_RELEASEGROUPID',
|
||||
'MUSICBRAINZ_ALBUMARTISTID': 'MUSICBRAINZ_ALBUMARTISTID',
|
||||
'MUSICBRAINZ_RELEASETRACKID': 'MUSICBRAINZ_RELEASETRACKID',
|
||||
}
|
||||
_MP4_TAG_MAP = {
|
||||
'MUSICBRAINZ_RECORDING_ID': 'MusicBrainz Track Id',
|
||||
'MUSICBRAINZ_ARTIST_ID': 'MusicBrainz Artist Id',
|
||||
'MUSICBRAINZ_RELEASE_ID': 'MusicBrainz Album Id',
|
||||
'MUSICBRAINZ_RELEASEGROUPID': 'MusicBrainz Release Group Id',
|
||||
'MUSICBRAINZ_ALBUMARTISTID': 'MusicBrainz Album Artist Id',
|
||||
'MUSICBRAINZ_RELEASETRACKID': 'MusicBrainz Release Track Id',
|
||||
'RELEASETYPE': 'MusicBrainz Album Type',
|
||||
'RELEASESTATUS': 'MusicBrainz Album Status',
|
||||
'RELEASECOUNTRY': 'MusicBrainz Album Release Country',
|
||||
}
|
||||
|
||||
# MP3 (ID3)
|
||||
if isinstance(audio_file.tags, ID3):
|
||||
for tag_name, value in id_tags.items():
|
||||
if tag_name == 'MUSICBRAINZ_RECORDING_ID':
|
||||
audio_file.tags.add(UFID(owner='http://musicbrainz.org', data=value.encode('ascii')))
|
||||
written.append('UFID:http://musicbrainz.org')
|
||||
elif tag_name == 'MUSICBRAINZ_ARTIST_ID':
|
||||
audio_file.tags.add(TXXX(encoding=3, desc='MusicBrainz Artist Id', text=[value]))
|
||||
written.append('TXXX:MusicBrainz Artist Id')
|
||||
elif tag_name == 'MUSICBRAINZ_RELEASE_ID':
|
||||
audio_file.tags.add(TXXX(encoding=3, desc='MusicBrainz Album Id', text=[value]))
|
||||
written.append('TXXX:MusicBrainz Album Id')
|
||||
for tag_name, value in filtered_tags.items():
|
||||
id3_spec = _ID3_TAG_MAP.get(tag_name)
|
||||
if id3_spec:
|
||||
frame_type, desc = id3_spec
|
||||
if frame_type == 'UFID':
|
||||
audio_file.tags.add(UFID(owner=desc, data=value.encode('ascii')))
|
||||
written.append(f'UFID:{desc}')
|
||||
elif frame_type == 'TDOR':
|
||||
audio_file.tags.add(TDOR(encoding=3, text=[value]))
|
||||
written.append('TDOR')
|
||||
elif frame_type == 'TMED':
|
||||
audio_file.tags.add(TMED(encoding=3, text=[value]))
|
||||
written.append('TMED')
|
||||
else: # TXXX
|
||||
audio_file.tags.add(TXXX(encoding=3, desc=desc, text=[value]))
|
||||
written.append(f'TXXX:{desc}')
|
||||
else:
|
||||
audio_file.tags.add(TXXX(encoding=3, desc=tag_name, text=[str(value)]))
|
||||
written.append(f'TXXX:{tag_name}')
|
||||
|
||||
# FLAC / OGG Vorbis
|
||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||
for tag_name, value in id_tags.items():
|
||||
if tag_name == 'MUSICBRAINZ_RECORDING_ID':
|
||||
audio_file['MUSICBRAINZ_TRACKID'] = [value]
|
||||
written.append('MUSICBRAINZ_TRACKID')
|
||||
elif tag_name == 'MUSICBRAINZ_ARTIST_ID':
|
||||
audio_file['MUSICBRAINZ_ARTISTID'] = [value]
|
||||
written.append('MUSICBRAINZ_ARTISTID')
|
||||
elif tag_name == 'MUSICBRAINZ_RELEASE_ID':
|
||||
audio_file['MUSICBRAINZ_ALBUMID'] = [value]
|
||||
written.append('MUSICBRAINZ_ALBUMID')
|
||||
else:
|
||||
audio_file[tag_name] = [str(value)]
|
||||
written.append(tag_name)
|
||||
for tag_name, value in filtered_tags.items():
|
||||
vorbis_key = _VORBIS_TAG_MAP.get(tag_name, tag_name)
|
||||
audio_file[vorbis_key] = [str(value)]
|
||||
written.append(vorbis_key)
|
||||
|
||||
# MP4 (M4A/AAC)
|
||||
elif isinstance(audio_file, MP4):
|
||||
for tag_name, value in id_tags.items():
|
||||
if tag_name == 'MUSICBRAINZ_RECORDING_ID':
|
||||
key = '----:com.apple.iTunes:MusicBrainz Track Id'
|
||||
elif tag_name == 'MUSICBRAINZ_ARTIST_ID':
|
||||
key = '----:com.apple.iTunes:MusicBrainz Artist Id'
|
||||
elif tag_name == 'MUSICBRAINZ_RELEASE_ID':
|
||||
key = '----:com.apple.iTunes:MusicBrainz Album Id'
|
||||
else:
|
||||
key = f'----:com.apple.iTunes:{tag_name}'
|
||||
for tag_name, value in filtered_tags.items():
|
||||
mp4_desc = _MP4_TAG_MAP.get(tag_name, tag_name)
|
||||
key = f'----:com.apple.iTunes:{mp4_desc}'
|
||||
audio_file[key] = [MP4FreeForm(str(value).encode('utf-8'))]
|
||||
written.append(key)
|
||||
|
||||
|
|
@ -15321,7 +15462,7 @@ def _embed_source_ids(audio_file, metadata: dict):
|
|||
print(f"🔗 Embedded IDs: {', '.join(written)}")
|
||||
|
||||
# ── 3b. Write BPM tag (from Deezer) ──
|
||||
if deezer_bpm and deezer_bpm > 0:
|
||||
if tag_cfg['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)]))
|
||||
|
|
@ -15332,7 +15473,7 @@ def _embed_source_ids(audio_file, metadata: dict):
|
|||
print(f"🥁 BPM: {bpm_int}")
|
||||
|
||||
# ── 3c. Write mood tag (from AudioDB) ──
|
||||
if audiodb_mood:
|
||||
if tag_cfg['mood_style'] 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)):
|
||||
|
|
@ -15342,7 +15483,7 @@ def _embed_source_ids(audio_file, metadata: dict):
|
|||
print(f"🎭 Mood: {audiodb_mood}")
|
||||
|
||||
# ── 3d. Write style tag (from AudioDB) ──
|
||||
if audiodb_style:
|
||||
if tag_cfg['mood_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)):
|
||||
|
|
@ -15352,55 +15493,58 @@ def _embed_source_ids(audio_file, metadata: dict):
|
|||
print(f"🎨 Style: {audiodb_style}")
|
||||
|
||||
# ── 4. Merge genres (Spotify + MusicBrainz + AudioDB + Last.fm) and overwrite tag ──
|
||||
enrichment_genres = mb_genres + ([audiodb_genre] if audiodb_genre else []) + lastfm_tags
|
||||
if enrichment_genres:
|
||||
spotify_genres = [g.strip() for g in metadata.get('genre', '').split(',') if g.strip()]
|
||||
seen = set()
|
||||
merged = []
|
||||
for g in spotify_genres + enrichment_genres:
|
||||
key = g.strip().lower()
|
||||
if key and key not in seen:
|
||||
seen.add(key)
|
||||
merged.append(g.strip().title())
|
||||
if len(merged) >= 5:
|
||||
break
|
||||
if tag_cfg['genre_merge']:
|
||||
enrichment_genres = mb_genres + ([audiodb_genre] if audiodb_genre else []) + lastfm_tags
|
||||
if enrichment_genres:
|
||||
spotify_genres = [g.strip() for g in metadata.get('genre', '').split(',') if g.strip()]
|
||||
seen = set()
|
||||
merged = []
|
||||
for g in spotify_genres + enrichment_genres:
|
||||
key = g.strip().lower()
|
||||
if key and key not in seen:
|
||||
seen.add(key)
|
||||
merged.append(g.strip().title())
|
||||
if len(merged) >= 5:
|
||||
break
|
||||
|
||||
if merged:
|
||||
genre_string = ', '.join(merged)
|
||||
if isinstance(audio_file.tags, ID3):
|
||||
audio_file.tags.add(TCON(encoding=3, text=[genre_string]))
|
||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||
audio_file['GENRE'] = [genre_string]
|
||||
elif isinstance(audio_file, MP4):
|
||||
audio_file['\xa9gen'] = [genre_string]
|
||||
print(f"🎶 Genres merged: {genre_string}")
|
||||
if merged:
|
||||
genre_string = ', '.join(merged)
|
||||
if isinstance(audio_file.tags, ID3):
|
||||
audio_file.tags.add(TCON(encoding=3, text=[genre_string]))
|
||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||
audio_file['GENRE'] = [genre_string]
|
||||
elif isinstance(audio_file, MP4):
|
||||
audio_file['\xa9gen'] = [genre_string]
|
||||
print(f"🎶 Genres merged: {genre_string}")
|
||||
|
||||
# ── 5. Write ISRC if available (MusicBrainz → Deezer → Tidal → Qobuz fallback) ──
|
||||
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}")
|
||||
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}")
|
||||
|
||||
# ── 6. Write copyright tag (Tidal → Qobuz fallback) ──
|
||||
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]}")
|
||||
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]}")
|
||||
|
||||
# ── 7. Write label/publisher tag (from Qobuz) ──
|
||||
if qobuz_label:
|
||||
if tag_cfg['copyright_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)):
|
||||
|
|
@ -15410,7 +15554,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 lastfm_url:
|
||||
if tag_cfg['urls'] 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)):
|
||||
|
|
@ -15418,7 +15562,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 genius_url:
|
||||
if tag_cfg['urls'] 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)):
|
||||
|
|
|
|||
|
|
@ -4387,6 +4387,68 @@
|
|||
</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>
|
||||
</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">
|
||||
<label>Supported Formats:</label>
|
||||
<div class="supported-formats">MP3, FLAC, MP4/M4A, OGG</div>
|
||||
|
|
|
|||
|
|
@ -4887,6 +4887,16 @@ 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;
|
||||
document.getElementById('post-processing-options').style.display = settings.metadata_enhancement?.enabled !== false ? 'block' : 'none';
|
||||
|
||||
// Populate File Organization settings
|
||||
|
|
@ -5723,7 +5733,19 @@ async function saveSettings(quiet = false) {
|
|||
enabled: document.getElementById('metadata-enabled').checked,
|
||||
embed_album_art: document.getElementById('embed-album-art').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: {
|
||||
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
|
||||
}
|
||||
},
|
||||
musicbrainz: {
|
||||
embed_tags: document.getElementById('embed-musicbrainz').checked
|
||||
|
|
|
|||
|
|
@ -2090,6 +2090,22 @@ body {
|
|||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px 16px;
|
||||
}
|
||||
.tag-embed-grid {
|
||||
gap: 8px 16px;
|
||||
}
|
||||
.tag-embed-label {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
}
|
||||
.tag-embed-desc {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-size: 0.75em;
|
||||
opacity: 0.5;
|
||||
margin-top: -2px;
|
||||
margin-left: 22px;
|
||||
}
|
||||
|
||||
/* Read-only Fields */
|
||||
.readonly-field {
|
||||
|
|
|
|||
Loading…
Reference in a new issue