Skip to content

Importing metadata from local launchbox installation#2922

Merged
gantoine merged 23 commits intorommapp:masterfrom
gravelfreeman:launchbox-handler
Mar 8, 2026
Merged

Importing metadata from local launchbox installation#2922
gantoine merged 23 commits intorommapp:masterfrom
gravelfreeman:launchbox-handler

Conversation

@gravelfreeman
Copy link
Copy Markdown
Contributor

Description

Launchbox handler overhaul. RomM currently only fetches metadata from the LaunchBox database, but many users already have a well-maintained local LaunchBox library. This lets RomM reuse that work instead of scraping everything again.

Supported :

  • Metadata
  • Images
  • Manuals

Requires implementation :

  • Videos

How does RomM access your local LaunchBox installation?

You can mount it to /temp as read only either the full root of Launchbox folder, or just the Data, Images, and Manuals directories as follow :

/temp/Data
/temp/Images
/temp/Manuals

I added a Remote switch in the frontend so you can toggle remote metadata/media fetching.

  • If switch is OFF and there's no /temp folder Launchbox handler will exit.

  • If switch is OFF and there's /temp folder Launchbox handler will search for platforms in mounted Launchbox structure under /temp/Data/Platform/<Platform Name.xml>.

  • If switch is ON and there's no /temp folder Launchbox handler will scrape platforms in your default roms library.

  • If switch is ON and there's /temp folder Launchbox handler will search for platforms in mounted Launchbox structure under /temp/Data/Platform/<Platform Name.xml> and match roms with the local Launchbox metadata in priority and then fallback to Remote metadata.

If you prefer, you can also copy platform XML files one by one into /temp/Data/<Platform Name>.xml to import platforms individually.

Checklist
Please check all that apply.

  • I've tested the changes locally
  • I've updated relevant comments
  • I've assigned reviewers for this PR
  • I've added unit tests that cover the changes

Screenshots (if applicable)

image

Copilot AI review requested due to automatic review settings January 19, 2026 18:48
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @gravelfreeman, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a major enhancement to the LaunchBox integration within RomM, shifting from an exclusive remote metadata fetching model to one that prioritizes and leverages local LaunchBox installations. Users can now mount their local LaunchBox directories to import game metadata, images, and manuals, reducing reliance on external scraping. The changes include robust logic for identifying and processing local XML files and media, alongside a new user interface control to manage the interplay between local and remote data sources, offering greater flexibility and efficiency for managing game libraries.

Highlights

  • Local LaunchBox Integration: RomM now supports importing metadata, images, and manuals directly from a local LaunchBox installation, allowing users to reuse their existing curated libraries.
  • Flexible Data Sourcing: The system prioritizes local LaunchBox metadata and media if available, with a fallback to remote fetching if local data is not found or if remote enrichment is explicitly enabled.
  • Frontend Control for Remote Fetching: A new 'Remote' switch has been added to the frontend, giving users granular control over whether remote LaunchBox metadata and media fetching is active during scans.
  • Docker Image Optimization: A .dockerignore file was added to significantly reduce the size of Docker images by excluding unnecessary development, testing, and documentation files.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a major overhaul of the Launchbox handler to support importing metadata from a local Launchbox installation, in addition to the existing remote database scraping. This is a great feature for users who have already curated their local libraries. The changes are extensive, touching the backend scanning logic, the Launchbox metadata handler, and the frontend UI to add a toggle for this new functionality. The implementation correctly prioritizes local metadata and media, with a fallback to remote sources.

My review focuses on the new logic in launchbox_handler.py, where I've identified a few areas for improvement regarding exception handling, performance, and potential data loss from aggressive filtering. These are important to address to ensure the new feature is robust and reliable. The rest of the changes, including the frontend updates and plumbing the new setting through the backend, look solid.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for importing metadata, images, and manuals from local LaunchBox installations, allowing users to reuse existing LaunchBox libraries instead of re-scraping metadata. The implementation includes a frontend toggle to control whether remote metadata fetching is enabled, and backend logic to parse local XML files and resolve media assets from the mounted LaunchBox directory structure.

Changes:

  • Added UI toggle for LaunchBox remote metadata fetching with local storage persistence
  • Implemented local XML parsing for LaunchBox platform data with caching
  • Added media file resolution for images, screenshots, and manuals from local LaunchBox directories

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
frontend/src/views/Scan.vue Added Remote switch toggle to control LaunchBox remote metadata fetching
frontend/src/locales/en_US/scan.json Added English translation for LaunchBox remote feature
frontend/src/locales/fr_FR/scan.json Added French translation for LaunchBox remote feature
backend/handler/metadata/launchbox_handler.py Major overhaul with local XML parsing, media resolution, and hybrid local/remote metadata support
backend/handler/scan_handler.py Added launchbox_remote_enabled parameter propagation through scan functions
backend/endpoints/sockets/scan.py Threaded launchbox_remote_enabled parameter through WebSocket scan handlers
backend/tasks/scheduled/update_launchbox_metadata.py Changed condition to use LAUNCHBOX_API_ENABLED directly instead of handler check
.dockerignore Added comprehensive ignore patterns for Docker build optimization

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@gantoine gantoine self-requested a review January 19, 2026 22:00
@gantoine gantoine changed the title add support for importing metadata from a local launchbox installation Importing metadata from a local launchbox installation Jan 19, 2026
@gantoine gantoine changed the title Importing metadata from a local launchbox installation Importing metadata from local launchbox installation Jan 19, 2026
@gantoine
Copy link
Copy Markdown
Member

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant and well-implemented feature: the ability to import metadata and media directly from a local LaunchBox installation. The extensive refactoring in launchbox_handler.py has improved modularity and clarity, breaking down complex logic into smaller, more manageable functions. The new caching mechanism for local XML files, along with the robust handling of local media paths and prioritization over remote sources, demonstrates a thorough approach to the feature. The integration with the existing scanning process and the addition of a UI toggle for remote LaunchBox fetching are also well-executed. This feature will greatly benefit users with existing LaunchBox libraries.

@gantoine gantoine assigned gantoine and unassigned gantoine Jan 29, 2026
@gantoine gantoine self-assigned this Feb 7, 2026
@gantoine gantoine marked this pull request as draft February 7, 2026 03:31
@gantoine gantoine marked this pull request as ready for review March 8, 2026 00:39
@gantoine gantoine requested a review from Copilot March 8, 2026 00:39
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 8, 2026

Greptile Summary

This PR refactors the LaunchBox handler from a single file into a structured package (handler/, local_source/, remote_source/, media/, platforms/, utils/, types/) and adds first-class support for reading metadata, images, and manuals from a locally-mounted LaunchBox installation at ${ROMM_BASE_PATH}/launchbox. A new frontend toggle lets users choose between local-only, remote-only, or hybrid metadata fetching per scan.

Key changes:

  • LocalSource parses LaunchBox platform XMLs, indexes games by ApplicationPath filename and Title, with in-memory mtime-based cache invalidation.
  • RemoteSource wraps the existing Redis-backed lookups unchanged.
  • media.py introduces _get_cover, _get_screenshots, _get_images, and _get_manuals which prefer local files when available and fall back to remote URLs.
  • scan_handler.py threads a new launchbox_remote_enabled flag from the socket event down to get_rom.
  • search.py now guards on is_cloud_enabled() instead of is_enabled(), which breaks the search endpoint for local-only users (already flagged in previous threads).

Notable new finding: remote_media_req is constructed with platform_name=None and fs_name="" for ROMs matched only via the remote source. Because _build_local_media_context returns None when platform_name is absent, local images are completely bypassed for those ROMs even when a full local LaunchBox image library is mounted. Users with both local images and remote API enabled will only benefit from local artwork for ROMs that also appear in their local XML. Threading platform_name and fs_name into remote_media_req would address this.

Additional notes:

  • UPS.SFAM and UPS.SNES share LaunchBox ID 53 with different name values, causing local XML lookup to fail for SFAM (looks for Super Famicom.xml vs the standard Super Nintendo Entertainment System.xml) and a silent reverse-lookup collision — already flagged.
  • The region_hint comma-split logic adds the full joined string as the first directory candidate, which can never match a real LaunchBox region directory.
  • Several other issues (TOCTOU mtime, blocking sync I/O in async, UPDATE fast path bypassing local source, toggle visibility in UI) were flagged in previous review threads.

Confidence Score: 2/5

  • Not safe to merge — the local-image bypass for remote-only matched ROMs means the flagship "hybrid" mode silently delivers incomplete results, and several existing thread issues (search 503 for local users, UPDATE fast path, SFAM platform collision) remain unresolved.
  • Core feature logic is sound and well-structured, but multiple functional gaps accumulate: local images are skipped for the majority of ROMs in hybrid mode, local-only users are blocked from search, SFAM local XML lookup is broken, the UPDATE scan fast path bypasses local source, and the UI toggle is hidden after dropdown close. These are not edge cases — they affect the primary workflows the PR introduces.
  • backend/handler/metadata/launchbox_handler/handler.py and media.py (local-image bypass), backend/endpoints/search.py (is_cloud_enabled guard), backend/handler/scan_handler.py (UPDATE fast path), backend/handler/metadata/launchbox_handler/platforms.py (SFAM/SNES ID collision).

Important Files Changed

Filename Overview
backend/handler/metadata/launchbox_handler/handler.py Core handler orchestrating local/remote source selection; remote_media_req is called without platform_name, silently disabling local image lookup for remote-only matched ROMs.
backend/handler/metadata/launchbox_handler/media.py New media resolution logic for covers, screenshots, images, and manuals; _build_local_media_context returns None when platform_name is None (blocking all local lookups for remote-only matches), and the region_hint comma-split logic prepends the combined string as an unmatchable first candidate.
backend/handler/metadata/launchbox_handler/platforms.py Platform slug-to-LaunchBox-ID mapping; UPS.SFAM and UPS.SNES share id 53 with different name values causing reverse-lookup collision and wrong XML filename for SFAM local source (already flagged).
backend/handler/scan_handler.py Threads launchbox_remote_enabled through to handler; UPDATE fast path still bypasses local source when remote is also enabled (already flagged), and fast path guard condition uses launchbox_remote_enabled but not is_local_enabled().
backend/endpoints/search.py Changed is_enabled() to is_cloud_enabled() for LaunchBox guard, which blocks local-only LaunchBox users with a 503 (already flagged).
frontend/src/views/Scan.vue Adds Local/Cloud toggle persisted to localStorage and threaded through the scan socket event; toggle is only visible inside the autocomplete dropdown item slot (already flagged).

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[scan_rom called] --> B{launchbox platform?}
    B -- No --> Z[return LaunchboxRom null]
    B -- Yes --> C{ScanType.UPDATE\n& rom.launchbox_id\n& remote_enabled?}
    C -- Yes --> D[get_rom_by_id\nremote only]
    C -- No --> E[get_rom\nfs_name + platform_slug]

    E --> F[LocalSource.get_rom\nparse XML / mtime cache]
    F -- local match --> G[build local_media_req\nplatform_name + fs_name + title]
    F -- no match --> H{LAUNCHBOX_TAG_REGEX\nin filename?}

    H -- yes --> I[get_rom_by_id\nby embedded ID tag]
    I -- found --> R[return ROM]
    I -- not found --> J{remote_available?}

    H -- no --> J
    J -- No --> Z
    J -- Yes --> K[RemoteSource.get_rom\nRedis name index]
    K -- found --> L[build remote_media_req\nplatform_name=None ⚠️\nfs_name= ⚠️]
    K -- not found --> Z

    G --> M[fetch_images remote]
    L --> M

    G --> N[_build_local_media_context\nplatform + stems + regions]
    L --> O[_build_local_media_context\nplatform_name=None → returns None ⚠️]

    N --> P[_get_cover / _get_screenshots\n_get_images / _get_manuals\nlocal files override remote]
    O --> Q[ALL local image lookups\nskipped silently ⚠️]

    P --> R
    Q --> R
    M --> N
    M --> O
Loading

Last reviewed commit: aa4abe6

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@gantoine gantoine merged commit 055e973 into rommapp:master Mar 8, 2026
4 checks passed
Comment on lines +130 to +142
else:
search_term = fs_name

search_term = search_term.lower()

# Check if game is scummvm shortname
if platform_slug == UPS.SCUMMVM:
search_term = await self._scummvm_format(search_term)
fallback_rom = LaunchboxRom(launchbox_id=None, name=search_term)

index_entry = await self._remote.get_rom(
search_term,
platform_slug,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local images silently skipped for remote-only matched ROMs

When a ROM has no local XML entry but is matched via the remote source, remote_media_req is constructed with platform_name=None and fs_name="". Inside _build_local_media_context, the very first guard is:

if not req.platform_name:
    return None

This causes None to be returned immediately, so every downstream image function (_get_cover, _get_screenshots, _get_images) skips local lookups entirely. A user who mounts a full local LaunchBox image library under /romm/launchbox/Images/ will receive no locally-cached artwork for any ROM that wasn't indexed in their local XML — even though the images are present on disk.

The fix is to thread both platform_name and the original fs_name through remote_media_req so the local media context can be built:

# In remote_media_req: add platform_name and fs_name params
def remote_media_req(
    *,
    platform_name: str | None,
    fs_name: str,
    remote: dict | None,
    remote_images: list[dict] | None,
    remote_enabled: bool,
) -> MediaRequest:
    title = ((remote or {}).get("Name") or "").strip()
    return MediaRequest(
        platform_name,
        fs_name,
        title,
        None,
        remote_images,
        remote_enabled,
    )

And at the call site in get_rom:

platform_name = get_platform(platform_slug).get("name")
media_req = remote_media_req(
    platform_name=platform_name,
    fs_name=fs_name,
    remote=index_entry,
    remote_images=remote_images,
    remote_enabled=remote_available,
)

Comment on lines +100 to +108
preferred_regions.extend(
[r.strip() for r in region_hint.split(",") if r.strip()]
)

return {
"base": base,
"stems": stems,
"preferred_regions": preferred_regions,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combined region_hint string added as first (unmatchable) search path

When region_hint contains a comma (e.g., "North America, Europe"), the full joined string is appended to preferred_regions before the split parts:

preferred_regions.append(region_hint)          # → "North America, Europe"
if "," in region_hint:
    preferred_regions.extend([...])             # → "North America", "Europe"

The first entry "North America, Europe" will never match a real LaunchBox directory (which uses individual region names). It is tried first for every stem, burning iterations on a path that cannot exist. Consider only appending the split parts when a comma is present:

if region_hint:
    if "," in region_hint:
        preferred_regions.extend(
            [r.strip() for r in region_hint.split(",") if r.strip()]
        )
    else:
        preferred_regions.append(region_hint)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants