This commit is contained in:
TylerCG 2026-08-22 03:16:53 -04:00
parent f32177e69a
commit 1fe0feef33
7 changed files with 3382 additions and 60 deletions

View File

@ -3311,3 +3311,4 @@ anime,Chaos Dragon (2015),Chaos Dragon - Sekiryuu Seneki 8 - [EHX].mkv,7538.8,31
anime,Chaos Dragon (2015),Chaos Dragon - Sekiryuu Seneki 9 - [EHX].mkv,7570.84,364.14,4.8,1920x1080,1920x1080,3,32,CQ anime,Chaos Dragon (2015),Chaos Dragon - Sekiryuu Seneki 9 - [EHX].mkv,7570.84,364.14,4.8,1920x1080,1920x1080,3,32,CQ
anime,Chaos Dragon (2015),Chaos Dragon - Sekiryuu Seneki 2 - [EHX].mkv,6744.61,396.4,5.9,1920x1080,1920x1080,3,32,CQ anime,Chaos Dragon (2015),Chaos Dragon - Sekiryuu Seneki 2 - [EHX].mkv,6744.61,396.4,5.9,1920x1080,1920x1080,3,32,CQ
movie,N/A,The Mandalorian and Grogu (2026) h264 TrueHD Atmos 7.1 Remux-1080p BTM - [EHX].mkv,42712.99,1982.05,4.6,1920x1080,1920x1080,8,32,CQ movie,N/A,The Mandalorian and Grogu (2026) h264 TrueHD Atmos 7.1 Remux-1080p BTM - [EHX].mkv,42712.99,1982.05,4.6,1920x1080,1920x1080,8,32,CQ
movie,N/A,Dungeons and Dragons - Honor Among Thieves_t00 - [EHX].mkv,31591.75,2174.97,6.9,1920x1080,1920x804,2,28,CQ

Can't render this file because it has a wrong number of fields in line 14.

View File

@ -369,11 +369,12 @@ def choose_audio_bitrate(channels: int, bitrate_kbps: int, audio_config: dict, i
# Check if source is within -10kbps of a standard bitrate # Check if source is within -10kbps of a standard bitrate
matched_br = find_nearest_bitrate(bitrate_kbps, multi_bitrates) matched_br = find_nearest_bitrate(bitrate_kbps, multi_bitrates)
if matched_br > 0: if matched_br > 0:
# Within threshold of a standard bitrate, use that one with EAC3 # Within threshold of a standard bitrate, use that one with EAC3 (or Opus for 8ch)
logger.info(f"Multi-channel {output_channels}ch audio: matched bitrate {matched_br/1000:.0f}k → EAC3") codec = "opus" if output_channels == 8 else "eac3"
return ("eac3", matched_br, output_channels) logger.info(f"Multi-channel {output_channels}ch audio: matched bitrate {matched_br/1000:.0f}k → {codec.upper()}")
return (codec, matched_br, output_channels)
# Not within threshold - force EAC3 encoding with appropriate bitrate # Not within threshold - force EAC3/Opus encoding with appropriate bitrate
# EXCEPT: for very low bitrate audio, copy original to avoid quality loss # EXCEPT: for very low bitrate audio, copy original to avoid quality loss
if bitrate_kbps < (low_br / 1000): if bitrate_kbps < (low_br / 1000):
# Source bitrate is below minimum for multi-channel encoding # Source bitrate is below minimum for multi-channel encoding
@ -381,17 +382,22 @@ def choose_audio_bitrate(channels: int, bitrate_kbps: int, audio_config: dict, i
logger.info(f"Multi-channel audio {bitrate_kbps}kbps < {low_br/1000:.0f}k minimum - copying original {output_channels}ch to avoid quality loss") logger.info(f"Multi-channel audio {bitrate_kbps}kbps < {low_br/1000:.0f}k minimum - copying original {output_channels}ch to avoid quality loss")
return ("copy", 0, output_channels) return ("copy", 0, output_channels)
elif bitrate_kbps < (medium_br / 1000): elif bitrate_kbps < (medium_br / 1000):
# Below medium, use low with EAC3 # Below medium, use low bitrate
logger.info(f"Multi-channel {output_channels}ch audio {bitrate_kbps}kbps: forcing EAC3 at low {low_br/1000:.0f}k") codec = "opus" if output_channels == 8 else "eac3"
return ("eac3", low_br, output_channels) logger.info(f"Multi-channel {output_channels}ch audio {bitrate_kbps}kbps: forcing {codec.upper()} at low {low_br/1000:.0f}k")
return (codec, low_br, output_channels)
elif bitrate_kbps >= (high_br / 1000) and resolution == "2160": elif bitrate_kbps >= (high_br / 1000) and resolution == "2160":
# High bitrate on 4K - use high EAC3 # High bitrate on 4K - use high bitrate codec (but cap Opus at medium)
logger.info(f"Multi-channel {output_channels}ch audio {bitrate_kbps}kbps (4K): forcing EAC3 at high {high_br/1000:.0f}k") codec = "opus" if output_channels == 8 else "eac3"
return ("eac3", high_br, output_channels) target_br = medium_br if output_channels == 8 else high_br
br_label = "medium" if output_channels == 8 else "high"
logger.info(f"Multi-channel {output_channels}ch audio {bitrate_kbps}kbps (4K): forcing {codec.upper()} at {br_label} {target_br/1000:.0f}k")
return (codec, target_br, output_channels)
else: else:
# Default to medium for 1080p and below # Default to medium for 1080p and below
logger.info(f"Multi-channel {output_channels}ch audio {bitrate_kbps}kbps: forcing EAC3 at medium {medium_br/1000:.0f}k") codec = "opus" if output_channels == 8 else "eac3"
return ("eac3", medium_br, output_channels) logger.info(f"Multi-channel {output_channels}ch audio {bitrate_kbps}kbps: forcing {codec.upper()} at medium {medium_br/1000:.0f}k")
return (codec, medium_br, output_channels)
def filter_audio_streams(input_file: Path, streams: list) -> list: def filter_audio_streams(input_file: Path, streams: list) -> list:
""" """

View File

@ -44,7 +44,7 @@ def check_encoder_available(encoder_codec: str) -> bool:
def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, scale_height: int, 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, 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, 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, src_bit_depth: int = None, unforce_subs: bool = False, no_encode: bool = False, color_bit: int = None, crop_height: int = None, audio_titles: dict = None, audio_channels: dict = None, is_hdr: bool = False, skip_audio_check: bool = False): audio_filter_config: dict = None, test_mode: bool = False, strip_all_titles: bool = False, src_bit_depth: int = None, unforce_subs: bool = False, no_encode: bool = False, color_bit: int = None, crop_height: int = None, audio_titles: dict = None, audio_channels: dict = None, is_hdr: bool = False, skip_audio_check: bool = False, use_remux_for_subs: bool = False):
""" """
Execute FFmpeg encoding/re-muxing with structured console output. Execute FFmpeg encoding/re-muxing with structured console output.
@ -68,6 +68,7 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
unforce_subs: If True, remove forced flag from subtitle tracks unforce_subs: If True, remove forced flag from subtitle tracks
no_encode: If True, copy video/audio (re-mux only, skip encoding) no_encode: If True, copy video/audio (re-mux only, skip encoding)
color_bit: If specified (8 or 10), forces HEVC color bit depth. 8-bit uses yuv420p, 10-bit uses p010le. color_bit: If specified (8 or 10), forces HEVC color bit depth. 8-bit uses yuv420p, 10-bit uses p010le.
use_remux_for_subs: If True, add subtitles in a separate remux pass (for PGS handling)
crop_height: If specified, crop video to this height (centered). E.g., 816 for 1920x816 from 1920x1080 source. crop_height: If specified, crop video to this height (centered). E.g., 816 for 1920x816 from 1920x1080 source.
audio_titles: Dict mapping stream index to custom title. E.g., {1: "Commentary"} sets stream 1 title to "Commentary". audio_titles: Dict mapping stream index to custom title. E.g., {1: "Commentary"} sets stream 1 title to "Commentary".
audio_channels: Dict mapping stream index to channel count. E.g., {0: 2, 1: 6} forces track 0 to stereo, track 1 to 5.1. Only 2 or 6 allowed. audio_channels: Dict mapping stream index to channel count. E.g., {0: 2, 1: 6} forces track 0 to stereo, track 1 to 5.1. Only 2 or 6 allowed.
@ -133,8 +134,9 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
encoder_pix_fmt = "p010le" encoder_pix_fmt = "p010le"
encoder_bit_depth = "10-bit" encoder_bit_depth = "10-bit"
logger.info(f"Using --color-bit {color_bit}: HEVC NVENC 10-bit (p010le)") logger.info(f"Using --color-bit {color_bit}: HEVC NVENC 10-bit (p010le)")
# Auto-select encoder based on detected source bit depth if provided (only if --color-bit not specified) # Auto-select encoder based on detected source bit depth only if user didn't explicitly specify one
elif src_bit_depth is not None and color_bit is None: # (this only runs if encoder is None, otherwise user selection takes precedence)
elif encoder is None and src_bit_depth is not None and color_bit is None:
if src_bit_depth >= 10: if src_bit_depth >= 10:
# Source is 10-bit or higher - use HEVC NVENC # Source is 10-bit or higher - use HEVC NVENC
encoder_name = "HEVC NVENC" encoder_name = "HEVC NVENC"
@ -327,18 +329,10 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
cmd.extend(["-map","0:v:0"]) # Map only first actual video stream (skips attached pictures) cmd.extend(["-map","0:v:0"]) # Map only first actual video stream (skips attached pictures)
# Map only selected audio streams
if streams:
for index, _, _, _, _, _, _ in streams:
cmd.extend(["-map", f"0:{index}"])
else:
# Fallback: if no audio streams detected, include all audio from source
logger.warning("No audio streams detected, including all audio from source")
cmd.extend(["-map", "0:a"])
# Build audio filters for streams that need re-encoding with channel layout conversion # Build audio filters for streams that need re-encoding with channel layout conversion
# This is needed when downmixing (e.g., 7.1 TrueHD to 5.1 EAC3) to ensure proper channel remixing # This is needed when downmixing (e.g., 7.1 TrueHD to 5.1 EAC3) to ensure proper channel remixing
audio_filters_list = [] audio_filters_list = []
for i, (index, channels, avg_bitrate, src_lang, meta_bitrate, title, codec_name) in enumerate(streams): for i, (index, channels, avg_bitrate, src_lang, meta_bitrate, title, codec_name) in enumerate(streams):
# Determine output channels # Determine output channels
final_title = audio_titles.get(index, title) if audio_titles else title final_title = audio_titles.get(index, title) if audio_titles else title
@ -365,30 +359,34 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
codec, br, final_channels = choose_audio_bitrate(output_channels, avg_bitrate, audio_config, is_1080_class, is_commentary, resolution_str, codec_name) codec, br, final_channels = choose_audio_bitrate(output_channels, avg_bitrate, audio_config, is_1080_class, is_commentary, resolution_str, codec_name)
if codec != "copy" and channels != final_channels: if codec != "copy" and channels != final_channels:
# Need to downmix/remix channels # Need to downmix/remix channels
# Use actual stream index, not enumeration index
if final_channels >= 6: if final_channels >= 6:
audio_filters_list.append(f"[0:a:{i}]aformat=channel_layouts=5.1[a{i}]") audio_filters_list.append(f"[0:a:{i}]aformat=channel_layouts=5.1[a{i}]")
else: else:
audio_filters_list.append(f"[0:a:{i}]aformat=channel_layouts=stereo[a{i}]") audio_filters_list.append(f"[0:a:{i}]aformat=channel_layouts=stereo[a{i}]")
# Add audio filter chain if any filters are needed # Map only selected audio streams
if audio_filters_list: if streams:
# Build complex filter: mark inputs and concat outputs for index, _, _, _, _, _, _ in streams:
# For simple case with one audio stream, we can use -af instead of -filter_complex cmd.extend(["-map", f"0:{index}"])
if len(audio_filters_list) == 1 and len(streams) == 1: else:
# Single audio stream: use simple -af filter # Fallback: if no audio streams detected, include all audio from source
filter_spec = audio_filters_list[0] logger.warning("No audio streams detected, including all audio from source")
# Extract just the filter part (between brackets) cmd.extend(["-map", "0:a"])
if '[0:a:0]' in filter_spec and '[a0]' in filter_spec:
filter_content = filter_spec.split(']')[0].replace('[0:a:0]', '')
cmd.extend(["-af", filter_content])
logger.info(f"Applied audio filter for stream 0: {filter_content}")
# Add subtitle mapping if present # Add subtitle mapping if present
if subtitle_files: if subtitle_files:
for i, _ in enumerate(subtitle_files): if not use_remux_for_subs:
cmd.extend(["-map", f"{i+1}:s"]) # Include subtitles in main encode (no PGS in source)
else: for i, _ in enumerate(subtitle_files):
cmd.extend(["-map", "0:s?"]) cmd.extend(["-map", f"{i+1}:s"])
else:
# Skip subtitles in main encode (has PGS - will add via remux after)
# This avoids conflicts with bitmap subtitle codecs
pass
# Video codec: copy if no_encode, otherwise use specified encoder # Video codec: copy if no_encode, otherwise use specified encoder
if no_encode: if no_encode:
@ -489,8 +487,15 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
cmd += [f"-metadata:s:a:{i}", "title="] cmd += [f"-metadata:s:a:{i}", "title="]
else: else:
# Re-encode with target bitrate # Re-encode with target bitrate
# EAC3 for multichannel, AAC for stereo # Opus for 8-channel, EAC3 for multichannel, AAC for stereo
if codec == "eac3": if codec == "opus":
# Opus (supports 7.1/8-channel surround)
cmd += [
f"-c:a:{i}", "libopus",
f"-b:a:{i}", str(br),
f"-ac:{i}", str(final_channels)
]
elif codec == "eac3":
# Enhanced AC-3 (5.1 surround) # Enhanced AC-3 (5.1 surround)
cmd += [ cmd += [
f"-c:a:{i}", "eac3", f"-c:a:{i}", "eac3",
@ -687,4 +692,56 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
test_preview_file.unlink() test_preview_file.unlink()
logger.info(f"Cleaned up test preview copy: {test_preview_file}") logger.info(f"Cleaned up test preview copy: {test_preview_file}")
# If using remux mode (PGS present), add subtitles (both embedded and external) via remux pass
if use_remux_for_subs:
logger.info(f"Adding subtitles via remux pass (PGS handling)...")
temp_remux_file = output_file.parent / f"{output_file.stem}_remux.mkv"
# Build remux command: input is encoded file, add subtitles from original + external
remux_cmd = [
"ffmpeg", "-y",
"-i", str(output_file), # Encoded video+audio
"-i", str(input_file), # Original file for embedded subtitles
]
# Add external subtitle files if present
if subtitle_files:
for sub_file in subtitle_files:
remux_cmd.extend(["-i", str(sub_file)])
# Build mapping: all from encoded, subtitles from original, then external subs
remux_cmd.extend([
"-map", "0", # All streams from encoded file
"-map", "1:s?", # All subtitle streams from original
])
# Add external subtitle mappings
if subtitle_files:
for i in range(len(subtitle_files)):
remux_cmd.extend(["-map", f"{i+2}:s"])
remux_cmd.extend([
"-c", "copy", # Copy all streams (no re-encoding)
str(temp_remux_file)
])
try:
logger.info(f"Remux command: {' '.join(remux_cmd)}")
remux_result = subprocess.run(remux_cmd, capture_output=True, text=True, check=False)
if remux_result.returncode == 0:
# Remux succeeded - replace output with remuxed version
output_file.unlink()
temp_remux_file.rename(output_file)
logger.info(f"Successfully added subtitles via remux")
else:
# Remux failed - keep original encoded file (without subtitles)
logger.warning(f"Remux failed, keeping encoded file without subtitles: {remux_result.stderr[:200]}")
if temp_remux_file.exists():
temp_remux_file.unlink()
except Exception as e:
logger.warning(f"Remux error: {e}")
if temp_remux_file.exists():
temp_remux_file.unlink()
return orig_size, out_size, reduction_ratio return orig_size, out_size, reduction_ratio

View File

@ -12,7 +12,7 @@ from core.audio_handler import get_audio_streams
from core.encode_engine import run_ffmpeg from core.encode_engine import run_ffmpeg
from core.file_transfer import copy_with_progress from core.file_transfer import copy_with_progress
from core.logger_helper import setup_logger, setup_failure_logger from core.logger_helper import setup_logger, setup_failure_logger
from core.video_handler import get_source_resolution, determine_target_resolution, get_source_bit_depth, has_forced_subtitles from core.video_handler import get_source_resolution, determine_target_resolution, get_source_bit_depth, has_forced_subtitles, has_pgs_subtitles
logger = setup_logger(Path(__file__).parent.parent / "logs") logger = setup_logger(Path(__file__).parent.parent / "logs")
failure_logger = setup_failure_logger(Path(__file__).parent.parent / "logs") failure_logger = setup_failure_logger(Path(__file__).parent.parent / "logs")
@ -304,14 +304,20 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
# Width stays the same - cropping removes letterboxing vertically only # Width stays the same - cropping removes letterboxing vertically only
logger.info(f"Adjusted target resolution for crop: {res_width}x{res_height} (width preserved)") logger.info(f"Adjusted target resolution for crop: {res_width}x{res_height} (width preserved)")
# Auto-select encoder based on detected source bit depth # Use user-specified encoder if provided, otherwise auto-select based on bit depth
if src_bit_depth >= 10: if encoder:
# Source is 10-bit or higher - use HEVC NVENC # User explicitly specified encoder
selected_encoder = "hevc" selected_encoder = encoder
logger.info(f"Using user-specified encoder: {selected_encoder.upper()}")
else: else:
# Source is 8-bit - use AV1 NVENC # Auto-select encoder based on detected source bit depth
selected_encoder = "av1" if src_bit_depth >= 10:
logger.info(f"Auto-selected {selected_encoder.upper()} encoder for detected {src_bit_depth}-bit source") # Source is 10-bit or higher - use HEVC NVENC
selected_encoder = "hevc"
else:
# Source is 8-bit - use AV1 NVENC
selected_encoder = "av1"
logger.info(f"Auto-selected {selected_encoder.upper()} encoder for detected {src_bit_depth}-bit source")
# Log resolution decision # Log resolution decision
if explicit_resolution: if explicit_resolution:
@ -385,10 +391,16 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
logger.info(f"First audio stream is 'und', replacing with default language: {effective_audio_language}") logger.info(f"First audio stream is 'und', replacing with default language: {effective_audio_language}")
print(f"🔄 Audio stream detected as 'und', will tag as: {effective_audio_language}") print(f"🔄 Audio stream detected as 'und', will tag as: {effective_audio_language}")
# Check if source has PGS subtitles (use remux workflow if yes)
has_pgs = has_pgs_subtitles(file)
use_remux = has_pgs
if has_pgs:
logger.info(f"Source has PGS subtitles - will use remux workflow for subtitle handling")
orig_size, out_size, reduction_ratio = run_ffmpeg( orig_size, out_size, reduction_ratio = run_ffmpeg(
temp_input, temp_output, file_cq, res_width, res_height, src_width, src_height, temp_input, temp_output, file_cq, res_width, res_height, src_width, src_height,
filter_flags, audio_config, method, bitrate_config, actual_encoder, [subtitle_file] if subtitle_file else None, effective_audio_language, filter_flags, audio_config, method, bitrate_config, actual_encoder, [subtitle_file] if subtitle_file else None, effective_audio_language,
audio_filter_config, test_mode, strip_all_titles, src_bit_depth, unforce_subs, no_encode, color_bit, crop_height, audio_titles, audio_channels, False, skip_audio_check audio_filter_config, test_mode, strip_all_titles, src_bit_depth, unforce_subs, no_encode, color_bit, crop_height, audio_titles, audio_channels, False, skip_audio_check, use_remux
) )
# Check if encode met size target # Check if encode met size target
@ -430,7 +442,8 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
'subtitle_file': subtitle_file, 'subtitle_file': subtitle_file,
'src_bit_depth': src_bit_depth, 'src_bit_depth': src_bit_depth,
'encoder': actual_encoder, 'encoder': actual_encoder,
'effective_audio_language': effective_audio_language 'effective_audio_language': effective_audio_language,
'use_remux': use_remux
}) })
consecutive_failures += 1 consecutive_failures += 1
if consecutive_failures >= max_consecutive: if consecutive_failures >= max_consecutive:
@ -483,7 +496,8 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
'file_cq': file_cq, 'file_cq': file_cq,
'is_tv': is_tv, 'is_tv': is_tv,
'subtitle_file': subtitle_file, 'subtitle_file': subtitle_file,
'effective_audio_language': effective_audio_language 'effective_audio_language': effective_audio_language,
'use_remux': use_remux
}) })
consecutive_failures += 1 consecutive_failures += 1
if consecutive_failures >= max_consecutive: if consecutive_failures >= max_consecutive:
@ -518,7 +532,7 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
_save_successful_encoding( _save_successful_encoding(
file, temp_input, temp_output, orig_size, out_size, file, temp_input, temp_output, orig_size, out_size,
reduction_ratio, method, src_width, src_height, res_width, res_height, reduction_ratio, method, src_width, src_height, res_width, res_height,
file_cq, tracker_file, folder, is_tv, suffix, config, test_mode, subtitle_file, travel_output_folder, replace_file, wait_seconds, combined_suffix file_cq, tracker_file, folder, is_tv, suffix, config, test_mode, subtitle_file, travel_output_folder, replace_file, wait_seconds, combined_suffix, use_remux
) )
# In test mode, stop after first successful file # In test mode, stop after first successful file
@ -572,7 +586,7 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
file_data['src_width'], file_data['src_height'], file_data['src_width'], file_data['src_height'],
filter_flags, audio_config, "Bitrate", bitrate_config, file_data.get('encoder', encoder), filter_flags, audio_config, "Bitrate", bitrate_config, file_data.get('encoder', encoder),
[file_data.get('subtitle_file')] if file_data.get('subtitle_file') else None, file_data.get('effective_audio_language'), None, test_mode, strip_all_titles, [file_data.get('subtitle_file')] if file_data.get('subtitle_file') else None, file_data.get('effective_audio_language'), None, test_mode, strip_all_titles,
file_data.get('src_bit_depth'), unforce_subs, no_encode, color_bit, crop_height, audio_titles, audio_channels, False, skip_audio_check file_data.get('src_bit_depth'), unforce_subs, no_encode, color_bit, crop_height, audio_titles, audio_channels, False, skip_audio_check, file_data.get('use_remux', False)
) )
# Check if bitrate also failed # Check if bitrate also failed
@ -596,7 +610,7 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
file_data['res_width'], file_data['res_height'], file_data['res_width'], file_data['res_height'],
file_data['file_cq'], tracker_file, file_data['file_cq'], tracker_file,
folder, file_data['is_tv'], suffix, config, False, folder, file_data['is_tv'], suffix, config, False,
file_data.get('subtitle_file'), travel_output_folder, replace_file, wait_seconds, combined_suffix file_data.get('subtitle_file'), travel_output_folder, replace_file, wait_seconds, combined_suffix, file_data.get('use_remux', False)
) )
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@ -645,7 +659,7 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size, def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size,
reduction_ratio, method, src_width, src_height, res_width, res_height, reduction_ratio, method, src_width, src_height, res_width, res_height,
file_cq, tracker_file, folder, is_tv, suffix, config=None, test_mode=False, subtitle_file=None, travel_output_folder=None, replace_file: bool = False, wait_seconds: int = 0, combined_suffix: str = None): file_cq, tracker_file, folder, is_tv, suffix, config=None, test_mode=False, subtitle_file=None, travel_output_folder=None, replace_file: bool = False, wait_seconds: int = 0, combined_suffix: str = None, use_remux: bool = False):
"""Helper function to save successfully encoded files with [EHX] tag and clean up subtitle files.""" """Helper function to save successfully encoded files with [EHX] tag and clean up subtitle files."""
# In test mode, show ratio and skip file move/cleanup # In test mode, show ratio and skip file move/cleanup
@ -756,12 +770,12 @@ def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size
else: else:
logger.info(f"Featurettes file preserved at origin: {file.name}") logger.info(f"Featurettes file preserved at origin: {file.name}")
# Clean up subtitle file if it was embedded # Clean up subtitle file after encoding (it's now embedded in the video)
if subtitle_file and subtitle_file.exists(): if subtitle_file and subtitle_file.exists():
try: try:
subtitle_file.unlink() subtitle_file.unlink()
print(f"🗑️ Removed embedded subtitle: {subtitle_file.name}") print(f"🗑️ Removed subtitle file: {subtitle_file.name}")
logger.info(f"Removed embedded subtitle: {subtitle_file.name}") logger.info(f"Removed subtitle file: {subtitle_file.name}")
except Exception as e: except Exception as e:
logger.warning(f"Could not delete subtitle file {subtitle_file.name}: {e}") logger.warning(f"Could not delete subtitle file {subtitle_file.name}: {e}")
except Exception as e: except Exception as e:

View File

@ -321,6 +321,23 @@ def get_subtitle_stream_codecs(input_file: Path) -> dict:
logger.warning(f"Failed to get subtitle stream codecs for {input_file.name}: {e}") logger.warning(f"Failed to get subtitle stream codecs for {input_file.name}: {e}")
return {} return {}
def has_pgs_subtitles(input_file: Path) -> bool:
"""
Check if the input file has PGS (presentation graphics) subtitles.
PGS is the codec used for bitmap subtitles in Blu-ray discs.
Returns True if at least one subtitle stream uses 'hdmv_pgs_subtitle' codec.
"""
try:
subtitle_codecs = get_subtitle_stream_codecs(input_file)
for codec_name in subtitle_codecs.values():
if codec_name.lower() == "hdmv_pgs_subtitle":
logger.debug(f"Found PGS subtitle stream in {input_file.name}")
return True
return False
except Exception as e:
logger.warning(f"Failed to check for PGS subtitles in {input_file.name}: {e}")
return False
def has_forced_subtitles(input_file: Path) -> bool: def has_forced_subtitles(input_file: Path) -> bool:
""" """
Check if the input file has any subtitles with the forced flag set. Check if the input file has any subtitles with the forced flag set.

File diff suppressed because it is too large Load Diff

View File

@ -184,3 +184,6 @@
2026-08-22 00:50:44 | Dungeons and Dragons - Honor Among Thieves_t00.mkv | Unexpected error: [WinError 32] The process cannot access the file because it is being used by another process: 'C:\\U 2026-08-22 00:50:44 | Dungeons and Dragons - Honor Among Thieves_t00.mkv | Unexpected error: [WinError 32] The process cannot access the file because it is being used by another process: 'C:\\U
2026-08-22 01:04:47 | Dungeons and Dragons - Honor Among Thieves_t00.mkv | Unexpected error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing 2026-08-22 01:04:47 | Dungeons and Dragons - Honor Among Thieves_t00.mkv | Unexpected error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing
2026-08-22 01:11:39 | Dungeons and Dragons - Honor Among Thieves_t00.mkv | CQ error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing 2026-08-22 01:11:39 | Dungeons and Dragons - Honor Among Thieves_t00.mkv | CQ error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing
2026-08-22 02:21:31 | Dungeons and Dragons - Honor Among Thieves_t00.mkv | CQ error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing\\Dungeons and Dragons - Honor Among Thieves_t00.mkv', '-vf', 'crop=in_w:804:0:138,scale=1920:804:flags=lanczos,setsar=1:1', '-map', '0:v:0', '-map', '0:1', '-af', '[0:a:0', '-c:v', 'av1_nvenc', '-preset', 'p7', '-pix_fmt', 'yuv420p', '-cq', '32', '-c:a:0', 'eac3', '-b:a:0', '448000', '-ac:0', '6', '-metadata:s:a:0', 'title=', '-c:s', 'copy', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing\\Dungeons and Dragons - Honor Among Thieves_t00 - [EHX].mkv']' returned non-zero exit status 4294967274.
2026-08-22 02:23:58 | Dungeons and Dragons - Honor Among Thieves_t00.mkv | CQ error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing\\Dungeons and Dragons - Honor Among Thieves_t00.mkv', '-i', 'C:\\Users\\Tyler\\Videos\\BluRay Rips\\4k DND\\Dungeons and Dragons - Honor Among Thieves_t00.en.srt', '-vf', 'crop=in_w:804:0:138,scale=1920:804:flags=lanczos,setsar=1:1', '-map', '0:v:0', '-map', '0:1', '-af', '[0:a:0', '-map', '1:s', '-c:v', 'av1_nvenc', '-preset', 'p7', '-pix_fmt', 'yuv420p', '-cq', '32', '-c:a:0', 'eac3', '-b:a:0', '448000', '-ac:0', '6', '-metadata:s:a:0', 'title=', '-c:s', 'srt', '-metadata:s:s:0', 'language=eng', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing\\Dungeons and Dragons - Honor Among Thieves_t00 - [EHX].mkv']' returned non-zero exit status 4294967274.
2026-08-22 02:27:13 | Dungeons and Dragons - Honor Among Thieves_t00.mkv | CQ error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing\\Dungeons and Dragons - Honor Among Thieves_t00.mkv', '-i', 'C:\\Users\\Tyler\\Videos\\BluRay Rips\\4k DND\\Dungeons and Dragons - Honor Among Thieves_t00.en.srt', '-vf', 'crop=in_w:804:0:138,scale=1920:804:flags=lanczos,setsar=1:1', '-map', '0:v:0', '-map', '0:1', '-filter_complex', '[0:a:0]aformat=channel_layouts=5.1[a0]', '-map', '1:s', '-c:v', 'av1_nvenc', '-preset', 'p7', '-pix_fmt', 'yuv420p', '-cq', '32', '-c:a:0', 'eac3', '-b:a:0', '448000', '-ac:0', '6', '-metadata:s:a:0', 'title=', '-c:s', 'srt', '-metadata:s:s:0', 'language=eng', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing\\Dungeons and Dragons - Honor Among Thieves_t00 - [EHX].mkv']' returned non-zero exit status 4294967274.