#!/usr/bin/env python3 import os import time import threading import subprocess import logging from logging.handlers import TimedRotatingFileHandler from watchdog.observers.polling import PollingObserver as Observer from watchdog.events import FileSystemEventHandler # Configuration WATCH_DIR = os.environ.get("WATCH_DIR", "/books") DEBOUNCE_DELAY = int(os.environ.get("DEBOUNCE_DELAY", "2")) DEBUG = os.environ.get("DEBUG", "false").lower() == "true" LOG_DIR = os.path.join(WATCH_DIR, "/logs") os.makedirs(LOG_DIR, exist_ok=True) # Set up logging with monthly rollover log_file_path = os.path.join(LOG_DIR, "watcher.log") logger = logging.getLogger("WatcherLogger") logger.setLevel(logging.DEBUG) formatter = logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s", "%Y-%m-%d %H:%M:%S") # Timed rotating handler (monthly) handler = TimedRotatingFileHandler(log_file_path, when="midnight", interval=1) handler.suffix = "%Y-%m" # files will be like watcher.log.2025-09 handler.setFormatter(formatter) logger.addHandler(handler) # Optionally log to console if DEBUG if DEBUG: console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) logger.addHandler(console_handler) class WatcherHandler(FileSystemEventHandler): def __init__(self): super().__init__() self.debounce_timers = {} self.processed_folders = set() def on_any_event(self, event): if event.event_type == "moved": logger.info(f"{'DIR' if event.is_directory else 'FILE'} MOVED: {event.src_path} -> {event.dest_path}") else: logger.info(f"{'DIR' if event.is_directory else 'FILE'} {event.event_type.upper()}: {event.src_path}") if event.is_directory and event.event_type in ("created", "moved", "modified"): path_to_process = event.dest_path if event.event_type == "moved" else event.src_path self.schedule_process(path_to_process) def schedule_process(self, path): if path in self.debounce_timers: self.debounce_timers[path].cancel() timer = threading.Timer(DEBOUNCE_DELAY, self.process_new_dir, args=[path]) self.debounce_timers[path] = timer timer.start() def process_new_dir(self, path): if path in self.debounce_timers: del self.debounce_timers[path] if not os.path.exists(path): logger.warning(f"Skipping {path}, folder no longer exists") return folder_name = os.path.basename(path) if "Books 1-" in folder_name: logger.info(f"Skipping folder {folder_name} due to 'Books 1-'") return m4b_files = [ f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) and f.lower().endswith(".m4b") and not f.startswith("._") ] if not m4b_files: logger.info(f"No .m4b files in {path}, skipping") return if path in self.processed_folders: logger.info(f"Folder already processed: {path}") return self.processed_folders.add(path) logger.info(f"Folder ready: {path} contains {len(m4b_files)} .m4b file(s)") try: result = subprocess.run( ["python3", "main.py", "-da", path], check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True ) logger.info(result.stdout) logger.info(f"Processed folder: {path}") except subprocess.CalledProcessError as e: logger.error(f"Error processing folder {path}:\n{e.output if hasattr(e, 'output') else e}") def main(): if not os.path.exists(WATCH_DIR): logger.error(f"Path does not exist: {WATCH_DIR}") return event_handler = WatcherHandler() observer = Observer() observer.schedule(event_handler, WATCH_DIR, recursive=True) observer.start() logger.info(f"Watching: {WATCH_DIR}") try: while True: time.sleep(1) except KeyboardInterrupt: observer.stop() observer.join() if __name__ == "__main__": main()