diff --git a/audio_splitter/ffmpeg.py b/audio_splitter/ffmpeg.py index b5119b4..4471f58 100644 --- a/audio_splitter/ffmpeg.py +++ b/audio_splitter/ffmpeg.py @@ -269,6 +269,109 @@ def extract_cover_image(input_file: str, output_path: str) -> bool: return True +def _build_inputs(cmd: List[str], input_file: str, cover_image_path: Optional[str]) -> None: + """Add input files to the command.""" + if cover_image_path: + cmd.extend(['-i', cover_image_path]) + cmd.extend(['-i', input_file]) + + +def _build_time_options(cmd: List[str], start_seconds: int, duration_seconds: int) -> None: + """Add time-based options to the command.""" + cmd.extend(['-ss', format_time(start_seconds), '-t', format_time(duration_seconds)]) + + +def _build_metadata(cmd: List[str], metadata: Optional[Dict]) -> None: + """Add metadata options to the command.""" + cmd.append('-map_metadata') + cmd.append('-1') + if metadata: + for key, value in metadata.items(): + if value is not None and value != '': + cmd.extend(['-metadata', f"{key}={value}"]) + + +def _build_cover_image_mapping( + cmd: List[str], + audio_codec: Optional[str], + video_codec: Optional[str], + video_quality: Optional[int], +) -> None: + """Build stream mapping for cover image extraction (audio + cover video).""" + cmd.extend(['-map', '1:a:0', '-map', '0:v:0']) + + if video_codec and video_codec != 'copy': + cmd.extend(['-c:v', video_codec]) + else: + cmd.extend(['-c:v', 'png']) + + if audio_codec and audio_codec != 'copy': + cmd.extend(['-c:a', audio_codec]) + else: + cmd.extend(['-c:a', 'copy']) + + cmd.append('-sn') + + if video_quality is not None: + cmd.extend(['-q:v', str(video_quality)]) + + +def _build_standard_mapping( + cmd: List[str], + stream_info: Dict, + audio_codec: Optional[str], + video_codec: Optional[str], + subtitle_codec: Optional[str], + video_quality: Optional[int], + drop_video: bool, + drop_subs: bool, +) -> None: + """Build stream mapping for standard extraction (no cover image).""" + if drop_video and drop_subs: + cmd.extend(['-map', '0:a:0']) + elif drop_video: + cmd.extend(['-map', '0:a:0', '-map', '0:s?']) + elif drop_subs: + cmd.extend(['-map', '0:a:0', '-map', '0:v:0']) + else: + cmd.extend(['-map', '0']) + + if audio_codec and audio_codec != 'copy': + cmd.extend(['-c:a', audio_codec]) + else: + cmd.extend(['-c:a', 'copy']) + + if not drop_video and stream_info.get('has_video'): + if video_codec and video_codec != 'copy': + cmd.extend(['-c:v', video_codec]) + if video_quality is not None: + cmd.extend(['-q:v', str(video_quality)]) + else: + cmd.extend(['-c:v', 'copy']) + else: + cmd.append('-vn') + + if not drop_subs and stream_info.get('has_subtitle'): + if subtitle_codec and subtitle_codec != 'copy': + cmd.extend(['-c:s', subtitle_codec]) + else: + cmd.extend(['-c:s', 'copy']) + else: + cmd.append('-sn') + + +def _build_format(cmd: List[str], format_opt: Optional[str]) -> None: + """Add output format option if specified.""" + if format_opt: + ffmpeg_format = FORMAT_INFO.get(format_opt, {}).get('ffmpeg', format_opt) + cmd.extend(['-f', ffmpeg_format]) + + +def _build_output(cmd: List[str], output_path: str) -> None: + """Add output file to the command.""" + cmd.extend(['-y', output_path]) + + def build_ffmpeg_command( input_file: str, start_seconds: int, @@ -309,93 +412,21 @@ def build_ffmpeg_command( """ cmd = ['ffmpeg'] - # Add cover image as first input if provided + _build_inputs(cmd, input_file, cover_image_path) + _build_time_options(cmd, start_seconds, duration_seconds) + _build_metadata(cmd, metadata) + if cover_image_path: - cmd.extend(['-i', cover_image_path]) - - # Add main input file - cmd.extend(['-i', input_file]) - - # Time options - cmd.extend(['-ss', format_time(start_seconds), '-t', format_time(duration_seconds)]) - - # Clear all original metadata. - cmd.append('-map_metadata') - cmd.append('-1') - - # Apply custom metadata. - if metadata: - for key, value in metadata.items(): - if value is not None and value != '': - cmd.extend(['-metadata', f"{key}={value}"]) - - # ---------- Stream mapping and codecs ---------- - if cover_image_path: - # We have two inputs: index 0 = cover image, index 1 = main input - # Map audio from main input (index 1) and video from cover image (index 0) - cmd.extend(['-map', '1:a:0', '-map', '0:v:0']) - - # Video codec: use user-specified codec if provided, otherwise fallback to png - if video_codec and video_codec != 'copy': - cmd.extend(['-c:v', video_codec]) - else: - cmd.extend(['-c:v', 'png']) - - # Audio codec - if audio_codec and audio_codec != 'copy': - cmd.extend(['-c:a', audio_codec]) - else: - cmd.extend(['-c:a', 'copy']) - - # Subtitle: we don't copy subtitles when using cover image (they would be from main input) - cmd.append('-sn') - - # Video quality if specified - if video_quality is not None: - cmd.extend(['-q:v', str(video_quality)]) - + _build_cover_image_mapping(cmd, audio_codec, video_codec, video_quality) else: - # Standard mapping (no cover image) - if drop_video and drop_subs: - cmd.extend(['-map', '0:a:0']) - elif drop_video: - cmd.extend(['-map', '0:a:0', '-map', '0:s?']) - elif drop_subs: - cmd.extend(['-map', '0:a:0', '-map', '0:v:0']) - else: - cmd.extend(['-map', '0']) + _build_standard_mapping( + cmd, stream_info, audio_codec, video_codec, subtitle_codec, + video_quality, drop_video, drop_subs + ) - # Audio codec - if audio_codec and audio_codec != 'copy': - cmd.extend(['-c:a', audio_codec]) - else: - cmd.extend(['-c:a', 'copy']) + _build_format(cmd, format_opt) + _build_output(cmd, output_path) - # Video codec - if not drop_video and stream_info.get('has_video'): - if video_codec and video_codec != 'copy': - cmd.extend(['-c:v', video_codec]) - # Add video quality if specified (only when re-encoding) - if video_quality is not None: - cmd.extend(['-q:v', str(video_quality)]) - else: - cmd.extend(['-c:v', 'copy']) - else: - cmd.append('-vn') - - # Subtitle codec - if not drop_subs and stream_info.get('has_subtitle'): - if subtitle_codec and subtitle_codec != 'copy': - cmd.extend(['-c:s', subtitle_codec]) - else: - cmd.extend(['-c:s', 'copy']) - else: - cmd.append('-sn') - - # Output format - if format_opt: - ffmpeg_format = FORMAT_INFO.get(format_opt, {}).get('ffmpeg', format_opt) - cmd.extend(['-f', ffmpeg_format]) - - cmd.extend(['-y', output_path]) return cmd + +