From 748532be7c42f759ccb1439019a40b084c2022d9 Mon Sep 17 00:00:00 2001 From: Maxim Vershinin Date: Fri, 28 Aug 2026 11:04:40 +0500 Subject: [PATCH] FIX: codec/container/file_extension autodetection when default values are used --- audio_splitter/core.py | 38 ++++++----- audio_splitter/main.py | 150 ++++++++++++++++++++++++++++++----------- 2 files changed, 132 insertions(+), 56 deletions(-) diff --git a/audio_splitter/core.py b/audio_splitter/core.py index 2f49e3f..bfb35ef 100644 --- a/audio_splitter/core.py +++ b/audio_splitter/core.py @@ -6,7 +6,7 @@ import subprocess import sys from typing import List, Dict, Any -from .constants import FORMAT_INFO +from .constants import FORMAT_INFO, CODEC_TO_EXTENSION_MAP from .ffmpeg import ( get_audio_duration, 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. tracks: List of dicts, each containing parsed fields (ts, tn, an, ...). 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) - video_codec: video codec (copy or encoder) - subtitle_codec: subtitle codec (copy or encoder) + - video_quality: integer or None - drop_video: bool - drop_subs: 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 # -------------------------------------------------------------------------- - # Use args.container if provided, otherwise auto-detect user_container = getattr(args, 'container', None) output_container = determine_output_format( 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}") # -------------------------------------------------------------------------- - # 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) 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) 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}'") # -------------------------------------------------------------------------- - # 6. Handle attached picture (cover art) + # 7. Handle attached picture (cover art) # -------------------------------------------------------------------------- cover_image_path = getattr(args, 'cover_image', None) if cover_image_path and not os.path.exists(cover_image_path): 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 is_attached_picture(input_file): 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: 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 # -------------------------------------------------------------------------- @@ -199,7 +209,6 @@ def split_audio(input_file: str, output_directory: str, tracks: List[Dict[str, A drop_subs=args.drop_subs, ) - print(cmd) print(f"Extracting track {idx}: {track_name} " 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: print(f"ERROR extracting track {idx}:") print(result.stderr) - # Optionally stop on first error? We'll continue. else: print(f" -> Saved to: {output_path}") diff --git a/audio_splitter/main.py b/audio_splitter/main.py index 070be8f..fce53e3 100644 --- a/audio_splitter/main.py +++ b/audio_splitter/main.py @@ -31,7 +31,6 @@ from .defaults import ( DEFAULT_REPLACE_BAD_CHARS, DEFAULT_SKIP_EXISTING, DEFAULT_DELETE_ORIGINAL, - DEFAULT_TRANSCODE_TO, ) from .core import split_audio from .tracklist import read_tracklist, parse_format @@ -48,18 +47,26 @@ def main(): parser.add_argument('tracklist_file', help='Tracklist file') # Container and codec options - parser.add_argument('--container', default=DEFAULT_FORMAT, - help="Output container format (default: %(default)s)") - parser.add_argument('--audio-codec', default='copy', - help="Audio codec (copy or encoder name, e.g., libopus)") - parser.add_argument('--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('--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") + parser.add_argument( + '--container', + default=None, + help="Output container format (auto-detect if not specified)" + ) + parser.add_argument( + '--audio-codec', + default='copy', + help="Audio codec (copy or encoder name, e.g., libopus)" + ) + parser.add_argument( + '--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( '--video-quality', '-vq', type=int, @@ -67,44 +74,105 @@ def main(): help="Video quality (integer, encoder-specific; usually 1-31, lower=better). " "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 - parser.add_argument('--number-tracks', action='store_true', default=DEFAULT_NUMBER_TRACKS, - help="Prepend track numbers") - parser.add_argument('--output-template', default=DEFAULT_OUTPUT_TEMPLATE, - help="Output filename template (default: %(default)s)") - 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") + parser.add_argument( + '--number-tracks', + action='store_true', + default=DEFAULT_NUMBER_TRACKS, + help="Prepend track numbers" + ) + parser.add_argument( + '--output-template', + default=DEFAULT_OUTPUT_TEMPLATE, + help="Output filename template (default: %(default)s)" + ) + 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 parser.add_argument('--album', default=DEFAULT_ALBUM, help="Album name") parser.add_argument('--comment', default=DEFAULT_COMMENT, help="Comment") - parser.add_argument('--no-comment', action='store_true', default=DEFAULT_NO_COMMENT, - help="Ignore comment") - parser.add_argument('--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)") + parser.add_argument( + '--no-comment', + action='store_true', + default=DEFAULT_NO_COMMENT, + help="Ignore comment" + ) + parser.add_argument( + '--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 - parser.add_argument('--tracklist-format', default=DEFAULT_TRACKLIST_FORMAT, - help="Tracklist format (default: %(default)s)") + parser.add_argument( + '--tracklist-format', + default=DEFAULT_TRACKLIST_FORMAT, + help="Tracklist format (default: %(default)s)" + ) # Other - parser.add_argument('--delete-original', action='store_true', default=DEFAULT_DELETE_ORIGINAL, - help="Delete original file after split") - 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: _splits)") + parser.add_argument( + '--delete-original', + action='store_true', + default=DEFAULT_DELETE_ORIGINAL, + help="Delete original file after split" + ) + 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: _splits)" + ) args = parser.parse_args()