diff --git a/backend/endpoints/roms/__init__.py b/backend/endpoints/roms/__init__.py index 2b0ea742b..9a9127e1e 100644 --- a/backend/endpoints/roms/__init__.py +++ b/backend/endpoints/roms/__init__.py @@ -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"]: diff --git a/backend/endpoints/search.py b/backend/endpoints/search.py index a8d0ebb83..2aba3c82a 100644 --- a/backend/endpoints/search.py +++ b/backend/endpoints/search.py @@ -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 diff --git a/backend/handler/filesystem/base_handler.py b/backend/handler/filesystem/base_handler.py index b93042cf4..b36a65d62 100644 --- a/backend/handler/filesystem/base_handler.py +++ b/backend/handler/filesystem/base_handler.py @@ -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) diff --git a/backend/handler/metadata/igdb_handler.py b/backend/handler/metadata/igdb_handler.py index ca44d6ff4..c4fbebe62 100644 --- a/backend/handler/metadata/igdb_handler.py +++ b/backend/handler/metadata/igdb_handler.py @@ -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 diff --git a/backend/handler/metadata/ss_handler.py b/backend/handler/metadata/ss_handler.py index a493275e6..f455c834a 100644 --- a/backend/handler/metadata/ss_handler.py +++ b/backend/handler/metadata/ss_handler.py @@ -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"] diff --git a/backend/handler/scan_handler.py b/backend/handler/scan_handler.py index 03ab3d2dd..3df727a29 100644 --- a/backend/handler/scan_handler.py +++ b/backend/handler/scan_handler.py @@ -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) diff --git a/backend/tests/handler/metadata/test_ss_handler.py b/backend/tests/handler/metadata/test_ss_handler.py index 18baf149e..4da1478ad 100644 --- a/backend/tests/handler/metadata/test_ss_handler.py +++ b/backend/tests/handler/metadata/test_ss_handler.py @@ -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: