Skip to content

Commit effd081

Browse files
authored
Merge pull request #3286 from rommapp/local-lb-fix
Fix local LaunchBox handler: paths, arcade, videos, remote-match images
2 parents 547c64c + 8999b66 commit effd081

12 files changed

Lines changed: 658 additions & 76 deletions

File tree

backend/endpoints/sockets/scan.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,16 @@ async def _identify_rom(
414414
_added_rom.gamelist_metadata[f"{media_type.value}_path"],
415415
)
416416

417+
# Handle special media files from LaunchBox
418+
if _added_rom.launchbox_metadata and MetadataSource.LAUNCHBOX in metadata_sources:
419+
preferred_media_types = get_preferred_media_types()
420+
for media_type in preferred_media_types:
421+
if _added_rom.launchbox_metadata.get(f"{media_type.value}_path"):
422+
await fs_resource_handler.store_media_file(
423+
_added_rom.launchbox_metadata[f"{media_type.value}_url"],
424+
_added_rom.launchbox_metadata[f"{media_type.value}_path"],
425+
)
426+
417427
# Store normal and locked badges
418428
if _added_rom.ra_metadata and MetadataSource.RA in metadata_sources:
419429
for ach in _added_rom.ra_metadata.get("achievements", []):

backend/handler/filesystem/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .assets_handler import FSAssetsHandler
22
from .firmware_handler import FSFirmwareHandler
3+
from .launchbox_handler import FSLaunchboxHandler
34
from .platforms_handler import FSPlatformsHandler
45
from .resources_handler import FSResourcesHandler
56
from .roms_handler import FSRomsHandler
@@ -11,3 +12,4 @@
1112
fs_rom_handler = FSRomsHandler()
1213
fs_resource_handler = FSResourcesHandler()
1314
fs_sync_handler = FSSyncHandler()
15+
fs_launchbox_handler = FSLaunchboxHandler()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from pathlib import Path
2+
3+
from config import ROMM_BASE_PATH
4+
from handler.filesystem.base_handler import FSHandler
5+
6+
7+
class FSLaunchboxHandler(FSHandler):
8+
def __init__(self) -> None:
9+
super().__init__(base_path=str(Path(ROMM_BASE_PATH) / "launchbox"))

backend/handler/filesystem/resources_handler.py

Lines changed: 56 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,32 @@
1919

2020
from .base_handler import CoverSize, FSHandler
2121

22+
LOCAL_FILE_SCHEMES = ("file://", "launchbox-file://")
23+
24+
25+
def _resolve_local_file_uri(uri: str) -> Path | None:
26+
"""Resolve a local-file URI to an absolute Path, or None if unsafe/unknown.
27+
28+
`file://` resolves under the ROM library root. `launchbox-file://` resolves
29+
under the LaunchBox data root — LaunchBox metadata produces paths relative
30+
to `/romm/launchbox`, which is not the same as the library root.
31+
"""
32+
from handler.filesystem import fs_launchbox_handler, fs_rom_handler
33+
34+
if uri.startswith("launchbox-file://"):
35+
try:
36+
return fs_launchbox_handler.validate_path(uri[len("launchbox-file://") :])
37+
except ValueError:
38+
return None
39+
40+
if uri.startswith("file://"):
41+
try:
42+
return fs_rom_handler.validate_path(uri[len("file://") :])
43+
except ValueError:
44+
return None
45+
46+
return None
47+
2248

2349
def _content_type_essence(header_value: str) -> str:
2450
"""Return the MIME type token (before parameters), lowercased."""
@@ -95,27 +121,21 @@ async def _store_cover(
95121
cover_file = f"{entity.fs_resources_path}/cover"
96122
await self.make_directory(cover_file)
97123

98-
# Handle file:// URLs for gamelist.xml
99-
if url_cover.startswith("file://"):
124+
# Handle local-file URIs from metadata handlers (gamelist, LaunchBox)
125+
if url_cover.startswith(LOCAL_FILE_SCHEMES):
100126
try:
101-
from handler.filesystem import fs_rom_handler
102-
103-
validated = fs_rom_handler.validate_path(
104-
url_cover[7:] # Remove "file://" prefix
105-
)
106-
if await AnyioPath(validated).exists():
107-
# Copy the file to the resources directory
108-
dest_path = f"{cover_file}/{size.value}.png"
109-
await self.copy_file(validated, dest_path)
110-
111-
if ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP:
112-
self.image_converter.convert_to_webp(
113-
self.validate_path(f"{cover_file}/{size.value}.png"),
114-
force=True,
115-
)
116-
else:
117-
log.warning(f"Cover file not found: {str(validated)}")
127+
resolved = _resolve_local_file_uri(url_cover)
128+
if resolved is None or not await AnyioPath(resolved).exists():
129+
log.warning(f"Cover file not found: {url_cover}")
118130
return None
131+
dest_path = f"{cover_file}/{size.value}.png"
132+
await self.copy_file(resolved, dest_path)
133+
134+
if ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP:
135+
self.image_converter.convert_to_webp(
136+
self.validate_path(f"{cover_file}/{size.value}.png"),
137+
force=True,
138+
)
119139
except Exception as exc:
120140
log.error(f"Unable to copy cover file {url_cover}: {str(exc)}")
121141
return None
@@ -275,20 +295,14 @@ async def _store_screenshot(self, rom: Rom, url_screenhot: str, idx: int):
275295
screenshot_path = f"{rom.fs_resources_path}/screenshots"
276296
await self.make_directory(screenshot_path)
277297

278-
# Handle file:// URLs for gamelist.xml
279-
if url_screenhot.startswith("file://"):
298+
# Handle local-file URIs from metadata handlers (gamelist, LaunchBox)
299+
if url_screenhot.startswith(LOCAL_FILE_SCHEMES):
280300
try:
281-
from handler.filesystem import fs_rom_handler
282-
283-
validated = fs_rom_handler.validate_path(
284-
url_screenhot[7:] # Remove "file://" prefix
285-
)
286-
if await AnyioPath(validated).exists():
287-
# Copy the file to the resources directory
288-
await self.copy_file(validated, f"{screenshot_path}/{idx}.jpg")
289-
else:
290-
log.warning(f"Screenshot file not found: {str(validated)}")
301+
resolved = _resolve_local_file_uri(url_screenhot)
302+
if resolved is None or not await AnyioPath(resolved).exists():
303+
log.warning(f"Screenshot file not found: {url_screenhot}")
291304
return None
305+
await self.copy_file(resolved, f"{screenshot_path}/{idx}.jpg")
292306
except Exception as exc:
293307
log.error(f"Unable to copy screenshot file {url_screenhot}: {str(exc)}")
294308
return None
@@ -395,20 +409,14 @@ async def _store_manual(self, rom: Rom, url_manual: str):
395409
manual_path = f"{rom.fs_resources_path}/manual"
396410
await self.make_directory(manual_path)
397411

398-
# Handle file:// URLs for gamelist.xml
399-
if url_manual.startswith("file://"):
412+
# Handle local-file URIs from metadata handlers (gamelist, LaunchBox)
413+
if url_manual.startswith(LOCAL_FILE_SCHEMES):
400414
try:
401-
from handler.filesystem import fs_rom_handler
402-
403-
validated = fs_rom_handler.validate_path(
404-
url_manual[7:] # Remove "file://" prefix
405-
)
406-
if await AnyioPath(validated).exists():
407-
# Copy the file to the resources directory
408-
await self.copy_file(validated, f"{manual_path}/{rom.id}.pdf")
409-
else:
410-
log.warning(f"Manual file not found: {str(validated)}")
415+
resolved = _resolve_local_file_uri(url_manual)
416+
if resolved is None or not await AnyioPath(resolved).exists():
417+
log.warning(f"Manual file not found: {url_manual}")
411418
return None
419+
await self.copy_file(resolved, f"{manual_path}/{rom.id}.pdf")
412420
except Exception as exc:
413421
log.error(f"Unable to copy manual file {url_manual}: {str(exc)}")
414422
return None
@@ -542,17 +550,12 @@ async def store_media_file(self, url_media: str, dest_path: str) -> None:
542550
# Ensure destination directory exists
543551
await self.make_directory(directory)
544552

545-
# Handle file:// URLs for gamelist.xml
546-
if url_media.startswith("file://"):
553+
# Handle local-file URIs from metadata handlers (gamelist, LaunchBox)
554+
if url_media.startswith(LOCAL_FILE_SCHEMES):
547555
try:
548-
from handler.filesystem import fs_rom_handler
549-
550-
validated = fs_rom_handler.validate_path(
551-
url_media[7:] # Remove "file://" prefix
552-
)
553-
file_path = AnyioPath(validated)
554-
if await file_path.exists():
555-
await self.copy_file(Path(str(file_path)), dest_path)
556+
resolved = _resolve_local_file_uri(url_media)
557+
if resolved is not None and await AnyioPath(resolved).exists():
558+
await self.copy_file(resolved, dest_path)
556559
except Exception as exc:
557560
log.error(f"Unable to copy media file {url_media}: {str(exc)}")
558561
return None

backend/handler/metadata/launchbox_handler/handler.py

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,16 @@ async def get_rom(
128128
else:
129129
search_term = fs_name
130130

131+
# Resolve MAME arcade filename (e.g. wrlok_l3.zip) to its full title
132+
# via LaunchBox's Mame.xml before name-based lookup.
133+
if platform_slug == UPS.ARCADE:
134+
mame_entry = await self._remote.get_mame_entry(fs_name)
135+
if mame_entry:
136+
name = (mame_entry.get("Name") or "").strip()
137+
if name:
138+
search_term = name
139+
fallback_rom = LaunchboxRom(launchbox_id=None, name=name)
140+
131141
# We replace " - "/"- " with ": " to match Launchbox's naming convention
132142
search_term = re.sub(DASH_COLON_REGEX, ": ", search_term).lower()
133143

@@ -152,6 +162,8 @@ async def get_rom(
152162
remote=index_entry,
153163
remote_images=remote_images,
154164
remote_enabled=remote_available,
165+
platform_name=get_platform(platform_slug).get("name"),
166+
fs_name=fs_name,
155167
)
156168

157169
return build_rom(
@@ -162,7 +174,12 @@ async def get_rom(
162174
)
163175

164176
async def get_rom_by_id(
165-
self, database_id: int, *, remote_enabled: bool = True
177+
self,
178+
database_id: int,
179+
*,
180+
remote_enabled: bool = True,
181+
fs_name: str | None = None,
182+
platform_slug: str | None = None,
166183
) -> LaunchboxRom:
167184
if not self.is_enabled():
168185
return LaunchboxRom(launchbox_id=None)
@@ -174,17 +191,42 @@ async def get_rom_by_id(
174191
if not remote:
175192
return LaunchboxRom(launchbox_id=None)
176193

194+
# Merge local-only fields when a local LaunchBox install has the same game
195+
local: dict[str, str] | None = None
196+
if fs_name and platform_slug:
197+
candidate = await self._local.get_rom(fs_name, platform_slug)
198+
if (
199+
candidate is not None
200+
and safe_int(candidate.get("DatabaseID")) == database_id
201+
):
202+
local = candidate
203+
204+
platform_name = (
205+
get_platform(platform_slug).get("name") if platform_slug else None
206+
)
177207
remote_images = await self._remote.fetch_images(
178208
remote=remote, remote_enabled=remote_enabled
179209
)
180-
media_req = remote_media_req(
181-
remote=remote,
182-
remote_images=remote_images,
183-
remote_enabled=remote_enabled,
184-
)
210+
if local is not None:
211+
media_req = local_media_req(
212+
platform_name=platform_name,
213+
fs_name=fs_name or "",
214+
local=local,
215+
remote=remote,
216+
remote_images=remote_images,
217+
remote_enabled=remote_enabled,
218+
)
219+
else:
220+
media_req = remote_media_req(
221+
remote=remote,
222+
remote_images=remote_images,
223+
remote_enabled=remote_enabled,
224+
platform_name=platform_name,
225+
fs_name=fs_name or "",
226+
)
185227

186228
return build_rom(
187-
local=None,
229+
local=local,
188230
remote=remote,
189231
launchbox_id=database_id,
190232
media_req=media_req,

0 commit comments

Comments
 (0)