add extract-audio and loop through dir for all commands

This commit is contained in:
Lucas Faber authored and Lucas Vivohost committed 2026-09-14 09:20:47 +02:00
1 parent e3011ec767
commit 0d5f084cd2
1 file changed
+94 -46
+94 -46
View File
@@ -14,6 +14,7 @@ import json
import ffmpeg import ffmpeg
import re import re
from pathlib import Path from pathlib import Path
VIDEO_EXTS = {".mp4", ".mkv", ".avi", ".mov", ".webm"}
# ---------------------------- # ----------------------------
# OUTPUT ENFORCEMENT (clean + centralized) # OUTPUT ENFORCEMENT (clean + centralized)
@@ -24,6 +25,7 @@ REQUIRES_OUTPUT = {
"convert-video", "convert-video",
"remove-subs", "remove-subs",
"remove-audio", "remove-audio",
"extract-audio",
"downsize", "downsize",
"filter-audio", "filter-audio",
"cut", "cut",
@@ -220,7 +222,7 @@ def cmd_convert_av(args):
# Prevents FFmpeg from trying to guess/add extra default streams # Prevents FFmpeg from trying to guess/add extra default streams
map_metadata=0 map_metadata=0
) )
return [out] return out
def cmd_convert_audio(args): def cmd_convert_audio(args):
""" """
@@ -242,7 +244,7 @@ def cmd_convert_audio(args):
map='0', map='0',
map_metadata=0 map_metadata=0
) )
return [out] return out
def cmd_convert_video(args): def cmd_convert_video(args):
""" """
@@ -261,7 +263,7 @@ def cmd_convert_video(args):
map='0', map='0',
map_metadata=0 map_metadata=0
) )
return [out] return out
def cmd_remove_subs(args): def cmd_remove_subs(args):
""" """
@@ -282,12 +284,11 @@ def cmd_remove_subs(args):
map='0', map='0',
map_metadata=0 map_metadata=0
) )
return [out] return out
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).
2. 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(args.input)
@@ -295,35 +296,47 @@ def cmd_remove_audio_track(args):
remove_idx = select_audio_stream(args.input, args.audio_index) remove_idx = select_audio_stream(args.input, args.audio_index)
streams_to_keep = [] streams_to_keep = []
extracted_audio = None
for s in probe["streams"]: for s in probe["streams"]:
idx = s["index"] idx = s["index"]
if idx == remove_idx: if idx != remove_idx:
extracted_audio = inp[str(idx)]
else:
streams_to_keep.append(inp[str(idx)]) streams_to_keep.append(inp[str(idx)])
video_out = ffmpeg.output( out = ffmpeg.output(
*streams_to_keep, *streams_to_keep,
args.output, args.output,
vcodec="copy", vcodec="copy",
acodec="copy", acodec="copy",
scodec= get_scodec(args.output), scodec= get_scodec(args.output),
) )
return out
outputs = [video_out] def cmd_extract_audio_track(args):
"""
1. Saves that specific audio track as a standalone MP3 (Extra Output).
"""
inp = ffmpeg.input(args.input)
probe = ffmpeg.probe(args.input)
if args.output_audio: output_audio = Path(args.output)
audio_out = ffmpeg.output( output_audio = str(output_audio.with_name(f"{output_audio.stem}.mp3"))
extracted_audio,
args.output_audio,
acodec='libmp3lame',
audio_bitrate='192k'
)
outputs.append(audio_out)
return outputs remove_idx = select_audio_stream(args.input, args.audio_index)
extracted_audio = None
for s in probe["streams"]:
idx = s["index"]
if idx == remove_idx:
extracted_audio = inp[str(idx)]
out = ffmpeg.output(
extracted_audio,
output_audio,
acodec='libmp3lame',
audio_bitrate='192k'
)
return out
def cmd_downsize(args): def cmd_downsize(args):
""" """
@@ -356,15 +369,15 @@ def cmd_downsize(args):
acodec="copy", acodec="copy",
scodec=get_scodec(args.output) scodec=get_scodec(args.output)
) )
return [out] return out
def cmd_filter_audio(args): def cmd_filter_audio(args):
""" """
Multi-stage audio mastering chain. Multi-stage audio mastering chain.
FFmpeg Equivalent: FFmpeg Equivalent:
ffmpeg -i [input] -filter_complex "[0:a:X]highpass...,acompressor...,equalizer...[out]" \ ffmpeg -i [input] -filter_complex "[0:a:X]highpass...,acompressor...,equalizer...out" \
-map 0:v -map [out] -c:v copy -c:a libvorbis [output] -map 0:v -map out -c:v copy -c:a libvorbis [output]
Filter Descriptions: Filter Descriptions:
1. highpass: Cuts frequencies below 100Hz (removes low-end rumble). 1. highpass: Cuts frequencies below 100Hz (removes low-end rumble).
@@ -384,6 +397,7 @@ def cmd_filter_audio(args):
for s in probe["streams"] for s in probe["streams"]
if s.get("codec_type") == "audio" if s.get("codec_type") == "audio"
] ]
filtered_audios = [] filtered_audios = []
for idx in audio_streams: for idx in audio_streams:
audio = inp[str(idx)] audio = inp[str(idx)]
@@ -400,15 +414,14 @@ def cmd_filter_audio(args):
) )
filtered_audios.append(audio_filtered) filtered_audios.append(audio_filtered)
else: else:
filtered_audios.append(audio) filtered_audios.append(audio)
info = ffmpeg.probe(args.input)
streams = info.get("streams", []) streams = probe.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"]
subs = {} subs = []
if len(subtitle_streams) > 0: if subtitle_streams:
subs = {inp["s"]} subs.append(inp["s"])
out = ffmpeg.output( out = ffmpeg.output(
inp.video, inp.video,
@@ -420,7 +433,7 @@ def cmd_filter_audio(args):
scodec=get_scodec(args.output), scodec=get_scodec(args.output),
) )
return [out] return out
def cmd_cut_segment(args): def cmd_cut_segment(args):
""" """
@@ -452,7 +465,7 @@ def cmd_cut_segment(args):
map="0" map="0"
) )
return [out] return out
def cmd_set_primary_audio(args): def cmd_set_primary_audio(args):
""" """
@@ -514,7 +527,7 @@ def cmd_set_primary_audio(args):
**out_kwargs **out_kwargs
) )
return [out] return out
def cmd_test(args): def cmd_test(args):
""" """
@@ -568,10 +581,6 @@ def cmd_test(args):
if hasattr(args, "output") and args.output: if hasattr(args, "output") and args.output:
out_files.append(args.output) out_files.append(args.output)
if hasattr(args, "output_audio") and args.output_audio:
out_files.append(args.output_audio)
for i, f in enumerate(out_files): for i, f in enumerate(out_files):
if not os.path.exists(f): if not os.path.exists(f):
print(f"{red_tag("FAIL")} Missing output: {f}") print(f"{red_tag("FAIL")} Missing output: {f}")
@@ -708,12 +717,20 @@ def cmd_test(args):
for test_file in test_files: for test_file in test_files:
video_streams,audio_streams,subtitle_streams = get_streams(test_file) video_streams,audio_streams,subtitle_streams = get_streams(test_file)
args.input = test_file args.input = test_file
test_dict = [{"video_count": len(video_streams), "audio_count": 1, "subtitle_count": len(subtitle_streams)}, test_dict = [{"video_count": len(video_streams), "audio_count": 1, "subtitle_count": len(subtitle_streams)}]
{"video_count": 0, "audio_count": 1, "subtitle_count": 0}]
args.output = os.path.join(test_dir, "out_remove_audio" + os.path.splitext(test_file)[1]) args.output = os.path.join(test_dir, "out_remove_audio" + os.path.splitext(test_file)[1])
args.output_audio = os.path.join(test_dir, "extracted_audio_" + os.path.splitext(test_file)[1][1:] + ".mp3")
ret += run_and_probe("remove-audio-track", cmd_remove_audio_track, args, test_dict) ret += run_and_probe("remove-audio-track", cmd_remove_audio_track, args, test_dict)
# ---------- EXTRACT AUDIO TRACK ----------
args = Args()
args.audio_index = 1
for test_file in test_files:
args.input = test_file
test_dict = [{"video_count": 0, "audio_count": 1, "subtitle_count": 0}]
args.output = os.path.join(test_dir, "extracted_audio_" + os.path.splitext(test_file)[1][1:] + ".mp3")
ret += run_and_probe("remove-audio-track", cmd_extract_audio_track, args, test_dict)
# ---------- CONVERT AV ---------- # ---------- CONVERT AV ----------
args = Args() args = Args()
for test_file in test_files: for test_file in test_files:
@@ -864,10 +881,16 @@ def main():
# REMOVE AUDIO TRACK # REMOVE AUDIO TRACK
# ---------------------------- # ----------------------------
p_ra = sub.add_parser("remove-audio", aliases=["ra"], parents=[base_p], help="Remove one audio track") p_ra = sub.add_parser("remove-audio", aliases=["ra"], parents=[base_p], help="Remove one audio track")
p_ra.add_argument("-oa", "--output-audio")
p_ra.add_argument("-a", "--audio-index", type=int) p_ra.add_argument("-a", "--audio-index", type=int)
p_ra.set_defaults(func=cmd_remove_audio_track, command="remove-audio") p_ra.set_defaults(func=cmd_remove_audio_track, command="remove-audio")
# ----------------------------
# REMOVE AUDIO TRACK
# ----------------------------
p_ea = sub.add_parser("extract-audio", 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="extract-audio")
# ---------------------------- # ----------------------------
# DOWNSIZE # DOWNSIZE
# ---------------------------- # ----------------------------
@@ -931,11 +954,13 @@ def main():
# ---------------------------- # ----------------------------
if args.command != "test": if args.command != "test":
args.input = ensure(getattr(args, "input", None), f"Input file ({Path.cwd()})") args.input = ensure(getattr(args, "input", None), f"Input file ({Path.cwd()})")
if not Path(args.input).is_file(): if not Path(args.input).expanduser().exists():
parser.error("Input file does not exist!") parser.error("Input file does not exist!")
if args.command in REQUIRES_OUTPUT and not getattr(args, "output", None): if args.command in REQUIRES_OUTPUT and not getattr(args, "output", None):
args.output = ensure(getattr(args, "output", None), "Output file") args.output = ensure(getattr(args, "output", None), "Output file (enter for default file)")
if args.output == "" or args.output is None:
input_path = Path(args.input)
args.output = str(input_path.with_name(f"{input_path.stem}_{args.command}{input_path.suffix}"))
# ---------------------------- # ----------------------------
# COMMAND-SPECIFIC REQUIRED FIELDS # COMMAND-SPECIFIC REQUIRED FIELDS
# ---------------------------- # ----------------------------
@@ -960,17 +985,40 @@ def main():
args.height = ensure(getattr(args, "height", None), "height: eg 720") args.height = ensure(getattr(args, "height", None), "height: eg 720")
args.crf = ensure(getattr(args, "crf", None), "crf: eg 23") args.crf = ensure(getattr(args, "crf", None), "crf: eg 23")
if args.command in ["filter-audio", "remove-audio", "set-primary-audio"]: if args.command in ["filter-audio", "remove-audio", "extract-audio", "set-primary-audio"]:
args.audio_index = ensure(getattr(args, "audio_index", None), "Audio index") args.audio_index = ensure(getattr(args, "audio_index", None), "Audio index")
if args.audio_index is None:
args.audio_index = 1
# ---------------------------- # ----------------------------
# EXECUTE COMMAND # EXECUTE COMMAND
# ---------------------------- # ----------------------------
try: try:
outputs = args.func(args) if args.command == "test":
args.func(args)
sys.exit(0)
outputs = []
input_path = Path(args.input)
# ---- DIRECTORY ----
if input_path.is_dir():
for file in input_path.iterdir():
if file.suffix.lower() not in VIDEO_EXTS:
continue
print(f"Processing File: {file}")
out_path = file.with_name(f"{file.stem}_{args.command}{file.suffix}")
args.input = str(file)
args.output = str(out_path)
outputs.append(args.func(args))
# ---- SINGLE FILE ----
elif input_path.is_file():
outputs.append(args.func(args))
else:
raise ValueError(f"Invalid input path: {args.input}")
if outputs is None: if outputs is None:
return raise ValueError(f"No Output: {outputs}")
if not isinstance(outputs, list): if not isinstance(outputs, list):
outputs = [outputs] outputs = [outputs]