prefer rom's own region tag for ScreenScraper and IGDB artwork

When a ROM filename carries a region tag (e.g. (Europe)), use that
region first when picking artwork and localized titles, falling back to
the configured scan.priority.region. Previously the configured priority
was the only signal, so a US-first config would force US covers onto
European ROMs even when an EU asset was available.

Adds a shared name->provider-shortcode map and threads the rom through
the IGDB and SS lookup APIs so the rom-aware locale/region selection
can run for both providers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Georges-Antoine Assi
2026-05-13 09:06:11 -04:00
parent 547c64c4f5
commit 944514acc0
7 changed files with 83 additions and 32 deletions

View File

@@ -1256,7 +1256,7 @@ async def update_rom(
cleaned_data.update({"ss_id": None, "ss_metadata": {}})
if cleaned_data["igdb_id"] and int(cleaned_data["igdb_id"]) != rom.igdb_id:
igdb_rom = await meta_igdb_handler.get_rom_by_id(cleaned_data["igdb_id"])
igdb_rom = await meta_igdb_handler.get_rom_by_id(rom, cleaned_data["igdb_id"])
if igdb_rom.get("igdb_id"):
cleaned_data.update(igdb_rom)
elif rom.igdb_id and not cleaned_data["igdb_id"]:

View File

@@ -100,7 +100,7 @@ async def search_rom(
if search_by.lower() == "id":
try:
igdb_rom, moby_rom, ss_rom, lb_rom = await asyncio.gather(
meta_igdb_handler.get_matched_rom_by_id(int(search_term)),
meta_igdb_handler.get_matched_rom_by_id(rom, int(search_term)),
meta_moby_handler.get_matched_rom_by_id(int(search_term)),
meta_ss_handler.get_matched_rom_by_id(rom, int(search_term)),
meta_launchbox_handler.get_matched_rom_by_id(int(search_term)),
@@ -125,7 +125,7 @@ async def search_rom(
launchbox_matched_roms,
) = await asyncio.gather(
meta_igdb_handler.get_matched_roms_by_name(
search_term, get_main_platform_igdb_id(rom.platform)
rom, search_term, get_main_platform_igdb_id(rom.platform)
),
meta_moby_handler.get_matched_roms_by_name(
search_term, rom.platform.moby_id

View File

@@ -88,6 +88,34 @@ REGIONS = (
REGIONS_BY_SHORTCODE = {region[0]: region[1] for region in REGIONS}
REGIONS_NAME_KEYS = frozenset(region[1].lower() for region in REGIONS)
# Maps full REGIONS names to lowercase shortcodes used by metadata providers
REGION_NAME_TO_PROVIDER_SHORTCODE: dict[str, str] = {
"Australia": "au",
"Asia": "asi",
"Brazil": "br",
"Canada": "ca",
"China": "cn",
"England": "uk",
"Europe": "eu",
"Finland": "fi",
"France": "fr",
"Germany": "de",
"Greece": "gr",
"Holland": "nl",
"Hong Kong": "hk",
"Italy": "it",
"Japan": "jp",
"Korea": "kr",
"Netherlands": "nl",
"Norway": "no",
"Russia": "ru",
"Spain": "sp",
"Sweden": "se",
"Taiwan": "tw",
"USA": "us",
"World": "wor",
}
LANGUAGES_BY_SHORTCODE = {lang[0]: lang[1] for lang in LANGUAGES}
LANGUAGES_NAME_KEYS = frozenset(lang[1].lower() for lang in LANGUAGES)

View File

@@ -14,8 +14,10 @@ from adapters.services.igdb_types import (
)
from config import IGDB_CLIENT_ID, IGDB_CLIENT_SECRET, IS_PYTEST_RUN
from config.config_manager import config_manager as cm
from handler.filesystem.base_handler import REGION_NAME_TO_PROVIDER_SHORTCODE
from handler.redis_handler import async_cache
from logger.logger import log
from models.rom import Rom
from utils.context import ctx_httpx_client
from .base_handler import (
@@ -325,18 +327,22 @@ REGION_TO_IGDB_LOCALE: dict[str, str | None] = {
}
def get_igdb_preferred_locale() -> str | None:
"""Get IGDB locale from scan.priority.region configuration.
def get_igdb_preferred_locale(rom: Rom | None = None) -> str | None:
"""Get IGDB locale, preferring the rom's own region tag when available.
Maps region priority codes to IGDB's game_localizations region identifiers.
Returns the first matching region from the priority list, or None for default.
Checks the rom's tagged regions first, then falls back to scan.priority.region.
Returns:
IGDB region identifier (e.g., "ja-JP", "EU") or None for default
"""
config = cm.get_config()
if rom is not None and isinstance(rom.regions, list):
for region_name in rom.regions:
code = REGION_NAME_TO_PROVIDER_SHORTCODE.get(region_name)
if code and code in REGION_TO_IGDB_LOCALE:
return REGION_TO_IGDB_LOCALE[code]
# Check each region in priority order and return first match
config = cm.get_config()
for region in config.SCAN_REGION_PRIORITY:
if region.lower() in REGION_TO_IGDB_LOCALE:
return REGION_TO_IGDB_LOCALE[region.lower()]
@@ -587,7 +593,7 @@ class IGDBHandler(MetadataHandler):
return IGDBPlatform(igdb_id=None, slug=slug)
async def get_rom(self, fs_name: str, platform_igdb_id: int) -> IGDBRom:
async def get_rom(self, rom: Rom, fs_name: str, platform_igdb_id: int) -> IGDBRom:
from handler.filesystem import fs_rom_handler
if not self.is_enabled():
@@ -600,7 +606,7 @@ class IGDBHandler(MetadataHandler):
igdb_id_from_tag = self.extract_igdb_id_from_filename(fs_name)
if igdb_id_from_tag:
log.debug(f"Found IGDB ID tag in filename: {igdb_id_from_tag}")
rom_by_id = await self.get_rom_by_id(igdb_id_from_tag)
rom_by_id = await self.get_rom_by_id(rom, igdb_id_from_tag)
if rom_by_id["igdb_id"]:
log.debug(
f"Successfully matched ROM by IGDB ID tag: {fs_name} -> {igdb_id_from_tag}"
@@ -678,18 +684,20 @@ class IGDBHandler(MetadataHandler):
search_term = self.normalize_search_term(search_term)
log.debug("Searching for %s on IGDB with game_type", search_term)
rom = await self._search_rom(search_term, platform_igdb_id, with_game_type=True)
if not rom:
res = await self._search_rom(search_term, platform_igdb_id, with_game_type=True)
if not res:
log.debug("Searching for %s on IGDB without game_type", search_term)
rom = await self._search_rom(search_term, platform_igdb_id)
res = await self._search_rom(search_term, platform_igdb_id)
# IGDB search is fuzzy so no need to split the search term by special characters
if not rom:
if not res:
return fallback_rom
return build_igdb_rom(self, rom, get_igdb_preferred_locale(), platform_igdb_id)
return build_igdb_rom(
self, res, get_igdb_preferred_locale(rom=rom), platform_igdb_id
)
async def get_rom_by_id(self, igdb_id: int) -> IGDBRom:
async def get_rom_by_id(self, rom: Rom, igdb_id: int) -> IGDBRom:
if not self.is_enabled():
return IGDBRom(igdb_id=None)
@@ -701,17 +709,17 @@ class IGDBHandler(MetadataHandler):
if not roms:
return IGDBRom(igdb_id=None)
return build_igdb_rom(self, roms[0], get_igdb_preferred_locale(), None)
return build_igdb_rom(self, roms[0], get_igdb_preferred_locale(rom=rom), None)
async def get_matched_rom_by_id(self, igdb_id: int) -> IGDBRom | None:
async def get_matched_rom_by_id(self, rom: Rom, igdb_id: int) -> IGDBRom | None:
if not self.is_enabled():
return None
rom = await self.get_rom_by_id(igdb_id)
return rom if rom["igdb_id"] else None
result = await self.get_rom_by_id(rom, igdb_id)
return result if result["igdb_id"] else None
async def get_matched_roms_by_name(
self, search_term: str, platform_igdb_id: int | None
self, rom: Rom, search_term: str, platform_igdb_id: int | None
) -> list[IGDBRom]:
if not self.is_enabled():
return []
@@ -762,7 +770,7 @@ class IGDBHandler(MetadataHandler):
if rom["id"] not in unique_ids
]
preferred_locale = get_igdb_preferred_locale()
preferred_locale = get_igdb_preferred_locale(rom=rom)
return [
build_igdb_rom(self, rom, preferred_locale, platform_igdb_id)
for rom in matched_roms

View File

@@ -13,6 +13,7 @@ from config import SCREENSCRAPER_PASSWORD, SCREENSCRAPER_USER
from config.config_manager import MetadataMediaType
from config.config_manager import config_manager as cm
from handler.filesystem import fs_resource_handler
from handler.filesystem.base_handler import REGION_NAME_TO_PROVIDER_SHORTCODE
from logger.logger import log
from models.rom import Rom, RomFile
@@ -34,12 +35,21 @@ SS_DEV_PASSWORD: Final = base64.b64decode("eFRKd29PRmpPUUc=").decode()
SENSITIVE_KEYS = {"ssid", "sspassword"}
def get_preferred_regions() -> list[str]:
"""Get preferred regions from config"""
def get_preferred_regions(rom: Rom | None = None) -> list[str]:
"""Get preferred regions, prepending the rom's own region tags when available."""
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.get(region_name)
if code:
rom_codes.append(code)
config = cm.get_config()
return list(
dict.fromkeys(
config.SCAN_REGION_PRIORITY + ["us", "wor", "ss", "eu", "jp", "cus"]
rom_codes
+ config.SCAN_REGION_PRIORITY
+ ["us", "wor", "ss", "eu", "jp", "cus"]
)
) + ["unk"]
@@ -174,7 +184,7 @@ def extract_media_from_ss_game(rom: Rom, game: SSGame) -> SSMetadataMedia:
video_normalized_path=None,
)
for region in get_preferred_regions():
for region in get_preferred_regions(rom):
for media in game.get("medias", []):
if media.get("region", "unk") != region or media.get("parent") != "jeu":
continue
@@ -409,7 +419,7 @@ def build_ss_game(rom: Rom, game: SSGame) -> SSRom:
preferred_media_types = get_preferred_media_types()
res_name = ""
for region in get_preferred_regions():
for region in get_preferred_regions(rom):
res_name = next(
(
name["text"]

View File

@@ -457,7 +457,7 @@ async def scan_rom(
f"{hl(str(h_igdb_id), color=BLUE)} {emoji.EMOJI_ALIEN_MONSTER}",
extra=LOGGER_MODULE_NAME,
)
return await meta_igdb_handler.get_rom_by_id(h_igdb_id)
return await meta_igdb_handler.get_rom_by_id(rom, h_igdb_id)
# Use Playmatch matches to get the IGDB ID
if playmatch_rom["igdb_id"] is not None:
@@ -467,16 +467,20 @@ async def scan_rom(
extra=LOGGER_MODULE_NAME,
)
return await meta_igdb_handler.get_rom_by_id(playmatch_rom["igdb_id"])
return await meta_igdb_handler.get_rom_by_id(
rom, playmatch_rom["igdb_id"]
)
main_platform_igdb_id = get_main_platform_igdb_id(platform)
if scan_type == ScanType.UPDATE and rom.igdb_id:
# Use the ID to refetch the metadata from IGDB
return await meta_igdb_handler.get_rom_by_id(rom.igdb_id)
return await meta_igdb_handler.get_rom_by_id(rom, rom.igdb_id)
else:
# If no matches found, use the file name to get the IGDB ID
return await meta_igdb_handler.get_rom(
rom_attrs["fs_name"], main_platform_igdb_id or platform.igdb_id
rom,
rom_attrs["fs_name"],
main_platform_igdb_id or platform.igdb_id,
)
return IGDBRom(igdb_id=None)

View File

@@ -69,10 +69,11 @@ class TestGetPreferredRegions:
class TestExtractMediaFromSsGame:
"""Tests for extract_media_from_ss_game."""
def _make_rom(self) -> MagicMock:
def _make_rom(self, regions: list[str] | None = None) -> MagicMock:
rom = MagicMock()
rom.platform_id = 1
rom.id = 100
rom.regions = regions
return rom
def _make_game_with_cus_only(self) -> SSGame: