diff --git a/bin/vcmp b/bin/vcmp index bd48a79..089bd7c 100755 --- a/bin/vcmp +++ b/bin/vcmp @@ -13,7 +13,7 @@ video_extensions: list[str] = ["mp4", "mkv", "avi", "wmv", "mov", "m4v", "ts", " total_files: int = 0 -def setup_logging(verbose): +def setup_logging(verbose: bool) -> None: """Configure logging to both file and stdout.""" log_level = logging.DEBUG if verbose else logging.INFO @@ -40,7 +40,7 @@ def setup_logging(verbose): root_logger.addHandler(console_handler) root_logger.addHandler(file_handler) -def ffmpeg_command(input, output, nice=20): +def ffmpeg_command(input: pathlib.Path, output: pathlib.Path, nice: int = 20) -> list[str]: return [ "nice", "-n", str(nice), "ffmpeg", @@ -54,7 +54,7 @@ def ffmpeg_command(input, output, nice=20): str(output) ] -def clean_directory(directory_path): +def clean_directory(directory_path: pathlib.Path) -> None: """Yeet all files and directories from a directory.""" for item in directory_path.iterdir(): if item.is_dir(): @@ -62,10 +62,10 @@ def clean_directory(directory_path): else: item.unlink() # Yeet the file -def is_video(path): +def is_video(path: pathlib.Path) -> bool: return path.suffix.lower()[1:] in video_extensions -def get_video_duration(input_file): +def get_video_duration(input_file: pathlib.Path) -> float: cmd = [ "ffprobe", "-v", "error", @@ -73,8 +73,12 @@ def get_video_duration(input_file): "-of", "default=noprint_wrappers=1:nokey=1", str(input_file) ] - result = subprocess.run(cmd, capture_output=True, text=True) - return float(result.stdout) + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return float(result.stdout) + except (subprocess.CalledProcessError, ValueError): + logging.error(f"Error getting duration for {input_file}") + return 1.0 def run_progress(process, duration, file_index=0): current_time = None @@ -112,7 +116,7 @@ def run_progress(process, duration, file_index=0): except: pass -def run(filepath, cmd, temp_path, file_index): +def run(filepath: pathlib.Path, cmd: list[str], temp_path: pathlib.Path, file_index: int) -> None: duration = get_video_duration(filepath) xdg_state_home = os.environ.get('XDG_STATE_HOME', str(pathlib.Path.home() / '.local' / 'state')) ffmpeg_log_path = pathlib.Path(xdg_state_home) / 'vcmp' / 'ffmpeg.log' @@ -129,13 +133,23 @@ def run(filepath, cmd, temp_path, file_index): sys.stdout.write("\n") sys.stdout.flush() + # Wait for the process to complete and get the return code + process.wait() + if process.returncode != 0: logging.error(f"FFmpeg exited with code {process.returncode}") if temp_path.exists(): temp_path.unlink() - sys.exit(1) + raise subprocess.CalledProcessError(process.returncode, cmd) -def process_single_file(filepath, target_path, temp_dir, remove_source, nice, file_index): +def process_single_file( + filepath: pathlib.Path, + target_path: pathlib.Path, + temp_dir: pathlib.Path, + remove_source: bool, + nice: int, + file_index: int +) -> bool: # Return success/failure status temp_path = temp_dir / filepath.with_suffix(".mp4").name clean_directory(temp_dir) # Should be empty if mktemp was used, but clear it regardless. logging.info(f"Processing {filepath} to {target_path}") @@ -149,14 +163,14 @@ def process_single_file(filepath, target_path, temp_dir, remove_source, nice, fi logging.warning("Process interrupted by user") if temp_path.exists(): temp_path.unlink() - sys.exit(1) + raise # Re-raise KeyboardInterrupt to exit the program except subprocess.CalledProcessError as e: logging.error(f"FFmpeg error processing {filepath}: {e}") if temp_path.exists(): temp_path.unlink() - sys.exit(1) + return False else: - # File succesfully converted to av1, move to destination + # File successfully converted to av1, move to destination # Use shutil move because of inter-fs issues target_path.parent.mkdir(parents=True, exist_ok=True) shutil.move(str(temp_path), str(target_path)) @@ -164,13 +178,23 @@ def process_single_file(filepath, target_path, temp_dir, remove_source, nice, fi if remove_source: logging.info(f"Removing source file {filepath}") filepath.unlink() + return True -def process_directory(*, source_dir, destination_dir, temp_dir=None, remove_source=False, nice=20, count_only=False): +def process_directory( + *, + source_dir: pathlib.Path, + destination_dir: pathlib.Path, + temp_dir: pathlib.Path | None = None, + remove_source: bool = False, + nice: int = 20, + count_only: bool = False +) -> int: """Process all video files in source directory recursively.""" if not count_only: if not temp_dir: raise ValueError("Temporary directory is required for processing files") count = 0 + successful = 0 for dirpath, _dirnames, filenames in os.walk(source_dir): for filename in filenames: filepath = pathlib.Path(dirpath) / filename @@ -197,9 +221,11 @@ def process_directory(*, source_dir, destination_dir, temp_dir=None, remove_sour if count_only: continue - process_single_file(filepath, target_path, temp_dir, remove_source, nice, count) + if process_single_file(filepath, target_path, temp_dir, remove_source, nice, count): + successful += 1 - logging.info(f"Found {count} files to process") + if not count_only: + logging.info(f"Processed {count} files, {successful} successful") return count