diff --git a/media_editing/ffmpeg_handling.py b/media_editing/ffmpeg_handling.py index f56d319..4d24792 100755 --- a/media_editing/ffmpeg_handling.py +++ b/media_editing/ffmpeg_handling.py @@ -172,7 +172,7 @@ def move_override_file(input_path, backup_files=False,silent=False): 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 = { "overwrite_output": overwrite, } @@ -184,6 +184,9 @@ def run_ffmpeg(stream, overwrite=True, loud=False): quiet=True, ) + if dry_run: + logger.debug(f"Dry run: {stream}") + return try: out, err = ffmpeg.run(stream, **kwargs) logger.trace(out) @@ -358,7 +361,7 @@ def cmd_info(args): ffprobe -show_format -show_streams -of json [input] """ try: - info = ffmpeg.probe(args.input) + info = ffmpeg.probe(str(args.input)) logger.info(json.dumps(info, indent=2)) except ffmpeg.Error as e: logger.error(f"Error probing file:", e) @@ -443,7 +446,7 @@ def cmd_remove_subs(args): Parameters: - sn: Disables subtitle recording. """ - probe = ffmpeg.probe(args.input) + probe = ffmpeg.probe(str(args.input)) cmd = [ "ffmpeg", @@ -474,7 +477,9 @@ def cmd_remove_subs(args): ]) logger.debug("%s", " ".join(cmd)) - + if args.dry_run: + logger.debug(f"Dry run: {" ".join(cmd)}") + return "nooutputneeded" try: subprocess.run( cmd, @@ -488,12 +493,12 @@ def cmd_remove_subs(args): logger.error("STDOUT:\n%s", e.stdout) logger.error("STDERR:\n%s", e.stderr) raise - return "No output needed" + return "nooutputneeded" def cmd_remove_audio_track(args): """ 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) @@ -541,7 +546,8 @@ def cmd_remove_audio_track(args): cmd.append(str(args.output)) logger.debug(" ".join(cmd)) if args.dry_run: - return "No Output needed" + logger.debug(f"Dry run: {" ".join(cmd)}") + return "nooutputneeded" try: result = subprocess.run( cmd, @@ -551,7 +557,7 @@ def cmd_remove_audio_track(args): ) logger.debug("FFmpegsubprocess successful") logger.trace(result.stdout) - return "No output needed" + return "nooutputneeded" except subprocess.CalledProcessError as e: logging.error( @@ -567,7 +573,7 @@ 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) + probe = ffmpeg.probe(str(args.input)) output_audio = Path(args.output) 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) video = inp.video.filter("scale", args.width, args.height) - info = ffmpeg.probe(args.input) + info = ffmpeg.probe(str(args.input)) streams = info.get("streams", []) 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) inp = ffmpeg.input(args.input) - probe = ffmpeg.probe(args.input) + probe = ffmpeg.probe(str(args.input)) audio_streams = [] audio_titles = {} @@ -790,7 +796,7 @@ def cmd_set_primary_audio(args): inp = ffmpeg.input(args.input) primary_audio = select_audio_stream(args.input, args.audio_index) - probe = ffmpeg.probe(args.input) + probe = ffmpeg.probe(str(args.input)) audio_streams = [ s["index"] @@ -811,7 +817,7 @@ def cmd_set_primary_audio(args): else: out_kwargs[f"disposition:a:{i}"] = "0" - info = ffmpeg.probe(args.input) + info = ffmpeg.probe(str(args.input)) streams = info.get("streams", []) 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. """ - probe = ffmpeg.probe(args.input) + probe = ffmpeg.probe(str(args.input)) format_tags = probe.get("format", {}).get("tags", {}) if show_metadata: @@ -877,7 +883,9 @@ def cmd_set_metadata(args): "-c", f"set {args.key} '{args.value}'", args.input ] - + if args.dry_run: + logger.debug(f"Dry run: {" ".join(cmd)}") + return "nooutputneeded" result = subprocess.run(cmd, capture_output=True, text=True, @@ -966,7 +974,9 @@ def cmd_apply_chapters(args): "-c", "copy", args.output ] - + if args.dry_run: + logger.debug(f"Dry run: {" ".join(cmd)}") + return "nooutputneeded" result = subprocess.run(cmd, capture_output=True, text=True, @@ -987,10 +997,13 @@ def cmd_apply_chapters(args): audio = EasyID3(filename) audio["title"] = title audio["tracknumber"] = str(args.file_counter+1) + if args.dry_run: + logger.debug(f"Dry run") + return "nooutputneeded" audio.save() safe_rename(filename, new_path) - return "No output needed" + return "nooutputneeded" def cmd_remove_russian_from_db(args_in): """ @@ -1044,7 +1057,7 @@ def cmd_test(args): if not outputs: logger.error(f"No outputs returned.") return False - if outputs != "No output needed": + if outputs != "nooutputneeded": try: ffmpeg.run( outputs, @@ -1439,170 +1452,323 @@ def cmd_test(args): # ---------- CLI Setup ---------- # -def main(): - parser = argparse.ArgumentParser( - description="Media processing tool using ffmpeg-python (CLI + interactive fallback)", - formatter_class=argparse.ArgumentDefaultsHelpFormatter +def add_command( + subparsers, + name, + 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", ) - # ---------------------------- - # BASE ARGS (shared) - # ---------------------------- +def main(): + 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.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("--override", action="store_true", help="Override input") - base_p.add_argument("--backup", action="store_true", help="Backup original file when overriding") - base_p.add_argument("--loud", action="store_true", help="Run ffmpeg loud") + + base_p.add_argument("input", nargs="?", help="Input file or directory") + base_p.add_argument("-o", "--output", help="Output file") + base_p.add_argument("--override", action="store_true", help="Overwrite input") + 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("-r", "--recursive", action="store_true") - 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("-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"], + ) + + base_p.add_argument( + "--dry-run", + action="store_true", + help="Do not execute actions", + ) + + sub = parser.add_subparsers( + dest="command", + required=True, + ) + + # ========================================================= # 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 - 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 - # ---------------------------- - 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 - # ---------------------------- - 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 - # ---------------------------- - 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 - # ---------------------------- - 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"]) + # ========================================================= - # ---------------------------- - # REMOVE AUDIO TRACK - # ---------------------------- - cmd=commands["remove-audio"] - p_ra = sub.add_parser(str(cmd["name"]), aliases=["ra"], parents=[base_p], help="Remove one audio track") - p_ra.add_argument("-a", "--audio-index", type=int) - p_ra.set_defaults(func=cmd_remove_audio_track, command=cmd["name"]) + add_command( + sub, + "remove-subs", + cmd_remove_subs, + "Strip subtitle streams", + aliases=["rs"], + parents=[base_p], + ) - # ---------------------------- - # EXTRACT AUDIO TRACK - # ---------------------------- - 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"]) + # ========================================================= + # REMOVE AUDIO + # ========================================================= - # ---------------------------- + 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 - # ---------------------------- - 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 - # ---------------------------- - 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 - # ---------------------------- - 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 - # ---------------------------- - 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"]) + # ========================================================= - # ---------------------------- - # REMOVE ALL RUSSIAN AUDIO - # ---------------------------- - cmd = commands["remove-russian-from-db"] - p_ch = sub.add_parser(str(cmd["name"]), aliases=["rrfd"], parents=[base_p], - help="Remove all russian audio tracks from media files in path") - p_ch.set_defaults(func=cmd_remove_russian_from_db, command=cmd["name"]) + p = add_command( + sub, + "set-primary-audio", + cmd_set_primary_audio, + "Set default audio track", + aliases=["spa"], + parents=[base_p], + ) + add_audio_index(p) - # ---------------------------- - # APPLY CHAPTERS - # ---------------------------- - 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"]) + # ========================================================= + # REMOVE RUSSIAN AUDIO + # ========================================================= - # ---------------------------- - # METADATA - # ---------------------------- + add_command( + 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"] - 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"]) + # ========================================================= + # CHAPTERS + # ========================================================= - # SET METADATA - cmd = commands["metadata-set"] - p_ch = sub.add_parser(str(cmd["name"]),aliases=["ms"],parents=[base_p],help="Apply metadata key and value to input file") - p_ch.add_argument("-k","--key",help="Metadata key name)") - p_ch.add_argument("-v","--value",help="Metadata key value") - p_ch.set_defaults(func=cmd_set_metadata, command=cmd["name"]) + p = add_command( + sub, + "chapters", + cmd_apply_chapters, + "Apply chapter metadata", + 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 # ========================================================= + args = parser.parse_args() logging.basicConfig( @@ -1714,16 +1880,16 @@ def main(): out_path = file.with_name(f"{file.stem}_override{file.suffix}") else: 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 out = args.func(args) - if out != "No output needed": + if out != "nooutputneeded": outputs.append(out) counter+=1 # ---- SINGLE FILE ---- elif input_path.is_file(): out = args.func(args) - if out != "No output needed": + if out != "nooutputneeded": outputs.append(out) else: raise ValueError(f"Invalid input path: {args.input}")