fix: trigger RomM quick scan via socket
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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:-{}}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user