FEATURE: adds strict codec/container/file_extension verification, renames ambiguous options, adds flexible transcoding
This commit is contained in:
+67
-74
@@ -3,13 +3,22 @@
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
from .constants import FORMAT_INFO, CODEC_TO_CONTAINER_MAP, CONTAINER_INFO, CONTAINER_VIDEO_CODEC_SUPPORT, CODEC_TYPE_MAP
|
||||
from .constants import (
|
||||
FORMAT_INFO,
|
||||
CONTAINER_INFO,
|
||||
CODEC_TO_CONTAINER_MAP,
|
||||
COMPATIBILITY_MATRIX,
|
||||
is_audio_codec_supported,
|
||||
is_video_codec_supported,
|
||||
)
|
||||
from .defaults import DEFAULT_FORMAT
|
||||
|
||||
|
||||
def determine_default_format(container: Optional[str], codec: Optional[str]) -> Optional[str]:
|
||||
"""
|
||||
Given the container format and audio codec, determine the recommended output format.
|
||||
|
||||
This uses the CODEC_TO_CONTAINER_MAP to map codec → container.
|
||||
Uses the CODEC_TO_CONTAINER_MAP to map codec → container.
|
||||
If the codec is not found, it falls back to the container.
|
||||
|
||||
Args:
|
||||
@@ -22,69 +31,17 @@ def determine_default_format(container: Optional[str], codec: Optional[str]) ->
|
||||
if not container:
|
||||
return None
|
||||
|
||||
# Codec-based decision (highest priority)
|
||||
if codec and codec in CODEC_TO_CONTAINER_MAP:
|
||||
return CODEC_TO_CONTAINER_MAP[codec]
|
||||
fmt = CODEC_TO_CONTAINER_MAP[codec]
|
||||
if fmt in FORMAT_INFO:
|
||||
return fmt
|
||||
|
||||
# Container-based fallback (lowest priority)
|
||||
# Ensure the container is in FORMAT_INFO
|
||||
if container in FORMAT_INFO:
|
||||
return container
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def validate_format_compatibility(format_name: str, stream_info: Dict,
|
||||
drop_video: bool, drop_subs: bool, input_file: Optional[str] = None) -> None:
|
||||
"""
|
||||
Ensure the chosen container can accommodate the streams we intend to keep.
|
||||
"""
|
||||
info = FORMAT_INFO.get(format_name)
|
||||
if not info:
|
||||
print(f"Warning: Unknown format '{format_name}'. Proceeding, but may fail.")
|
||||
return
|
||||
|
||||
if info['audio_only']:
|
||||
if stream_info.get('has_video') and not drop_video:
|
||||
raise ValueError(
|
||||
f"Format '{format_name}' does not support video streams. "
|
||||
"Please use --drop-video or choose a container that supports video."
|
||||
)
|
||||
if stream_info.get('has_subtitle') and not drop_subs:
|
||||
raise ValueError(
|
||||
f"Format '{format_name}' does not support subtitle streams. "
|
||||
"Please use --drop-subs or choose a container that supports subtitles."
|
||||
)
|
||||
else:
|
||||
# If video is present and not dropped, check if the container supports the video codec
|
||||
if stream_info.get('has_video') and not drop_video and input_file:
|
||||
from .ffmpeg import get_video_codec
|
||||
video_codec = get_video_codec(input_file)
|
||||
if video_codec and not is_video_codec_supported(format_name, video_codec):
|
||||
raise ValueError(
|
||||
f"Container '{format_name}' does not support video codec '{video_codec}'. "
|
||||
"Please use --drop-video or choose a container that supports this video codec (e.g., MKV)."
|
||||
)
|
||||
|
||||
def is_video_codec_supported(container_name: str, video_codec: str) -> bool:
|
||||
"""
|
||||
Check if a given container supports a specific video codec.
|
||||
|
||||
Args:
|
||||
container_name: Name of the container (e.g., 'mkv', 'ogg')
|
||||
video_codec: Video codec name (e.g., 'theora', 'h264', 'png')
|
||||
|
||||
Returns:
|
||||
True if supported, False otherwise.
|
||||
"""
|
||||
support = CONTAINER_VIDEO_CODEC_SUPPORT.get(container_name, [])
|
||||
if not support:
|
||||
return False
|
||||
if '*' in support:
|
||||
return True
|
||||
return video_codec in support
|
||||
|
||||
|
||||
def determine_output_format(stream_info: Dict, user_format: Optional[str],
|
||||
transcode_audio: Optional[str], input_file: Optional[str] = None) -> str:
|
||||
"""
|
||||
@@ -93,42 +50,78 @@ def determine_output_format(stream_info: Dict, user_format: Optional[str],
|
||||
If user_format is provided, use it.
|
||||
Else, try to detect the input file's container and codec, and use the recommended format.
|
||||
If detection fails or format is not supported, fallback to:
|
||||
- MKV if video/subtitles exist and the video codec is not supported by the recommended container
|
||||
- MKV if video/subtitles exist
|
||||
- MP3 if the audio codec is MP3
|
||||
- MP4 (M4A) otherwise
|
||||
"""
|
||||
if user_format:
|
||||
return user_format
|
||||
|
||||
# Try to detect container and codec from input file
|
||||
if input_file:
|
||||
try:
|
||||
from .ffmpeg import get_container_format, get_audio_codec, get_video_codec
|
||||
from .ffmpeg import get_container_format, get_audio_codec
|
||||
container = get_container_format(input_file)
|
||||
audio_codec = get_audio_codec(input_file)
|
||||
video_codec = get_video_codec(input_file) # NEW: get video codec
|
||||
|
||||
# Determine recommended format based on audio codec
|
||||
fmt = determine_default_format(container, audio_codec)
|
||||
|
||||
# If video is present and not dropped, check if the recommended container supports the video codec
|
||||
if stream_info.get('has_video') and not transcode_audio: # transcode_audio doesn't affect video
|
||||
# If the recommended container doesn't support the video codec, fallback to MKV
|
||||
if fmt and not is_video_codec_supported(fmt, video_codec):
|
||||
fmt = 'matroska' # MKV supports virtually all video codecs
|
||||
# Optionally log a warning
|
||||
# print(f"Warning: Container '{fmt}' does not support video codec '{video_codec}'. Falling back to MKV.")
|
||||
|
||||
if fmt in FORMAT_INFO:
|
||||
return fmt
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback: legacy behavior
|
||||
# Fallback
|
||||
if stream_info.get('has_video') or stream_info.get('has_subtitle'):
|
||||
return 'matroska'
|
||||
audio_codec = stream_info.get('audio_codec', '')
|
||||
if audio_codec == 'mp3':
|
||||
return 'mp3'
|
||||
else:
|
||||
return 'mp4' # .m4a
|
||||
return 'mp4'
|
||||
|
||||
|
||||
def validate_format_compatibility(
|
||||
container: str,
|
||||
stream_info: Dict,
|
||||
drop_video: bool,
|
||||
drop_subs: bool,
|
||||
input_file: Optional[str] = None,
|
||||
audio_codec: Optional[str] = None,
|
||||
video_codec: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Ensure the chosen container and codec combination is valid.
|
||||
|
||||
Raises:
|
||||
ValueError: If the combination is incompatible.
|
||||
"""
|
||||
info = FORMAT_INFO.get(container)
|
||||
if not info:
|
||||
print(f"Warning: Unknown container '{container}'. Proceeding, but may fail.")
|
||||
return
|
||||
|
||||
# Check if container is audio-only and video is present (unless dropped)
|
||||
if info['audio_only'] and stream_info.get('has_video') and not drop_video:
|
||||
raise ValueError(
|
||||
f"Container '{container}' does not support video streams. "
|
||||
"Please use --drop-video or choose a container that supports video (e.g., MKV, MP4)."
|
||||
)
|
||||
|
||||
# Check if audio codec is supported
|
||||
if audio_codec and audio_codec != 'copy':
|
||||
if not is_audio_codec_supported(container, audio_codec):
|
||||
raise ValueError(
|
||||
f"Container '{container}' does not support audio codec '{audio_codec}'. "
|
||||
f"Please choose a different container or audio codec."
|
||||
)
|
||||
|
||||
# Check if video codec is supported (if video is present and not dropped)
|
||||
if stream_info.get('has_video') and not drop_video:
|
||||
# Determine the video codec from input file if not provided
|
||||
if video_codec is None and input_file:
|
||||
from .ffmpeg import get_video_codec
|
||||
video_codec = get_video_codec(input_file)
|
||||
if video_codec and video_codec != 'copy':
|
||||
if not is_video_codec_supported(container, video_codec):
|
||||
raise ValueError(
|
||||
f"Container '{container}' does not support video codec '{video_codec}'. "
|
||||
f"Please choose a different container, drop video, or transcode video to a supported codec."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user