feature (core): adds special handlers for audio+cover_image cases

This commit is contained in:
2026-08-31 14:19:07 +05:00
parent 747a349877
commit 580837fa73
3 changed files with 201 additions and 2 deletions
+31 -2
View File
@@ -23,6 +23,7 @@ from .formats import (
determine_output_format,
validate_format_compatibility,
)
from .handlers import get_handler, needs_drop_video
from .timestamp import parse_track_timestamps, resolve_end_times
from .filename import build_filename
from .metadata import build_metadata_dict
@@ -162,6 +163,14 @@ def split_audio(input_file: str, output_directory: str, tracks: List[Dict[str, A
else:
cover_image_path = None
# Check if we need special handling (e.g., Opus files need opustags)
input_audio_codec = stream_info.get('audio_codec')
cover_handler = get_handler(output_container, input_audio_codec)
needs_drop = needs_drop_video(output_container, input_audio_codec)
if cover_image_path and needs_drop:
print(f"Using special handler for {output_container} + {input_audio_codec}")
print("Temporarily dropping video for opustags post-processing.")
# --------------------------------------------------------------------------
# 8. Process each track
# --------------------------------------------------------------------------
@@ -208,6 +217,11 @@ def split_audio(input_file: str, output_directory: str, tracks: List[Dict[str, A
subtitle_enc = CODEC_NAME_TO_FFMPEG.get(subtitle_enc, subtitle_enc)
# Then build command with these mapped encoders
# Use temporary drop_video for handlers that need it
effective_drop_video = args.drop_video or (cover_image_path and needs_drop)
# For handlers that need opustags post-processing, don't pass cover_image_path
# to ffmpeg - it will process audio-only, then handler adds cover afterward
ffmpeg_cover_path = None if (cover_image_path and needs_drop) else cover_image_path
cmd = build_ffmpeg_command(
input_file=input_file,
start_seconds=start_seconds,
@@ -219,9 +233,9 @@ def split_audio(input_file: str, output_directory: str, tracks: List[Dict[str, A
video_codec=video_enc,
subtitle_codec=subtitle_enc,
metadata=metadata,
cover_image_path=cover_image_path,
cover_image_path=ffmpeg_cover_path,
video_quality=getattr(args, 'video_quality', None),
drop_video=args.drop_video,
drop_video=effective_drop_video,
drop_subs=args.drop_subs,
)
@@ -235,6 +249,21 @@ def split_audio(input_file: str, output_directory: str, tracks: List[Dict[str, A
print(result.stderr)
else:
print(f" -> Saved to: {output_path}")
# Apply cover image handler if needed
if cover_image_path and not args.drop_video and needs_drop:
print(f"Applying cover image via {output_container} handler...")
if not cover_handler(
input_file=input_file,
cover_image_path=cover_image_path,
output_path=output_path,
stream_info=stream_info,
audio_codec=audio_enc,
video_codec=video_enc,
video_quality=getattr(args, 'video_quality', None),
drop_video=args.drop_video,
drop_subs=args.drop_subs,
):
print(f"Warning: Failed to attach cover image to {output_path}")
# --------------------------------------------------------------------------
# 9. Delete original file if requested
+3
View File
@@ -310,6 +310,9 @@ def _build_cover_image_mapping(
else:
cmd.extend(['-c:a', 'copy'])
# Preserve attached_pic disposition for cover image
cmd.extend(['-disposition', 'attached_pic'])
cmd.append('-sn')
if video_quality is not None:
+167
View File
@@ -0,0 +1,167 @@
# audio_splitter/handlers.py
"""Handler system for cover image attachment.
This module provides a registry of handlers for different
codec + image combinations. Each handler is responsible for
attaching the cover image to the output file in the appropriate way.
"""
import os
import subprocess
from typing import Dict, Optional
def _run_opustags(input_file: str, cover_image_path: str, output_path: Optional[str] = None) -> bool:
"""
Run opustags to attach a cover image to an Opus file.
Args:
input_file: Path to the input Opus file.
cover_image_path: Path to the cover image.
output_path: Optional output path (if None, overwrites input).
Returns:
True if successful, False otherwise.
"""
cmd = ['opustags', '--set-cover', cover_image_path, input_file]
if output_path:
cmd.extend(['-o', output_path, '-y'])
else:
cmd.extend(['-i'])
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
if result.returncode != 0:
print(f"opustags failed: {result.stderr}")
return False
return True
def _handler_default(
input_file: str,
cover_image_path: str,
output_path: str,
stream_info: Dict,
audio_codec: str,
video_codec: str,
video_quality: Optional[int],
drop_video: bool,
drop_subs: bool,
) -> bool:
"""
Default handler: use ffmpeg with -disposition attached_pic.
This is the standard approach for formats like MP3, FLAC, etc.
"""
# This handler doesn't do anything - the cover is already handled
# by the ffmpeg command in core.py via build_ffmpeg_command()
# We just return True to indicate success.
return True
def _handler_opus_with_cover(
input_file: str,
cover_image_path: str,
output_path: str,
stream_info: Dict,
audio_codec: str,
video_codec: str,
video_quality: Optional[int],
drop_video: bool,
drop_subs: bool,
) -> bool:
"""
Handler for Opus files with cover image.
Uses opustags to attach the cover image after the file is processed.
"""
return _run_opustags(output_path, cover_image_path)
def _handler_mp3_with_cover(
input_file: str,
cover_image_path: str,
output_path: str,
stream_info: Dict,
audio_codec: str,
video_codec: str,
video_quality: Optional[int],
drop_video: bool,
drop_subs: bool,
) -> bool:
"""
Handler for MP3 files with cover image.
Uses ffmpeg with -disposition attached_pic.
"""
# MP3 handler - the cover is already attached by ffmpeg in core.py
return True
def _handler_flac_with_cover(
input_file: str,
cover_image_path: str,
output_path: str,
stream_info: Dict,
audio_codec: str,
video_codec: str,
video_quality: Optional[int],
drop_video: bool,
drop_subs: bool,
) -> bool:
"""
Handler for FLAC files with cover image.
Uses ffmpeg with -disposition attached_pic.
"""
# FLAC handler - the cover is already attached by ffmpeg in core.py
return True
# Handler registry: maps (output_container, input_codec) to handler function
HANDLER_REGISTRY: Dict[tuple, callable] = {
('opus', 'opus'): _handler_opus_with_cover,
('ogg', 'opus'): _handler_opus_with_cover,
('mp3', 'mp3'): _handler_mp3_with_cover,
('flac', 'flac'): _handler_flac_with_cover,
}
def get_handler(output_container: str, input_audio_codec: Optional[str] = None):
"""
Get the appropriate handler for the given container and input codec.
Args:
output_container: Output container format (e.g., 'opus', 'mp3', 'flac').
input_audio_codec: Input audio codec (e.g., 'opus', 'mp3', 'flac').
Returns:
Handler function, or the default handler if no specific handler is found.
"""
if input_audio_codec:
key = (output_container, input_audio_codec)
if key in HANDLER_REGISTRY:
return HANDLER_REGISTRY[key]
# Fall back to container-only key
if (output_container, None) in HANDLER_REGISTRY:
return HANDLER_REGISTRY[(output_container, None)]
# Default handler
return _handler_default
def needs_drop_video(output_container: str, input_audio_codec: Optional[str] = None) -> bool:
"""
Check if the handler requires dropping video even if user didn't specify --drop-video.
Args:
output_container: Output container format.
input_audio_codec: Input audio codec.
Returns:
True if video should be dropped, False otherwise.
"""
handler = get_handler(output_container, input_audio_codec)
# The opus handler requires dropping video
return handler == _handler_opus_with_cover