From 59a15a617755bb7866ee830fa34abeea5cf336e0 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 28 Sep 2023 13:05:56 -0400 Subject: [PATCH] Add video trimmer script --- .../nautilus/scripts/Open in Video Trimmer | 4 + configs/scripts/video_trimmer.py | 242 ++++++++++++++++++ install.conf.yaml | 4 + 3 files changed, 250 insertions(+) create mode 100755 configs/nautilus/scripts/Open in Video Trimmer create mode 100755 configs/scripts/video_trimmer.py diff --git a/configs/nautilus/scripts/Open in Video Trimmer b/configs/nautilus/scripts/Open in Video Trimmer new file mode 100755 index 0000000..2839891 --- /dev/null +++ b/configs/nautilus/scripts/Open in Video Trimmer @@ -0,0 +1,4 @@ +#! /bin/bash +set -e + +python3 ~/.config/ewconfig/configs/scripts/video_trimmer.py \ No newline at end of file diff --git a/configs/scripts/video_trimmer.py b/configs/scripts/video_trimmer.py new file mode 100755 index 0000000..659060d --- /dev/null +++ b/configs/scripts/video_trimmer.py @@ -0,0 +1,242 @@ +#! /usr/bin/env python3 +import os +import argparse +import sys +import re +import datetime +import subprocess +import tkinter as tk +from pathlib import Path + +ALLOWED_INPUT_FORMATS = ["mp4"] +TIMESTAMP_FORMAT_RE = re.compile(r"^\d{2}:\d{2}:\d{2}$") +RENDER_MODES = ["Video + Audio", "Video Only", "Audio Only"] + + +def open_in_video_player(file: Path): + os.system(f"xdg-open {file}") + + +def render( + input_file: Path, + output_file: Path, + start_timestamp: str, + end_timestamp: str, + mode: str, +): + # Construct the appropriate ffmpeg command + ffmpeg_command = ["ffmpeg"] + + # Add the input file + ffmpeg_command += ["-i", str(input_file)] + + # Add the start and end timestamps + ffmpeg_command += ["-ss", start_timestamp] + ffmpeg_command += ["-to", end_timestamp] + + # Add the mode + if mode == "Video + Audio": + ffmpeg_command += ["-c", "copy"] + elif mode == "Video Only": + ffmpeg_command += ["-c", "copy"] + ffmpeg_command += ["-an"] + elif mode == "Audio Only": + ffmpeg_command += ["-vn"] + + # Add the output file + ffmpeg_command += [str(output_file)] + + # Run the command. Open in a new terminal window + subprocess.call(ffmpeg_command) + + +def do_preview(input_file: Path, start_timestamp: str, end_timestamp: str, mode: str): + # Start by rendering to a tempfile with the same extension as the input file + temp_file = Path("/tmp") / f"{input_file.stem}_trimmed{input_file.suffix}" + temp_file.unlink(missing_ok=True) + render(input_file, temp_file, start_timestamp, end_timestamp, mode) + + # Display the temp file in a video player + open_in_video_player(temp_file) + + +def do_render(input_file: Path, start_timestamp: str, end_timestamp: str, mode: str): + # Create the new file beside the old one + start_time_str = start_timestamp.replace(":", ".") + end_time_str = end_timestamp.replace(":", ".") + output_file = ( + input_file.parent + / f"{input_file.stem}_trimmed_{start_time_str}_{end_time_str}_render{input_file.suffix}" + ) + output_file.unlink(missing_ok=True) + + # Call the render function + render(input_file, output_file, start_timestamp, end_timestamp, mode) + + # Copy the timestamp metadata from the original file (force overwrite) + subprocess.call(["ffmpeg", "-i", str(input_file), "-i", str(output_file), "-map_metadata", "0", "-map", "0", "-map", "1", "-c", "copy", "-y", str(output_file)]) + + # Set the file timestamp to the same as the original file + subprocess.call(["touch", "-r", str(input_file), str(output_file)]) + + +def build_gui(input_file: Path) -> tk.Tk: + # Build the GUI + root = tk.Tk() + root.title("Evan's Video Trimmer") + # root.geometry("280x500") + + # Add a section title + title = tk.Label(root, text="Input File") + title.grid(row=0, column=0, columnspan=2) + + # Add a button to open the original file + open_original_button = tk.Button( + root, + text="Open original file", + command=lambda: open_in_video_player(input_file), + ) + open_original_button.grid(row=2, column=0, columnspan=2, pady=2) + + # Add a horizontal separator + separator = tk.Frame(height=2, bd=1, relief=tk.SUNKEN) + separator.grid(row=3, column=0, columnspan=2, sticky=tk.W + tk.E, pady=2) + + # Add a section title + title = tk.Label(root, text="Output Controls") + title.grid(row=4, column=0, columnspan=2, pady=2) + + # Add an input field for start timestamp + start_timestamp = tk.StringVar() + start_timestamp.set("00:00:00") + start_timestamp_label = tk.Label(root, text="Start Timestamp") + start_timestamp_label.grid(row=5, column=0, sticky=tk.E) + start_timestamp_input = tk.Entry(root, textvariable=start_timestamp) + start_timestamp_input.grid(row=5, column=1) + + # Add an input field for end timestamp + end_timestamp = tk.StringVar() + end_timestamp.set("00:00:00") + end_timestamp_label = tk.Label(root, text="End Timestamp") + end_timestamp_label.grid(row=6, column=0, sticky=tk.E) + end_timestamp_input = tk.Entry(root, textvariable=end_timestamp) + end_timestamp_input.grid(row=6, column=1) + + # Add a "mode" dropdown + mode = tk.StringVar() + mode.set(RENDER_MODES[0]) + mode_label = tk.Label(root, text="Trim Mode") + mode_label.grid(row=7, column=0, sticky=tk.E) + mode_input = tk.OptionMenu(root, mode, *RENDER_MODES) + mode_input.grid(row=7, column=1, sticky="we") + + # Add a horizontal separator + separator = tk.Frame(height=2, bd=1, relief=tk.SUNKEN) + separator.grid(row=8, column=0, columnspan=2, sticky=tk.W + tk.E, pady=2) + + # Function to pre-validate inputs + def validate_inputs(): + # The start timestamp must be hh:mm:ss + if not TIMESTAMP_FORMAT_RE.match(start_timestamp.get()): + popup_error( + "Start timestamp must be in hh:mm:ss format", quit_on_close=False + ) + return False + # The end timestamp must be hh:mm:ss + if not TIMESTAMP_FORMAT_RE.match(end_timestamp.get()): + popup_error("End timestamp must be in hh:mm:ss format", quit_on_close=False) + return False + # The end timestamp must be after the start timestamp + start_time = datetime.datetime.strptime(start_timestamp.get(), "%H:%M:%S") + end_time = datetime.datetime.strptime(end_timestamp.get(), "%H:%M:%S") + if end_time <= start_time: + popup_error( + "End timestamp must be after start timestamp", quit_on_close=False + ) + return False + return True + + # Add a button to preview the output + preview_button = tk.Button( + root, + text="Preview Output", + command=lambda: do_preview( + input_file, start_timestamp.get(), end_timestamp.get(), mode.get() + ) + if validate_inputs() + else None, + ) + preview_button.grid(row=9, column=0, pady=2, sticky="we") + + # Add a button to render the output + render_button = tk.Button( + root, + text="Render", + command=lambda: do_render( + input_file, start_timestamp.get(), end_timestamp.get(), mode.get() + ) + if validate_inputs() + else None, + ) + render_button.grid(row=9, column=1, pady=2, sticky="we") + + return root + + +def popup_error(message: str, quit_on_close: bool = True): + # Make a popup window + popup = tk.Tk() + popup.wm_title("Error") + + # Add a message + label = tk.Label(popup, text=message) + label.pack(side="top", fill="x", pady=10) + + # Add a button to close the popup + button = tk.Button(popup, text="Okay", command=popup.destroy) + button.pack() + + # Run the popup + popup.mainloop() + + # Exit the program + if quit_on_close: + sys.exit(1) + + +def main() -> int: + # Handle program arguments + ap = argparse.ArgumentParser() + ap.add_argument( + "--file", help="File to open in Video Trimmer", type=str, required=False + ) + args = ap.parse_args() + + # Read the file + uncut_file = Path(args.file) if args.file else None + if not uncut_file: + # Try to read from env + if "NAUTILUS_SCRIPT_SELECTED_FILE_PATHS" not in os.environ: + popup_error("No file selected") + return 1 + uncut_file = Path( + os.environ["NAUTILUS_SCRIPT_SELECTED_FILE_PATHS"].splitlines()[0] + ) + + # Require one of the acceptable file types + if uncut_file.suffix[1:].lower() not in ALLOWED_INPUT_FORMATS: + popup_error( + f"File type not supported: {uncut_file.suffix}\n" + f"Supported types: {ALLOWED_INPUT_FORMATS}" + ) + return 1 + + # Build the GUI and run + root = build_gui(uncut_file) + root.mainloop() + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/install.conf.yaml b/install.conf.yaml index 5a437b2..8cd76c1 100644 --- a/install.conf.yaml +++ b/install.conf.yaml @@ -49,6 +49,9 @@ ~/.local/share/nautilus/scripts/Copy to web: path: configs/nautilus/scripts/Copy to web mode: 755 + ~/.local/share/nautilus/scripts/Open in Video Trimmer: + path: configs/nautilus/scripts/Open in Video Trimmer + mode: 755 ~/bin/run-logid: path: configs/scripts/run-logid mode: 755 @@ -78,6 +81,7 @@ - [chmod +x configs/scripts/catto, Making catto executable] - [chmod +x configs/scripts/aspath, Making aspath executable] - [chmod +x configs/scripts/fetch-steamdeck-screenshots, Making fetch-steamdeck-screenshots executable] + - [chmod +x configs/nautilus/scripts/*, Making nautilus scripts executable] # Configure GNOME - [sh ./helpers/configure-gnome.sh, Configuring GNOME] # Configure Termux if on Android