transcode.py: add logging

This commit is contained in:
2025-12-04 15:09:52 +01:00
parent 2324a79d3f
commit d93df7ece4
+74 -24
View File
@@ -10,6 +10,7 @@ import time
import sys import sys
import subprocess import subprocess
import os import os
import logging
from pathlib import Path from pathlib import Path
from watchdog.observers import Observer from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler from watchdog.events import FileSystemEventHandler
@@ -17,13 +18,45 @@ from watchdog.events import FileSystemEventHandler
# Supported video extensions to monitor # Supported video extensions to monitor
VIDEO_EXTENSIONS = {'.mkv', '.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.ts'} VIDEO_EXTENSIONS = {'.mkv', '.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.ts'}
def setup_logging():
"""
Sets up logging to both console (INFO) and file (DEBUG/Verbose).
Log file is stored in $XDG_CONFIG_HOME/transcoder/transcoder.log
"""
# Determine config directory
xdg_config = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
log_dir = Path(xdg_config) / "transcoder"
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / "transcoder.log"
# Create logger
logger = logging.getLogger()
logger.setLevel(logging.DEBUG) # Capture everything
# formatter
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
# File Handler (Verbose)
file_handler = logging.FileHandler(log_file)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# Console Handler (Concise)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter('[%(levelname)s] %(message)s'))
logger.addHandler(console_handler)
logging.info(f"Logging started. Verbose logs at: {log_file}")
def get_ffmpeg_command(input_path, output_path): def get_ffmpeg_command(input_path, output_path):
""" """
Constructs the FFmpeg command based on the Handbrake preset requirements. Constructs the FFmpeg command based on the Handbrake preset requirements.
""" """
return [ cmd = [
"ffmpeg", "ffmpeg",
"-n", # Never overwrite output files (extra safety) "-n", # Never overwrite output files
"-i", str(input_path), # Input file "-i", str(input_path), # Input file
"-c:v", "av1_nvenc", # Video Encoder "-c:v", "av1_nvenc", # Video Encoder
"-pix_fmt", "p010le", # 10-bit color "-pix_fmt", "p010le", # 10-bit color
@@ -39,42 +72,58 @@ def get_ffmpeg_command(input_path, output_path):
"-movflags", "+faststart", # Web optimization "-movflags", "+faststart", # Web optimization
str(output_path) # Output file str(output_path) # Output file
] ]
return cmd
def transcode_file(input_file, output_file): def transcode_file(input_file, output_file):
input_path = Path(input_file) input_path = Path(input_file)
output_path = Path(output_file) output_path = Path(output_file)
logging.debug(f"Processing request: {input_path} -> {output_path}")
# 1. Check existence logic # 1. Check existence logic
if output_path.exists(): if output_path.exists():
print(f"[SKIP] Output file already exists: {output_path.name}") logging.info(f"SKIP: Output file already exists: {output_path.name}")
return return
# 2. Check if file is ready (simple size stability check) # 2. Check if file is ready (simple size stability check)
# This prevents transcoding files that are still being copied into the folder logging.info(f"WAIT: Ensuring file is ready: {input_path.name}...")
print(f"[WAIT] Ensuring file is ready: {input_path.name}...")
try: try:
historical_size = -1 historical_size = -1
while True: while True:
current_size = input_path.stat().st_size current_size = input_path.stat().st_size
logging.debug(f"File stability check - Current: {current_size}, Previous: {historical_size}")
if current_size == historical_size and current_size > 0: if current_size == historical_size and current_size > 0:
logging.debug("File size stable. Proceeding.")
break break
historical_size = current_size historical_size = current_size
time.sleep(2) # Wait 2 seconds between checks time.sleep(2)
except FileNotFoundError: except FileNotFoundError:
return # File was deleted before we could process it logging.warning(f"File vanished during checks: {input_path.name}")
return
print(f"[START] Transcoding: {input_path.name} -> {output_path.name}") logging.info(f"START: Transcoding {input_path.name}")
cmd = get_ffmpeg_command(input_path, output_path) cmd = get_ffmpeg_command(input_path, output_path)
logging.debug(f"Executing FFmpeg command: {' '.join(cmd)}")
try: try:
# Run FFmpeg # Run FFmpeg
subprocess.run(cmd, check=True) # We capture output to prevent FFmpeg spamming the console, but log it on error
print(f"[DONE] Successfully created: {output_path.name}") result = subprocess.run(cmd, capture_output=True, text=True)
except subprocess.CalledProcessError as e:
print(f"[ERROR] FFmpeg failed for {input_path.name}") if result.returncode == 0:
logging.info(f"DONE: Successfully created {output_path.name}")
logging.debug(f"FFmpeg stdout: {result.stdout}")
else:
logging.error(f"FFmpeg failed for {input_path.name}")
logging.error(f"FFmpeg stderr: {result.stderr}")
except Exception as e:
logging.exception(f"Unexpected error during transcoding of {input_path.name}")
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n[STOP] Interrupted by user.") logging.warning("Interrupted by user during transcoding.")
sys.exit(0) sys.exit(0)
class NewFileHandler(FileSystemEventHandler): class NewFileHandler(FileSystemEventHandler):
@@ -84,11 +133,13 @@ class NewFileHandler(FileSystemEventHandler):
def on_created(self, event): def on_created(self, event):
if event.is_directory: if event.is_directory:
return return
logging.debug(f"Watchdog event (created): {event.src_path}")
self.process(event.src_path) self.process(event.src_path)
def on_moved(self, event): def on_moved(self, event):
if event.is_directory: if event.is_directory:
return return
logging.debug(f"Watchdog event (moved): {event.dest_path}")
self.process(event.dest_path) self.process(event.dest_path)
def process(self, file_path_str): def process(self, file_path_str):
@@ -96,19 +147,19 @@ class NewFileHandler(FileSystemEventHandler):
# Filter for video extensions # Filter for video extensions
if input_path.suffix.lower() not in VIDEO_EXTENSIONS: if input_path.suffix.lower() not in VIDEO_EXTENSIONS:
logging.debug(f"Ignored non-video file: {input_path.name}")
return return
# Determine output path
# We rename the file to avoid conflicts if input/output dirs are same (though not recommended)
new_filename = input_path.stem + ".mp4" new_filename = input_path.stem + ".mp4"
output_path = self.output_dir / new_filename output_path = self.output_dir / new_filename
transcode_file(input_path, output_path) transcode_file(input_path, output_path)
def main(): def main():
setup_logging()
parser = argparse.ArgumentParser(description="Nvidia AV1 Transcoder & Watcher") parser = argparse.ArgumentParser(description="Nvidia AV1 Transcoder & Watcher")
# Mutually exclusive group for modes
group = parser.add_mutually_exclusive_group(required=True) group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--input", type=str, help="Single file to transcode") group.add_argument("--input", type=str, help="Single file to transcode")
group.add_argument("--watch-dir", type=str, help="Directory to monitor for new files") group.add_argument("--watch-dir", type=str, help="Directory to monitor for new files")
@@ -121,10 +172,9 @@ def main():
if args.input: if args.input:
input_path = Path(args.input) input_path = Path(args.input)
if not input_path.exists(): if not input_path.exists():
print(f"Error: Input file '{args.input}' not found.") logging.critical(f"Input file '{args.input}' not found.")
sys.exit(1) sys.exit(1)
# If output dir provided, use it. Otherwise use source folder + _av1 suffix
if args.output_dir: if args.output_dir:
out_dir = Path(args.output_dir) out_dir = Path(args.output_dir)
out_dir.mkdir(parents=True, exist_ok=True) out_dir.mkdir(parents=True, exist_ok=True)
@@ -137,22 +187,21 @@ def main():
# --- Mode 2: Watch Directory --- # --- Mode 2: Watch Directory ---
elif args.watch_dir: elif args.watch_dir:
if not args.output_dir: if not args.output_dir:
print("Error: --output-dir is required when using --watch-dir") logging.critical("--output-dir is required when using --watch-dir")
sys.exit(1) sys.exit(1)
watch_dir = Path(args.watch_dir) watch_dir = Path(args.watch_dir)
output_dir = Path(args.output_dir) output_dir = Path(args.output_dir)
if not watch_dir.exists(): if not watch_dir.exists():
print(f"Error: Watch directory '{watch_dir}' does not exist.") logging.critical(f"Watch directory '{watch_dir}' does not exist.")
sys.exit(1) sys.exit(1)
# Create output dir if it doesn't exist
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
print(f"Monitoring {watch_dir}...") logging.info(f"Monitoring {watch_dir}...")
print(f"Outputting to {output_dir}") logging.info(f"Outputting to {output_dir}")
print("Press Ctrl+C to stop.") logging.info("Press Ctrl+C to stop.")
event_handler = NewFileHandler(output_dir) event_handler = NewFileHandler(output_dir)
observer = Observer() observer = Observer()
@@ -163,6 +212,7 @@ def main():
while True: while True:
time.sleep(1) time.sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:
logging.info("Stopping observer...")
observer.stop() observer.stop()
observer.join() observer.join()