From 28f11ab190ea041164a0dd80b4360137739e4888 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Thu, 4 Sep 2025 14:42:21 +0200 Subject: [PATCH] Enable editing multiple videos quickly by running croppa on a folder --- croppa/main.py | 127 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 105 insertions(+), 22 deletions(-) diff --git a/croppa/main.py b/croppa/main.py index 5279ada..0f62012 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -32,24 +32,29 @@ class VideoEditor: MAX_ZOOM = 10.0 ZOOM_INCREMENT = 0.25 - def __init__(self, video_path: str): - self.video_path = Path(video_path) - self.cap = cv2.VideoCapture(str(self.video_path)) + # Supported video extensions + VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v'} + + def __init__(self, path: str): + self.path = Path(path) - if not self.cap.isOpened(): - raise ValueError(f"Could not open video file: {video_path}") + # Video file management + self.video_files = [] + self.current_video_index = 0 - # Video properties - self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) - self.fps = self.cap.get(cv2.CAP_PROP_FPS) - self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + # Determine if path is file or directory + if self.path.is_file(): + self.video_files = [self.path] + elif self.path.is_dir(): + # Load all video files from directory + self.video_files = self._get_video_files_from_directory(self.path) + if not self.video_files: + raise ValueError(f"No video files found in directory: {path}") + else: + raise ValueError(f"Path does not exist: {path}") - # Playback state - self.current_frame = 0 - self.is_playing = False - self.playback_speed = 1.0 - self.current_display_frame = None + # Initialize with first video + self._load_video(self.video_files[0]) # Mouse and keyboard interaction self.mouse_dragging = False @@ -81,6 +86,65 @@ class VideoEditor: # Display offset for panning when zoomed self.display_offset = [0, 0] + def _get_video_files_from_directory(self, directory: Path) -> List[Path]: + """Get all video files from a directory, sorted by name""" + video_files = [] + for file_path in directory.iterdir(): + if file_path.is_file() and file_path.suffix.lower() in self.VIDEO_EXTENSIONS: + video_files.append(file_path) + return sorted(video_files) + + def _load_video(self, video_path: Path): + """Load a video file and initialize video properties""" + if hasattr(self, 'cap') and self.cap: + self.cap.release() + + self.video_path = video_path + self.cap = cv2.VideoCapture(str(self.video_path)) + + if not self.cap.isOpened(): + raise ValueError(f"Could not open video file: {video_path}") + + # Video properties + self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) + self.fps = self.cap.get(cv2.CAP_PROP_FPS) + self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + # Reset playback state for new video + self.current_frame = 0 + self.is_playing = False + self.playback_speed = 1.0 + self.current_display_frame = None + + # Reset crop, zoom, and cut settings for new video + self.crop_rect = None + self.crop_history = [] + self.zoom_factor = 1.0 + self.zoom_center = None + self.cut_start_frame = None + self.cut_end_frame = None + self.display_offset = [0, 0] + + print(f"Loaded video: {self.video_path.name} ({self.current_video_index + 1}/{len(self.video_files)})") + + def switch_to_video(self, index: int): + """Switch to a specific video by index""" + if 0 <= index < len(self.video_files): + self.current_video_index = index + self._load_video(self.video_files[index]) + self.load_current_frame() + + def next_video(self): + """Switch to the next video""" + next_index = (self.current_video_index + 1) % len(self.video_files) + self.switch_to_video(next_index) + + def previous_video(self): + """Switch to the previous video""" + prev_index = (self.current_video_index - 1) % len(self.video_files) + self.switch_to_video(prev_index) + def load_current_frame(self) -> bool: """Load the current frame into display cache""" self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.current_frame) @@ -273,17 +337,27 @@ class VideoEditor: cv2.putText(canvas, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) cv2.putText(canvas, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1) + # Add video navigation info + if len(self.video_files) > 1: + video_text = f"Video: {self.current_video_index + 1}/{len(self.video_files)} - {self.video_path.name}" + cv2.putText(canvas, video_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2) + cv2.putText(canvas, video_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1) + y_offset = 90 + else: + y_offset = 60 + # Add crop info if self.crop_rect: crop_text = f"Crop: {int(self.crop_rect[0])},{int(self.crop_rect[1])} {int(self.crop_rect[2])}x{int(self.crop_rect[3])}" - cv2.putText(canvas, crop_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2) - cv2.putText(canvas, crop_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1) + cv2.putText(canvas, crop_text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2) + cv2.putText(canvas, crop_text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1) + y_offset += 30 # Add cut info if self.cut_start_frame is not None or self.cut_end_frame is not None: cut_text = f"Cut: {self.cut_start_frame or '?'} - {self.cut_end_frame or '?'}" - cv2.putText(canvas, cut_text, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2) - cv2.putText(canvas, cut_text, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1) + cv2.putText(canvas, cut_text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2) + cv2.putText(canvas, cut_text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1) # Draw timeline self.draw_timeline(canvas) @@ -531,6 +605,9 @@ class VideoEditor: print(" Ctrl+Scroll: Zoom in/out") print(" 1: Set cut start point") print(" 2: Set cut end point") + if len(self.video_files) > 1: + print(" N: Next video") + print(" n: Previous video") print(" Enter: Render video") print(" Q/ESC: Quit") print() @@ -571,6 +648,12 @@ class VideoEditor: elif key == ord('2'): self.cut_end_frame = self.current_frame print(f"Set cut end at frame {self.current_frame}") + elif key == ord('n'): + if len(self.video_files) > 1: + self.previous_video() + elif key == ord('N'): + if len(self.video_files) > 1: + self.next_video() elif key == 13: # Enter output_name = f"{self.video_path.stem}_edited.mp4" self.render_video(str(self.video_path.parent / output_name)) @@ -585,12 +668,12 @@ class VideoEditor: def main(): parser = argparse.ArgumentParser(description="Fast Video Editor - Crop, Zoom, and Cut videos") - parser.add_argument("video", help="Path to video file") + parser.add_argument("video", help="Path to video file or directory containing videos") args = parser.parse_args() - if not os.path.isfile(args.video): - print(f"Error: {args.video} is not a valid file") + if not os.path.exists(args.video): + print(f"Error: {args.video} does not exist") sys.exit(1) try: