Files
audio_splitter/ffmpeg.py
T

224 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""FFmpeg / FFprobe interactions and command building."""
import subprocess
import json
from .constants import FORMAT_INFO
from .utils import format_time
def get_audio_duration(input_file: str) -> float:
"""
Retrieve the total duration (in seconds) of the input file using ffprobe.
Args:
input_file: Path to the media file.
Returns:
Duration in seconds as a float.
"""
cmd = [
'ffprobe', '-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
input_file
]
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
return float(result.stdout.strip())
def has_stream_type(input_file: str, stream_type: str) -> bool:
"""
Check whether the input file contains a stream of the given type.
Args:
input_file: Path to the media file.
stream_type: 'a' for audio, 'v' for video, 's' for subtitle.
Returns:
True if at least one such stream exists, False otherwise.
"""
cmd = [
'ffprobe', '-v', 'error',
'-select_streams', stream_type,
'-show_entries', 'stream=codec_type',
'-of', 'default=noprint_wrappers=1:nokey=1',
input_file
]
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
return bool(result.stdout.strip())
def get_audio_codec(input_file: str) -> str:
"""
Return the codec name of the first audio stream.
Args:
input_file: Path to the media file.
Returns:
Codec name as a lowercase string, or None if no audio stream exists.
"""
cmd = [
'ffprobe', '-v', 'error',
'-select_streams', 'a:0',
'-show_entries', 'stream=codec_name',
'-of', 'default=noprint_wrappers=1:nokey=1',
input_file
]
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
codec = result.stdout.strip().lower()
return codec if codec else None
def get_stream_info(input_file: str):
"""
Collect information about the streams present in the input file.
Args:
input_file: Path to the media file.
Returns:
A dictionary with keys: has_audio, has_video, has_subtitle, audio_codec.
"""
return {
'has_audio': has_stream_type(input_file, 'a'),
'has_video': has_stream_type(input_file, 'v'),
'has_subtitle': has_stream_type(input_file, 's'),
'audio_codec': get_audio_codec(input_file)
}
def build_ffmpeg_command(input_file, start_seconds, duration_seconds, output_path,
stream_info, format_opt, transcode_audio,
drop_video, drop_subs, metadata=None):
"""
Construct the FFmpeg command line as a list of arguments.
Args:
input_file: Path to the input media file.
start_seconds: Start time for the segment (in seconds).
duration_seconds: Duration of the segment (in seconds).
output_path: Destination path for the output file.
stream_info: Dictionary from get_stream_info().
format_opt: Output container format (e.g., 'mp3').
transcode_audio: Audio codec to transcode to (or None).
drop_video: True to remove video streams.
drop_subs: True to remove subtitle streams.
metadata: Optional dict of metadata key/value pairs to write.
Returns:
A list of commandline arguments suitable for subprocess.run().
"""
cmd = [
'ffmpeg',
'-i', input_file,
'-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 --------------------
# Map the streams we want to keep.
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'])
# -------------------- Audio codec --------------------
if transcode_audio:
cmd.extend(['-c:a', transcode_audio])
if transcode_audio in ('libmp3lame', 'mp3'):
cmd.extend(['-b:a', '192k'])
elif transcode_audio in ('libopus', 'opus'):
cmd.extend(['-b:a', '128k'])
else:
cmd.extend(['-c:a', 'copy'])
# -------------------- Video codec --------------------
if not drop_video and stream_info['has_video']:
cmd.extend(['-c:v', 'copy'])
else:
cmd.append('-vn')
# -------------------- Subtitle codec --------------------
if not drop_subs and stream_info['has_subtitle']:
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])
# Overwrite output if it already exists.
cmd.extend(['-y', output_path])
return cmd
def get_metadata(input_file: str) -> dict:
"""
Retrieve metadata from the input file using ffprobe with JSON output.
Returns a dict with:
- album: merged from all sources (last wins)
- title: merged from all sources (last wins)
- comments: list of (stream_index, comment) tuples
"""
cmd = [
'ffprobe', '-v', 'quiet',
'-print_format', 'json',
'-show_entries', 'format_tags:stream_tags',
input_file
]
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
if result.returncode != 0:
return {'album': None, 'title': None, 'comments': []}
try:
data = json.loads(result.stdout)
# Collect album and title (merged, last wins).
album = None
title = None
comments = [] # list of (stream_index, comment)
# Format tags.
fmt_tags = data.get('format', {}).get('tags', {})
album = fmt_tags.get('album') or album
title = fmt_tags.get('title') or title
# Format does not have a stream index; we'll treat it as -1 if needed.
# Stream tags.
for idx, stream in enumerate(data.get('streams', [])):
stream_tags = stream.get('tags', {})
# Album and title: update if present.
if 'album' in stream_tags:
album = stream_tags['album']
if 'title' in stream_tags:
title = stream_tags['title']
# Comment: collect all occurrences.
if 'comment' in stream_tags:
comments.append((idx, stream_tags['comment']))
return {
'album': album,
'title': title,
'comments': comments
}
except (json.JSONDecodeError, KeyError):
return {'album': None, 'title': None, 'comments': []}