hardbanrecords/Metadata-Engine
0
1import os2try:3 import numpy as np4except ImportError:5 np = None6import logging7 8try:9 import librosa10 import numpy as np11except ImportError:12 # Local analysis disabled13 librosa = None14 # numpy might be missing too if librosa is missing, handled by earlier lazy loading logic usually15 # but here we imported it at top level. Wait, top level import is at line 2.16 # If numpy is missing, line 2 crashes.17 # But I removed numpy from requirements. So line 2 WILL crash.18 pass19 20try:21 from mutagen.easyid3 import EasyID322 from mutagen.id3 import (23 ID3,24 TIT2,25 TPE1,26 TALB,27 TCON,28 TDRC,29 COMM,30 USLT,31 TPUB,32 TCOP,33 TCOM,34 TEXT,35 )36 from mutagen.wave import WAVE37 from mutagen.mp3 import MP338 from mutagen.flac import FLAC39except ImportError:40 # Should not happen as mutagen is in requirements41 ID3 = None42 MP3 = None43 FLAC = None44 WAVE = None45 46logger = logging.getLogger(__name__)47 48 49class MIRService:50 @staticmethod51 def is_available():52 return librosa is not None53 54 @staticmethod55 async def analyze_audio(file_path: str):56 """57 Uses Librosa to extract technical features from the audio file.58 Returns a dictionary of features.59 """60 if not MIRService.is_available():61 raise RuntimeError("Librosa/Mutagen libraries not installed on backend.")62 63 try:64 # Load audio (only first 60 seconds for performance, unless deep analysis requested)65 # Duration analysis requires full load or stream info.66 y, sr = librosa.load(file_path, duration=120)67 68 # 1. BPM & Beat Tracking69 tempo, _ = librosa.beat.beat_track(y=y, sr=sr)70 bpm = float(tempo)71 72 # 2. Spectral Features (Timbre/Brightness)73 spectral_centroid = np.mean(librosa.feature.spectral_centroid(y=y, sr=sr))74 spectral_rolloff = np.mean(librosa.feature.spectral_rolloff(y=y, sr=sr))75 76 # 3. Key / Tonality (Simple estimate)77 # Chromecast -> shift to major/minor78 chroma = librosa.feature.chroma_stft(y=y, sr=sr)79 key_idx = np.argmax(np.mean(chroma, axis=1))80 keys = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]81 path_key = keys[key_idx]82 83 # 4. Danceability (Rough estimate based on rhythm stability)84 onset_env = librosa.onset.onset_strength(y=y, sr=sr)85 pulse = librosa.beat.plp(onset_envelope=onset_env, sr=sr)86 danceability = float(87 np.mean(pulse)88 ) # Normalize logic would be needed for 0-189 90 return {91 "bpm": round(bpm, 1),92 "key": path_key,93 "duration": librosa.get_duration(y=y, sr=sr),94 "technical": {95 "spectral_centroid": float(spectral_centroid),96 "spectral_rolloff": float(spectral_rolloff),97 "danceability_score": round(danceability, 2),98 },99 }100 except Exception as e:101 logger.error(f"MIR Analysis Failed: {e}")102 raise e103 104 @staticmethod105 def write_metadata(file_path: str, metadata: dict):106 """107 Writes standard ID3/Vorbis tags using Mutagen.108 Supports MP3, WAV, FLAC.109 """110 if not MIRService.is_available():111 raise RuntimeError("Mutagen library not installed.")112 113 ext = os.path.splitext(file_path)[1].lower()114 115 try:116 audio = None117 if ext == ".mp3":118 audio = MP3(file_path, ID3=ID3)119 try:120 audio.add_tags()121 except Exception:122 pass # Tags might exist123 elif ext == ".wav":124 try:125 audio = WAVE(file_path)126 try:127 audio.add_tags()128 except Exception:129 pass130 except Exception:131 # Some WAVs are weird, standard open might fail132 return False133 elif ext == ".flac":134 audio = FLAC(file_path)135 136 if audio is None:137 return False138 139 # --- MP3 / WAV (ID3) Mapping ---140 if ext in [".mp3", ".wav"]:141 # Basic142 if "title" in metadata:143 audio.tags.add(TIT2(encoding=3, text=metadata["title"]))144 if "artist" in metadata:145 audio.tags.add(TPE1(encoding=3, text=metadata["artist"]))146 if "album" in metadata:147 audio.tags.add(TALB(encoding=3, text=metadata["album"]))148 if "genre" in metadata:149 audio.tags.add(TCON(encoding=3, text=metadata["genre"]))150 if "year" in metadata:151 audio.tags.add(TDRC(encoding=3, text=str(metadata["year"])))152 153 # Technical (BPM & Key) - Local Analysis Results154 from mutagen.id3 import TBPM, TKEY155 156 if "bpm" in metadata and metadata["bpm"]:157 audio.tags.add(TBPM(encoding=3, text=str(metadata["bpm"])))158 if "key" in metadata and metadata["key"]:159 audio.tags.add(TKEY(encoding=3, text=str(metadata["key"])))160 161 # Extended (Requested by user)162 if "publisher" in metadata:163 audio.tags.add(TPUB(encoding=3, text=metadata["publisher"]))164 if "label" in metadata:165 audio.tags.add(166 TPUB(encoding=3, text=metadata["label"])167 ) # Fallback if same168 if "copyright" in metadata:169 audio.tags.add(TCOP(encoding=3, text=metadata["copyright"]))170 if "composer" in metadata:171 audio.tags.add(TCOM(encoding=3, text=metadata["composer"]))172 if "lyricist" in metadata:173 audio.tags.add(TEXT(encoding=3, text=metadata["lyricist"]))174 175 # Lyrics176 if "lyrics" in metadata:177 audio.tags.add(178 USLT(179 encoding=3,180 lang="eng",181 desc="Lyrics",182 text=metadata["lyrics"],183 )184 )185 186 audio.save(v2_version=3) # Force ID3v2.3 for max compatibility187 return True188 189 # --- FLAC (Vorbis) Mapping ---190 if ext == ".flac":191 if "title" in metadata:192 audio["title"] = metadata["title"]193 if "artist" in metadata:194 audio["artist"] = metadata["artist"]195 if "album" in metadata:196 audio["album"] = metadata["album"]197 if "year" in metadata:198 audio["date"] = str(metadata["year"])199 if "genre" in metadata:200 audio["genre"] = metadata["genre"]201 if "copyright" in metadata:202 audio["copyright"] = metadata["copyright"]203 if "publisher" in metadata:204 audio["publisher"] = metadata["publisher"] or metadata.get("label")205 if "lyrics" in metadata:206 audio["lyrics"] = metadata["lyrics"]207 208 # Technical209 if "bpm" in metadata:210 audio["bpm"] = str(metadata["bpm"])211 if "key" in metadata:212 audio["initialkey"] = str(213 metadata["key"]214 ) # 'initialkey' is common Vorbis field215 216 audio.save()217 return True218 219 except Exception as e:220 logger.error(f"Tagging Failed: {e}")221 raise e222 