feat: add FFmpeg command parser with path resolution
This commit is contained in:
43
app/ffmpeg.py
Normal file
43
app/ffmpeg.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import shlex
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# FFmpeg options that take a value (not exhaustive, covers common ones)
|
||||||
|
OPTIONS_WITH_VALUES = {
|
||||||
|
"-c", "-c:v", "-c:a", "-b", "-b:v", "-b:a", "-r", "-s", "-ar", "-ac",
|
||||||
|
"-f", "-t", "-ss", "-to", "-vf", "-af", "-filter:v", "-filter:a",
|
||||||
|
"-preset", "-crf", "-qp", "-profile", "-level", "-pix_fmt", "-map",
|
||||||
|
"-metadata", "-disposition", "-threads", "-filter_complex",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_command(command: str) -> list[str]:
|
||||||
|
"""Parse FFmpeg command string into argument list."""
|
||||||
|
return shlex.split(command)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_paths(args: list[str], data_path: str) -> list[str]:
|
||||||
|
"""Resolve relative paths against the data directory."""
|
||||||
|
resolved = []
|
||||||
|
skip_next = False
|
||||||
|
|
||||||
|
for i, arg in enumerate(args):
|
||||||
|
if skip_next:
|
||||||
|
resolved.append(arg)
|
||||||
|
skip_next = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if this is an option that takes a value
|
||||||
|
if arg in OPTIONS_WITH_VALUES or arg.startswith("-"):
|
||||||
|
resolved.append(arg)
|
||||||
|
if arg in OPTIONS_WITH_VALUES:
|
||||||
|
skip_next = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
# This looks like a file path - resolve if relative
|
||||||
|
path = Path(arg)
|
||||||
|
if not path.is_absolute():
|
||||||
|
resolved.append(str(Path(data_path) / arg))
|
||||||
|
else:
|
||||||
|
resolved.append(arg)
|
||||||
|
|
||||||
|
return resolved
|
||||||
59
tests/test_ffmpeg.py
Normal file
59
tests/test_ffmpeg.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.ffmpeg import parse_command, resolve_paths
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_simple_command():
|
||||||
|
command = "-i input.mp4 output.mp4"
|
||||||
|
|
||||||
|
args = parse_command(command)
|
||||||
|
|
||||||
|
assert args == ["-i", "input.mp4", "output.mp4"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_command_with_options():
|
||||||
|
command = "-i input.mp4 -c:v libx264 -crf 23 output.mp4"
|
||||||
|
|
||||||
|
args = parse_command(command)
|
||||||
|
|
||||||
|
assert args == ["-i", "input.mp4", "-c:v", "libx264", "-crf", "23", "output.mp4"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_command_with_quotes():
|
||||||
|
command = '-i "input file.mp4" output.mp4'
|
||||||
|
|
||||||
|
args = parse_command(command)
|
||||||
|
|
||||||
|
assert args == ["-i", "input file.mp4", "output.mp4"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_paths():
|
||||||
|
args = ["-i", "input/video.mp4", "-c:v", "libx264", "output/result.mp4"]
|
||||||
|
data_path = "/data"
|
||||||
|
|
||||||
|
resolved = resolve_paths(args, data_path)
|
||||||
|
|
||||||
|
assert resolved == [
|
||||||
|
"-i", "/data/input/video.mp4",
|
||||||
|
"-c:v", "libx264",
|
||||||
|
"/data/output/result.mp4"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_paths_preserves_absolute():
|
||||||
|
args = ["-i", "/already/absolute.mp4", "output.mp4"]
|
||||||
|
data_path = "/data"
|
||||||
|
|
||||||
|
resolved = resolve_paths(args, data_path)
|
||||||
|
|
||||||
|
assert resolved == ["-i", "/already/absolute.mp4", "/data/output.mp4"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_paths_skips_options():
|
||||||
|
args = ["-c:v", "libx264", "-preset", "fast"]
|
||||||
|
data_path = "/data"
|
||||||
|
|
||||||
|
resolved = resolve_paths(args, data_path)
|
||||||
|
|
||||||
|
# Options and their values should not be resolved as paths
|
||||||
|
assert resolved == ["-c:v", "libx264", "-preset", "fast"]
|
||||||
Reference in New Issue
Block a user