From 61883814d284fcb322fd552d6f2618c9a8b6866e Mon Sep 17 00:00:00 2001 From: wheelz Date: Fri, 26 Jun 2026 05:52:11 +0000 Subject: [PATCH] fix: trigger RomM quick scan via socket --- romm-automation-import-worker.py | 92 ++++++++++++++++++++++++++--- romm-automation-truenas-compose.yml | 4 +- romm-automation-truenas.env.example | 19 +++--- 3 files changed, 99 insertions(+), 16 deletions(-) diff --git a/romm-automation-import-worker.py b/romm-automation-import-worker.py index cbd9a04..02c9b76 100644 --- a/romm-automation-import-worker.py +++ b/romm-automation-import-worker.py @@ -18,7 +18,9 @@ import json import os import shutil import struct +import subprocess import sys +import tempfile import time import traceback from datetime import datetime, timezone @@ -41,6 +43,8 @@ ROMM_SERVER_URL = os.environ.get("ROMM_SERVER_URL", "").rstrip("/") ROMM_USERNAME = os.environ.get("ROMM_USERNAME", "") ROMM_PASSWORD = os.environ.get("ROMM_PASSWORD", "") ROMM_API_TOKEN = os.environ.get("ROMM_API_TOKEN", "") +ROMM_SCAN_TASK = os.environ.get("ROMM_SCAN_TASK", "socket_quick_scan") +ROMM_SCAN_APIS = [api.strip() for api in os.environ.get("ROMM_SCAN_APIS", "igdb").split(",") if api.strip()] IGNORE_SUFFIXES = {".part", ".partial", ".tmp", ".crdownload"} @@ -219,12 +223,10 @@ def romm_token() -> str: return token -def trigger_romm_scan() -> None: - if not SCAN_AFTER_IMPORT: - return +def trigger_romm_scan_via_task_api(task_name: str) -> bool: token = romm_token() req = request.Request( - f"{ROMM_SERVER_URL}/api/tasks/run/scan_library", + f"{ROMM_SERVER_URL}/api/tasks/run/{task_name}", data=b"{}", method="POST", headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, @@ -232,16 +234,92 @@ def trigger_romm_scan() -> None: try: with request.urlopen(req, timeout=30) as resp: body = resp.read().decode("utf-8", errors="replace") - log(f"triggered RomM scan_library: HTTP {resp.status} {body[:300]}") + log(f"triggered RomM task {task_name}: HTTP {resp.status} {body[:300]}") + return True except error.HTTPError as exc: body = exc.read().decode("utf-8", errors="replace") if exc.code == 403: raise RuntimeError( "RomM scan trigger failed: HTTP 403 Forbidden. The RomM credential/token can log in, " - "but it does not have the tasks.run scope required to run scan_library. " + "but it does not have the tasks.run scope required to run tasks. " "Create a RomM API token with Run tasks/tasks.run permission and set ROMM_API_TOKEN." ) from exc - raise RuntimeError(f"RomM scan trigger failed: HTTP {exc.code} {body}") from exc + log(f"RomM task API trigger {task_name} did not run: HTTP {exc.code} {body[:300]}") + return False + + +def trigger_romm_scan_via_socket() -> None: + """Trigger the same Socket.IO quick scan used by RomM's web UI. + + Current RomM versions expose /api/tasks/run/scan_library, but that scheduled + task is not manual-runnable unless RomM's scheduled rescan is enabled. The web + UI manual scan uses the /ws/socket.io "scan" event instead. + """ + if not ROMM_SERVER_URL: + raise RuntimeError("ROMM_SERVER_URL is required for socket quick scan") + + options = { + "platforms": [], + "type": "quick", + "roms_ids": [], + "apis": ROMM_SCAN_APIS, + "launchbox_remote_enabled": True, + } + node_script = r""" +const { io } = require('socket.io-client'); +const url = process.env.ROMM_SERVER_URL; +const options = JSON.parse(process.env.ROMM_SOCKET_SCAN_OPTIONS || '{}'); +const socket = io(url, { path: '/ws/socket.io', transports: ['websocket'], timeout: 10000 }); +socket.on('connect', () => { + console.log('socket_connected'); + socket.emit('scan', options); + setTimeout(() => { console.log('socket_scan_emitted'); socket.close(); process.exit(0); }, 2000); +}); +socket.on('connect_error', (err) => { + console.error('socket_connect_error: ' + err.message); + process.exit(2); +}); +""" + with tempfile.NamedTemporaryFile("w", suffix=".js", delete=False) as fh: + fh.write(node_script) + script_path = fh.name + try: + env = os.environ.copy() + env["ROMM_SOCKET_SCAN_OPTIONS"] = json.dumps(options) + try: + npm_root = subprocess.check_output(["npm", "root", "-g"], text=True, timeout=10).strip() + env["NODE_PATH"] = npm_root + except Exception: + pass + result = subprocess.run( + ["node", script_path], + env=env, + text=True, + capture_output=True, + timeout=30, + check=False, + ) + output = (result.stdout + result.stderr).strip().replace("\n", " | ") + if result.returncode != 0: + raise RuntimeError(f"RomM socket quick scan failed rc={result.returncode}: {output}") + log(f"triggered RomM socket quick scan: {output}") + finally: + try: + Path(script_path).unlink(missing_ok=True) + except Exception: + pass + + +def trigger_romm_scan() -> None: + if not SCAN_AFTER_IMPORT: + return + + if ROMM_SCAN_TASK and ROMM_SCAN_TASK != "socket_quick_scan": + if trigger_romm_scan_via_task_api(ROMM_SCAN_TASK): + return + + # Preferred/default for this RomM install: mirrors the UI's manual quick scan. + trigger_romm_scan_via_socket() def import_file(path: Path) -> bool: diff --git a/romm-automation-truenas-compose.yml b/romm-automation-truenas-compose.yml index 29e16fd..92a512d 100644 --- a/romm-automation-truenas-compose.yml +++ b/romm-automation-truenas-compose.yml @@ -173,7 +173,7 @@ services: restart: unless-stopped profiles: ["validation"] working_dir: /work - command: ["sh", "-c", "apk add --no-cache python3 make g++ && npm install -g igir@latest && python3 /work/romm-automation-import-worker.py"] + command: ["sh", "-c", "apk add --no-cache python3 make g++ && npm install -g igir@latest socket.io-client@latest && python3 /work/romm-automation-import-worker.py"] environment: TZ: ${TZ:-America/Chicago} ROMM_SERVER_URL: ${ROMM_SERVER_URL:?set ROMM_SERVER_URL in .env} @@ -181,6 +181,8 @@ services: ROMM_PASSWORD: ${ROMM_PASSWORD:-} ROMM_API_TOKEN: ${ROMM_API_TOKEN:-} ROMM_SCAN_AFTER_IMPORT: ${ROMM_SCAN_AFTER_IMPORT:-true} + ROMM_SCAN_TASK: ${ROMM_SCAN_TASK:-socket_quick_scan} + ROMM_SCAN_APIS: ${ROMM_SCAN_APIS:-igdb} IMPORT_POLL_SECONDS: ${IMPORT_POLL_SECONDS:-60} IMPORT_QUIET_IDLE: ${IMPORT_QUIET_IDLE:-true} IMPORT_PLATFORM_MAP_JSON: ${IMPORT_PLATFORM_MAP_JSON:-{}} diff --git a/romm-automation-truenas.env.example b/romm-automation-truenas.env.example index 3338bba..2cee965 100644 --- a/romm-automation-truenas.env.example +++ b/romm-automation-truenas.env.example @@ -26,13 +26,16 @@ HOMARR_PORT=7775 # Valid values: romarr,discord,validation or comma-separated combinations. COMPOSE_PROFILES=validation -# Import worker: watches a single landing folder at staging/incoming, infers the -# RomM platform when safe, imports into RomM, then triggers RomM's scan_library -# task. Keep immediate scan enabled while testing; later we can debounce/batch -# scans if needed. +# Worker watches staging/incoming as a single landing folder, infers the +# RomM platform when safe, imports into RomM, then triggers the same Socket.IO +# quick scan that RomM's web UI manual scan uses. Keep immediate scan enabled +# while testing; later we can debounce/batch scans if needed. ROMM_SCAN_AFTER_IMPORT=true +ROMM_SCAN_TASK=socket_quick_scan +ROMM_SCAN_APIS=igdb IMPORT_POLL_SECONDS=60 IMPORT_QUIET_IDLE=true + # Optional JSON extension/platform overrides for ambiguous file types. # Example: {".iso":"ngc",".chd":"psx"} IMPORT_PLATFORM_MAP_JSON={} @@ -60,10 +63,10 @@ IGDB_CLIENT_SECRET=fill_me ROMM_SERVER_URL=http://your-romm-host:port ROMM_USERNAME=fill_me ROMM_PASSWORD=fill_me -# Optional but recommended for the import worker scan trigger. Create a RomM API -# token with the Run tasks / tasks.run permission and paste it in the real env. -# Username/password may pass basic library checks but still receive HTTP 403 when -# trying to run /api/tasks/run/scan_library without this scope. +# Optional RomM API token for task endpoints that require tasks.run. The current +# import worker defaults to the Socket.IO quick-scan path because this RomM +# install does not allow manual execution of the scheduled scan_library task. +# Keep the token here anyway for status/task API checks and future fallback use. ROMM_API_TOKEN= # Romarr image. Upstream docs list romarr/romarr:latest, but the public image was not pullable