add support for importing metadata from a local launchbox installation

This commit is contained in:
Gravel Freeman
2026-01-19 13:12:27 -05:00
parent e3a9db78f8
commit 732cc2a530
8 changed files with 1017 additions and 194 deletions

50
.dockerignore Normal file
View File

@@ -0,0 +1,50 @@
# Local secrets / env
.env
**/.env
# Git / editor / OS
.git
.gitignore
.gitattributes
**/.DS_Store
# IDE / dev containers
.vscode/
.devcontainer/
# Python virtualenvs & caches
.venv/
**/.venv/
**/venv/
**/__pycache__/
**/.mypy_cache/
**/.pytest_cache/
**/.ruff_cache/
# Node caches
**/node_modules/
**/.npm/
**/.vite/
# CI / metadata not needed in image
.github/
# Mock & test data (requested)
romm_mock/
**/romm_mock/
backend/tests/
**/romm_test/
# Local-only docker compose files/examples (not needed in image)
docker-compose.yml
examples/
# Local tools / docs not required for runtime
**/*.md
CODE_OF_CONDUCT.md
CONTRIBUTING.md
DEVELOPER_SETUP.md
SECURITY.md
# Note: we intentionally do NOT ignore `docker/` because `docker/Dockerfile`
# copies init scripts and nginx/gunicorn configs from that folder.

View File

@@ -230,6 +230,7 @@ async def _identify_rom(
scan_type: ScanType,
roms_ids: list[int],
metadata_sources: list[str],
launchbox_remote_enabled: bool,
socket_manager: socketio.AsyncRedisManager,
scan_stats: ScanStats,
calculate_hashes: bool = True,
@@ -321,6 +322,7 @@ async def _identify_rom(
fs_rom=fs_rom,
metadata_sources=metadata_sources,
newly_added=newly_added,
launchbox_remote_enabled=launchbox_remote_enabled,
socket_manager=socket_manager,
)
@@ -450,6 +452,7 @@ async def _identify_platform(
fs_platforms: list[str],
roms_ids: list[int],
metadata_sources: list[str],
launchbox_remote_enabled: bool,
socket_manager: socketio.AsyncRedisManager,
scan_stats: ScanStats,
calculate_hashes: bool = True,
@@ -539,6 +542,7 @@ async def _identify_platform(
scan_type=scan_type,
roms_ids=roms_ids,
metadata_sources=metadata_sources,
launchbox_remote_enabled=launchbox_remote_enabled,
socket_manager=socket_manager,
scan_stats=scan_stats,
calculate_hashes=calculate_hashes,
@@ -589,6 +593,7 @@ async def scan_platforms(
metadata_sources: list[str],
scan_type: ScanType = ScanType.QUICK,
roms_ids: list[int] | None = None,
launchbox_remote_enabled: bool = True,
) -> ScanStats:
"""Scan all the listed platforms and fetch metadata from different sources
@@ -663,6 +668,7 @@ async def scan_platforms(
fs_platforms=fs_platforms,
roms_ids=roms_ids,
metadata_sources=metadata_sources,
launchbox_remote_enabled=launchbox_remote_enabled,
socket_manager=socket_manager,
scan_stats=scan_stats,
calculate_hashes=calculate_hashes,
@@ -702,6 +708,7 @@ async def scan_handler(_sid: str, options: dict[str, Any]):
scan_type = ScanType[options.get("type", "quick").upper()]
roms_ids = options.get("roms_ids", [])
metadata_sources = options.get("apis", [])
launchbox_remote_enabled = bool(options.get("launchbox_remote_enabled", True))
if DEV_MODE:
return await scan_platforms(
@@ -709,6 +716,7 @@ async def scan_handler(_sid: str, options: dict[str, Any]):
metadata_sources=metadata_sources,
scan_type=scan_type,
roms_ids=roms_ids,
launchbox_remote_enabled=launchbox_remote_enabled,
)
return high_prio_queue.enqueue(
@@ -717,6 +725,7 @@ async def scan_handler(_sid: str, options: dict[str, Any]):
metadata_sources=metadata_sources,
scan_type=scan_type,
roms_ids=roms_ids,
launchbox_remote_enabled=launchbox_remote_enabled,
job_timeout=SCAN_TIMEOUT, # Timeout (default of 4 hours)
result_ttl=TASK_RESULT_TTL,
meta={

File diff suppressed because it is too large Load Diff

View File

@@ -287,6 +287,7 @@ async def scan_rom(
fs_rom: FSRom,
metadata_sources: list[str],
newly_added: bool,
launchbox_remote_enabled: bool = True,
socket_manager: socketio.AsyncRedisManager | None = None,
) -> Rom:
rom_attrs = {
@@ -585,13 +586,17 @@ async def scan_rom(
and rom.platform_slug in LAUNCHBOX_PLATFORM_LIST
)
):
if scan_type == ScanType.UPDATE and rom.launchbox_id:
return await meta_launchbox_handler.get_rom_by_id(rom.launchbox_id)
else:
return await meta_launchbox_handler.get_rom(
rom_attrs["fs_name"], platform_slug
if scan_type == ScanType.UPDATE and rom.launchbox_id and launchbox_remote_enabled:
return await meta_launchbox_handler.get_rom_by_id(
rom.launchbox_id, remote_enabled=True
)
return await meta_launchbox_handler.get_rom(
rom_attrs["fs_name"],
platform_slug,
remote_enabled=launchbox_remote_enabled,
)
return LaunchboxRom(launchbox_id=None)
async def fetch_ra_rom(hasheous_rom: HasheousRom) -> RAGameRom:

View File

@@ -7,6 +7,7 @@ from defusedxml import ElementTree as ET
from config import (
ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA,
LAUNCHBOX_API_ENABLED,
SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON,
)
from handler.metadata import meta_launchbox_handler
@@ -44,7 +45,10 @@ class UpdateLaunchboxMetadataTask(RemoteFilePullTask):
async def run(self, force: bool = False) -> dict[str, Any]:
update_stats = UpdateStats()
if not meta_launchbox_handler.is_enabled():
# This task pulls remote metadata from LaunchBox (Metadata.zip) and should
# only run when the LaunchBox API integration is enabled.
# `meta_launchbox_handler.is_enabled()` may also be true for local-only mode.
if not LAUNCHBOX_API_ENABLED:
log.warning("Launchbox API is not enabled, skipping metadata update")
return update_stats.to_dict()

View File

@@ -21,6 +21,7 @@
"hasheous-requires-hashes": "Hasheous requires hash calculation to be enabled",
"retroachievements-requires-hashes": "RetroAchievements requires hash calculation to be enabled",
"manage-library": "Manage library",
"launchbox-remote": "LaunchBox remote (enrich local with remote)",
"metadata-sources": "Metadata sources",
"new-platforms": "New platforms",
"new-platforms-desc": "Scan new platforms only (fastest)",

View File

@@ -21,6 +21,7 @@
"hasheous-requires-hashes": "Hasheous nécessite que le calcul de hachage soit activé",
"retroachievements-requires-hashes": "RetroAchievements nécessite que le calcul de hachage soit activé",
"manage-library": "Gérer la bibliothèque",
"launchbox-remote": "LaunchBox remote (enrichir le local avec le remote)",
"metadata-sources": "Sources de métadonnées",
"new-platforms": "Nouvelles plateformes",
"new-platforms-desc": "Scanner uniquement les plateformes récemment ajoutées (plus rapide)",

View File

@@ -16,6 +16,7 @@ import storePlatforms from "@/stores/platforms";
import storeScanning from "@/stores/scanning";
const LOCAL_STORAGE_METADATA_SOURCES_KEY = "scan.metadataSources";
const LOCAL_STORAGE_LAUNCHBOX_REMOTE_ENABLED_KEY = "scan.launchboxRemoteEnabled";
const { t } = useI18n();
const { xs, smAndDown } = useDisplay();
const scanningStore = storeScanning();
@@ -66,12 +67,20 @@ const storedMetadataSources = useLocalStorage(
LOCAL_STORAGE_METADATA_SOURCES_KEY,
[] as string[],
);
const launchboxRemoteEnabled = useLocalStorage(
LOCAL_STORAGE_LAUNCHBOX_REMOTE_ENABLED_KEY,
true,
);
const metadataSources = ref<MetadataOption[]>(
metadataOptions.value.filter(
(m) => storedMetadataSources.value.includes(m.value) && !m.disabled,
) || heartbeat.getEnabledMetadataOptions(),
);
const isLaunchboxSelected = computed(() =>
metadataSources.value.some((s) => s.value === "launchbox"),
);
watch(metadataOptions, (newOptions) => {
// Remove any sources that are now disabled
metadataSources.value = metadataSources.value.filter((s) =>
@@ -137,6 +146,7 @@ async function scan() {
platforms: platformsToScan.value,
type: scanType.value,
apis: metadataSources.value.map((s) => s.value),
launchbox_remote_enabled: launchboxRemoteEnabled.value,
});
}
@@ -350,6 +360,24 @@ async function stopScan() {
<v-img :src="item.raw.logo_path" />
</v-avatar>
</template>
<template #append v-if="item.raw.value === 'launchbox'">
<div class="d-flex align-center">
<span class="text-caption text-medium-emphasis mr-4">
Remote
</span>
<v-switch
v-model="launchboxRemoteEnabled"
color="primary"
density="compact"
hide-details
:disabled="!isLaunchboxSelected"
aria-label="Remote"
@click.stop
@mousedown.stop
/>
</div>
</template>
</v-list-item>
</template>
<template #chip="{ item }">