fix(ffmpeg handling): revise argparser, ffprobe requres string

add add_command function
one shared parent parser

fix(ffmpeg handling): ffprobe requres string not path

fix(ffmpeg handling): unify no output return string
This commit is contained in:
Lucas Vivo committed 2026-10-25 19:16:27 +01:00
1 parent 6e9cc10b46
commit 2d544d317f
1 file changed
+315 -149
+315 -149
View File
@@ -172,7 +172,7 @@ def move_override_file(input_path, backup_files=False,silent=False):
LOG_FILE = Path.home() / "ffmpeg_log.log" LOG_FILE = Path.home() / "ffmpeg_log.log"
def run_ffmpeg(stream, overwrite=True, loud=False): def run_ffmpeg(stream, overwrite=True, loud=False,dry_run = False):
kwargs = { kwargs = {
"overwrite_output": overwrite, "overwrite_output": overwrite,
} }
@@ -184,6 +184,9 @@ def run_ffmpeg(stream, overwrite=True, loud=False):
quiet=True, quiet=True,
) )
if dry_run:
logger.debug(f"Dry run: {stream}")
return
try: try:
out, err = ffmpeg.run(stream, **kwargs) out, err = ffmpeg.run(stream, **kwargs)
logger.trace(out) logger.trace(out)
@@ -358,7 +361,7 @@ def cmd_info(args):
ffprobe -show_format -show_streams -of json [input] ffprobe -show_format -show_streams -of json [input]
""" """
try: try:
info = ffmpeg.probe(args.input) info = ffmpeg.probe(str(args.input))
logger.info(json.dumps(info, indent=2)) logger.info(json.dumps(info, indent=2))
except ffmpeg.Error as e: except ffmpeg.Error as e:
logger.error(f"Error probing file:", e) logger.error(f"Error probing file:", e)
@@ -443,7 +446,7 @@ def cmd_remove_subs(args):
Parameters: Parameters:
- sn: Disables subtitle recording. - sn: Disables subtitle recording.
""" """
probe = ffmpeg.probe(args.input) probe = ffmpeg.probe(str(args.input))
cmd = [ cmd = [
"ffmpeg", "ffmpeg",
@@ -474,7 +477,9 @@ def cmd_remove_subs(args):
]) ])
logger.debug("%s", " ".join(cmd)) logger.debug("%s", " ".join(cmd))
if args.dry_run:
logger.debug(f"Dry run: {" ".join(cmd)}")
return "nooutputneeded"
try: try:
subprocess.run( subprocess.run(
cmd, cmd,
@@ -488,12 +493,12 @@ def cmd_remove_subs(args):
logger.error("STDOUT:\n%s", e.stdout) logger.error("STDOUT:\n%s", e.stdout)
logger.error("STDERR:\n%s", e.stderr) logger.error("STDERR:\n%s", e.stderr)
raise raise
return "No output needed" return "nooutputneeded"
def cmd_remove_audio_track(args): def cmd_remove_audio_track(args):
""" """
1. Removes a specific audio track from the video (Main Output). 1. Removes a specific audio track from the video (Main Output).
""" """
probe = ffmpeg.probe(args.input) probe = ffmpeg.probe(str(args.input))
remove_idx = select_audio_stream(args.input, args.audio_index) remove_idx = select_audio_stream(args.input, args.audio_index)
@@ -541,7 +546,8 @@ def cmd_remove_audio_track(args):
cmd.append(str(args.output)) cmd.append(str(args.output))
logger.debug(" ".join(cmd)) logger.debug(" ".join(cmd))
if args.dry_run: if args.dry_run:
return "No Output needed" logger.debug(f"Dry run: {" ".join(cmd)}")
return "nooutputneeded"
try: try:
result = subprocess.run( result = subprocess.run(
cmd, cmd,
@@ -551,7 +557,7 @@ def cmd_remove_audio_track(args):
) )
logger.debug("FFmpegsubprocess successful") logger.debug("FFmpegsubprocess successful")
logger.trace(result.stdout) logger.trace(result.stdout)
return "No output needed" return "nooutputneeded"
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
logging.error( logging.error(
@@ -567,7 +573,7 @@ def cmd_extract_audio_track(args):
1. Saves that specific audio track as a standalone MP3 (Extra Output). 1. Saves that specific audio track as a standalone MP3 (Extra Output).
""" """
inp = ffmpeg.input(args.input) inp = ffmpeg.input(args.input)
probe = ffmpeg.probe(args.input) probe = ffmpeg.probe(str(args.input))
output_audio = Path(args.output) output_audio = Path(args.output)
output_audio = str(output_audio.with_name(f"{output_audio.stem}.mp3")) output_audio = str(output_audio.with_name(f"{output_audio.stem}.mp3"))
@@ -602,7 +608,7 @@ def cmd_downsize(args):
""" """
inp = ffmpeg.input(args.input) inp = ffmpeg.input(args.input)
video = inp.video.filter("scale", args.width, args.height) video = inp.video.filter("scale", args.width, args.height)
info = ffmpeg.probe(args.input) info = ffmpeg.probe(str(args.input))
streams = info.get("streams", []) streams = info.get("streams", [])
subtitle_streams = [s for s in streams if s.get("codec_type") == "subtitle"] subtitle_streams = [s for s in streams if s.get("codec_type") == "subtitle"]
@@ -642,7 +648,7 @@ def cmd_filter_audio(args):
idx_filt = select_audio_stream(args.input, args.audio_index) idx_filt = select_audio_stream(args.input, args.audio_index)
inp = ffmpeg.input(args.input) inp = ffmpeg.input(args.input)
probe = ffmpeg.probe(args.input) probe = ffmpeg.probe(str(args.input))
audio_streams = [] audio_streams = []
audio_titles = {} audio_titles = {}
@@ -790,7 +796,7 @@ def cmd_set_primary_audio(args):
inp = ffmpeg.input(args.input) inp = ffmpeg.input(args.input)
primary_audio = select_audio_stream(args.input, args.audio_index) primary_audio = select_audio_stream(args.input, args.audio_index)
probe = ffmpeg.probe(args.input) probe = ffmpeg.probe(str(args.input))
audio_streams = [ audio_streams = [
s["index"] s["index"]
@@ -811,7 +817,7 @@ def cmd_set_primary_audio(args):
else: else:
out_kwargs[f"disposition:a:{i}"] = "0" out_kwargs[f"disposition:a:{i}"] = "0"
info = ffmpeg.probe(args.input) info = ffmpeg.probe(str(args.input))
streams = info.get("streams", []) streams = info.get("streams", [])
subtitle_streams = [s for s in streams if s.get("codec_type") == "subtitle"] subtitle_streams = [s for s in streams if s.get("codec_type") == "subtitle"]
@@ -837,7 +843,7 @@ def cmd_show_metadata(args,show_metadata=False):
Prints metadata directly from ffprobe output. Prints metadata directly from ffprobe output.
""" """
probe = ffmpeg.probe(args.input) probe = ffmpeg.probe(str(args.input))
format_tags = probe.get("format", {}).get("tags", {}) format_tags = probe.get("format", {}).get("tags", {})
if show_metadata: if show_metadata:
@@ -877,7 +883,9 @@ def cmd_set_metadata(args):
"-c", f"set {args.key} '{args.value}'", "-c", f"set {args.key} '{args.value}'",
args.input args.input
] ]
if args.dry_run:
logger.debug(f"Dry run: {" ".join(cmd)}")
return "nooutputneeded"
result = subprocess.run(cmd, result = subprocess.run(cmd,
capture_output=True, capture_output=True,
text=True, text=True,
@@ -966,7 +974,9 @@ def cmd_apply_chapters(args):
"-c", "copy", "-c", "copy",
args.output args.output
] ]
if args.dry_run:
logger.debug(f"Dry run: {" ".join(cmd)}")
return "nooutputneeded"
result = subprocess.run(cmd, result = subprocess.run(cmd,
capture_output=True, capture_output=True,
text=True, text=True,
@@ -987,10 +997,13 @@ def cmd_apply_chapters(args):
audio = EasyID3(filename) audio = EasyID3(filename)
audio["title"] = title audio["title"] = title
audio["tracknumber"] = str(args.file_counter+1) audio["tracknumber"] = str(args.file_counter+1)
if args.dry_run:
logger.debug(f"Dry run")
return "nooutputneeded"
audio.save() audio.save()
safe_rename(filename, new_path) safe_rename(filename, new_path)
return "No output needed" return "nooutputneeded"
def cmd_remove_russian_from_db(args_in): def cmd_remove_russian_from_db(args_in):
""" """
@@ -1044,7 +1057,7 @@ def cmd_test(args):
if not outputs: if not outputs:
logger.error(f"No outputs returned.") logger.error(f"No outputs returned.")
return False return False
if outputs != "No output needed": if outputs != "nooutputneeded":
try: try:
ffmpeg.run( ffmpeg.run(
outputs, outputs,
@@ -1439,170 +1452,323 @@ def cmd_test(args):
# ---------- CLI Setup ---------- # # ---------- CLI Setup ---------- #
def main(): def add_command(
parser = argparse.ArgumentParser( subparsers,
description="Media processing tool using ffmpeg-python (CLI + interactive fallback)", name,
formatter_class=argparse.ArgumentDefaultsHelpFormatter func,
help_text,
aliases=None,
parents=None,
):
parser = subparsers.add_parser(
name,
aliases=aliases or [],
parents=parents or [],
help=help_text,
)
parser.set_defaults(
func=func,
command=name,
)
return parser
def add_audio_index(parser):
parser.add_argument(
"-a",
"--audio-index",
type=int,
help="Audio track index",
) )
# ---------------------------- def main():
# BASE ARGS (shared) parser = argparse.ArgumentParser(
# ---------------------------- description="Media processing tool using ffmpeg-python",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
# =========================================================
# SHARED ARGS
# =========================================================
base_p = argparse.ArgumentParser(add_help=False) base_p = argparse.ArgumentParser(add_help=False)
base_p.add_argument("input", nargs="?", help="Path to input media file")
base_p.add_argument("-o", "--output", help="Path for output file") base_p.add_argument("input", nargs="?", help="Input file or directory")
base_p.add_argument("--override", action="store_true", help="Override input") base_p.add_argument("-o", "--output", help="Output file")
base_p.add_argument("--backup", action="store_true", help="Backup original file when overriding") base_p.add_argument("--override", action="store_true", help="Overwrite input")
base_p.add_argument("--loud", action="store_true", help="Run ffmpeg loud") base_p.add_argument("--backup", action="store_true", help="Backup original")
base_p.add_argument("--loud", action="store_true", help="Show ffmpeg output")
base_p.add_argument("--kidflag", action="store_true", help="Use kid3 instead of ffmpeg") base_p.add_argument("--kidflag", action="store_true", help="Use kid3 instead of ffmpeg")
base_p.add_argument("-r", "--recursive", action="store_true") base_p.add_argument("-r", "--recursive", action="store_true", help="Process recursively")
base_p.add_argument("-ll","--log-level",type=str.upper,default="INFO",
choices=["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help="Set log level", )
base_p.add_argument("--dry-run",action="store_true",help="Dry run only",)
sub = parser.add_subparsers(dest="command") # command is optional now
# ---------------------------- base_p.add_argument(
"-ll",
"--log-level",
type=str.upper,
default="INFO",
choices=["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
)
base_p.add_argument(
"--dry-run",
action="store_true",
help="Do not execute actions",
)
sub = parser.add_subparsers(
dest="command",
required=True,
)
# =========================================================
# INFO # INFO
# ---------------------------- # =========================================================
cmd=commands["info"]
p_info = sub.add_parser(str(cmd["name"]), aliases=["i"], parents=[base_p], help="Inspect media metadata")
p_info.set_defaults(func=cmd_info, command=cmd["name"])
add_command(
sub,
"info",
cmd_info,
"Inspect media metadata",
aliases=["i"],
parents=[base_p],
)
# =========================================================
# TEST # TEST
cmd=commands["test"] # =========================================================
p_test = sub.add_parser(str(cmd["name"]), aliases=["t"], parents=[base_p], help="Run test function")
p_test.add_argument("--choices", nargs="+",choices=list(commands.keys()),
default=list(commands.keys()),
help="Run a predefined test")
p_test.set_defaults(func=cmd_test, command=cmd["name"])
# ---------------------------- p = add_command(
sub,
"test",
cmd_test,
"Run tests",
aliases=["t"],
parents=[base_p],
)
p.add_argument(
"--choices",
nargs="+",
choices=list(commands.keys()),
default=list(commands.keys()),
)
# =========================================================
# CONVERT AV # CONVERT AV
# ---------------------------- # =========================================================
cmd=commands["convert-av"]
p_av = sub.add_parser(str(cmd["name"]), aliases=["cav"], parents=[base_p], help="Full re-encode of video/audio")
p_av.add_argument("--video-codec", default="libx264",
help="Video codec to use (default: libx264, e.g. libx265, vp9)")
p_av.add_argument("--audio-codec", default="aac", help="Audio codec to use (default: aac, e.g. mp3, opus)")
p_av.add_argument("--preset", default="medium",
help="Encoding speed/quality preset (default: medium, e.g. ultrafast, fast, slow, veryslow)")
p_av.add_argument("--quality", type=int,
help="Output quality level (e.g. CRF value or bitrate depending on encoder)")
p_av.set_defaults(func=cmd_convert_av, command=cmd["name"])
# ---------------------------- p = add_command(
sub,
"convert-av",
cmd_convert_av,
"Full video/audio re-encode",
aliases=["cav"],
parents=[base_p],
)
p.add_argument("--video-codec", default="libx264")
p.add_argument("--audio-codec", default="aac")
p.add_argument("--preset", default="medium")
p.add_argument("--quality", type=int)
# =========================================================
# CONVERT AUDIO # CONVERT AUDIO
# ---------------------------- # =========================================================
cmd=commands["convert-audio"]
p_a = sub.add_parser(str(cmd["name"]), aliases=["ca"], parents=[base_p], help="Change audio format only")
p_a.add_argument("--audio-codec", default="aac", help="Audio codec to use (default: aac, e.g. mp3, opus)")
p_a.set_defaults(func=cmd_convert_audio, command=cmd["name"])
# ---------------------------- p = add_command(
sub,
"convert-audio",
cmd_convert_audio,
"Convert audio only",
aliases=["ca"],
parents=[base_p],
)
p.add_argument("--audio-codec", default="aac")
# =========================================================
# CONVERT VIDEO # CONVERT VIDEO
# ---------------------------- # =========================================================
cmd=commands["convert-video"]
p_v = sub.add_parser(str(cmd["name"]), aliases=["cv"], parents=[base_p], help="Change video format only")
p_v.add_argument("--video-codec", default="libx264",
help="Video codec to use (default: libx264, e.g. libx265, vp9)")
p_v.set_defaults(func=cmd_convert_video, command=cmd["name"])
# ---------------------------- p = add_command(
sub,
"convert-video",
cmd_convert_video,
"Convert video only",
aliases=["cv"],
parents=[base_p],
)
p.add_argument("--video-codec", default="libx264")
# =========================================================
# REMOVE SUBS # REMOVE SUBS
# ---------------------------- # =========================================================
cmd=commands["remove-subs"]
p_rs = sub.add_parser(str(cmd["name"]), aliases=["rs"], parents=[base_p], help="Strip all subtitle streams (-sn)")
p_rs.set_defaults(func=cmd_remove_subs, command=cmd["name"])
# ---------------------------- add_command(
# REMOVE AUDIO TRACK sub,
# ---------------------------- "remove-subs",
cmd=commands["remove-audio"] cmd_remove_subs,
p_ra = sub.add_parser(str(cmd["name"]), aliases=["ra"], parents=[base_p], help="Remove one audio track") "Strip subtitle streams",
p_ra.add_argument("-a", "--audio-index", type=int) aliases=["rs"],
p_ra.set_defaults(func=cmd_remove_audio_track, command=cmd["name"]) parents=[base_p],
)
# ---------------------------- # =========================================================
# EXTRACT AUDIO TRACK # REMOVE AUDIO
# ---------------------------- # =========================================================
cmd=commands["extract-audio"]
p_ea = sub.add_parser(str(cmd["name"]), aliases=["ea"], parents=[base_p], help="Extract one audio track")
p_ea.add_argument("-a", "--audio-index", type=int)
p_ea.set_defaults(func=cmd_extract_audio_track, command=cmd["name"])
# ---------------------------- p = add_command(
sub,
"remove-audio",
cmd_remove_audio_track,
"Remove audio track",
aliases=["ra"],
parents=[base_p],
)
add_audio_index(p)
# =========================================================
# EXTRACT AUDIO
# =========================================================
p = add_command(
sub,
"extract-audio",
cmd_extract_audio_track,
"Extract audio track",
aliases=["ea"],
parents=[base_p],
)
add_audio_index(p)
# =========================================================
# DOWNSIZE # DOWNSIZE
# ---------------------------- # =========================================================
cmd=commands["downsize"]
p_ds = sub.add_parser(str(cmd["name"]), aliases=["d"], parents=[base_p], help="Scale video dimensions (-vf scale)")
p_ds.add_argument("-w", "--width", type=int, help="width: eg 1080")
p_ds.add_argument("-he", "--height", type=int, help="height: eg 720")
p_ds.add_argument("-c" , "--crf", type=int, help="crf: eg 23")
p_ds.set_defaults(func=cmd_downsize, command=cmd["name"])
# ---------------------------- p = add_command(
sub,
"downsize",
cmd_downsize,
"Scale video",
aliases=["d"],
parents=[base_p],
)
p.add_argument("-w", "--width", type=int)
p.add_argument("-he", "--height", type=int)
p.add_argument("-c", "--crf", type=int)
# =========================================================
# FILTER AUDIO # FILTER AUDIO
# ---------------------------- # =========================================================
cmd=commands["filter-audio"]
p_fa = sub.add_parser(str(cmd["name"]), aliases=["fa"], parents=[base_p], help="Apply audio enhancement chain")
p_fa.add_argument("-a", "--audio-index", type=int)
p_fa.set_defaults(func=cmd_filter_audio, command=cmd["name"])
# ---------------------------- p = add_command(
sub,
"filter-audio",
cmd_filter_audio,
"Apply audio filters",
aliases=["fa"],
parents=[base_p],
)
add_audio_index(p)
# =========================================================
# CUT # CUT
# ---------------------------- # =========================================================
cmd=commands["cut"]
p_c = sub.add_parser(str(cmd["name"]), aliases=["c"], parents=[base_p], help="Cut X seconds out of video")
p_c.add_argument("--start")
p_c.add_argument("--duration")
p_c.set_defaults(func=cmd_cut_segment, command=cmd["name"])
# ---------------------------- p = add_command(
sub,
"cut",
cmd_cut_segment,
"Cut media segment",
aliases=["c"],
parents=[base_p],
)
p.add_argument("--start")
p.add_argument("--duration")
# =========================================================
# SET PRIMARY AUDIO # SET PRIMARY AUDIO
# ---------------------------- # =========================================================
cmd=commands["set-primary-audio"]
p_pa = sub.add_parser(str(cmd["name"]), aliases=["spa"], parents=[base_p], help="Set primary audio track")
p_pa.add_argument("-a", "--audio-index", type=int)
p_pa.set_defaults(func=cmd_set_primary_audio, command=cmd["name"])
# ---------------------------- p = add_command(
# REMOVE ALL RUSSIAN AUDIO sub,
# ---------------------------- "set-primary-audio",
cmd = commands["remove-russian-from-db"] cmd_set_primary_audio,
p_ch = sub.add_parser(str(cmd["name"]), aliases=["rrfd"], parents=[base_p], "Set default audio track",
help="Remove all russian audio tracks from media files in path") aliases=["spa"],
p_ch.set_defaults(func=cmd_remove_russian_from_db, command=cmd["name"]) parents=[base_p],
)
add_audio_index(p)
# ---------------------------- # =========================================================
# APPLY CHAPTERS # REMOVE RUSSIAN AUDIO
# ---------------------------- # =========================================================
cmd=commands["chapters"]
p_ch = sub.add_parser(str(cmd["name"]),aliases=["ch"],parents=[base_p],help="Apply chapter metadata from ffmetadata file")
p_ch.add_argument("-cf","--chapter-file",help="Path to ffmetadata chapters file (e.g. chapters.txt)")
p_ch.set_defaults(func=cmd_apply_chapters, command=cmd["name"])
# ---------------------------- add_command(
# METADATA sub,
# ---------------------------- "remove-russian-from-db",
cmd_remove_russian_from_db,
"Remove russian audio tracks",
aliases=["rrfd"],
parents=[base_p],
)
# PRINT METADATA # =========================================================
cmd=commands["metadata-print"] # CHAPTERS
p_ch = sub.add_parser(str(cmd["name"]), aliases=["mp"], parents=[base_p], # =========================================================
help="Show metadata")
p_ch.set_defaults(func=cmd_show_metadata, command=cmd["name"])
# SET METADATA p = add_command(
cmd = commands["metadata-set"] sub,
p_ch = sub.add_parser(str(cmd["name"]),aliases=["ms"],parents=[base_p],help="Apply metadata key and value to input file") "chapters",
p_ch.add_argument("-k","--key",help="Metadata key name)") cmd_apply_chapters,
p_ch.add_argument("-v","--value",help="Metadata key value") "Apply chapter metadata",
p_ch.set_defaults(func=cmd_set_metadata, command=cmd["name"]) aliases=["ch"],
parents=[base_p],
)
p.add_argument(
"-cf",
"--chapter-file",
help="ffmetadata chapter file",
)
# =========================================================
# METADATA PRINT
# =========================================================
add_command(
sub,
"metadata-print",
cmd_show_metadata,
"Show metadata",
aliases=["mp"],
parents=[base_p],
)
# =========================================================
# METADATA SET
# =========================================================
p = add_command(
sub,
"metadata-set",
cmd_set_metadata,
"Set metadata value",
aliases=["ms"],
parents=[base_p],
)
p.add_argument("-k", "--key")
p.add_argument("-v", "--value")
# ========================================================= # =========================================================
# PARSE # PARSE
# ========================================================= # =========================================================
args = parser.parse_args() args = parser.parse_args()
logging.basicConfig( logging.basicConfig(
@@ -1714,16 +1880,16 @@ def main():
out_path = file.with_name(f"{file.stem}_override{file.suffix}") out_path = file.with_name(f"{file.stem}_override{file.suffix}")
else: else:
out_path = file.with_name(f"{file.stem}_{args.command}{file.suffix}") out_path = file.with_name(f"{file.stem}_{args.command}{file.suffix}")
args.output = str(out_path) args.output = out_path
args.file_counter = counter args.file_counter = counter
out = args.func(args) out = args.func(args)
if out != "No output needed": if out != "nooutputneeded":
outputs.append(out) outputs.append(out)
counter+=1 counter+=1
# ---- SINGLE FILE ---- # ---- SINGLE FILE ----
elif input_path.is_file(): elif input_path.is_file():
out = args.func(args) out = args.func(args)
if out != "No output needed": if out != "nooutputneeded":
outputs.append(out) outputs.append(out)
else: else:
raise ValueError(f"Invalid input path: {args.input}") raise ValueError(f"Invalid input path: {args.input}")