diff --git a/transcode.py b/transcode.py index 1ec87e2..75ebb60 100644 --- a/transcode.py +++ b/transcode.py @@ -13,6 +13,7 @@ import json import logging import os import re +import select import signal import subprocess import sys @@ -310,9 +311,7 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal 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. + # 1. Check existence logic if transcode_output_path.exists(): logging.info( leftalign(f"SKIP: Output file already exists: {transcode_output_path}") @@ -320,6 +319,10 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal return # 2. Check if file is ready (simple size stability check) + if not input_path.exists(): + logging.warning(f"File vanished during checks: {input_path}") + return + logging.info(leftalign("WAIT: Ensuring file is ready...")) try: historical_size = -1 @@ -389,15 +392,22 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal regex_elapsed = re.compile(r"elapsed=([0-9:.]+)") transcoding_duration = None - # Write FFmpeg output with timestamps as it's generated + # Helper to check if process is alive + def is_alive(p): + return p.poll() is None + + # Non-blocking read loop using select try: - while True: - try: - output = process.stdout.readline() # pyright: ignore[reportOptionalMemberAccess] - if output == "" and process.poll() is not None: - break + while is_alive(process): + # select.select() waits up to 0.5s for data to be ready. + # This prevents the script from blocking indefinitely on .readline() + # if the FFmpeg process hangs (e.g., D-state on network drive). + reads = [process.stdout.fileno()] + ret = select.select(reads, [], [], 0.5) + + if reads[0] in ret[0]: + output = process.stdout.readline() 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] @@ -406,36 +416,56 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal ) 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 + elif is_alive(process): + # Output is empty but process is alive? + # Could be buffering or momentary pause. + time.sleep(0.1) continue - else: + + except KeyboardInterrupt: + if not graceful_exit: + logging.info( + " > [GRACEFUL EXIT] Signal received. Processing will finish, then script will exit." + ) + logging.info(" > Press Ctrl+C again to FORCE QUIT immediately.") + graceful_exit = True + # Resume waiting for the process to finish naturally. + while is_alive(process): + try: + # We still need to drain the output pipe to prevent FFmpeg from blocking + reads = [process.stdout.fileno()] + ret = select.select(reads, [], [], 0.5) + if reads[0] in ret[0]: + output = process.stdout.readline() + if output: + timestamp = datetime.datetime.now().strftime( + "%Y-%m-%d %H:%M:%S" + ) + f_log.write(f"[{timestamp}] {output.rstrip()}\n") + except KeyboardInterrupt: + # User pressed Ctrl+C a second time inside the graceful wait loop logging.warning( - " >> [FORCE QUIT] Signal received again. Terminating process..." + " >> [FORCE QUIT] Signal received again. Killing 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 (e.g. if we forced kill inside loop) - if replace_mode and use_temp_file and transcode_output_path.exists(): + else: + # This block handles the case where the interrupt happens exactly + # as we re-enter the loop or in a race condition + logging.warning(" >> [FORCE QUIT] Killing process...") + process.terminate() try: - transcode_output_path.unlink() - except Exception: - pass - raise e + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + raise + + # Wait for process to complete and get return code + result = process.wait() if result == 0: logging.info( @@ -526,7 +556,7 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal else: logging.error( - f"FFmpeg failed for {input_path}. See log at {ffmpeg_log_file}" + f"FFmpeg failed for {input_path} with exit code {result}. See log at {ffmpeg_log_file}" ) if replace_mode and use_temp_file and transcode_output_path.exists(): try: @@ -541,6 +571,11 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal except Exception as e: logging.exception(f"Unexpected error during transcoding of {input_path}: {e}") + if replace_mode and use_temp_file and transcode_output_path.exists(): + try: + transcode_output_path.unlink() + except Exception: + pass except KeyboardInterrupt: # This catches the re-raised exception from the inner loop (2nd Ctrl+C / Force Quit) logging.info("Transcoding aborted by user.")