From fe5af3ef9c085f41a34c2ada405c8cc5c617dbec Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Wed, 20 May 2026 17:20:54 -0400 Subject: [PATCH] fix(screenscraper): respect region priority for multi-region ROMs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For ROMs tagged with multiple regions (e.g. "(Japan, USA)"), filename order previously decided which region's name and box art won. Now reorder the rom's filename-tagged regions by SCAN_REGION_PRIORITY before prepending, so the user's configured preference wins among the regions the file is actually tagged as. Untagged priority regions still cannot outrank a filename-tagged region. Also tweak the Total Rescan → Complete Rescan label in en_GB/en_US scan locales. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/handler/metadata/ss_handler.py | 22 ++++++++---- .../tests/handler/metadata/test_ss_handler.py | 34 +++++++++++++++++++ frontend/src/locales/en_GB/scan.json | 2 +- frontend/src/locales/en_US/scan.json | 2 +- 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/backend/handler/metadata/ss_handler.py b/backend/handler/metadata/ss_handler.py index 7f44c3a0cf..25074297ca 100644 --- a/backend/handler/metadata/ss_handler.py +++ b/backend/handler/metadata/ss_handler.py @@ -34,21 +34,29 @@ def get_preferred_regions(rom: Rom | None = None) -> list[str]: - """Get preferred regions, prepending the rom's own region tags when available.""" + """Get preferred regions, prepending the rom's own region tags when available. + + When a rom is tagged with multiple regions (e.g. "(Japan, USA)"), the rom's + own tags are reordered according to the user's SCAN_REGION_PRIORITY so the + user's preference wins among the regions the file is actually tagged as. + Filename-tagged regions not present in the priority list keep their relative + order and follow the prioritized ones. + """ + config = cm.get_config() + priority = config.SCAN_REGION_PRIORITY + rom_codes: list[str] = [] if rom is not None and isinstance(rom.regions, list): for region_name in rom.regions: code = region_name_to_provider_shortcode(region_name) if code: rom_codes.append(code) + rom_codes.sort( + key=lambda code: priority.index(code) if code in priority else len(priority) + ) - config = cm.get_config() return list( - dict.fromkeys( - rom_codes - + config.SCAN_REGION_PRIORITY - + ["us", "wor", "ss", "eu", "jp", "cus"] - ) + dict.fromkeys(rom_codes + priority + ["us", "wor", "ss", "eu", "jp", "cus"]) ) + ["unk"] diff --git a/backend/tests/handler/metadata/test_ss_handler.py b/backend/tests/handler/metadata/test_ss_handler.py index 3b7e996f6a..2bb2a3ce13 100644 --- a/backend/tests/handler/metadata/test_ss_handler.py +++ b/backend/tests/handler/metadata/test_ss_handler.py @@ -68,6 +68,40 @@ def test_no_duplicates(self): assert len(regions) == len(set(regions)) + def test_multi_region_rom_respects_user_priority(self): + """For a multi-region ROM, the user's priority order wins among the + regions the file is tagged as.""" + rom = MagicMock() + rom.regions = ["Japan", "USA"] + config = _make_config(region_priority=["us", "eu"]) + with patch("handler.metadata.ss_handler.cm.get_config", return_value=config): + regions = get_preferred_regions(rom) + + assert regions.index("us") < regions.index("jp") + + def test_multi_region_rom_untagged_priority_does_not_win(self): + """A region in SCAN_REGION_PRIORITY that the file is NOT tagged as + should not outrank a region the file IS tagged as.""" + rom = MagicMock() + rom.regions = ["Japan", "USA"] + config = _make_config(region_priority=["eu", "us"]) + with patch("handler.metadata.ss_handler.cm.get_config", return_value=config): + regions = get_preferred_regions(rom) + + assert regions.index("us") < regions.index("eu") + assert regions.index("jp") < regions.index("eu") + + def test_multi_region_rom_unprioritized_tags_preserve_order(self): + """Filename regions not in the priority list keep their filename order + and follow the prioritized ones.""" + rom = MagicMock() + rom.regions = ["Japan", "Brazil"] + config = _make_config(region_priority=["us"]) + with patch("handler.metadata.ss_handler.cm.get_config", return_value=config): + regions = get_preferred_regions(rom) + + assert regions.index("jp") < regions.index("br") + class TestExtractMediaFromSsGame: """Tests for extract_media_from_ss_game.""" diff --git a/frontend/src/locales/en_GB/scan.json b/frontend/src/locales/en_GB/scan.json index 1255f4fc62..4fdac228a7 100644 --- a/frontend/src/locales/en_GB/scan.json +++ b/frontend/src/locales/en_GB/scan.json @@ -37,7 +37,7 @@ "roms-scanned-with-details": "ROMs: {n_scanned_roms} scanned out of {n_total_roms}, with {n_new_roms} new and {n_identified_roms} identified", "scan": "Scan", "scan-options": "Scan options", - "scan-types-info": "New Platforms: This will only look for platforms that are not already in RomM.

Quick Scan: Scans for games that are not in the library yet (fastest).

Unmatched Games: Attempts to match games that are not matched with the selected metadata sources.
For example, selecting IGDB and ScreenScraper will scan games that are not matched with IGDB or ScreenScraper.

Update Metadata: Updates the metadata for games that have been matched with selected metadata sources using the external ID (e.g. IGDB ID).
For example, selecting IGDB and ScreenScraper will update the metadata for games that are matched with IGDB or ScreenScraper, and will use igdb_id and/or ssfr_id to refetch the metadata from the respective providers.

Recalculate Hashes: Recalculates hashes for all files in the selected platforms.

Total Rescan: Rescans and rematches all games in the selected platforms (slowest).
This will wipe all existing metadata matches, including the external IDs, and attempt to match them again, like on a fresh scan. Saves, states and notes will be preserved.", + "scan-types-info": "New Platforms: This will only look for platforms that are not already in RomM.

Quick Scan: Scans for games that are not in the library yet (fastest).

Unmatched Games: Attempts to match games that are not matched with the selected metadata sources.
For example, selecting IGDB and ScreenScraper will scan games that are not matched with IGDB or ScreenScraper.

Update Metadata: Updates the metadata for games that have been matched with selected metadata sources using the external ID (e.g. IGDB ID).
For example, selecting IGDB and ScreenScraper will update the metadata for games that are matched with IGDB or ScreenScraper, and will use igdb_id and/or ssfr_id to refetch the metadata from the respective providers.

Recalculate Hashes: Recalculates hashes for all files in the selected platforms.

Complete Rescan: Rescans and rematches all games in the selected platforms (slowest).
This will wipe all existing metadata matches, including the external IDs, and attempt to match them again, like on a fresh scan. Saves, states and notes will be preserved.", "scan-types-more-info": "More information", "select-one-source": "Please select at least one metadata source to enrich your library with artwork and metadata", "unmatched-games": "Unmatched games", diff --git a/frontend/src/locales/en_US/scan.json b/frontend/src/locales/en_US/scan.json index 1255f4fc62..4fdac228a7 100644 --- a/frontend/src/locales/en_US/scan.json +++ b/frontend/src/locales/en_US/scan.json @@ -37,7 +37,7 @@ "roms-scanned-with-details": "ROMs: {n_scanned_roms} scanned out of {n_total_roms}, with {n_new_roms} new and {n_identified_roms} identified", "scan": "Scan", "scan-options": "Scan options", - "scan-types-info": "New Platforms: This will only look for platforms that are not already in RomM.

Quick Scan: Scans for games that are not in the library yet (fastest).

Unmatched Games: Attempts to match games that are not matched with the selected metadata sources.
For example, selecting IGDB and ScreenScraper will scan games that are not matched with IGDB or ScreenScraper.

Update Metadata: Updates the metadata for games that have been matched with selected metadata sources using the external ID (e.g. IGDB ID).
For example, selecting IGDB and ScreenScraper will update the metadata for games that are matched with IGDB or ScreenScraper, and will use igdb_id and/or ssfr_id to refetch the metadata from the respective providers.

Recalculate Hashes: Recalculates hashes for all files in the selected platforms.

Total Rescan: Rescans and rematches all games in the selected platforms (slowest).
This will wipe all existing metadata matches, including the external IDs, and attempt to match them again, like on a fresh scan. Saves, states and notes will be preserved.", + "scan-types-info": "New Platforms: This will only look for platforms that are not already in RomM.

Quick Scan: Scans for games that are not in the library yet (fastest).

Unmatched Games: Attempts to match games that are not matched with the selected metadata sources.
For example, selecting IGDB and ScreenScraper will scan games that are not matched with IGDB or ScreenScraper.

Update Metadata: Updates the metadata for games that have been matched with selected metadata sources using the external ID (e.g. IGDB ID).
For example, selecting IGDB and ScreenScraper will update the metadata for games that are matched with IGDB or ScreenScraper, and will use igdb_id and/or ssfr_id to refetch the metadata from the respective providers.

Recalculate Hashes: Recalculates hashes for all files in the selected platforms.

Complete Rescan: Rescans and rematches all games in the selected platforms (slowest).
This will wipe all existing metadata matches, including the external IDs, and attempt to match them again, like on a fresh scan. Saves, states and notes will be preserved.", "scan-types-more-info": "More information", "select-one-source": "Please select at least one metadata source to enrich your library with artwork and metadata", "unmatched-games": "Unmatched games",