diff --git a/fcco_converter/converter.py b/fcco_converter/converter.py index 2e1f8bd..f5ec032 100644 --- a/fcco_converter/converter.py +++ b/fcco_converter/converter.py @@ -115,6 +115,9 @@ def _build_ffmpeg_cmd( else: cmd.append("-n") + # Keine Video/Bilder-Streams übernehmen (verhindert Konflikte mit eigenen Covern) + cmd.append("-vn") + cmd.append(str(output_path)) return cmd @@ -190,6 +193,8 @@ def convert_file_entry( audio_settings: dict | None = None, threads: int = 0, progress_callback: Callable[[int, int, str], None] | None = None, + cover_data: bytes | None = None, + cover_mime: str = "image/jpeg", ) -> tuple[int, str, str | None]: """Konvertiert eine Datei und schreibt die Metadaten. @@ -227,7 +232,6 @@ def convert_file_entry( # ID3-Metadaten nur für MP3 schreiben if output_format == "mp3": - cover_path = Path(meta["cover"]) if meta.get("cover") else None apply_id3_tags( mp3_path=output_file, title=title, @@ -235,7 +239,8 @@ def convert_file_entry( album=meta.get("album", ""), track_number=index + 1, year=meta.get("year", ""), - cover_path=cover_path, + cover_data=cover_data, + cover_mime=cover_mime, ) return (index, f"{title}.{output_format}", None) @@ -266,6 +271,19 @@ def run_batch_conversion( successful_files = [] errors = [] + # Cover-Bild einmalig in den Arbeitsspeicher laden, um Dateisperren (Locking) + # bei parallelem Zugriff durch Windows Defender/Antivirus zu vermeiden + cover_data = None + cover_mime = "image/jpeg" + cover_path_str = meta.get("cover") + if cover_path_str: + cp = Path(cover_path_str) + if cp.exists(): + if cp.suffix.lower() == ".png": + cover_mime = "image/png" + with open(cp, "rb") as f: + cover_data = f.read() + with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = { executor.submit( @@ -280,6 +298,8 @@ def run_batch_conversion( audio_settings=audio_settings, threads=0, progress_callback=progress_callback, + cover_data=cover_data, + cover_mime=cover_mime, ): idx for idx, entry in enumerate(entries) } diff --git a/fcco_converter/metadata.py b/fcco_converter/metadata.py index e7a4fba..52007bb 100644 --- a/fcco_converter/metadata.py +++ b/fcco_converter/metadata.py @@ -51,6 +51,8 @@ def extract_event_from_folder(file_path: Path) -> tuple[str, str]: return "", "" +from mutagen.id3 import ID3, APIC, TIT2, TPE1, TALB, TRCK, TDRC + def apply_id3_tags( mp3_path: Path, title: str, @@ -58,31 +60,31 @@ def apply_id3_tags( album: str, track_number: int, year: str, - cover_path: Path | None = None, + cover_data: bytes | None = None, + cover_mime: str = "image/jpeg", ) -> None: """Schreibt ID3-Metadaten und optional ein Cover in die MP3-Datei.""" - audiofile = EasyID3(str(mp3_path)) - audiofile["title"] = title - audiofile["artist"] = artist or "Unbekannt" - audiofile["album"] = album or "Unbekanntes Album" - audiofile["tracknumber"] = str(track_number) - audiofile["date"] = year - audiofile.save() + try: + tags = ID3(str(mp3_path)) + except Exception: + tags = ID3() - if cover_path and cover_path.exists(): - audio_id3 = ID3(str(mp3_path)) - mime = "image/jpeg" - suffix = cover_path.suffix.lower() - if suffix == ".png": - mime = "image/png" - with open(cover_path, "rb") as img: - audio_id3.add( - APIC( - encoding=3, - mime=mime, - type=3, - desc="Cover", - data=img.read(), - ) + tags.add(TIT2(encoding=3, text=title)) + tags.add(TPE1(encoding=3, text=artist or "Unbekannt")) + tags.add(TALB(encoding=3, text=album or "Unbekanntes Album")) + tags.add(TRCK(encoding=3, text=str(track_number))) + if year: + tags.add(TDRC(encoding=3, text=year)) + + if cover_data: + tags.add( + APIC( + encoding=3, + mime=cover_mime, + type=3, + desc="Cover", + data=cover_data, ) - audio_id3.save() + ) + + tags.save(str(mp3_path), v2_version=3)