From 0d415c7e4906255c2070cb89f59c44ef44b26a0f Mon Sep 17 00:00:00 2001 From: TylerCG <117808427+TylerCG@users.noreply.github.com> Date: Wed, 31 Dec 2025 11:53:09 -0500 Subject: [PATCH] should be working --- config.xml | 1 + core/ffmpeg_helper.py | 79 +++++++++- logs/conversion.log | 29 ++++ main.py | 331 ++++++++++++++++++++++++++++++++++-------- 4 files changed, 379 insertions(+), 61 deletions(-) diff --git a/config.xml b/config.xml index e368cda..b087713 100644 --- a/config.xml +++ b/config.xml @@ -83,6 +83,7 @@ 192000 + 384000 448000 diff --git a/core/ffmpeg_helper.py b/core/ffmpeg_helper.py index f7b4870..7f647a3 100644 --- a/core/ffmpeg_helper.py +++ b/core/ffmpeg_helper.py @@ -1,6 +1,8 @@ # core/ffmpeg_helper.py import json +import os import subprocess +import tempfile from pathlib import Path from typing import Tuple @@ -9,10 +11,66 @@ from core.logger_helper import setup_logger logger = setup_logger(Path(__file__).parent.parent / "logs") # ============================= -# STREAM ANALYSIS +# ROBUST BITRATE CALCULATION # ============================= +def calculate_stream_bitrate(input_file: Path, stream_index: int) -> int: + """ + Extract audio stream to temporary file using -c copy, calculate bitrate from file size and duration. + Returns bitrate in kbps. + + Formula: bitrate_kbps = (file_size_bytes * 8) / duration_seconds / 1000 + """ + temp_fd, temp_audio_path = tempfile.mkstemp(suffix=".aac", dir=None) + os.close(temp_fd) + + try: + # Step 1: Extract audio stream with -c copy (lossless extraction) + extract_cmd = [ + "ffmpeg", "-y", "-i", str(input_file), + "-map", f"0:a:{stream_index}", + "-c", "copy", + temp_audio_path + ] + logger.debug(f"Extracting audio stream {stream_index} to temporary file...") + subprocess.run(extract_cmd, capture_output=True, text=True, check=True) + + # Step 2: Get duration using ffprobe + duration_cmd = [ + "ffprobe", "-v", "error", + "-show_entries", "format=duration", + "-of", "default=noprint_wrappers=1:nokey=1:noprint_wrappers=1", + temp_audio_path + ] + duration_result = subprocess.run(duration_cmd, capture_output=True, text=True, check=True) + duration_seconds = float(duration_result.stdout.strip()) + + # Step 3: Get file size and calculate bitrate + file_size_bytes = os.path.getsize(temp_audio_path) + bitrate_kbps = int((file_size_bytes * 8) / duration_seconds / 1000) + + logger.debug(f"Stream {stream_index}: size={file_size_bytes} bytes, duration={duration_seconds:.2f}s, calculated_bitrate={bitrate_kbps} kbps") + return bitrate_kbps + + except Exception as e: + logger.warning(f"Failed to calculate bitrate for stream {stream_index}: {e}. Falling back to metadata.") + return 0 + + finally: + # Clean up temporary audio file + try: + if os.path.exists(temp_audio_path): + os.remove(temp_audio_path) + logger.debug(f"Deleted temporary audio file: {temp_audio_path}") + except Exception as e: + logger.warning(f"Could not delete temporary file {temp_audio_path}: {e}") + + def get_audio_streams(input_file: Path): - """Return a list of (index, channels, bitrate_kbps, lang)""" + """Return a list of (index, channels, bitrate_kbps, lang) + + Uses robust bitrate calculation by extracting each stream and computing + bitrate from file size and duration instead of relying on metadata. + """ cmd = [ "ffprobe", "-v", "error", "-select_streams", "a", @@ -22,12 +80,25 @@ def get_audio_streams(input_file: Path): result = subprocess.run(cmd, capture_output=True, text=True) data = json.loads(result.stdout or "{}") streams = [] - for s in data.get("streams", []): + + for i, s in enumerate(data.get("streams", [])): index = s["index"] channels = s.get("channels", 2) - bitrate = int(int(s.get("bit_rate", 128000)) / 1000) lang = s.get("tags", {}).get("language", "und") + + # Calculate robust bitrate from extraction + calculated_bitrate = calculate_stream_bitrate(input_file, i) + + # Fallback to metadata if calculation fails + if calculated_bitrate == 0: + bitrate = int(int(s.get("bit_rate", 128000)) / 1000) + logger.info(f"Stream {index}: Using metadata bitrate {bitrate} kbps (calculation failed)") + else: + bitrate = calculated_bitrate + logger.info(f"Stream {index}: Using calculated bitrate {bitrate} kbps") + streams.append((index, channels, bitrate, lang)) + return streams # ============================= diff --git a/logs/conversion.log b/logs/conversion.log index 38bd90a..3494831 100644 --- a/logs/conversion.log +++ b/logs/conversion.log @@ -4162,3 +4162,32 @@ 2025-12-29 22:06:32 [INFO] Moved 2025-12-29 21-53-23 -EHX.mp4 β†’ 2025-12-29 21-53-23 -EHX.mp4 2025-12-29 22:06:32 [INFO] Tracked conversion: 2025-12-29 21-53-23 -EHX.mp4, 343.3MB β†’ 47.9MB (14.0%), method=CQ 2025-12-29 22:06:32 [INFO] Deleted original and processing copy for 2025-12-29 21-53-23.mp4 +2025-12-31 11:28:51 [INFO] Processing: How the Grinch Stole Christmas (2000) x265 AAC 5.1 Bluray-1080p Tigole.mkv +2025-12-31 11:29:39 [INFO] Copied How the Grinch Stole Christmas (2000) x265 AAC 5.1 Bluray-1080p Tigole.mkv β†’ How the Grinch Stole Christmas (2000) x265 AAC 5.1 Bluray-1080p Tigole.mkv +2025-12-31 11:29:51 [INFO] +🧩 ENCODE SETTINGS + β€’ Resolution: 1920x1080 + β€’ Scale Filter: lanczos + β€’ CQ: 32 + β€’ Video Encoder: av1_nvenc (preset p1, pix_fmt p010le) + β€’ Audio Streams: +2025-12-31 11:29:51 [INFO] - Stream #1: 6chβ†’6ch, src=und, detected=229kbps, action=ENCODE, target=384kbps +2025-12-31 11:29:51 [INFO] - Stream #2: 2chβ†’2ch, src=und, detected=73kbps, action=COPY (preserve), target=73kbps +2025-12-31 11:29:51 [INFO] Running CQ encode: How the Grinch Stole Christmas (2000) x265 AAC 5.1 Bluray-1080p Tigole -EHX.mkv +2025-12-31 11:41:23 [INFO] Processing: How the Grinch Stole Christmas (2000) x265 AAC 5.1 Bluray-1080p Tigole.mkv +2025-12-31 11:42:12 [INFO] Copied How the Grinch Stole Christmas (2000) x265 AAC 5.1 Bluray-1080p Tigole.mkv β†’ How the Grinch Stole Christmas (2000) x265 AAC 5.1 Bluray-1080p Tigole.mkv +2025-12-31 11:42:12 [INFO] Source resolution detected: 1920x1040 +2025-12-31 11:43:05 [INFO] Processing: How the Grinch Stole Christmas (2000) x265 AAC 5.1 Bluray-1080p Tigole.mkv +2025-12-31 11:43:53 [INFO] Copied How the Grinch Stole Christmas (2000) x265 AAC 5.1 Bluray-1080p Tigole.mkv β†’ How the Grinch Stole Christmas (2000) x265 AAC 5.1 Bluray-1080p Tigole.mkv +2025-12-31 11:43:53 [INFO] Source resolution detected: 1920x1040 +2025-12-31 11:44:03 [INFO] +🧩 ENCODE SETTINGS + β€’ Resolution: 1920x1080 + β€’ Scale Filter: lanczos + β€’ CQ: 32 + β€’ Video Encoder: av1_nvenc (preset p1, pix_fmt p010le) + β€’ Audio Streams: +2025-12-31 11:44:03 [INFO] - Stream #1: 6chβ†’6ch, src=und, detected=484kbps, action=ENCODE, target=448kbps +2025-12-31 11:44:03 [INFO] - Stream #2: 2chβ†’2ch, src=und, detected=65kbps, action=COPY (preserve), target=65kbps +2025-12-31 11:44:03 [INFO] Running CQ encode: How the Grinch Stole Christmas (2000) x265 AAC 5.1 Bluray-1080p Tigole -EHX.mkv +2025-12-31 11:52:44 [INFO] πŸ“¦ Original: 5626.25 MB β†’ Encoded: 3268.49 MB (58.1% of original) diff --git a/main.py b/main.py index 7dd7b0e..91b0e70 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ import os import shutil import subprocess import sys +import tempfile from pathlib import Path from core.config_helper import load_config_xml @@ -31,22 +32,133 @@ if not TRACKER_FILE.exists(): # ============================= # AUDIO BUCKET LOGIC # ============================= -def choose_audio_bitrate(channels: int, bitrate_kbps: int, audio_config: dict, is_1080_class: bool) -> int: - """Choose audio bitrate based on channel count and detected bitrate""" +def calculate_stream_bitrate(input_file: Path, stream_index: int) -> int: + """ + Extract audio stream to temporary file using -c copy, capture bitrate from ffmpeg output. + Returns bitrate in kbps. Falls back to 0 (and uses metadata) if extraction fails. + + Uses ffmpeg's reported bitrate which is more accurate than calculating from file size/duration. + """ + temp_fd, temp_audio_path = tempfile.mkstemp(suffix=".aac", dir=None) + os.close(temp_fd) + + try: + # Step 1: Extract audio stream with -c copy (lossless extraction) + # ffmpeg outputs bitrate info to stderr + extract_cmd = [ + "ffmpeg", "-y", "-i", str(input_file), + "-map", f"0:a:{stream_index}", + "-c", "copy", + temp_audio_path + ] + logger.debug(f"Extracting audio stream {stream_index} to temporary file for bitrate calculation...") + result = subprocess.run(extract_cmd, capture_output=True, text=True, check=True) + + # Step 2: Parse bitrate from ffmpeg's output (stderr) + # Look for line like: "bitrate= 457.7kbits/s" + bitrate_kbps = 0 + for line in result.stderr.split("\n"): + if "bitrate=" in line: + # Extract bitrate value from line like "size= 352162KiB time=01:45:03.05 bitrate= 457.7kbits/s" + parts = line.split("bitrate=") + if len(parts) > 1: + bitrate_str = parts[1].strip().split("kbits/s")[0].strip() + try: + bitrate_kbps = int(float(bitrate_str)) + logger.debug(f"Stream {stream_index}: Extracted bitrate from ffmpeg output: {bitrate_kbps} kbps") + break + except ValueError: + continue + + # If we couldn't parse bitrate from output, fall back to calculation + if bitrate_kbps == 0: + logger.debug(f"Stream {stream_index}: Could not parse bitrate from ffmpeg output, calculating from file size...") + file_size_bytes = os.path.getsize(temp_audio_path) + + # Get duration using ffprobe + duration_cmd = [ + "ffprobe", "-v", "error", + "-show_entries", "format=duration", + "-of", "default=noprint_wrappers=1:nokey=1:noprint_wrappers=1", + temp_audio_path + ] + duration_result = subprocess.run(duration_cmd, capture_output=True, text=True, check=True) + duration_seconds = float(duration_result.stdout.strip()) + + bitrate_kbps = int((file_size_bytes * 8) / duration_seconds / 1000) + logger.debug(f"Stream {stream_index}: Calculated bitrate from file: {bitrate_kbps} kbps") + + return bitrate_kbps + + except Exception as e: + logger.warning(f"Failed to calculate bitrate for stream {stream_index}: {e}. Will fall back to metadata.") + return 0 + + finally: + # Clean up temporary audio file + try: + if os.path.exists(temp_audio_path): + os.remove(temp_audio_path) + logger.debug(f"Deleted temporary audio file: {temp_audio_path}") + except Exception as e: + logger.warning(f"Could not delete temporary file {temp_audio_path}: {e}") + + +def choose_audio_bitrate(channels: int, bitrate_kbps: int, audio_config: dict, is_1080_class: bool) -> tuple: + """ + Choose audio codec and bitrate based on channel count, detected bitrate, and resolution. + + Returns tuple: (codec, target_bitrate_bps) + - codec: "aac", "libopus", or "copy" (to preserve original without re-encoding) + - target_bitrate_bps: target bitrate in bits/sec (0 if using "copy") + + Rules: + Stereo + 1080p: + - Above 192k β†’ high (192k) with AAC + - At/below 192k β†’ preserve (copy) + + Stereo + 720p: + - Above 160k β†’ medium (160k) with AAC + - At/below 160k β†’ preserve (copy) + + Multi-channel: + - Below 384k β†’ low (384k) with AAC + - 384k to below medium β†’ low (384k) with AAC + - Medium and above β†’ medium with AAC + """ # Normalize to 2ch or 6ch output output_channels = 6 if channels >= 6 else 2 if output_channels == 2: + # Stereo logic if is_1080_class: - # Stereo-channel 1080p+: always use high - return audio_config["stereo"]["high"] + # 1080p+ stereo + high_br = audio_config["stereo"]["high"] + if bitrate_kbps > (high_br / 1000): # Above 192k + return ("aac", high_br) + else: + # Preserve original + return ("copy", 0) else: - # Stereo-channel 720p: always use medium - return audio_config["stereo"]["medium"] + # 720p stereo + medium_br = audio_config["stereo"]["medium"] + if bitrate_kbps > (medium_br / 1000): # Above 160k + return ("aac", medium_br) + else: + # Preserve original + return ("copy", 0) else: - # Multi-channel (6ch+): always use medium - return audio_config["multi_channel"]["medium"] + # Multi-channel (6ch+) logic + low_br = audio_config["multi_channel"]["low"] + medium_br = audio_config["multi_channel"]["medium"] + + if bitrate_kbps < (medium_br / 1000): + # Below medium, use low + return ("aac", low_br) + else: + # Medium and above, use medium + return ("aac", medium_br) # ============================= # PATH NORMALIZATION @@ -60,68 +172,109 @@ def normalize_path_for_service(local_path: str, path_mappings: dict) -> str: # ============================= # AUDIO STREAMS DETECTION # ============================= +def get_source_resolution(input_file: Path) -> tuple: + """ + Get source video resolution (width, height). + Returns tuple: (width, height) + """ + try: + cmd = [ + "ffprobe", "-v", "error", + "-select_streams", "v:0", + "-show_entries", "stream=width,height", + "-of", "default=noprint_wrappers=1:nokey=1:noprint_wrappers=1", + str(input_file) + ] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + lines = result.stdout.strip().split("\n") + width = int(lines[0]) if len(lines) > 0 else 1920 + height = int(lines[1]) if len(lines) > 1 else 1080 + logger.info(f"Source resolution detected: {width}x{height}") + return (width, height) + except Exception as e: + logger.warning(f"Failed to detect source resolution: {e}. Defaulting to 1920x1080") + return (1920, 1080) + + def get_audio_streams(input_file: Path): + """ + Detect audio streams and calculate robust bitrates by extracting each stream. + Returns list of (index, channels, calculated_bitrate_kbps, language, metadata_bitrate_kbps) + """ cmd = [ "ffprobe","-v","error","-select_streams","a", - "-show_entries","stream=index,channels,duration,bit_rate,tags=language", + "-show_entries","stream=index,channels,bit_rate,tags=language", "-of","json", str(input_file) ] result = subprocess.run(cmd, capture_output=True, text=True) data = json.loads(result.stdout) streams = [] - for s in data.get("streams", []): + + for stream_num, s in enumerate(data.get("streams", [])): index = s["index"] channels = s.get("channels", 2) src_lang = s.get("tags", {}).get("language", "und") bit_rate_meta = int(s.get("bit_rate", 0)) if s.get("bit_rate") else 0 - # For multi-channel (6ch+), don't look at bitrate - always use medium - if channels >= 6: - avg_bitrate_kbps = 0 # Placeholder, won't be used - else: - # For stereo: try metadata first, then estimate, then default - if bit_rate_meta: - avg_bitrate_kbps = int(bit_rate_meta / 1000) - else: - try: - duration = float(s.get("duration", 0)) - if duration: - fmt_cmd = [ - "ffprobe","-v","error","-show_entries","format=size,duration", - "-of","json", str(input_file) - ] - fmt_result = subprocess.run(fmt_cmd, capture_output=True, text=True) - fmt_data = json.loads(fmt_result.stdout) - size_bytes = int(fmt_data.get("format", {}).get("size", 0)) - total_duration = float(fmt_data.get("format", {}).get("duration", duration)) - n_streams = len(data.get("streams", [])) - avg_bitrate_kbps = int((size_bytes*8/n_streams)/total_duration/1000) - else: - # Default for stereo - avg_bitrate_kbps = 160 - except Exception: - # Default: stereo only - avg_bitrate_kbps = 160 + # Calculate robust bitrate by extracting the audio stream + calculated_bitrate_kbps = calculate_stream_bitrate(input_file, stream_num) - streams.append((index, channels, avg_bitrate_kbps, src_lang, int(bit_rate_meta / 1000))) + # If calculation failed, fall back to metadata + if calculated_bitrate_kbps == 0: + calculated_bitrate_kbps = int(bit_rate_meta / 1000) if bit_rate_meta else 160 + logger.info(f"Stream {index}: Using fallback bitrate {calculated_bitrate_kbps} kbps") + + streams.append((index, channels, calculated_bitrate_kbps, src_lang, int(bit_rate_meta / 1000) if bit_rate_meta else 0)) + return streams + # ============================= # FFmpeg ENCODE # ============================= def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, scale_height: int, filter_flags: str, audio_config: dict, method: str, bitrate_config: dict): + # Get source resolution + src_width, src_height = get_source_resolution(input_file) + streams = get_audio_streams(input_file) - header = f"\n🧩 ENCODE SETTINGS\n β€’ Resolution: {scale_width}x{scale_height}\n β€’ Scale Filter: {filter_flags}\n β€’ CQ: {cq if method=='CQ' else 'N/A'}\n β€’ Video Encoder: av1_nvenc (preset p1, pix_fmt p010le)\n β€’ Audio Streams:" + + # Log comprehensive encode settings + header = f"\n🧩 ENCODE SETTINGS" logger.info(header) print(" ") - + + 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: av1_nvenc (preset p1, pix_fmt p010le)") + 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) 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 - br = choose_audio_bitrate(output_channels, avg_bitrate, audio_config, is_1080_class) - line = f" - Stream #{index}: {channels}chβ†’{output_channels}ch, src={src_lang}, detected_bitrate={avg_bitrate}kbps, bucket_target={br/1000:.0f}kbps" + 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" + + line = f" - Stream #{index}: {channels}chβ†’{output_channels}ch | Lang: {src_lang} | Detected: {avg_bitrate}kbps | Action: {action} | Target: {bitrate_display}" print(line) logger.info(line) @@ -144,8 +297,19 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s # 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 - br = choose_audio_bitrate(output_channels, avg_bitrate, audio_config, is_1080_class) - cmd += [f"-c:a:{i}","aac",f"-b:a:{i}",str(br),f"-ac:{i}",str(output_channels,),f"-channel_layout:a:{i}", "5.1" if output_channels == 6 else "stereo"] + 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"] + else: + # Re-encode with target bitrate + cmd += [ + f"-c:a:{i}", codec, + f"-b:a:{i}", str(br), + f"-ac:{i}", str(output_channels), + f"-channel_layout:a:{i}", "5.1" if output_channels == 6 else "stereo" + ] cmd += ["-c:s","copy",str(output_file)] @@ -157,9 +321,17 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s 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) - logger.info(msg) return orig_size, out_size, reduction_ratio @@ -180,17 +352,14 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str, ignore_tags = config["ignore_tags"] reduction_ratio_threshold = config["reduction_ratio_threshold"] - res_height = 1080 if resolution=="1080" else 720 - res_width = 1920 if resolution=="1080" else 1280 + # Resolution logic: explicit arg takes precedence, else use smart defaults + explicit_resolution = resolution # Will be None if not specified + filter_flags = filters_config.get("default","lanczos") folder_lower = str(folder).lower() - if "\\tv\\" in folder_lower or "/tv/" in folder_lower: + is_tv = "\\tv\\" in folder_lower or "/tv/" in folder_lower + if is_tv: filter_flags = filters_config.get("tv","bicubic") - cq_default = config["encode"]["cq"].get(f"tv_{resolution}",32) - else: - cq_default = config["encode"]["cq"].get(f"movie_{resolution}",32) - if cq is None: - cq = cq_default processing_folder = Path(config["processing_folder"]) processing_folder.mkdir(parents=True, exist_ok=True) @@ -213,11 +382,55 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str, temp_input = processing_folder / file.name shutil.copy2(file, temp_input) logger.info(f"Copied {file.name} β†’ {temp_input.name}") + + # Detect source resolution and determine target resolution + src_width, src_height = get_source_resolution(temp_input) + + # Smart resolution logic + if explicit_resolution: + # User explicitly specified resolution - always use it + target_resolution = explicit_resolution + if target_resolution == "1080": + res_height = 1080 + res_width = 1920 + elif target_resolution == "720": + res_height = 720 + res_width = 1280 + else: # 480 + res_height = 480 + res_width = 854 + logger.info(f"Using explicitly specified resolution: {res_width}x{res_height}") + else: + # No explicit resolution - use smart defaults + if src_height > 1080: + # Scale down anything above 1080p to 1080p + target_resolution = "1080" + res_height = 1080 + res_width = 1920 + print(f"⚠️ Source {src_width}x{src_height} is above 1080p. Scaling down to 1080p.") + logger.info(f"Source {src_width}x{src_height} detected. Scaling to 1080p.") + else: + # Preserve source resolution (480p, 720p, 1080p, etc.) + res_height = src_height + res_width = src_width + if src_height <= 720: + target_resolution = "720" + print(f"ℹ️ Source {src_width}x{src_height} is 720p or lower. Preserving resolution.") + logger.info(f"Source {src_width}x{src_height} (<=720p). Preserving source resolution.") + else: + target_resolution = "1080" + print(f"ℹ️ Source {src_width}x{src_height} is at or below 1080p. Preserving resolution.") + logger.info(f"Source {src_width}x{src_height} (<=1080p). Preserving source resolution.") + + # Set CQ based on content type and target resolution + content_cq = config["encode"]["cq"].get(f"tv_{target_resolution}" if is_tv else f"movie_{target_resolution}", 32) + file_cq = cq if cq is not None else content_cq + temp_output = processing_folder / f"{file.stem}{suffix}{file.suffix}" method = "Bitrate" if use_bitrate else "CQ" try: - orig_size, out_size, reduction_ratio = run_ffmpeg(temp_input, temp_output, cq, res_width, res_height, filter_flags, audio_config, method, bitrate_config) + orig_size, out_size, reduction_ratio = run_ffmpeg(temp_input, temp_output, file_cq, res_width, res_height, filter_flags, audio_config, method, bitrate_config) except subprocess.CalledProcessError as e: print(f"❌ FFmpeg failed: {e}") logger.error(f"FFmpeg failed: {e}") @@ -274,7 +487,11 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str, writer = csv.writer(f) writer.writerow([f_type, show, dest_file.name, orig_size_mb, proc_size_mb, percentage, method]) - logger.info(f"Tracked conversion: {dest_file.name}, {orig_size_mb}MB β†’ {proc_size_mb}MB ({percentage}%), method={method}") + # Enhanced logging with all conversion details + logger.info(f"\nβœ… CONVERSION COMPLETE: {dest_file.name}") + logger.info(f" Type: {f_type.upper()} | Show: {show}") + logger.info(f" Size: {orig_size_mb}MB β†’ {proc_size_mb}MB ({percentage}% of original, {100-percentage:.1f}% reduction)") + logger.info(f" Method: {method} | Status: SUCCESS") print(f"πŸ“ Logged conversion: {dest_file.name} ({percentage}%), method={method}") try: @@ -292,8 +509,8 @@ def main(): parser = argparse.ArgumentParser(description="Batch encode videos with logging and tracker") parser.add_argument("folder", help="Path to folder containing videos") parser.add_argument("--cq", type=int, help="Override default CQ") - parser.add_argument("--m", "--mode", dest="transcode_mode", default="cq", choices=["cq","bitrate"], help="Target resolution") - parser.add_argument("--r", "--resolution", dest="resolution", default="1080", choices=["720","1080"], help="Target resolution") + parser.add_argument("--m", "--mode", dest="transcode_mode", default="cq", choices=["cq","bitrate"], help="Encode mode (cq or bitrate)") + parser.add_argument("--r", "--resolution", dest="resolution", default=None, choices=["480","720","1080"], help="Force target resolution (if not specified, preserves source if <=1080p, else scales to 1080p)") args = parser.parse_args() config_path = Path(__file__).parent / "config.xml"