{
    "sourceFile": "monitor.py",
    "activeCommit": 0,
    "commits": [
        {
            "activePatchIndex": 0,
            "patches": [
                {
                    "date": 1745506961196,
                    "content": "Index: \n===================================================================\n--- \n+++ \n"
                }
            ],
            "date": 1745506961196,
            "name": "Commit-0",
            "content": "# monitor.py\n\nimport os\nimport sys\nimport time\nimport logging\nimport re\nimport shutil\nfrom pathlib import Path\nfrom watchdog.observers.polling import PollingObserver as Observer # Use polling for better compatibility\nfrom watchdog.events import FileSystemEventHandler, FileCreatedEvent\n\n# --- Import from local modules ---\ntry:\n    # Assuming main.py is in the same directory\n    from main import run_processing, setup_logging, ConfigurationError, AssetProcessingError\nexcept ImportError as e:\n    print(f\"ERROR: Failed to import required functions/classes from main.py: {e}\")\n    print(\"Ensure main.py is in the same directory as monitor.py.\")\n    sys.exit(1)\n\n# --- Configuration ---\n# Read from environment variables with defaults\nINPUT_DIR = Path(os.environ.get('INPUT_DIR', '/data/input'))\nOUTPUT_DIR = Path(os.environ.get('OUTPUT_DIR', '/data/output'))\nPROCESSED_DIR = Path(os.environ.get('PROCESSED_DIR', '/data/processed'))\nERROR_DIR = Path(os.environ.get('ERROR_DIR', '/data/error'))\nLOG_LEVEL_STR = os.environ.get('LOG_LEVEL', 'INFO').upper()\nPOLL_INTERVAL = int(os.environ.get('POLL_INTERVAL', '5'))\nPROCESS_DELAY = int(os.environ.get('PROCESS_DELAY', '2'))\n# Default workers for monitor - can be overridden if needed via env var\nDEFAULT_WORKERS = max(1, os.cpu_count() // 2 if os.cpu_count() else 1)\nNUM_WORKERS = int(os.environ.get('NUM_WORKERS', str(DEFAULT_WORKERS)))\n\n# --- Logging Setup ---\nlog_level = getattr(logging, LOG_LEVEL_STR, logging.INFO)\n# Use the setup_logging from main.py but configure the level directly\n# We don't have a 'verbose' flag here, so call basicConfig directly\nlog_format = '%(asctime)s [%(levelname)-8s] %(name)s: %(message)s'\ndate_format = '%Y-%m-%d %H:%M:%S'\nlogging.basicConfig(level=log_level, format=log_format, datefmt=date_format, handlers=[logging.StreamHandler(sys.stdout)])\nlog = logging.getLogger(\"monitor\")\nlog.info(f\"Logging level set to: {logging.getLevelName(log_level)}\")\nlog.info(f\"Monitoring Input Directory: {INPUT_DIR}\")\nlog.info(f\"Output Directory: {OUTPUT_DIR}\")\nlog.info(f\"Processed Files Directory: {PROCESSED_DIR}\")\nlog.info(f\"Error Files Directory: {ERROR_DIR}\")\nlog.info(f\"Polling Interval: {POLL_INTERVAL}s\")\nlog.info(f\"Processing Delay: {PROCESS_DELAY}s\")\nlog.info(f\"Max Workers: {NUM_WORKERS}\")\n\n\n# --- Preset Validation ---\nPRESET_DIR = Path(__file__).parent / \"Presets\"\nPRESET_FILENAME_REGEX = re.compile(r\"^\\[?([a-zA-Z0-9_-]+)\\]?_.*\\.(zip|rar|7z)$\", re.IGNORECASE)\n\ndef validate_preset(preset_name: str) -> bool:\n    \"\"\"Checks if the preset JSON file exists.\"\"\"\n    if not preset_name:\n        return False\n    preset_file = PRESET_DIR / f\"{preset_name}.json\"\n    exists = preset_file.is_file()\n    if not exists:\n        log.warning(f\"Preset file not found: {preset_file}\")\n    return exists\n\n# --- Watchdog Event Handler ---\nclass ZipHandler(FileSystemEventHandler):\n    \"\"\"Handles file system events for new ZIP files.\"\"\"\n\n    def __init__(self, input_dir: Path, output_dir: Path, processed_dir: Path, error_dir: Path):\n        self.input_dir = input_dir.resolve()\n        self.output_dir = output_dir.resolve()\n        self.processed_dir = processed_dir.resolve()\n        self.error_dir = error_dir.resolve()\n        # Ensure target directories exist\n        self.output_dir.mkdir(parents=True, exist_ok=True)\n        self.processed_dir.mkdir(parents=True, exist_ok=True)\n        self.error_dir.mkdir(parents=True, exist_ok=True)\n        log.info(\"Handler initialized, target directories ensured.\")\n\n    def on_created(self, event: FileCreatedEvent):\n        \"\"\"Called when a file or directory is created.\"\"\"\n        if event.is_directory:\n            return\n\n        src_path = Path(event.src_path)\n        log.debug(f\"File creation event detected: {src_path}\")\n\n        # Check if the file has a supported archive extension\n        supported_suffixes = ['.zip', '.rar', '.7z']\n        if src_path.suffix.lower() not in supported_suffixes:\n            log.debug(f\"Ignoring file with unsupported extension: {src_path.name}\")\n            return\n\n        log.info(f\"Detected new ZIP file: {src_path.name}. Waiting {PROCESS_DELAY}s before processing...\")\n        time.sleep(PROCESS_DELAY)\n\n        # Re-check if file still exists (might have been temporary)\n        if not src_path.exists():\n            log.warning(f\"File disappeared after delay: {src_path.name}\")\n            return\n\n        log.info(f\"Processing file: {src_path.name}\")\n\n        # --- Extract Preset Name ---\n        match = PRESET_FILENAME_REGEX.match(src_path.name)\n        if not match:\n            log.warning(f\"Filename '{src_path.name}' does not match expected format '[preset]_filename.zip'. Ignoring.\")\n            # Optionally move to an 'ignored' or 'error' directory? For now, leave it.\n            return\n\n        preset_name = match.group(1)\n        log.info(f\"Extracted preset name: '{preset_name}' from {src_path.name}\")\n\n        # --- Validate Preset ---\n        if not validate_preset(preset_name):\n            log.error(f\"Preset '{preset_name}' is not valid (missing {PRESET_DIR / f'{preset_name}.json'}). Ignoring file {src_path.name}.\")\n            # Move to error dir if preset is invalid? Let's do that.\n            self.move_file(src_path, self.error_dir, \"invalid_preset\")\n            return\n\n        # --- Run Processing ---\n        try:\n            log.info(f\"Starting asset processing for '{src_path.name}' using preset '{preset_name}'...\")\n            # run_processing expects a list of inputs\n            results = run_processing(\n                valid_inputs=[str(src_path)],\n                preset_name=preset_name,\n                output_dir_for_processor=str(self.output_dir), # Pass absolute output path\n                overwrite=False, # Default to no overwrite for monitored files? Or make configurable? Let's default to False.\n                num_workers=NUM_WORKERS\n            )\n\n            # --- Handle Results ---\n            # Check overall status based on counts\n            processed = results.get(\"processed\", 0)\n            skipped = results.get(\"skipped\", 0)\n            failed = results.get(\"failed\", 0)\n            pool_error = results.get(\"pool_error\")\n\n            if pool_error:\n                 log.error(f\"Processing pool error for {src_path.name}: {pool_error}\")\n                 self.move_file(src_path, self.error_dir, \"pool_error\")\n            elif failed > 0:\n                log.error(f\"Processing failed for {src_path.name}. Check worker logs for details.\")\n                # Log specific errors if available in results_list\n                for res_path, status, err_msg in results.get(\"results_list\", []):\n                    if status == \"failed\":\n                        log.error(f\"  - Failure reason: {err_msg}\")\n                self.move_file(src_path, self.error_dir, \"processing_failed\")\n            elif processed > 0:\n                log.info(f\"Successfully processed {src_path.name}.\")\n                self.move_file(src_path, self.processed_dir, \"processed\")\n            elif skipped > 0:\n                log.info(f\"Processing skipped for {src_path.name} (likely already exists).\")\n                self.move_file(src_path, self.processed_dir, \"skipped\")\n            else:\n                # Should not happen if input was valid zip\n                log.warning(f\"Processing finished for {src_path.name} with unexpected status (0 processed, 0 skipped, 0 failed). Moving to error dir.\")\n                self.move_file(src_path, self.error_dir, \"unknown_status\")\n\n        except (ConfigurationError, AssetProcessingError) as e:\n             log.error(f\"Asset processing error for {src_path.name}: {e}\", exc_info=True)\n             self.move_file(src_path, self.error_dir, \"processing_exception\")\n        except Exception as e:\n            log.exception(f\"Unexpected error during processing trigger for {src_path.name}: {e}\")\n            self.move_file(src_path, self.error_dir, \"monitor_exception\")\n\n\n    def move_file(self, src: Path, dest_dir: Path, reason: str):\n        \"\"\"Safely moves a file, handling potential name collisions.\"\"\"\n        if not src.exists():\n            log.warning(f\"Source file {src} does not exist, cannot move for reason: {reason}.\")\n            return\n        try:\n            dest_path = dest_dir / src.name\n            # Handle potential name collision in destination\n            counter = 1\n            while dest_path.exists():\n                dest_path = dest_dir / f\"{src.stem}_{counter}{src.suffix}\"\n                counter += 1\n                if counter > 100: # Safety break\n                     log.error(f\"Could not find unique name for {src.name} in {dest_dir} after 100 attempts. Aborting move.\")\n                     return\n\n            log.info(f\"Moving '{src.name}' to '{dest_dir.name}/' directory (Reason: {reason}). Final path: {dest_path.name}\")\n            shutil.move(str(src), str(dest_path))\n        except Exception as e:\n            log.exception(f\"Failed to move file {src.name} to {dest_dir}: {e}\")\n\n\n# --- Main Monitor Loop ---\nif __name__ == \"__main__\":\n    # Ensure input directory exists\n    if not INPUT_DIR.is_dir():\n        log.error(f\"Input directory does not exist or is not a directory: {INPUT_DIR}\")\n        log.error(\"Please create the directory or mount a volume correctly.\")\n        sys.exit(1)\n\n    event_handler = ZipHandler(INPUT_DIR, OUTPUT_DIR, PROCESSED_DIR, ERROR_DIR)\n    observer = Observer()\n    observer.schedule(event_handler, str(INPUT_DIR), recursive=False) # Don't watch subdirectories\n\n    log.info(\"Starting file system monitor...\")\n    observer.start()\n    log.info(\"Monitor started. Press Ctrl+C to stop.\")\n\n    try:\n        while True:\n            # Keep the main thread alive, observer runs in background thread\n            time.sleep(1)\n    except KeyboardInterrupt:\n        log.info(\"Keyboard interrupt received, stopping monitor...\")\n        observer.stop()\n    except Exception as e:\n        log.exception(f\"An unexpected error occurred in the main loop: {e}\")\n        observer.stop()\n\n    observer.join()\n    log.info(\"Monitor stopped.\")"
        }
    ]
}