from pathlib import Path import re from mutagen.id3 import ID3, TALB, TIT2, TPE1, delete as id3_delete class MetadataManager: def apply_mp3_tags(self, file_path: Path, metadata: dict, fallback_title: str) -> None: track_title = str(metadata.get("mp3_title") or metadata.get("title") or fallback_title).strip() track_artist = str(metadata.get("artist", "")).strip() track_album = str(metadata.get("album", "")).strip() track_cover = str(metadata.get("cover_path", "")).strip() try: id3_delete(str(file_path)) except Exception: pass tags = ID3() tags.delall("TIT2") tags.add(TIT2(encoding=3, text=track_title or fallback_title)) tags.delall("TPE1") if track_artist: tags.add(TPE1(encoding=3, text=track_artist)) tags.delall("TALB") if track_album: tags.add(TALB(encoding=3, text=track_album)) tags.save(str(file_path), v2_version=4) # Use eyeD3 for cover art (Windows Explorer compatibility) if track_cover: try: import eyed3 from PIL import Image import io audiofile = eyed3.load(str(file_path)) if audiofile is not None: if audiofile.tag is None: audiofile.initTag() cover_file = Path(track_cover) if cover_file.exists() and cover_file.is_file(): # Always convert to JPEG and resize to 999x999 try: img = Image.open(cover_file) img = img.convert("RGB") img = img.resize((999, 999), Image.LANCZOS) buf = io.BytesIO() img.save(buf, format="JPEG") image_data = buf.getvalue() mime_type = "image/jpeg" except Exception as e: print(f"[eyeD3] Failed to process image: {e}") image_data = cover_file.read_bytes() mime_type = "image/jpeg" audiofile.tag.images.set(3, image_data, mime_type, u"Cover") audiofile.tag.version = (2, 3, 0) audiofile.tag.save(version=(2, 3, 0)) except Exception as e: print(f"[eyeD3] Failed to embed cover: {e}") def sanitize_filename_part(self, value: str) -> str: clean_value = re.sub(r"[<>:\"/\\|?*]", "_", value).strip() clean_value = re.sub(r"\s+", " ", clean_value) return clean_value.strip(". ") def rename_output_file(self, file_path: Path, title: str, artist: str) -> Path: safe_title = self.sanitize_filename_part(title) safe_artist = self.sanitize_filename_part(artist) if safe_artist and safe_title: base_name = f"{safe_artist} - {safe_title}" else: base_name = safe_title or self.sanitize_filename_part(file_path.stem) target_path = file_path.with_name(f"{base_name}{file_path.suffix}") if target_path == file_path: return file_path counter = 2 while target_path.exists(): target_path = file_path.with_name(f"{base_name} ({counter}){file_path.suffix}") counter += 1 try: file_path.rename(target_path) return target_path except Exception: return file_path