From 158688ec4447f55d52590e7176832c825d5e963d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Sat, 6 Dec 2025 18:55:19 +0100 Subject: [PATCH] transcode.py: handle Ctrl+C better --- transcode.py | 73 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/transcode.py b/transcode.py index 8d43307..78ca68e 100644 --- a/transcode.py +++ b/transcode.py @@ -12,6 +12,7 @@ import json import logging import os import re +import signal import subprocess import sys import time @@ -200,7 +201,6 @@ def get_ffmpeg_command(input_path, output_path): def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=False): - exit_after_next = False input_path = Path(input_file) logging.debug(f"Processing request for: {input_path}") @@ -285,6 +285,8 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal cmd = get_ffmpeg_command(input_path, transcode_output_path) logging.debug(f"Executing FFmpeg command: {' '.join(cmd)}") + graceful_exit = False + try: with open(ffmpeg_log_file, "w", encoding="utf-8", errors="replace") as f_log: # Write the command itself to the top of the log @@ -292,12 +294,15 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal f_log.flush() # Run FFmpeg and process output in real-time with timestamps + # NOTE: start_new_session=True ensures Ctrl+C doesn't kill ffmpeg immediately + # allowing us to handle the first press gracefully in Python. process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, + start_new_session=True, ) regex_elapsed = re.compile(r"elapsed=([0-9:.]+)") @@ -305,24 +310,44 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal # Write FFmpeg output with timestamps as it's generated try: while True: - output = process.stdout.readline() # pyright: ignore[reportOptionalMemberAccess] - if output == "" and process.poll() is not None: - break - if output: - # capture the elapsed time for later use, only keep the last occurence - if regex_result := re.findall(regex_elapsed, output): - transcoding_duration = regex_result[0] + try: + output = process.stdout.readline() # pyright: ignore[reportOptionalMemberAccess] + if output == "" and process.poll() is not None: + break + if output: + # capture the elapsed time for later use, only keep the last occurence + if regex_result := re.findall(regex_elapsed, output): + transcoding_duration = regex_result[0] - timestamp = datetime.datetime.now().strftime( - "%Y-%m-%d %H:%M:%S" - ) - f_log.write(f"[{timestamp}] {output.rstrip()}\n") - f_log.flush() + timestamp = datetime.datetime.now().strftime( + "%Y-%m-%d %H:%M:%S" + ) + f_log.write(f"[{timestamp}] {output.rstrip()}\n") + f_log.flush() + except KeyboardInterrupt: + if not graceful_exit: + logging.info( + " > [GRACEFUL EXIT] Signal received. Finishing current file, then exiting script. Press Ctrl+C again to FORCE QUIT." + ) + graceful_exit = True + # Continue loop, process is still running because of start_new_session=True + continue + else: + logging.warning( + " >> [FORCE QUIT] Signal received again. Terminating process..." + ) + # Explicitly terminate the process group or process + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + raise # Re-raise to trigger outer cleanup # Wait for process to complete and get return code result = process.wait() except Exception as e: - # Ensure we clean up temporary files on error + # Ensure we clean up temporary files on error (e.g. if we forced kill inside loop) if replace_mode and use_temp_file and transcode_output_path.exists(): try: transcode_output_path.unlink() @@ -410,19 +435,23 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal except Exception: pass - if exit_after_next: - logging.info("Quitting early due to user interrupt.") + # If a graceful exit was requested, we exit now that the file is fully processed + if graceful_exit: + logging.info("Exiting script gracefully as requested.") sys.exit(0) except Exception as e: logging.exception(f"Unexpected error during transcoding of {input_path}: {e}") except KeyboardInterrupt: - logging.info( - "Will quit after the current file is transcoded. Press Ctrl+C again to force quit." - ) - if exit_after_next: - sys.exit(0) - exit_after_next = True + # This catches the re-raised exception from the inner loop (2nd Ctrl+C / Force Quit) + logging.info("Transcoding aborted by user.") + # Ensure cleanup happened (redundant check but safe) + if replace_mode and use_temp_file and transcode_output_path.exists(): + try: + transcode_output_path.unlink() + except Exception: + pass + sys.exit(1) class NewFileHandler(FileSystemEventHandler):