fix: trigger RomM quick scan via socket

This commit is contained in:
2026-06-26 05:52:11 +00:00
parent dd6dbb457e
commit 61883814d2
3 changed files with 99 additions and 16 deletions

View File

@@ -18,7 +18,9 @@ import json
import os import os
import shutil import shutil
import struct import struct
import subprocess
import sys import sys
import tempfile
import time import time
import traceback import traceback
from datetime import datetime, timezone 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_USERNAME = os.environ.get("ROMM_USERNAME", "")
ROMM_PASSWORD = os.environ.get("ROMM_PASSWORD", "") ROMM_PASSWORD = os.environ.get("ROMM_PASSWORD", "")
ROMM_API_TOKEN = os.environ.get("ROMM_API_TOKEN", "") 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"} IGNORE_SUFFIXES = {".part", ".partial", ".tmp", ".crdownload"}
@@ -219,12 +223,10 @@ def romm_token() -> str:
return token return token
def trigger_romm_scan() -> None: def trigger_romm_scan_via_task_api(task_name: str) -> bool:
if not SCAN_AFTER_IMPORT:
return
token = romm_token() token = romm_token()
req = request.Request( req = request.Request(
f"{ROMM_SERVER_URL}/api/tasks/run/scan_library", f"{ROMM_SERVER_URL}/api/tasks/run/{task_name}",
data=b"{}", data=b"{}",
method="POST", method="POST",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
@@ -232,16 +234,92 @@ def trigger_romm_scan() -> None:
try: try:
with request.urlopen(req, timeout=30) as resp: with request.urlopen(req, timeout=30) as resp:
body = resp.read().decode("utf-8", errors="replace") 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: except error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace") body = exc.read().decode("utf-8", errors="replace")
if exc.code == 403: if exc.code == 403:
raise RuntimeError( raise RuntimeError(
"RomM scan trigger failed: HTTP 403 Forbidden. The RomM credential/token can log in, " "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." "Create a RomM API token with Run tasks/tasks.run permission and set ROMM_API_TOKEN."
) from exc ) 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: def import_file(path: Path) -> bool:

View File

@@ -173,7 +173,7 @@ services:
restart: unless-stopped restart: unless-stopped
profiles: ["validation"] profiles: ["validation"]
working_dir: /work 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: environment:
TZ: ${TZ:-America/Chicago} TZ: ${TZ:-America/Chicago}
ROMM_SERVER_URL: ${ROMM_SERVER_URL:?set ROMM_SERVER_URL in .env} ROMM_SERVER_URL: ${ROMM_SERVER_URL:?set ROMM_SERVER_URL in .env}
@@ -181,6 +181,8 @@ services:
ROMM_PASSWORD: ${ROMM_PASSWORD:-} ROMM_PASSWORD: ${ROMM_PASSWORD:-}
ROMM_API_TOKEN: ${ROMM_API_TOKEN:-} ROMM_API_TOKEN: ${ROMM_API_TOKEN:-}
ROMM_SCAN_AFTER_IMPORT: ${ROMM_SCAN_AFTER_IMPORT:-true} 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_POLL_SECONDS: ${IMPORT_POLL_SECONDS:-60}
IMPORT_QUIET_IDLE: ${IMPORT_QUIET_IDLE:-true} IMPORT_QUIET_IDLE: ${IMPORT_QUIET_IDLE:-true}
IMPORT_PLATFORM_MAP_JSON: ${IMPORT_PLATFORM_MAP_JSON:-{}} IMPORT_PLATFORM_MAP_JSON: ${IMPORT_PLATFORM_MAP_JSON:-{}}

View File

@@ -26,13 +26,16 @@ HOMARR_PORT=7775
# Valid values: romarr,discord,validation or comma-separated combinations. # Valid values: romarr,discord,validation or comma-separated combinations.
COMPOSE_PROFILES=validation COMPOSE_PROFILES=validation
# Import worker: watches a single landing folder at staging/incoming, infers the # Worker watches staging/incoming as a single landing folder, infers the
# RomM platform when safe, imports into RomM, then triggers RomM's scan_library # RomM platform when safe, imports into RomM, then triggers the same Socket.IO
# task. Keep immediate scan enabled while testing; later we can debounce/batch # quick scan that RomM's web UI manual scan uses. Keep immediate scan enabled
# scans if needed. # while testing; later we can debounce/batch scans if needed.
ROMM_SCAN_AFTER_IMPORT=true ROMM_SCAN_AFTER_IMPORT=true
ROMM_SCAN_TASK=socket_quick_scan
ROMM_SCAN_APIS=igdb
IMPORT_POLL_SECONDS=60 IMPORT_POLL_SECONDS=60
IMPORT_QUIET_IDLE=true IMPORT_QUIET_IDLE=true
# Optional JSON extension/platform overrides for ambiguous file types. # Optional JSON extension/platform overrides for ambiguous file types.
# Example: {".iso":"ngc",".chd":"psx"} # Example: {".iso":"ngc",".chd":"psx"}
IMPORT_PLATFORM_MAP_JSON={} IMPORT_PLATFORM_MAP_JSON={}
@@ -60,10 +63,10 @@ IGDB_CLIENT_SECRET=fill_me
ROMM_SERVER_URL=http://your-romm-host:port ROMM_SERVER_URL=http://your-romm-host:port
ROMM_USERNAME=fill_me ROMM_USERNAME=fill_me
ROMM_PASSWORD=fill_me ROMM_PASSWORD=fill_me
# Optional but recommended for the import worker scan trigger. Create a RomM API # Optional RomM API token for task endpoints that require tasks.run. The current
# token with the Run tasks / tasks.run permission and paste it in the real env. # import worker defaults to the Socket.IO quick-scan path because this RomM
# Username/password may pass basic library checks but still receive HTTP 403 when # install does not allow manual execution of the scheduled scan_library task.
# trying to run /api/tasks/run/scan_library without this scope. # Keep the token here anyway for status/task API checks and future fallback use.
ROMM_API_TOKEN= ROMM_API_TOKEN=
# Romarr image. Upstream docs list romarr/romarr:latest, but the public image was not pullable # Romarr image. Upstream docs list romarr/romarr:latest, but the public image was not pullable