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 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:

View File

@@ -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:-{}}

View File

@@ -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