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: