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.
|
# to split one album into multiple entries.
|
||||||
_mb_release_cache = {} # (album_lower, artist_lower) -> mbid_string_or_empty
|
_mb_release_cache = {} # (album_lower, artist_lower) -> mbid_string_or_empty
|
||||||
_mb_release_cache_lock = threading.Lock()
|
_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):
|
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)
|
# METADATA & COVER ART HELPERS (Ported from downloads.py)
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
from mutagen import File as MutagenFile
|
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.flac import FLAC, Picture
|
||||||
from mutagen.mp4 import MP4, MP4Cover, MP4FreeForm
|
from mutagen.mp4 import MP4, MP4Cover, MP4FreeForm
|
||||||
from mutagen.oggvorbis import OggVorbis
|
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 ──
|
# ── Embed audio quality tag ──
|
||||||
quality = context.get('_audio_quality', '')
|
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):
|
if isinstance(audio_file.tags, ID3):
|
||||||
audio_file.tags.add(TXXX(encoding=3, desc='QUALITY', text=[quality]))
|
audio_file.tags.add(TXXX(encoding=3, desc='QUALITY', text=[quality]))
|
||||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||||
|
|
@ -14965,6 +14967,20 @@ def _embed_source_ids(audio_file, metadata: dict):
|
||||||
Operates on a non-easy-mode MutagenFile object (caller must save).
|
Operates on a non-easy-mode MutagenFile object (caller must save).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# ── Tag category config (all default to True) ──
|
||||||
|
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) ──
|
# ── Helper: normalize + compare names (same logic as enrichment workers) ──
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
def _names_match(a: str, b: str, threshold: float = 0.75) -> bool:
|
def _names_match(a: str, b: str, threshold: float = 0.75) -> bool:
|
||||||
|
|
@ -14994,6 +15010,7 @@ def _embed_source_ids(audio_file, metadata: dict):
|
||||||
# via _api_call_lock, so no pause/resume needed.
|
# via _api_call_lock, so no pause/resume needed.
|
||||||
recording_mbid = None
|
recording_mbid = None
|
||||||
artist_mbid = None
|
artist_mbid = None
|
||||||
|
_rc_mbid = ''
|
||||||
mb_genres = []
|
mb_genres = []
|
||||||
isrc = None
|
isrc = None
|
||||||
track_title = metadata.get('title', '')
|
track_title = metadata.get('title', '')
|
||||||
|
|
@ -15063,6 +15080,82 @@ def _embed_source_ids(audio_file, metadata: dict):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ MusicBrainz lookup failed (non-fatal): {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 ──
|
# ── 2b. Deezer lookup for BPM, ISRC fallback, and source IDs ──
|
||||||
deezer_bpm = None
|
deezer_bpm = None
|
||||||
deezer_isrc = 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:
|
if not id_tags and not deezer_bpm and not deezer_isrc and not audiodb_mood and not audiodb_style:
|
||||||
return
|
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 = []
|
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)
|
# MP3 (ID3)
|
||||||
if isinstance(audio_file.tags, ID3):
|
if isinstance(audio_file.tags, ID3):
|
||||||
for tag_name, value in id_tags.items():
|
for tag_name, value in filtered_tags.items():
|
||||||
if tag_name == 'MUSICBRAINZ_RECORDING_ID':
|
id3_spec = _ID3_TAG_MAP.get(tag_name)
|
||||||
audio_file.tags.add(UFID(owner='http://musicbrainz.org', data=value.encode('ascii')))
|
if id3_spec:
|
||||||
written.append('UFID:http://musicbrainz.org')
|
frame_type, desc = id3_spec
|
||||||
elif tag_name == 'MUSICBRAINZ_ARTIST_ID':
|
if frame_type == 'UFID':
|
||||||
audio_file.tags.add(TXXX(encoding=3, desc='MusicBrainz Artist Id', text=[value]))
|
audio_file.tags.add(UFID(owner=desc, data=value.encode('ascii')))
|
||||||
written.append('TXXX:MusicBrainz Artist Id')
|
written.append(f'UFID:{desc}')
|
||||||
elif tag_name == 'MUSICBRAINZ_RELEASE_ID':
|
elif frame_type == 'TDOR':
|
||||||
audio_file.tags.add(TXXX(encoding=3, desc='MusicBrainz Album Id', text=[value]))
|
audio_file.tags.add(TDOR(encoding=3, text=[value]))
|
||||||
written.append('TXXX:MusicBrainz Album Id')
|
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:
|
else:
|
||||||
audio_file.tags.add(TXXX(encoding=3, desc=tag_name, text=[str(value)]))
|
audio_file.tags.add(TXXX(encoding=3, desc=tag_name, text=[str(value)]))
|
||||||
written.append(f'TXXX:{tag_name}')
|
written.append(f'TXXX:{tag_name}')
|
||||||
|
|
||||||
# FLAC / OGG Vorbis
|
# FLAC / OGG Vorbis
|
||||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||||
for tag_name, value in id_tags.items():
|
for tag_name, value in filtered_tags.items():
|
||||||
if tag_name == 'MUSICBRAINZ_RECORDING_ID':
|
vorbis_key = _VORBIS_TAG_MAP.get(tag_name, tag_name)
|
||||||
audio_file['MUSICBRAINZ_TRACKID'] = [value]
|
audio_file[vorbis_key] = [str(value)]
|
||||||
written.append('MUSICBRAINZ_TRACKID')
|
written.append(vorbis_key)
|
||||||
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)
|
|
||||||
|
|
||||||
# MP4 (M4A/AAC)
|
# MP4 (M4A/AAC)
|
||||||
elif isinstance(audio_file, MP4):
|
elif isinstance(audio_file, MP4):
|
||||||
for tag_name, value in id_tags.items():
|
for tag_name, value in filtered_tags.items():
|
||||||
if tag_name == 'MUSICBRAINZ_RECORDING_ID':
|
mp4_desc = _MP4_TAG_MAP.get(tag_name, tag_name)
|
||||||
key = '----:com.apple.iTunes:MusicBrainz Track Id'
|
key = f'----:com.apple.iTunes:{mp4_desc}'
|
||||||
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}'
|
|
||||||
audio_file[key] = [MP4FreeForm(str(value).encode('utf-8'))]
|
audio_file[key] = [MP4FreeForm(str(value).encode('utf-8'))]
|
||||||
written.append(key)
|
written.append(key)
|
||||||
|
|
||||||
|
|
@ -15321,7 +15462,7 @@ def _embed_source_ids(audio_file, metadata: dict):
|
||||||
print(f"🔗 Embedded IDs: {', '.join(written)}")
|
print(f"🔗 Embedded IDs: {', '.join(written)}")
|
||||||
|
|
||||||
# ── 3b. Write BPM tag (from Deezer) ──
|
# ── 3b. Write BPM tag (from Deezer) ──
|
||||||
if deezer_bpm and deezer_bpm > 0:
|
if tag_cfg['bpm'] and deezer_bpm and deezer_bpm > 0:
|
||||||
bpm_int = int(deezer_bpm)
|
bpm_int = int(deezer_bpm)
|
||||||
if isinstance(audio_file.tags, ID3):
|
if isinstance(audio_file.tags, ID3):
|
||||||
audio_file.tags.add(TBPM(encoding=3, text=[str(bpm_int)]))
|
audio_file.tags.add(TBPM(encoding=3, text=[str(bpm_int)]))
|
||||||
|
|
@ -15332,7 +15473,7 @@ def _embed_source_ids(audio_file, metadata: dict):
|
||||||
print(f"🥁 BPM: {bpm_int}")
|
print(f"🥁 BPM: {bpm_int}")
|
||||||
|
|
||||||
# ── 3c. Write mood tag (from AudioDB) ──
|
# ── 3c. Write mood tag (from AudioDB) ──
|
||||||
if audiodb_mood:
|
if tag_cfg['mood_style'] and audiodb_mood:
|
||||||
if isinstance(audio_file.tags, ID3):
|
if isinstance(audio_file.tags, ID3):
|
||||||
audio_file.tags.add(TXXX(encoding=3, desc='MOOD', text=[audiodb_mood]))
|
audio_file.tags.add(TXXX(encoding=3, desc='MOOD', text=[audiodb_mood]))
|
||||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||||
|
|
@ -15342,7 +15483,7 @@ def _embed_source_ids(audio_file, metadata: dict):
|
||||||
print(f"🎭 Mood: {audiodb_mood}")
|
print(f"🎭 Mood: {audiodb_mood}")
|
||||||
|
|
||||||
# ── 3d. Write style tag (from AudioDB) ──
|
# ── 3d. Write style tag (from AudioDB) ──
|
||||||
if audiodb_style:
|
if tag_cfg['mood_style'] and audiodb_style:
|
||||||
if isinstance(audio_file.tags, ID3):
|
if isinstance(audio_file.tags, ID3):
|
||||||
audio_file.tags.add(TXXX(encoding=3, desc='STYLE', text=[audiodb_style]))
|
audio_file.tags.add(TXXX(encoding=3, desc='STYLE', text=[audiodb_style]))
|
||||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||||
|
|
@ -15352,55 +15493,58 @@ def _embed_source_ids(audio_file, metadata: dict):
|
||||||
print(f"🎨 Style: {audiodb_style}")
|
print(f"🎨 Style: {audiodb_style}")
|
||||||
|
|
||||||
# ── 4. Merge genres (Spotify + MusicBrainz + AudioDB + Last.fm) and overwrite tag ──
|
# ── 4. Merge genres (Spotify + MusicBrainz + AudioDB + Last.fm) and overwrite tag ──
|
||||||
enrichment_genres = mb_genres + ([audiodb_genre] if audiodb_genre else []) + lastfm_tags
|
if tag_cfg['genre_merge']:
|
||||||
if enrichment_genres:
|
enrichment_genres = mb_genres + ([audiodb_genre] if audiodb_genre else []) + lastfm_tags
|
||||||
spotify_genres = [g.strip() for g in metadata.get('genre', '').split(',') if g.strip()]
|
if enrichment_genres:
|
||||||
seen = set()
|
spotify_genres = [g.strip() for g in metadata.get('genre', '').split(',') if g.strip()]
|
||||||
merged = []
|
seen = set()
|
||||||
for g in spotify_genres + enrichment_genres:
|
merged = []
|
||||||
key = g.strip().lower()
|
for g in spotify_genres + enrichment_genres:
|
||||||
if key and key not in seen:
|
key = g.strip().lower()
|
||||||
seen.add(key)
|
if key and key not in seen:
|
||||||
merged.append(g.strip().title())
|
seen.add(key)
|
||||||
if len(merged) >= 5:
|
merged.append(g.strip().title())
|
||||||
break
|
if len(merged) >= 5:
|
||||||
|
break
|
||||||
|
|
||||||
if merged:
|
if merged:
|
||||||
genre_string = ', '.join(merged)
|
genre_string = ', '.join(merged)
|
||||||
if isinstance(audio_file.tags, ID3):
|
if isinstance(audio_file.tags, ID3):
|
||||||
audio_file.tags.add(TCON(encoding=3, text=[genre_string]))
|
audio_file.tags.add(TCON(encoding=3, text=[genre_string]))
|
||||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||||
audio_file['GENRE'] = [genre_string]
|
audio_file['GENRE'] = [genre_string]
|
||||||
elif isinstance(audio_file, MP4):
|
elif isinstance(audio_file, MP4):
|
||||||
audio_file['\xa9gen'] = [genre_string]
|
audio_file['\xa9gen'] = [genre_string]
|
||||||
print(f"🎶 Genres merged: {genre_string}")
|
print(f"🎶 Genres merged: {genre_string}")
|
||||||
|
|
||||||
# ── 5. Write ISRC if available (MusicBrainz → Deezer → Tidal → Qobuz fallback) ──
|
# ── 5. Write ISRC if available (MusicBrainz → Deezer → Tidal → Qobuz fallback) ──
|
||||||
final_isrc = isrc or deezer_isrc or tidal_isrc or qobuz_isrc
|
if tag_cfg['isrc']:
|
||||||
if final_isrc:
|
final_isrc = isrc or deezer_isrc or tidal_isrc or qobuz_isrc
|
||||||
if isinstance(audio_file.tags, ID3):
|
if final_isrc:
|
||||||
audio_file.tags.add(TSRC(encoding=3, text=[final_isrc]))
|
if isinstance(audio_file.tags, ID3):
|
||||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
audio_file.tags.add(TSRC(encoding=3, text=[final_isrc]))
|
||||||
audio_file['ISRC'] = [final_isrc]
|
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||||
elif isinstance(audio_file, MP4):
|
audio_file['ISRC'] = [final_isrc]
|
||||||
audio_file['----:com.apple.iTunes:ISRC'] = [MP4FreeForm(final_isrc.encode('utf-8'))]
|
elif isinstance(audio_file, MP4):
|
||||||
source = "MusicBrainz" if isrc else "Deezer" if deezer_isrc else "Tidal" if tidal_isrc else "Qobuz"
|
audio_file['----:com.apple.iTunes:ISRC'] = [MP4FreeForm(final_isrc.encode('utf-8'))]
|
||||||
print(f"🔖 ISRC ({source}): {final_isrc}")
|
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) ──
|
# ── 6. Write copyright tag (Tidal → Qobuz fallback) ──
|
||||||
final_copyright = tidal_copyright or qobuz_copyright
|
if tag_cfg['copyright_label']:
|
||||||
if final_copyright:
|
final_copyright = tidal_copyright or qobuz_copyright
|
||||||
if isinstance(audio_file.tags, ID3):
|
if final_copyright:
|
||||||
audio_file.tags.add(TCOP(encoding=3, text=[final_copyright]))
|
if isinstance(audio_file.tags, ID3):
|
||||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
audio_file.tags.add(TCOP(encoding=3, text=[final_copyright]))
|
||||||
audio_file['COPYRIGHT'] = [final_copyright]
|
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||||
elif isinstance(audio_file, MP4):
|
audio_file['COPYRIGHT'] = [final_copyright]
|
||||||
audio_file['cprt'] = [final_copyright]
|
elif isinstance(audio_file, MP4):
|
||||||
source = "Tidal" if tidal_copyright else "Qobuz"
|
audio_file['cprt'] = [final_copyright]
|
||||||
print(f"©️ Copyright ({source}): {final_copyright[:60]}")
|
source = "Tidal" if tidal_copyright else "Qobuz"
|
||||||
|
print(f"©️ Copyright ({source}): {final_copyright[:60]}")
|
||||||
|
|
||||||
# ── 7. Write label/publisher tag (from Qobuz) ──
|
# ── 7. Write label/publisher tag (from Qobuz) ──
|
||||||
if qobuz_label:
|
if tag_cfg['copyright_label'] and qobuz_label:
|
||||||
if isinstance(audio_file.tags, ID3):
|
if isinstance(audio_file.tags, ID3):
|
||||||
audio_file.tags.add(TPUB(encoding=3, text=[qobuz_label]))
|
audio_file.tags.add(TPUB(encoding=3, text=[qobuz_label]))
|
||||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||||
|
|
@ -15410,7 +15554,7 @@ def _embed_source_ids(audio_file, metadata: dict):
|
||||||
print(f"🏷️ Label (Qobuz): {qobuz_label}")
|
print(f"🏷️ Label (Qobuz): {qobuz_label}")
|
||||||
|
|
||||||
# ── 8. Write Last.fm and Genius URLs as custom tags ──
|
# ── 8. Write Last.fm and Genius URLs as custom tags ──
|
||||||
if lastfm_url:
|
if tag_cfg['urls'] and lastfm_url:
|
||||||
if isinstance(audio_file.tags, ID3):
|
if isinstance(audio_file.tags, ID3):
|
||||||
audio_file.tags.add(TXXX(encoding=3, desc='LASTFM_URL', text=[lastfm_url]))
|
audio_file.tags.add(TXXX(encoding=3, desc='LASTFM_URL', text=[lastfm_url]))
|
||||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||||
|
|
@ -15418,7 +15562,7 @@ def _embed_source_ids(audio_file, metadata: dict):
|
||||||
elif isinstance(audio_file, MP4):
|
elif isinstance(audio_file, MP4):
|
||||||
audio_file['----:com.apple.iTunes:LASTFM_URL'] = [MP4FreeForm(lastfm_url.encode('utf-8'))]
|
audio_file['----:com.apple.iTunes:LASTFM_URL'] = [MP4FreeForm(lastfm_url.encode('utf-8'))]
|
||||||
|
|
||||||
if genius_url:
|
if tag_cfg['urls'] and genius_url:
|
||||||
if isinstance(audio_file.tags, ID3):
|
if isinstance(audio_file.tags, ID3):
|
||||||
audio_file.tags.add(TXXX(encoding=3, desc='GENIUS_URL', text=[genius_url]))
|
audio_file.tags.add(TXXX(encoding=3, desc='GENIUS_URL', text=[genius_url]))
|
||||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||||
|
|
|
||||||
|
|
@ -4387,6 +4387,68 @@
|
||||||
</div>
|
</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>
|
||||||
|
</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">
|
<div class="form-group">
|
||||||
<label>Supported Formats:</label>
|
<label>Supported Formats:</label>
|
||||||
<div class="supported-formats">MP3, FLAC, MP4/M4A, OGG</div>
|
<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-qobuz').checked = settings.qobuz?.embed_tags !== false;
|
||||||
document.getElementById('embed-lastfm').checked = settings.lastfm?.embed_tags !== false;
|
document.getElementById('embed-lastfm').checked = settings.lastfm?.embed_tags !== false;
|
||||||
document.getElementById('embed-genius').checked = settings.genius?.embed_tags !== false;
|
document.getElementById('embed-genius').checked = settings.genius?.embed_tags !== false;
|
||||||
|
document.getElementById('tag-musicbrainz-ids').checked = settings.metadata_enhancement?.tags?.musicbrainz_ids !== false;
|
||||||
|
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';
|
document.getElementById('post-processing-options').style.display = settings.metadata_enhancement?.enabled !== false ? 'block' : 'none';
|
||||||
|
|
||||||
// Populate File Organization settings
|
// Populate File Organization settings
|
||||||
|
|
@ -5723,7 +5733,19 @@ async function saveSettings(quiet = false) {
|
||||||
enabled: document.getElementById('metadata-enabled').checked,
|
enabled: document.getElementById('metadata-enabled').checked,
|
||||||
embed_album_art: document.getElementById('embed-album-art').checked,
|
embed_album_art: document.getElementById('embed-album-art').checked,
|
||||||
cover_art_download: document.getElementById('cover-art-download').checked,
|
cover_art_download: document.getElementById('cover-art-download').checked,
|
||||||
lrclib_enabled: document.getElementById('lrclib-enabled').checked
|
lrclib_enabled: document.getElementById('lrclib-enabled').checked,
|
||||||
|
tags: {
|
||||||
|
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: {
|
musicbrainz: {
|
||||||
embed_tags: document.getElementById('embed-musicbrainz').checked
|
embed_tags: document.getElementById('embed-musicbrainz').checked
|
||||||
|
|
|
||||||
|
|
@ -2090,6 +2090,22 @@ body {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 4px 16px;
|
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 */
|
/* Read-only Fields */
|
||||||
.readonly-field {
|
.readonly-field {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue