Embed Deezer & AudioDB metadata (BPM, mood, style, ISRC) in post-processing
This commit is contained in:
parent
e7e939bdd5
commit
8e89040b19
1 changed files with 117 additions and 15 deletions
132
web_server.py
132
web_server.py
|
|
@ -8115,7 +8115,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
|
||||
from mutagen.id3 import ID3, TIT2, TPE1, TALB, TDRC, TRCK, TCON, TPE2, TPOS, TXXX, APIC, UFID, TSRC, TBPM
|
||||
from mutagen.flac import FLAC, Picture
|
||||
from mutagen.mp4 import MP4, MP4Cover, MP4FreeForm
|
||||
from mutagen.oggvorbis import OggVorbis
|
||||
|
|
@ -8518,13 +8518,23 @@ def _embed_album_art_metadata(audio_file, metadata: dict):
|
|||
|
||||
def _embed_source_ids(audio_file, metadata: dict):
|
||||
"""
|
||||
Lookup MusicBrainz recording MBID, ISRC, and genres, then embed them along
|
||||
Lookup MusicBrainz, Deezer, and AudioDB metadata, then embed them along
|
||||
with Spotify/iTunes source IDs as custom tags into the audio file.
|
||||
One file write, one shot. Concurrent calls are safe — the global rate
|
||||
limiter in musicbrainz_client.py serializes all MB API access.
|
||||
Tags written: source IDs, BPM (Deezer), mood/style (AudioDB), ISRC
|
||||
(MB→Deezer fallback), and merged genres (Spotify+MB+AudioDB).
|
||||
One file write, one shot. Concurrent calls are safe — each service has
|
||||
its own global rate limiter.
|
||||
Operates on a non-easy-mode MutagenFile object (caller must save).
|
||||
"""
|
||||
try:
|
||||
# ── 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:
|
||||
if not a or not b:
|
||||
return False
|
||||
norm = lambda s: re.sub(r'[^a-z0-9 ]', '', re.sub(r'\(.*?\)', '', s).lower()).strip()
|
||||
return SequenceMatcher(None, norm(a), norm(b)).ratio() >= threshold
|
||||
|
||||
# ── 1. Collect Spotify / iTunes IDs already in metadata ──
|
||||
id_tags = {}
|
||||
if metadata.get('spotify_track_id'):
|
||||
|
|
@ -8540,7 +8550,7 @@ def _embed_source_ids(audio_file, metadata: dict):
|
|||
if metadata.get('itunes_album_id'):
|
||||
id_tags['ITUNES_ALBUM_ID'] = metadata['itunes_album_id']
|
||||
|
||||
# ── 2. MusicBrainz lookup for MBID, genres, and ISRC ──
|
||||
# ── 2a. MusicBrainz lookup for MBID, genres, and ISRC ──
|
||||
# The global rate limiter in musicbrainz_client.py serializes all API
|
||||
# calls (worker + any number of post-processing threads) to 1 req/sec
|
||||
# via _api_call_lock, so no pause/resume needed.
|
||||
|
|
@ -8592,7 +8602,65 @@ def _embed_source_ids(audio_file, metadata: dict):
|
|||
except Exception as e:
|
||||
print(f"⚠️ MusicBrainz lookup failed (non-fatal): {e}")
|
||||
|
||||
if not id_tags:
|
||||
# ── 2b. Deezer lookup for BPM, ISRC fallback, and source IDs ──
|
||||
deezer_bpm = None
|
||||
deezer_isrc = None
|
||||
if not config_manager.get('deezer.embed_tags', True):
|
||||
pass
|
||||
elif track_title and artist_name:
|
||||
try:
|
||||
dz_client = deezer_worker.client if deezer_worker else None
|
||||
if dz_client:
|
||||
dz_result = dz_client.search_track(artist_name, track_title)
|
||||
if dz_result and _names_match(dz_result.get('title', ''), track_title) and \
|
||||
_names_match(dz_result.get('artist', {}).get('name', ''), artist_name):
|
||||
dz_track_id = dz_result['id']
|
||||
id_tags['DEEZER_TRACK_ID'] = str(dz_track_id)
|
||||
dz_artist_id = dz_result.get('artist', {}).get('id')
|
||||
if dz_artist_id:
|
||||
id_tags['DEEZER_ARTIST_ID'] = str(dz_artist_id)
|
||||
print(f"🎵 Deezer track matched: {dz_track_id}")
|
||||
|
||||
# Get full track details for BPM and ISRC
|
||||
dz_details = dz_client.get_track(dz_track_id)
|
||||
if dz_details:
|
||||
bpm_val = dz_details.get('bpm')
|
||||
if bpm_val and bpm_val > 0:
|
||||
deezer_bpm = bpm_val
|
||||
dz_isrc = dz_details.get('isrc')
|
||||
if dz_isrc:
|
||||
deezer_isrc = dz_isrc
|
||||
else:
|
||||
print("⚠️ Deezer worker not available, skipping Deezer lookup")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Deezer lookup failed (non-fatal): {e}")
|
||||
|
||||
# ── 2c. AudioDB lookup for mood, style, genre, and source ID ──
|
||||
audiodb_mood = None
|
||||
audiodb_style = None
|
||||
audiodb_genre = None
|
||||
if not config_manager.get('audiodb.embed_tags', True):
|
||||
pass
|
||||
elif track_title and artist_name:
|
||||
try:
|
||||
adb_client = audiodb_worker.client if audiodb_worker else None
|
||||
if adb_client:
|
||||
adb_result = adb_client.search_track(artist_name, track_title)
|
||||
if adb_result and _names_match(adb_result.get('strTrack', ''), track_title) and \
|
||||
_names_match(adb_result.get('strArtist', ''), artist_name):
|
||||
adb_track_id = adb_result.get('idTrack')
|
||||
if adb_track_id:
|
||||
id_tags['AUDIODB_TRACK_ID'] = str(adb_track_id)
|
||||
print(f"🎵 AudioDB track matched: {adb_track_id}")
|
||||
audiodb_mood = adb_result.get('strMood') or None
|
||||
audiodb_style = adb_result.get('strStyle') or None
|
||||
audiodb_genre = adb_result.get('strGenre') or None
|
||||
else:
|
||||
print("⚠️ AudioDB worker not available, skipping AudioDB lookup")
|
||||
except Exception as e:
|
||||
print(f"⚠️ AudioDB lookup failed (non-fatal): {e}")
|
||||
|
||||
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 ──
|
||||
|
|
@ -8639,12 +8707,44 @@ def _embed_source_ids(audio_file, metadata: dict):
|
|||
if written:
|
||||
print(f"🔗 Embedded IDs: {', '.join(written)}")
|
||||
|
||||
# ── 4. Merge genres (Spotify + MusicBrainz) and overwrite tag ──
|
||||
if mb_genres:
|
||||
# ── 3b. Write BPM tag (from Deezer) ──
|
||||
if 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)]))
|
||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||
audio_file['BPM'] = [str(bpm_int)]
|
||||
elif isinstance(audio_file, MP4):
|
||||
audio_file['tmpo'] = [bpm_int]
|
||||
print(f"🥁 BPM: {bpm_int}")
|
||||
|
||||
# ── 3c. Write mood tag (from AudioDB) ──
|
||||
if 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)):
|
||||
audio_file['MOOD'] = [audiodb_mood]
|
||||
elif isinstance(audio_file, MP4):
|
||||
audio_file['----:com.apple.iTunes:MOOD'] = [MP4FreeForm(audiodb_mood.encode('utf-8'))]
|
||||
print(f"🎭 Mood: {audiodb_mood}")
|
||||
|
||||
# ── 3d. Write style tag (from AudioDB) ──
|
||||
if 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)):
|
||||
audio_file['STYLE'] = [audiodb_style]
|
||||
elif isinstance(audio_file, MP4):
|
||||
audio_file['----:com.apple.iTunes:STYLE'] = [MP4FreeForm(audiodb_style.encode('utf-8'))]
|
||||
print(f"🎨 Style: {audiodb_style}")
|
||||
|
||||
# ── 4. Merge genres (Spotify + MusicBrainz + AudioDB) and overwrite tag ──
|
||||
enrichment_genres = mb_genres + ([audiodb_genre] if audiodb_genre else [])
|
||||
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 + mb_genres:
|
||||
for g in spotify_genres + enrichment_genres:
|
||||
key = g.strip().lower()
|
||||
if key and key not in seen:
|
||||
seen.add(key)
|
||||
|
|
@ -8662,15 +8762,17 @@ def _embed_source_ids(audio_file, metadata: dict):
|
|||
audio_file['\xa9gen'] = [genre_string]
|
||||
print(f"🎶 Genres merged: {genre_string}")
|
||||
|
||||
# ── 5. Write ISRC if available ──
|
||||
if isrc:
|
||||
# ── 5. Write ISRC if available (MusicBrainz first, Deezer fallback) ──
|
||||
final_isrc = isrc or deezer_isrc
|
||||
if final_isrc:
|
||||
if isinstance(audio_file.tags, ID3):
|
||||
audio_file.tags.add(TSRC(encoding=3, text=[isrc]))
|
||||
audio_file.tags.add(TSRC(encoding=3, text=[final_isrc]))
|
||||
elif isinstance(audio_file, (FLAC, OggVorbis)):
|
||||
audio_file['ISRC'] = [isrc]
|
||||
audio_file['ISRC'] = [final_isrc]
|
||||
elif isinstance(audio_file, MP4):
|
||||
audio_file['----:com.apple.iTunes:ISRC'] = [MP4FreeForm(isrc.encode('utf-8'))]
|
||||
print(f"🔖 ISRC: {isrc}")
|
||||
audio_file['----:com.apple.iTunes:ISRC'] = [MP4FreeForm(final_isrc.encode('utf-8'))]
|
||||
source = "MusicBrainz" if isrc else "Deezer"
|
||||
print(f"🔖 ISRC ({source}): {final_isrc}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error embedding source IDs (non-fatal): {e}")
|
||||
|
|
|
|||
Loading…
Reference in a new issue