diff options
Diffstat (limited to 'videdit.py')
| -rw-r--r-- | videdit.py | 286 |
1 files changed, 286 insertions, 0 deletions
diff --git a/videdit.py b/videdit.py new file mode 100644 index 0000000..698ce86 --- /dev/null +++ b/videdit.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +""" +Video Clip Extractor GUI +A simple GUI application for extracting video clips using ffmpeg +""" + +import tkinter as tk +from tkinter import filedialog, messagebox, ttk +import subprocess +import os +import threading +from pathlib import Path +import re + +class VideoClipperGUI: + def __init__(self, root): + self.root = root + self.root.title("Video Clip Extractor") + self.root.geometry("600x400") + + # Variables + self.input_file = tk.StringVar() + self.start_time = tk.StringVar(value="00:00:00") + self.end_time = tk.StringVar(value="00:00:10") + self.output_dir = tk.StringVar(value=os.getcwd()) + + self.setup_ui() + + def setup_ui(self): + # Main frame + main_frame = ttk.Frame(self.root, padding="10") + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # Configure grid weights + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + main_frame.columnconfigure(1, weight=1) + + # Input file selection + ttk.Label(main_frame, text="Input Video File:").grid(row=0, column=0, sticky=tk.W, pady=5) + + file_frame = ttk.Frame(main_frame) + file_frame.grid(row=0, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5) + file_frame.columnconfigure(0, weight=1) + + self.file_entry = ttk.Entry(file_frame, textvariable=self.input_file, state="readonly") + self.file_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5)) + + ttk.Button(file_frame, text="Browse", command=self.browse_file).grid(row=0, column=1) + + # Time inputs + ttk.Label(main_frame, text="Start Time:").grid(row=1, column=0, sticky=tk.W, pady=5) + self.start_entry = ttk.Entry(main_frame, textvariable=self.start_time, width=15) + self.start_entry.grid(row=1, column=1, sticky=tk.W, pady=5) + ttk.Label(main_frame, text="(HH:MM:SS or seconds)").grid(row=1, column=2, sticky=tk.W, padx=(5, 0)) + + ttk.Label(main_frame, text="End Time:").grid(row=2, column=0, sticky=tk.W, pady=5) + self.end_entry = ttk.Entry(main_frame, textvariable=self.end_time, width=15) + self.end_entry.grid(row=2, column=1, sticky=tk.W, pady=5) + ttk.Label(main_frame, text="(HH:MM:SS or seconds)").grid(row=2, column=2, sticky=tk.W, padx=(5, 0)) + + # Output directory + ttk.Label(main_frame, text="Output Directory:").grid(row=3, column=0, sticky=tk.W, pady=5) + + output_frame = ttk.Frame(main_frame) + output_frame.grid(row=3, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5) + output_frame.columnconfigure(0, weight=1) + + self.output_entry = ttk.Entry(output_frame, textvariable=self.output_dir) + self.output_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5)) + + ttk.Button(output_frame, text="Browse", command=self.browse_output_dir).grid(row=0, column=1) + + # Options frame + options_frame = ttk.LabelFrame(main_frame, text="Options", padding="5") + options_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10) + options_frame.columnconfigure(1, weight=1) + + # Quality/encoding options + ttk.Label(options_frame, text="Quality:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5)) + self.quality_var = tk.StringVar(value="copy") + quality_combo = ttk.Combobox(options_frame, textvariable=self.quality_var, width=15) + quality_combo['values'] = ("copy (fastest)", "high", "medium", "low") + quality_combo.grid(row=0, column=1, sticky=tk.W) + + # Extract button + self.extract_btn = ttk.Button(main_frame, text="Extract Clip", command=self.extract_clip) + self.extract_btn.grid(row=5, column=0, columnspan=3, pady=20) + + # Progress bar + self.progress = ttk.Progressbar(main_frame, mode='indeterminate') + self.progress.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5) + + # Status/output text + text_frame = ttk.LabelFrame(main_frame, text="Output", padding="5") + text_frame.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5) + text_frame.columnconfigure(0, weight=1) + text_frame.rowconfigure(0, weight=1) + main_frame.rowconfigure(7, weight=1) + + self.output_text = tk.Text(text_frame, height=8, wrap=tk.WORD) + scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=self.output_text.yview) + self.output_text.configure(yscrollcommand=scrollbar.set) + + self.output_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) + + # Initial message + self.log_message("Ready to extract video clips. Select a video file to begin.") + + def browse_file(self): + """Open file dialog to select input video file""" + filetypes = [ + ("Video files", "*.mp4 *.avi *.mkv *.mov *.wmv *.flv *.webm *.m4v"), + ("All files", "*.*") + ] + + filename = filedialog.askopenfilename( + title="Select Video File", + filetypes=filetypes + ) + + if filename: + self.input_file.set(filename) + self.log_message(f"Selected input file: {os.path.basename(filename)}") + + def browse_output_dir(self): + """Open directory dialog to select output directory""" + directory = filedialog.askdirectory(title="Select Output Directory") + if directory: + self.output_dir.set(directory) + + def validate_time_format(self, time_str): + """Validate and normalize time format""" + time_str = time_str.strip() + + # If it's just numbers, treat as seconds + if re.match(r'^\d+(\.\d+)?$', time_str): + return float(time_str) + + # If it's HH:MM:SS format + if re.match(r'^\d{1,2}:\d{2}:\d{2}(\.\d+)?$', time_str): + return time_str + + # If it's MM:SS format, convert to HH:MM:SS + if re.match(r'^\d{1,2}:\d{2}(\.\d+)?$', time_str): + return f"00:{time_str}" + + raise ValueError(f"Invalid time format: {time_str}") + + def log_message(self, message): + """Add message to output text widget""" + self.output_text.insert(tk.END, f"{message}\n") + self.output_text.see(tk.END) + self.root.update_idletasks() + + def get_ffmpeg_quality_params(self): + """Get ffmpeg parameters based on quality setting""" + quality = self.quality_var.get() + + if "copy" in quality: + return ["-c", "copy"] + elif "high" in quality: + return ["-c:v", "libx264", "-crf", "18", "-c:a", "aac", "-b:a", "192k"] + elif "medium" in quality: + return ["-c:v", "libx264", "-crf", "23", "-c:a", "aac", "-b:a", "128k"] + elif "low" in quality: + return ["-c:v", "libx264", "-crf", "28", "-c:a", "aac", "-b:a", "96k"] + else: + return ["-c", "copy"] + + def extract_clip(self): + """Extract video clip using ffmpeg""" + # Validation + if not self.input_file.get(): + messagebox.showerror("Error", "Please select an input video file") + return + + if not os.path.isfile(self.input_file.get()): + messagebox.showerror("Error", "Input file does not exist") + return + + try: + start_time = self.validate_time_format(self.start_time.get()) + end_time = self.validate_time_format(self.end_time.get()) + except ValueError as e: + messagebox.showerror("Error", str(e)) + return + + # Disable extract button and start progress + self.extract_btn.config(state="disabled") + self.progress.start() + + # Run extraction in separate thread + thread = threading.Thread(target=self._run_extraction, args=(start_time, end_time)) + thread.daemon = True + thread.start() + + def _run_extraction(self, start_time, end_time): + """Run ffmpeg extraction in background thread""" + try: + # Generate output filename + input_path = Path(self.input_file.get()) + timestamp = str(int(os.path.getctime(self.input_file.get()))) + output_filename = f"{input_path.stem}_clip_{timestamp}{input_path.suffix}" + output_path = os.path.join(self.output_dir.get(), output_filename) + + # Build ffmpeg command + cmd = ["ffmpeg", "-i", self.input_file.get()] + + # Add start time + if isinstance(start_time, (int, float)) and start_time > 0: + cmd.extend(["-ss", str(start_time)]) + elif isinstance(start_time, str) and start_time != "00:00:00": + cmd.extend(["-ss", start_time]) + + # Add duration or end time + if isinstance(end_time, (int, float)) and isinstance(start_time, (int, float)): + duration = end_time - start_time + cmd.extend(["-t", str(duration)]) + elif isinstance(end_time, str): + cmd.extend(["-to", end_time]) + + # Add quality parameters + cmd.extend(self.get_ffmpeg_quality_params()) + + # Add output file + cmd.append(output_path) + + self.log_message(f"Starting extraction...") + self.log_message(f"Command: {' '.join(cmd)}") + + # Run ffmpeg + process = subprocess.run( + cmd, + capture_output=True, + text=True, + cwd=self.output_dir.get() + ) + + # Handle results + if process.returncode == 0: + self.log_message(f"✓ Successfully extracted clip to: {output_filename}") + self.log_message(f"Output file size: {self._get_file_size(output_path)}") + else: + self.log_message(f"✗ Error during extraction:") + self.log_message(process.stderr) + + except Exception as e: + self.log_message(f"✗ Exception occurred: {str(e)}") + + finally: + # Re-enable button and stop progress + self.root.after(0, self._extraction_finished) + + def _extraction_finished(self): + """Called when extraction is finished""" + self.progress.stop() + self.extract_btn.config(state="normal") + + def _get_file_size(self, filepath): + """Get human-readable file size""" + try: + size = os.path.getsize(filepath) + for unit in ['B', 'KB', 'MB', 'GB']: + if size < 1024.0: + return f"{size:.1f} {unit}" + size /= 1024.0 + return f"{size:.1f} TB" + except: + return "Unknown" + +def main(): + # Check if ffmpeg is available + try: + subprocess.run(["ffmpeg", "-version"], capture_output=True, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + messagebox.showerror("Error", "ffmpeg not found. Please install ffmpeg and ensure it's in your PATH.") + return + + root = tk.Tk() + app = VideoClipperGUI(root) + root.mainloop() + +if __name__ == "__main__": + main()
\ No newline at end of file |
