FIX: codec/container/file_extension autodetection when default values are used
This commit is contained in:
+23
-15
@@ -6,7 +6,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
from .constants import FORMAT_INFO
|
from .constants import FORMAT_INFO, CODEC_TO_EXTENSION_MAP
|
||||||
from .ffmpeg import (
|
from .ffmpeg import (
|
||||||
get_audio_duration,
|
get_audio_duration,
|
||||||
get_stream_info,
|
get_stream_info,
|
||||||
@@ -34,10 +34,11 @@ def split_audio(input_file: str, output_directory: str, tracks: List[Dict[str, A
|
|||||||
output_directory: Directory where output files will be saved.
|
output_directory: Directory where output files will be saved.
|
||||||
tracks: List of dicts, each containing parsed fields (ts, tn, an, ...).
|
tracks: List of dicts, each containing parsed fields (ts, tn, an, ...).
|
||||||
args: Parsed command‑line arguments (namespace) with attributes:
|
args: Parsed command‑line arguments (namespace) with attributes:
|
||||||
- container: output container name
|
- container: output container name (or None for auto-detect)
|
||||||
- audio_codec: audio codec (copy or encoder)
|
- audio_codec: audio codec (copy or encoder)
|
||||||
- video_codec: video codec (copy or encoder)
|
- video_codec: video codec (copy or encoder)
|
||||||
- subtitle_codec: subtitle codec (copy or encoder)
|
- subtitle_codec: subtitle codec (copy or encoder)
|
||||||
|
- video_quality: integer or None
|
||||||
- drop_video: bool
|
- drop_video: bool
|
||||||
- drop_subs: bool
|
- drop_subs: bool
|
||||||
- number_tracks: bool
|
- number_tracks: bool
|
||||||
@@ -77,7 +78,6 @@ def split_audio(input_file: str, output_directory: str, tracks: List[Dict[str, A
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# 2. Determine output container
|
# 2. Determine output container
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# Use args.container if provided, otherwise auto-detect
|
|
||||||
user_container = getattr(args, 'container', None)
|
user_container = getattr(args, 'container', None)
|
||||||
output_container = determine_output_format(
|
output_container = determine_output_format(
|
||||||
stream_info,
|
stream_info,
|
||||||
@@ -104,13 +104,30 @@ def split_audio(input_file: str, output_directory: str, tracks: List[Dict[str, A
|
|||||||
raise RuntimeError(f"Compatibility error: {e}")
|
raise RuntimeError(f"Compatibility error: {e}")
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# 4. Parse timestamps
|
# 4. Determine output audio codec and extension
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Determine the audio codec that will be used in the output
|
||||||
|
if args.audio_codec != 'copy':
|
||||||
|
output_audio_codec = args.audio_codec
|
||||||
|
else:
|
||||||
|
output_audio_codec = stream_info.get('audio_codec', '')
|
||||||
|
|
||||||
|
# Choose extension based on audio codec if possible, otherwise fallback to container default
|
||||||
|
if output_audio_codec in CODEC_TO_EXTENSION_MAP:
|
||||||
|
extension = CODEC_TO_EXTENSION_MAP[output_audio_codec]
|
||||||
|
else:
|
||||||
|
extension = FORMAT_INFO.get(output_container, {}).get('ext', '.mkv')
|
||||||
|
|
||||||
|
print(f"Output extension: {extension}")
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# 5. Parse timestamps
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
track_times = parse_track_timestamps(tracks)
|
track_times = parse_track_timestamps(tracks)
|
||||||
resolved_times = resolve_end_times(track_times, total_duration)
|
resolved_times = resolve_end_times(track_times, total_duration)
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# 5. Fetch original metadata (for fallbacks)
|
# 6. Fetch original metadata (for fallbacks)
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
input_metadata = get_metadata(input_file)
|
input_metadata = get_metadata(input_file)
|
||||||
original_album = input_metadata.get('album')
|
original_album = input_metadata.get('album')
|
||||||
@@ -127,13 +144,12 @@ def split_audio(input_file: str, output_directory: str, tracks: List[Dict[str, A
|
|||||||
print(f" Stream {idx}: '{comment}'")
|
print(f" Stream {idx}: '{comment}'")
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# 6. Handle attached picture (cover art)
|
# 7. Handle attached picture (cover art)
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
cover_image_path = getattr(args, 'cover_image', None)
|
cover_image_path = getattr(args, 'cover_image', None)
|
||||||
if cover_image_path and not os.path.exists(cover_image_path):
|
if cover_image_path and not os.path.exists(cover_image_path):
|
||||||
cover_image_path = None
|
cover_image_path = None
|
||||||
|
|
||||||
# If not provided via args (CLI case), try to detect and extract
|
|
||||||
if not cover_image_path and not args.drop_video and stream_info.get('has_video'):
|
if not cover_image_path and not args.drop_video and stream_info.get('has_video'):
|
||||||
if is_attached_picture(input_file):
|
if is_attached_picture(input_file):
|
||||||
cover_image_path = os.path.join(output_directory, 'cover.png')
|
cover_image_path = os.path.join(output_directory, 'cover.png')
|
||||||
@@ -142,12 +158,6 @@ def split_audio(input_file: str, output_directory: str, tracks: List[Dict[str, A
|
|||||||
else:
|
else:
|
||||||
cover_image_path = None
|
cover_image_path = None
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# 7. Determine file extension
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
extension_info = FORMAT_INFO.get(output_container, {})
|
|
||||||
extension = extension_info.get('ext', '.mkv')
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# 8. Process each track
|
# 8. Process each track
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -199,7 +209,6 @@ def split_audio(input_file: str, output_directory: str, tracks: List[Dict[str, A
|
|||||||
drop_subs=args.drop_subs,
|
drop_subs=args.drop_subs,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(cmd)
|
|
||||||
print(f"Extracting track {idx}: {track_name} "
|
print(f"Extracting track {idx}: {track_name} "
|
||||||
f"({format_time(start_seconds)} - {format_time(end_seconds)})")
|
f"({format_time(start_seconds)} - {format_time(end_seconds)})")
|
||||||
|
|
||||||
@@ -208,7 +217,6 @@ def split_audio(input_file: str, output_directory: str, tracks: List[Dict[str, A
|
|||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print(f"ERROR extracting track {idx}:")
|
print(f"ERROR extracting track {idx}:")
|
||||||
print(result.stderr)
|
print(result.stderr)
|
||||||
# Optionally stop on first error? We'll continue.
|
|
||||||
else:
|
else:
|
||||||
print(f" -> Saved to: {output_path}")
|
print(f" -> Saved to: {output_path}")
|
||||||
|
|
||||||
|
|||||||
+109
-41
@@ -31,7 +31,6 @@ from .defaults import (
|
|||||||
DEFAULT_REPLACE_BAD_CHARS,
|
DEFAULT_REPLACE_BAD_CHARS,
|
||||||
DEFAULT_SKIP_EXISTING,
|
DEFAULT_SKIP_EXISTING,
|
||||||
DEFAULT_DELETE_ORIGINAL,
|
DEFAULT_DELETE_ORIGINAL,
|
||||||
DEFAULT_TRANSCODE_TO,
|
|
||||||
)
|
)
|
||||||
from .core import split_audio
|
from .core import split_audio
|
||||||
from .tracklist import read_tracklist, parse_format
|
from .tracklist import read_tracklist, parse_format
|
||||||
@@ -48,18 +47,26 @@ def main():
|
|||||||
parser.add_argument('tracklist_file', help='Tracklist file')
|
parser.add_argument('tracklist_file', help='Tracklist file')
|
||||||
|
|
||||||
# Container and codec options
|
# Container and codec options
|
||||||
parser.add_argument('--container', default=DEFAULT_FORMAT,
|
parser.add_argument(
|
||||||
help="Output container format (default: %(default)s)")
|
'--container',
|
||||||
parser.add_argument('--audio-codec', default='copy',
|
default=None,
|
||||||
help="Audio codec (copy or encoder name, e.g., libopus)")
|
help="Output container format (auto-detect if not specified)"
|
||||||
parser.add_argument('--video-codec', default='copy',
|
)
|
||||||
help="Video codec (copy or encoder name, e.g., libx264)")
|
parser.add_argument(
|
||||||
parser.add_argument('--subtitle-codec', default='copy',
|
'--audio-codec',
|
||||||
help="Subtitle codec (copy or encoder name, e.g., srt)")
|
default='copy',
|
||||||
parser.add_argument('--drop-video', action='store_true', default=DEFAULT_DROP_VIDEO,
|
help="Audio codec (copy or encoder name, e.g., libopus)"
|
||||||
help="Drop video streams")
|
)
|
||||||
parser.add_argument('--drop-subs', action='store_true', default=DEFAULT_DROP_SUBS,
|
parser.add_argument(
|
||||||
help="Drop subtitle streams")
|
'--video-codec',
|
||||||
|
default='copy',
|
||||||
|
help="Video codec (copy or encoder name, e.g., libx264)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--subtitle-codec',
|
||||||
|
default='copy',
|
||||||
|
help="Subtitle codec (copy or encoder name, e.g., srt)"
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--video-quality', '-vq',
|
'--video-quality', '-vq',
|
||||||
type=int,
|
type=int,
|
||||||
@@ -67,44 +74,105 @@ def main():
|
|||||||
help="Video quality (integer, encoder-specific; usually 1-31, lower=better). "
|
help="Video quality (integer, encoder-specific; usually 1-31, lower=better). "
|
||||||
"If omitted, FFmpeg default is used."
|
"If omitted, FFmpeg default is used."
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--drop-video',
|
||||||
|
action='store_true',
|
||||||
|
default=DEFAULT_DROP_VIDEO,
|
||||||
|
help="Drop video streams"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--drop-subs',
|
||||||
|
action='store_true',
|
||||||
|
default=DEFAULT_DROP_SUBS,
|
||||||
|
help="Drop subtitle streams"
|
||||||
|
)
|
||||||
|
|
||||||
# Filename options
|
# Filename options
|
||||||
parser.add_argument('--number-tracks', action='store_true', default=DEFAULT_NUMBER_TRACKS,
|
parser.add_argument(
|
||||||
help="Prepend track numbers")
|
'--number-tracks',
|
||||||
parser.add_argument('--output-template', default=DEFAULT_OUTPUT_TEMPLATE,
|
action='store_true',
|
||||||
help="Output filename template (default: %(default)s)")
|
default=DEFAULT_NUMBER_TRACKS,
|
||||||
parser.add_argument('--replace-bad-chars', action='store_true', default=DEFAULT_REPLACE_BAD_CHARS,
|
help="Prepend track numbers"
|
||||||
help="Replace bad characters")
|
)
|
||||||
parser.add_argument('--replacement-char', default=DEFAULT_REPLACEMENT_CHAR,
|
parser.add_argument(
|
||||||
help="Replacement character (default: %(default)s)")
|
'--output-template',
|
||||||
parser.add_argument('--bad-chars', default=DEFAULT_BAD_CHARS,
|
default=DEFAULT_OUTPUT_TEMPLATE,
|
||||||
help="Bad characters to replace (default: %(default)s)")
|
help="Output filename template (default: %(default)s)"
|
||||||
parser.add_argument('--skip-existing', action='store_true', default=DEFAULT_SKIP_EXISTING,
|
)
|
||||||
help="Skip existing output files")
|
parser.add_argument(
|
||||||
|
'--replace-bad-chars',
|
||||||
|
action='store_true',
|
||||||
|
default=DEFAULT_REPLACE_BAD_CHARS,
|
||||||
|
help="Replace bad characters"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--replacement-char',
|
||||||
|
default=DEFAULT_REPLACEMENT_CHAR,
|
||||||
|
help="Replacement character (default: %(default)s)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--bad-chars',
|
||||||
|
default=DEFAULT_BAD_CHARS,
|
||||||
|
help="Bad characters to replace (default: %(default)s)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--skip-existing',
|
||||||
|
action='store_true',
|
||||||
|
default=DEFAULT_SKIP_EXISTING,
|
||||||
|
help="Skip existing output files"
|
||||||
|
)
|
||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
parser.add_argument('--album', default=DEFAULT_ALBUM, help="Album name")
|
parser.add_argument('--album', default=DEFAULT_ALBUM, help="Album name")
|
||||||
parser.add_argument('--comment', default=DEFAULT_COMMENT, help="Comment")
|
parser.add_argument('--comment', default=DEFAULT_COMMENT, help="Comment")
|
||||||
parser.add_argument('--no-comment', action='store_true', default=DEFAULT_NO_COMMENT,
|
parser.add_argument(
|
||||||
help="Ignore comment")
|
'--no-comment',
|
||||||
parser.add_argument('--comment-stream', type=int, default=DEFAULT_COMMENT_STREAM,
|
action='store_true',
|
||||||
help="Comment stream index")
|
default=DEFAULT_NO_COMMENT,
|
||||||
parser.add_argument('--merge-comments', action='store_true', default=DEFAULT_MERGE_COMMENTS,
|
help="Ignore comment"
|
||||||
help="Merge all comments")
|
)
|
||||||
parser.add_argument('--comment-separator', default=DEFAULT_COMMENT_SEPARATOR,
|
parser.add_argument(
|
||||||
help="Separator for merged comments (default: %(default)s)")
|
'--comment-stream',
|
||||||
|
type=int,
|
||||||
|
default=DEFAULT_COMMENT_STREAM,
|
||||||
|
help="Comment stream index"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--merge-comments',
|
||||||
|
action='store_true',
|
||||||
|
default=DEFAULT_MERGE_COMMENTS,
|
||||||
|
help="Merge all comments"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--comment-separator',
|
||||||
|
default=DEFAULT_COMMENT_SEPARATOR,
|
||||||
|
help="Separator for merged comments (default: %(default)s)"
|
||||||
|
)
|
||||||
|
|
||||||
# Tracklist format
|
# Tracklist format
|
||||||
parser.add_argument('--tracklist-format', default=DEFAULT_TRACKLIST_FORMAT,
|
parser.add_argument(
|
||||||
help="Tracklist format (default: %(default)s)")
|
'--tracklist-format',
|
||||||
|
default=DEFAULT_TRACKLIST_FORMAT,
|
||||||
|
help="Tracklist format (default: %(default)s)"
|
||||||
|
)
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
parser.add_argument('--delete-original', action='store_true', default=DEFAULT_DELETE_ORIGINAL,
|
parser.add_argument(
|
||||||
help="Delete original file after split")
|
'--delete-original',
|
||||||
parser.add_argument('--dry-run', action='store_true',
|
action='store_true',
|
||||||
help="Parse and display tracklist without splitting")
|
default=DEFAULT_DELETE_ORIGINAL,
|
||||||
parser.add_argument('--output-dir', '-o', default=None,
|
help="Delete original file after split"
|
||||||
help="Output directory (default: <input_basename>_splits)")
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--dry-run',
|
||||||
|
action='store_true',
|
||||||
|
help="Parse and display tracklist without splitting"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--output-dir', '-o',
|
||||||
|
default=None,
|
||||||
|
help="Output directory (default: <input_basename>_splits)"
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user