diff --git a/transcode.py b/transcode.py index 52a9830..df592f3 100644 --- a/transcode.py +++ b/transcode.py @@ -123,15 +123,39 @@ def get_ffmpeg_command(input_path, output_path): ] return cmd -def transcode_file(input_file, output_file, skip_av1=True): +def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=False): input_path = Path(input_file) - output_path = Path(output_file) + + logging.debug(f"Processing request for: {input_path}") - logging.debug(f"Processing request: {input_path} -> {output_path}") + target_path = None + use_temp_file = False + + if replace_mode: + logging.warning("Policy: --replace is enabled. --output-dir is ignored. Transcoding in-place.") + target_path = input_path.with_suffix('.mp4') + + # If the target is the same as input (e.g. input is already .mp4), use a temp file + if target_path == input_path: + use_temp_file = True + transcode_output_path = input_path.with_suffix('.tmp.mp4') + logging.debug(f"Input and output filenames match. Using temp file: {transcode_output_path}") + else: + transcode_output_path = target_path + else: + if not output_file: + logging.error("No output file specified and not in replace mode.") + return + target_path = Path(output_file) + transcode_output_path = target_path - # 1. Check existence logic - if output_path.exists(): - logging.info(f"SKIP: Output file already exists: {output_path.name}") + logging.debug(f"Final transcode target: {transcode_output_path}") + + # 1. Check existence logic (Skip if target exists, unless we are using a temp file for replacement) + # If replace_mode is True and extensions differ, we still want to respect -n (no overwrite) + # for the destination file if it already exists. + if transcode_output_path.exists(): + logging.info(f"SKIP: Output file already exists: {transcode_output_path.name}") return # 2. Check if file is ready (simple size stability check) @@ -167,9 +191,11 @@ def transcode_file(input_file, output_file, skip_av1=True): ffmpeg_log_file = ffmpeg_logs_dir / f"{input_path.name}.log" logging.info(f"START: Transcoding {input_path.name}") + if replace_mode: + logging.info(f" Outputting to temporary file: {input_path.name}") logging.info(f" FFmpeg details logging to: {ffmpeg_log_file}") - cmd = get_ffmpeg_command(input_path, output_path) + cmd = get_ffmpeg_command(input_path, transcode_output_path) logging.debug(f"Executing FFmpeg command: {' '.join(cmd)}") try: @@ -182,7 +208,24 @@ def transcode_file(input_file, output_file, skip_av1=True): result = subprocess.run(cmd, stdout=f_log, stderr=subprocess.STDOUT, text=True) if result.returncode == 0: - logging.info(f"DONE: Successfully created {output_path.name}") + logging.info(f"DONE: Successfully created {transcode_output_path.name}") + + if replace_mode: + if use_temp_file: + # Rename temp file to overwrite original + try: + transcode_output_path.replace(input_path) + logging.info(f"REPLACE: Overwrote original file {input_path.name} with new version.") + except OSError as e: + logging.error(f"Failed to replace original file: {e}") + else: + # Different extensions (e.g. mkv -> mp4). Delete original. + try: + input_path.unlink() + logging.info(f"DELETE: Removed original file {input_path.name}") + except OSError as e: + logging.error(f"Failed to delete original file: {e}") + else: logging.error(f"FFmpeg failed for {input_path.name}. See log at {ffmpeg_log_file}") @@ -193,9 +236,10 @@ def transcode_file(input_file, output_file, skip_av1=True): sys.exit(0) class NewFileHandler(FileSystemEventHandler): - def __init__(self, output_dir, skip_av1=True): - self.output_dir = Path(output_dir) + def __init__(self, output_dir=None, skip_av1=True, replace_mode=False): + self.output_dir = Path(output_dir) if output_dir else None self.skip_av1 = skip_av1 + self.replace_mode = replace_mode def on_created(self, event): if event.is_directory: @@ -217,10 +261,12 @@ class NewFileHandler(FileSystemEventHandler): logging.debug(f"Ignored non-video file: {input_path.name}") return - new_filename = input_path.stem + ".mp4" - output_path = self.output_dir / new_filename + output_path = None + if not self.replace_mode and self.output_dir: + new_filename = input_path.stem + ".mp4" + output_path = self.output_dir / new_filename - transcode_file(input_path, output_path, skip_av1=self.skip_av1) + transcode_file(input_path, output_path, skip_av1=self.skip_av1, replace_mode=self.replace_mode) def main(): setup_logging() @@ -232,20 +278,37 @@ def main(): parser.add_argument("--watch-dir", type=str, help="Directory to monitor for new files") parser.add_argument("--output-dir", type=str, help="Output directory") parser.add_argument("--no-skip-av1", action="store_true", help="Force transcoding even if input is already AV1") + parser.add_argument("--replace", action="store_true", help="Replace original files with transcoded versions (Ignores --output-dir)") # Set defaults from config # We accept both hyphenated and underscore keys from JSON for user convenience default_watch = config.get("watch-dir") or config.get("watch_dir") default_output = config.get("output-dir") or config.get("output_dir") default_no_skip = config.get("no-skip-av1") or config.get("no_skip_av1") or False + default_replace = config.get("replace") or False parser.set_defaults( watch_dir=default_watch, output_dir=default_output, - no_skip_av1=default_no_skip + no_skip_av1=default_no_skip, + replace=default_replace ) args = parser.parse_args() + + skip_av1 = not args.no_skip_av1 + + # --- Initial Policy Logging --- + if args.replace: + logging.info("Policy: REPLACE mode enabled. Original files will be overwritten/deleted.") + if args.output_dir: + logging.warning("Warning: --output-dir is specified but will be IGNORED due to --replace mode.") + + if args.no_skip_av1: + logging.info("Policy: Force transcoding all files (including AV1).") + else: + logging.info("Policy: Skipping files that are already AV1.") + # --- Mode 1: Single File --- if args.input: @@ -254,42 +317,45 @@ def main(): logging.critical(f"Input file '{args.input}' not found.") sys.exit(1) - if args.output_dir: - out_dir = Path(args.output_dir) - out_dir.mkdir(parents=True, exist_ok=True) - output_path = out_dir / (input_path.stem + ".mp4") - else: - output_path = input_path.parent / (input_path.stem + "_av1.mp4") + output_path = None + if not args.replace: + if args.output_dir: + out_dir = Path(args.output_dir) + out_dir.mkdir(parents=True, exist_ok=True) + output_path = out_dir / (input_path.stem + ".mp4") + else: + output_path = input_path.parent / (input_path.stem + "_av1.mp4") - transcode_file(input_path, output_path, skip_av1=not args.no_skip_av1) + transcode_file(input_path, output_path, skip_av1=skip_av1, replace_mode=args.replace) return # --- Mode 2: Watch Directory --- if args.watch_dir: - if not args.output_dir: + # If replace mode is OFF, output-dir is required. + if not args.replace and not args.output_dir: logging.critical("Output directory is not specified in CLI (--output-dir) or Config.") + logging.critical("Either specify --output-dir OR enable --replace mode.") sys.exit(1) watch_dir = Path(args.watch_dir) - output_dir = Path(args.output_dir) - + if not watch_dir.exists(): logging.critical(f"Watch directory '{watch_dir}' does not exist.") sys.exit(1) - - output_dir.mkdir(parents=True, exist_ok=True) - + logging.info(f"Monitoring {watch_dir}...") - logging.info(f"Outputting to {output_dir}") - if args.no_skip_av1: - logging.info("Policy: Force transcoding all files (including AV1).") + output_dir_path = None + if not args.replace: + output_dir_path = Path(args.output_dir) + output_dir_path.mkdir(parents=True, exist_ok=True) + logging.info(f"Outputting to {output_dir_path}") else: - logging.info("Policy: Skipping files that are already AV1.") + logging.info("Outputting in-place (replacing originals).") logging.info("Press Ctrl+C to stop.") - event_handler = NewFileHandler(output_dir, skip_av1=not args.no_skip_av1) + event_handler = NewFileHandler(output_dir_path, skip_av1=skip_av1, replace_mode=args.replace) observer = Observer() observer.schedule(event_handler, str(watch_dir), recursive=False) observer.start()