transcode.py: improve interrupt handling
This commit is contained in:
+66
-31
@@ -13,6 +13,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import select
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
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}")
|
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)
|
# 1. Check existence logic
|
||||||
# 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():
|
if transcode_output_path.exists():
|
||||||
logging.info(
|
logging.info(
|
||||||
leftalign(f"SKIP: Output file already exists: {transcode_output_path}")
|
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
|
return
|
||||||
|
|
||||||
# 2. Check if file is ready (simple size stability check)
|
# 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..."))
|
logging.info(leftalign("WAIT: Ensuring file is ready..."))
|
||||||
try:
|
try:
|
||||||
historical_size = -1
|
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:.]+)")
|
regex_elapsed = re.compile(r"elapsed=([0-9:.]+)")
|
||||||
transcoding_duration = None
|
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:
|
try:
|
||||||
while True:
|
while is_alive(process):
|
||||||
try:
|
# select.select() waits up to 0.5s for data to be ready.
|
||||||
output = process.stdout.readline() # pyright: ignore[reportOptionalMemberAccess]
|
# This prevents the script from blocking indefinitely on .readline()
|
||||||
if output == "" and process.poll() is not None:
|
# if the FFmpeg process hangs (e.g., D-state on network drive).
|
||||||
break
|
reads = [process.stdout.fileno()]
|
||||||
|
ret = select.select(reads, [], [], 0.5)
|
||||||
|
|
||||||
|
if reads[0] in ret[0]:
|
||||||
|
output = process.stdout.readline()
|
||||||
if output:
|
if output:
|
||||||
# capture the elapsed time for later use, only keep the last occurence
|
|
||||||
if regex_result := re.findall(regex_elapsed, output):
|
if regex_result := re.findall(regex_elapsed, output):
|
||||||
transcoding_duration = regex_result[0]
|
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.write(f"[{timestamp}] {output.rstrip()}\n")
|
||||||
f_log.flush()
|
f_log.flush()
|
||||||
except KeyboardInterrupt:
|
elif is_alive(process):
|
||||||
if not graceful_exit:
|
# Output is empty but process is alive?
|
||||||
logging.info(
|
# Could be buffering or momentary pause.
|
||||||
" > [GRACEFUL EXIT] Signal received. Finishing current file, then exiting script. Press Ctrl+C again to FORCE QUIT."
|
time.sleep(0.1)
|
||||||
)
|
|
||||||
graceful_exit = True
|
|
||||||
# Continue loop, process is still running because of start_new_session=True
|
|
||||||
continue
|
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(
|
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()
|
process.terminate()
|
||||||
try:
|
try:
|
||||||
process.wait(timeout=5)
|
process.wait(timeout=5)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
process.kill()
|
process.kill()
|
||||||
raise # Re-raise to trigger outer cleanup
|
raise # Re-raise to trigger outer cleanup
|
||||||
|
else:
|
||||||
# Wait for process to complete and get return code
|
# This block handles the case where the interrupt happens exactly
|
||||||
result = process.wait()
|
# as we re-enter the loop or in a race condition
|
||||||
except Exception as e:
|
logging.warning(" >> [FORCE QUIT] Killing process...")
|
||||||
# Ensure we clean up temporary files on error (e.g. if we forced kill inside loop)
|
process.terminate()
|
||||||
if replace_mode and use_temp_file and transcode_output_path.exists():
|
|
||||||
try:
|
try:
|
||||||
transcode_output_path.unlink()
|
process.wait(timeout=5)
|
||||||
except Exception:
|
except subprocess.TimeoutExpired:
|
||||||
pass
|
process.kill()
|
||||||
raise e
|
raise
|
||||||
|
|
||||||
|
# Wait for process to complete and get return code
|
||||||
|
result = process.wait()
|
||||||
|
|
||||||
if result == 0:
|
if result == 0:
|
||||||
logging.info(
|
logging.info(
|
||||||
@@ -526,7 +556,7 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
logging.error(
|
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():
|
if replace_mode and use_temp_file and transcode_output_path.exists():
|
||||||
try:
|
try:
|
||||||
@@ -541,6 +571,11 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(f"Unexpected error during transcoding of {input_path}: {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:
|
except KeyboardInterrupt:
|
||||||
# This catches the re-raised exception from the inner loop (2nd Ctrl+C / Force Quit)
|
# This catches the re-raised exception from the inner loop (2nd Ctrl+C / Force Quit)
|
||||||
logging.info("Transcoding aborted by user.")
|
logging.info("Transcoding aborted by user.")
|
||||||
|
|||||||
Reference in New Issue
Block a user