# core/encode_engine.py """FFmpeg encoding engine with comprehensive logging.""" import subprocess from pathlib import Path from core.audio_handler import get_audio_streams, choose_audio_bitrate, filter_audio_streams, prompt_user_audio_selection, prompt_for_title_stripping from core.logger_helper import setup_logger logger = setup_logger(Path(__file__).parent.parent / "logs") def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, scale_height: int, src_width: int, src_height: int, filter_flags: str, audio_config: dict, method: str, bitrate_config: dict, encoder: str = "nvenc", subtitle_files: list = None, audio_language: str = None, audio_filter_config: dict = None, test_mode: bool = False, strip_all_titles: bool = False): """ Run FFmpeg encode with comprehensive logging. Args: strip_all_titles: If True, strip all title metadata from all audio tracks Returns tuple: (orig_size, out_size, reduction_ratio) """ streams = get_audio_streams(input_file) # Apply audio filter if enabled if audio_filter_config and audio_filter_config.get("enabled", False): # Check if pre-selected streams provided if audio_filter_config.get("preselected"): # Use pre-selected streams (skip interactive) preselected_str = audio_filter_config["preselected"] try: selected_indices = set() for part in preselected_str.split(","): idx = int(part.strip()) selected_indices.add(idx) # Filter to only selected streams streams = [s for s in streams if s[0] in selected_indices] # Add strip_title field (False by default for pre-selected) streams = [s + (False,) for s in streams] logger.info(f"Pre-selected audio streams: {[s[0] for s in streams]}") except ValueError: logger.warning(f"Invalid audio_select format: {preselected_str}. Using all streams.") streams = [s + (False,) for s in streams] else: # Check if interactive mode requested (via --filter-audio CLI flag) # If audio_filter_config came from CLI, it has "interactive": True if "interactive" in audio_filter_config and audio_filter_config.get("interactive", False): # Interactive audio selection (show prompt to user) streams = prompt_user_audio_selection(streams) # Prompt for title stripping after stream selection streams = prompt_for_title_stripping(streams) else: # Automatic filtering from config (keep best English + Commentary) streams = filter_audio_streams(input_file, streams) # Add strip_title field (False by default for automatic filtering) streams = [s + (False,) for s in streams] else: # No filtering - add strip_title field as False streams = [s + (False,) for s in streams] # Log comprehensive encode settings header = f"\n🧩 ENCODE SETTINGS" logger.info(header) print(" ") # Determine encoder display name and settings if encoder == "av1": encoder_name = "AV1 NVENC" encoder_codec = "av1_nvenc" encoder_preset = "p7" encoder_pix_fmt = "yuv420p" encoder_bit_depth = "8-bit" else: # default hevc = HEVC NVENC encoder_name = "HEVC NVENC" encoder_codec = "hevc_nvenc" encoder_preset = "slow" encoder_pix_fmt = "p010le" encoder_bit_depth = "10-bit" logger.info(f" Video:") logger.info(f" β€’ Source Resolution: {src_width}x{src_height}") logger.info(f" β€’ Target Resolution: {scale_width}x{scale_height}") logger.info(f" β€’ Encoder: {encoder_name} (preset {encoder_preset}, {encoder_bit_depth}, pix_fmt {encoder_pix_fmt})") logger.info(f" β€’ Scale Filter: {filter_flags}") logger.info(f" β€’ Encode Method: {method}") if method == "CQ": logger.info(f" β€’ CQ Value: {cq}") else: res_key = "1080" if scale_height >= 1080 or scale_width >= 1920 else "720" vb = bitrate_config.get(f"bitrate_{res_key}", "900k") maxrate = bitrate_config.get(f"maxrate_{res_key}", "1250k") logger.info(f" β€’ Bitrate: {vb}, Max: {maxrate}") logger.info(f" Audio Streams ({len(streams)} detected):") print(" ") for (index, channels, avg_bitrate, src_lang, meta_bitrate, title, strip_title) in streams: # Normalize to 2ch or 6ch output is_1080_class = scale_height >= 1080 or scale_width >= 1920 output_channels = 6 if is_1080_class and channels >= 6 else 2 codec, br = choose_audio_bitrate(output_channels, avg_bitrate, audio_config, is_1080_class) if codec == "copy": action = "COPY (preserve)" bitrate_display = f"{avg_bitrate}kbps" else: action = "ENCODE" bitrate_display = f"{br/1000:.0f}kbps" # Include title in display if present title_info = f" | Title: {title}" if title else "" line = f" - Stream #{index}: {channels}chβ†’{output_channels}ch | Lang: {src_lang} | Detected: {avg_bitrate}kbps | Action: {action} | Target: {bitrate_display}{title_info}" print(line) logger.info(line) cmd = ["ffmpeg","-y","-i",str(input_file)] # Add subtitle inputs if present if subtitle_files: for sub_file in subtitle_files: cmd.extend(["-i", str(sub_file)]) # In test mode, only encode first 15 minutes if test_mode: cmd.extend(["-t", "900"]) # 900 seconds = 15 minutes cmd.extend([ "-vf",f"scale={scale_width}:{scale_height}:flags={filter_flags}:force_original_aspect_ratio=decrease", "-map","0:v"]) # Map only selected audio streams for index, _, _, _, _, _, _ in streams: cmd.extend(["-map", f"0:{index}"]) # Add subtitle mapping if present if subtitle_files: for i, _ in enumerate(subtitle_files): cmd.extend(["-map", f"{i+1}:s"]) else: cmd.extend(["-map", "0:s?"]) cmd.extend([ "-c:v", encoder_codec, "-preset", encoder_preset, "-pix_fmt", encoder_pix_fmt]) if method=="CQ": cmd += ["-cq", str(cq)] else: # Use bitrate config (fallback mode) res_key = "1080" if scale_height >= 1080 or scale_width >= 1920 else "720" vb = bitrate_config.get(f"bitrate_{res_key}", "900k") maxrate = bitrate_config.get(f"maxrate_{res_key}", "1250k") bufsize = bitrate_config.get(f"bufsize_{res_key}", "1800k") cmd += ["-b:v", vb, "-maxrate", maxrate, "-bufsize", bufsize] for i, (index, channels, avg_bitrate, src_lang, meta_bitrate, title, strip_title) in enumerate(streams): # Normalize to 2ch or 6ch output is_1080_class = scale_height >= 1080 or scale_width >= 1920 output_channels = 6 if is_1080_class and channels >= 6 else 2 codec, br = choose_audio_bitrate(output_channels, avg_bitrate, audio_config, is_1080_class) if codec == "copy": # Preserve original audio cmd += [f"-c:a:{i}", "copy"] # Only add language metadata if explicitly provided if audio_language: cmd += [f"-metadata:s:a:{i}", f"language={audio_language}"] # Strip title metadata if requested (but preserve commentary tracks) should_strip = strip_title or (strip_all_titles and not (title and "commentary" in title.lower())) if should_strip: cmd += [f"-metadata:s:a:{i}", "title="] else: # Re-encode with target bitrate # EAC3 for multichannel, AAC for stereo if codec == "eac3": # Enhanced AC-3 (5.1 surround) cmd += [ f"-c:a:{i}", "eac3", f"-b:a:{i}", str(br), f"-ac:{i}", str(output_channels), f"-channel_layout:a:{i}", "5.1" ] else: # AAC (stereo) cmd += [ f"-c:a:{i}", "aac", f"-b:a:{i}", str(br), f"-ac:{i}", str(output_channels), f"-channel_layout:a:{i}", "stereo" ] # Only add language metadata if explicitly provided if audio_language: cmd += [f"-metadata:s:a:{i}", f"language={audio_language}"] # Strip title metadata if requested (but preserve commentary tracks) should_strip = strip_title or (strip_all_titles and not (title and "commentary" in title.lower())) if should_strip: cmd += [f"-metadata:s:a:{i}", "title="] # Add subtitle codec and metadata if subtitles are present if subtitle_files: cmd += ["-c:s", "srt"] for i in range(len(subtitle_files)): cmd += ["-metadata:s:s:" + str(i), "language=eng"] else: cmd += ["-c:s", "copy"] cmd += [str(output_file)] print(f"\n🎬 Running {method} encode: {output_file.name}") logger.info(f"Running {method} encode: {output_file.name}") subprocess.run(cmd, check=True) orig_size = input_file.stat().st_size out_size = output_file.stat().st_size reduction_ratio = out_size / orig_size # Log comprehensive results logger.info(f"\nπŸ“Š ENCODE RESULTS:") logger.info(f" Original Size: {orig_size/1e6:.2f} MB") logger.info(f" Encoded Size: {out_size/1e6:.2f} MB") logger.info(f" Reduction: {reduction_ratio:.1%} of original ({(1-reduction_ratio):.1%} saved)") logger.info(f" Resolution: {src_width}x{src_height} β†’ {scale_width}x{scale_height}") logger.info(f" Audio Streams: {len(streams)} streams processed") msg = f"πŸ“¦ Original: {orig_size/1e6:.2f} MB β†’ Encoded: {out_size/1e6:.2f} MB ({reduction_ratio:.1%} of original)" print(msg) return orig_size, out_size, reduction_ratio