import os import sys import json import subprocess import math import re from pathlib import Path # ============================================================================= # 1. KONFIGURATION UND PFADE # ============================================================================= FFMPEG_EXE = r"C:\Software\ffmpeg.exe" FFPROBE_EXE = r"C:\Software\ffprobe.exe" OUTPUT_BASE_DIR = r"H:\VOD" def get_pvd_filename(input_path: str) -> str: """TITEL-EXTRAKTION (Blu-ray / ProRes / DVD)""" path = Path(input_path) path_parts = [p.upper() for p in path.parts] if "BDMV" in path_parts: bdmv_index = path_parts.index("BDMV") try: project_folder = path_parts[bdmv_index - 4] except IndexError: project_folder = path_parts[bdmv_index - 1] if bdmv_index > 0 else "UNKNOWN" clean_name = re.sub(r'^BEST_', '', project_folder, flags=re.IGNORECASE) words = [w.capitalize() for w in clean_name.split('_') if w.strip()] extracted_title = "".join(words) else: extracted_title = path.stem extracted_title = re.sub(r'[^A-Za-z0-9]', '', extracted_title) if not extracted_title: extracted_title = "UNKNOWN_TITLE" return f"{extracted_title}_DEU20_PVD.mp4" def probe_metadata(filepath: str) -> dict: cmd = [ FFPROBE_EXE, "-v", "quiet", "-print_format", "json", "-show_streams", "-show_format", filepath ] result = subprocess.run(cmd, capture_output=True, text=True, check=False) if result.returncode != 0: print(f"CRITICAL ERROR: ffprobe konnte die Datei nicht lesen.\n{result.stderr}") sys.exit(1) return json.loads(result.stdout) def main(): if len(sys.argv) < 2: print("ERROR: Ziehe eine Datei auf dieses Script oder die verknüpfte .bat") os.system("pause") sys.exit(1) input_file = sys.argv[1] if not os.path.isfile(input_file): print(f"ERROR: Eingabedatei nicht gefunden: {input_file}") os.system("pause") sys.exit(1) os.makedirs(OUTPUT_BASE_DIR, exist_ok=True) # --- SCHRITT 1: ANALYSE --- print(f"\n[1/5] Analysiere Quelle: {os.path.basename(input_file)}") metadata = probe_metadata(input_file) v_stream = next((s for s in metadata['streams'] if s['codec_type'] == 'video'), None) a_streams = [s for s in metadata['streams'] if s['codec_type'] == 'audio'] if not v_stream: print("Fehler: Kein Videostream gefunden!") sys.exit(1) if not a_streams: print("Fehler: Keine Audio-Tracks gefunden!") sys.exit(1) # === AUFLÖSUNG + PAL/NTSC + INTERLACED-ERKENNUNG + FARBRAUM === width = int(v_stream.get('width', 0)) height = int(v_stream.get('height', 0)) fps_raw = v_stream.get('r_frame_rate', '24/1') num, den = map(int, fps_raw.split('/')) fps = num / den keyint = math.ceil(fps) # SD / PAL / NTSC Erkennung if height <= 576: is_ntsc = (height <= 480 and fps >= 29.0) print(f" -> Auflösung: {width}x{height} @ {fps:.3f} fps → {'NTSC' if is_ntsc else 'PAL'} SD-Material") is_sd = True else: is_ntsc = False is_sd = False print(f" -> Auflösung: {width}x{height} @ {fps:.3f} fps → HD-Material") print(f" -> Keyint (1 Sek. GOP): {keyint}") # === INTERLACED-ERKENNUNG FÜR DVD === field_order = v_stream.get('field_order', 'unknown') scan_type = v_stream.get('scan_type', 'unknown') is_interlaced = (field_order != 'progressive' and field_order != 'unknown') or scan_type == 'interlaced' if is_interlaced: print(f" → **Interlaced erkannt** (field_order={field_order}, scan_type={scan_type}) → Deinterlacing mit bwdif (beste Qualität, 25 fps) aktiviert") else: print(" → Progressive Quelle → kein Deinterlacing nötig") # Ziel-Farbraum (Rec.601 PAL/NTSC vs Rec.709) if is_sd: if is_ntsc: target_prim = "smpte170m" target_trc = "smpte170m" target_space = "smpte170m" target_name = "Rec.601 NTSC (smpte170m)" else: target_prim = "bt470bg" target_trc = "bt470bg" target_space = "bt470bg" target_name = "Rec.601 PAL (bt470bg)" else: target_prim = "bt709" target_trc = "bt709" target_space = "bt709" target_name = "Rec.709 HD" # Quell-Farbraum prüfen + echte Konvertierung source_prim = v_stream.get('color_primaries', 'unknown') source_trc = v_stream.get('color_transfer', 'unknown') source_space = v_stream.get('color_space', 'unknown') needs_conversion = ( source_prim == 'unknown' or source_prim != target_prim or source_trc != target_trc or source_space != target_space ) if needs_conversion: print(f" → Farbkonvertierung AKTIVIERT → Ziel: {target_name}") else: print(f" → Quell-Farbraum passt perfekt → keine Konvertierung") # --- SCHRITT 2: AUDIO-WAHL --- print("\n[2/5] Vorhandene Audio-Tracks:") for i, s in enumerate(a_streams): lang = s.get('tags', {}).get('language', 'und').upper() ch = s.get('channels', 2) codec = s.get('codec_name', 'pcm') print(f" [{i}] Index {s['index']}: {lang} ({ch} Kanäle, {codec})") selected_audio_list_idx = next( (i for i, s in enumerate(a_streams) if s.get('tags', {}).get('language', 'und').lower() in ('ger', 'deu')), None ) if selected_audio_list_idx is None: choice = input("\nKeine deutsche Spur automatisch erkannt.\nWelche Spur ist die deutsche Master-Spur? [Standard: 0]: ") or "0" try: selected_audio_list_idx = int(choice) if not 0 <= selected_audio_list_idx < len(a_streams): raise ValueError except Exception: print("Ungültige Eingabe → verwende Spur 0.") selected_audio_list_idx = 0 selected_audio_idx = a_streams[selected_audio_list_idx]['index'] print(f" → Gewählte Audio-Spur: Index {selected_audio_idx}") # --- SCHRITT 3: VF-FILTER (Deinterlace + Farbe + Subs) --- output_name = get_pvd_filename(input_file) output_path = os.path.join(OUTPUT_BASE_DIR, output_name) forced_srt = os.path.splitext(input_file)[0] + "_forced.srt" vf_filters = [] # 1. Deinterlacing mit bwdif (beste Qualität, behält originale Framerate = 25 fps bei PAL) if is_interlaced: vf_filters.append("bwdif=mode=0:parity=auto") # 2. Farbkonvertierung (falls nötig) if needs_conversion: conv = (f"colorspace=primaries={target_prim}:trc={target_trc}:matrix={target_space}:range=tv") vf_filters.append(conv) # 3. Forced SRT Burn-In if os.path.exists(forced_srt): print(f"\n[3/5] Forced SRT gefunden → wird eingebrannt: {os.path.basename(forced_srt)}") clean_srt_path = forced_srt.replace("\\", "/").replace(":", "\\:") vf_filters.append(f"subtitles='{clean_srt_path}':force_style='Fontsize=14,MarginV=38'") else: print("\n[3/5] Keine Forced SRT gefunden → Burn-In übersprungen.") # --- SCHRITT 4: ENCODING --- print(f"\n[4/5] Starte Encoding → {output_name}") print(f" → Ziel-Farbraum: {target_name}") if is_sd: bv = "8M" maxr = "10M" bufs = "15M" lvl = "3.1" print(" → SD-Parameter (Bitrate + Level)") else: bv = "28M" maxr = "35M" bufs = "50M" lvl = "4.1" print(" → HD-Parameter (Bitrate + Level)") ffmpeg_cmd = [ FFMPEG_EXE, "-hide_banner", "-y", "-i", input_file, "-map", f"0:{v_stream['index']}", "-map", f"0:{selected_audio_idx}", ] if vf_filters: vf_str = ",".join(vf_filters) ffmpeg_cmd.extend(["-vf", vf_str]) ffmpeg_cmd.extend([ "-c:v", "libx264", "-profile:v", "high", "-level", lvl, "-pix_fmt", "yuv420p", "-b:v", bv, "-maxrate", maxr, "-bufsize", bufs, "-preset", "slow", "-tune", "film", "-x264-params", f"keyint={keyint}:min-keyint=2:scenecut=40:bframes=3:aq-mode=2", "-color_primaries", target_prim, "-color_trc", target_trc, "-colorspace", target_space, ]) ffmpeg_cmd.extend([ "-c:a", "aac", "-b:a", "256k", "-ac", "2", "-ar", "48000", "-f", "mp4", "-movflags", "+faststart", "-metadata:s:a:0", "language=ger", "-metadata:s:v:0", "language=ger", "-map_chapters", "-1", "-map_metadata", "-1", "-avoid_negative_ts", "make_zero", output_path ]) print("\n--- FFmpeg Kommando ---") print(" ".join(ffmpeg_cmd)) print("------------------------\n") result = subprocess.run(ffmpeg_cmd, check=False) if result.returncode == 0: print(f"\n[5/5] ✅ ERFOLGREICH abgeschlossen!\nDatei: {output_path}") else: print(f"\n[5/5] ❌ FFmpeg-Fehler (Code {result.returncode})") os.system("pause") if __name__ == "__main__": main()