fixed
This commit is contained in:
parent
f32177e69a
commit
1fe0feef33
@ -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.
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
3224
logs/conversion.log
3224
logs/conversion.log
File diff suppressed because it is too large
Load Diff
@ -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.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user