mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 00:27:41 +01:00
Merge pull request #2700 from tvdu29/feature/metadata-locale-selection
feat: Add metadata locale selection for IGDB and ScreenScraper
This commit is contained in:
@@ -13,6 +13,7 @@ from adapters.services.igdb_types import (
|
||||
mark_list_expanded,
|
||||
)
|
||||
from config import IGDB_CLIENT_ID, IGDB_CLIENT_SECRET, IS_PYTEST_RUN
|
||||
from config.config_manager import config_manager as cm
|
||||
from handler.redis_handler import async_cache
|
||||
from logger.logger import log
|
||||
from utils.context import ctx_httpx_client
|
||||
@@ -209,6 +210,117 @@ def extract_metadata_from_igdb_rom(self: MetadataHandler, rom: Game) -> IGDBMeta
|
||||
)
|
||||
|
||||
|
||||
# Mapping from scan.priority.region codes to IGDB game_localizations region identifiers
|
||||
# IGDB's game_localizations provides regional titles and cover art, but NOT localized descriptions
|
||||
REGION_TO_IGDB_LOCALE: dict[str, str | None] = {
|
||||
"us": None, # United States - use default (no localization needed)
|
||||
"wor": None, # World - use default
|
||||
"eu": "EU", # Europe region
|
||||
"jp": "ja-JP", # Japan
|
||||
"kr": "ko-KR", # Korea
|
||||
"cn": "zh-CN", # China (Simplified Chinese)
|
||||
"tw": "zh-TW", # Taiwan (Traditional Chinese)
|
||||
}
|
||||
|
||||
|
||||
def get_igdb_preferred_locale() -> str | None:
|
||||
"""Get IGDB locale from scan.priority.region configuration.
|
||||
|
||||
Maps region priority codes to IGDB's game_localizations region identifiers.
|
||||
Returns the first matching region from the priority list, or None for default.
|
||||
|
||||
Returns:
|
||||
IGDB region identifier (e.g., "ja-JP", "EU") or None for default
|
||||
"""
|
||||
config = cm.get_config()
|
||||
|
||||
# Check each region in priority order and return first match
|
||||
for region in config.SCAN_REGION_PRIORITY:
|
||||
igdb_locale = REGION_TO_IGDB_LOCALE.get(region.lower())
|
||||
if igdb_locale is not None:
|
||||
return igdb_locale
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extract_localized_data(rom: Game, preferred_locale: str | None) -> tuple[str, str]:
|
||||
"""Extract localized name and cover URL based on preferred locale.
|
||||
|
||||
Returns (name, cover_url) - falls back to default if locale not found.
|
||||
"""
|
||||
default_name = rom.get("name", "")
|
||||
default_cover = pydash.get(rom, "cover.url", "")
|
||||
|
||||
if not preferred_locale:
|
||||
return default_name, default_cover
|
||||
|
||||
game_localizations = rom.get("game_localizations", [])
|
||||
if not game_localizations:
|
||||
return default_name, default_cover
|
||||
|
||||
assert mark_list_expanded(game_localizations)
|
||||
|
||||
for loc in game_localizations:
|
||||
region = loc.get("region")
|
||||
if not region:
|
||||
continue
|
||||
|
||||
assert mark_expanded(region)
|
||||
|
||||
# Match locale by region identifier (e.g., "ja-JP", "ko-KR", "EU")
|
||||
if region.get("identifier") == preferred_locale:
|
||||
localized_name = loc.get("name") or default_name
|
||||
localized_cover = loc.get("cover")
|
||||
|
||||
if localized_cover:
|
||||
assert mark_expanded(localized_cover)
|
||||
cover_url = localized_cover.get("url", "") or default_cover
|
||||
else:
|
||||
cover_url = default_cover
|
||||
|
||||
return localized_name, cover_url
|
||||
|
||||
# Locale not found, fall back to default
|
||||
log.warning(
|
||||
f"IGDB locale '{preferred_locale}' not found for '{default_name}', using default"
|
||||
)
|
||||
return default_name, default_cover
|
||||
|
||||
|
||||
def build_igdb_rom(
|
||||
handler: "IGDBHandler", rom: Game, preferred_locale: str | None
|
||||
) -> "IGDBRom":
|
||||
"""Build an IGDBRom from IGDB game data with localization support.
|
||||
|
||||
Args:
|
||||
handler: IGDBHandler instance for URL normalization
|
||||
rom: Game data from IGDB API
|
||||
preferred_locale: Locale code (e.g., "ja-JP") or None
|
||||
|
||||
Returns:
|
||||
IGDBRom with localized name/cover if available
|
||||
"""
|
||||
rom_screenshots = rom.get("screenshots", [])
|
||||
assert mark_list_expanded(rom_screenshots)
|
||||
|
||||
localized_name, localized_cover = extract_localized_data(rom, preferred_locale)
|
||||
|
||||
return IGDBRom(
|
||||
igdb_id=rom["id"],
|
||||
slug=rom.get("slug", ""),
|
||||
name=localized_name,
|
||||
summary=rom.get("summary", ""),
|
||||
url_cover=handler.normalize_cover_url(localized_cover).replace(
|
||||
"t_thumb", "t_1080p"
|
||||
),
|
||||
url_screenshots=[
|
||||
handler.normalize_cover_url(s.get("url", "")).replace("t_thumb", "t_720p")
|
||||
for s in rom_screenshots
|
||||
],
|
||||
igdb_metadata=extract_metadata_from_igdb_rom(handler, rom),
|
||||
)
|
||||
|
||||
|
||||
class IGDBHandler(MetadataHandler):
|
||||
def __init__(self) -> None:
|
||||
self.igdb_service = IGDBService(twitch_auth=TwitchAuth())
|
||||
@@ -464,23 +576,7 @@ class IGDBHandler(MetadataHandler):
|
||||
if not rom:
|
||||
return fallback_rom
|
||||
|
||||
rom_screenshots = rom.get("screenshots", [])
|
||||
assert mark_list_expanded(rom_screenshots)
|
||||
|
||||
return IGDBRom(
|
||||
igdb_id=rom["id"],
|
||||
slug=rom.get("slug", ""),
|
||||
name=rom.get("name", ""),
|
||||
summary=rom.get("summary", ""),
|
||||
url_cover=self.normalize_cover_url(
|
||||
pydash.get(rom, "cover.url", "")
|
||||
).replace("t_thumb", "t_1080p"),
|
||||
url_screenshots=[
|
||||
self.normalize_cover_url(s.get("url", "")).replace("t_thumb", "t_720p")
|
||||
for s in rom_screenshots
|
||||
],
|
||||
igdb_metadata=extract_metadata_from_igdb_rom(self, rom),
|
||||
)
|
||||
return build_igdb_rom(self, rom, get_igdb_preferred_locale())
|
||||
|
||||
async def get_rom_by_id(self, igdb_id: int) -> IGDBRom:
|
||||
if not self.is_enabled():
|
||||
@@ -494,24 +590,7 @@ class IGDBHandler(MetadataHandler):
|
||||
if not roms:
|
||||
return IGDBRom(igdb_id=None)
|
||||
|
||||
rom = roms[0]
|
||||
rom_screenshots = rom.get("screenshots", [])
|
||||
assert mark_list_expanded(rom_screenshots)
|
||||
|
||||
return IGDBRom(
|
||||
igdb_id=rom["id"],
|
||||
slug=rom.get("slug", ""),
|
||||
name=rom.get("name", ""),
|
||||
summary=rom.get("summary", ""),
|
||||
url_cover=self.normalize_cover_url(
|
||||
pydash.get(rom, "cover.url", "")
|
||||
).replace("t_thumb", "t_1080p"),
|
||||
url_screenshots=[
|
||||
self.normalize_cover_url(s.get("url", "")).replace("t_thumb", "t_720p")
|
||||
for s in rom_screenshots
|
||||
],
|
||||
igdb_metadata=extract_metadata_from_igdb_rom(self, rom),
|
||||
)
|
||||
return build_igdb_rom(self, roms[0], get_igdb_preferred_locale())
|
||||
|
||||
async def get_matched_rom_by_id(self, igdb_id: int) -> IGDBRom | None:
|
||||
if not self.is_enabled():
|
||||
@@ -572,33 +651,8 @@ class IGDBHandler(MetadataHandler):
|
||||
if rom["id"] not in unique_ids
|
||||
]
|
||||
|
||||
return [
|
||||
IGDBRom(
|
||||
{ # type: ignore[misc]
|
||||
k: v
|
||||
for k, v in {
|
||||
"igdb_id": rom["id"],
|
||||
"slug": rom.get("slug", ""),
|
||||
"name": rom.get("name", ""),
|
||||
"summary": rom.get("summary", ""),
|
||||
"url_cover": self.normalize_cover_url(
|
||||
pydash.get(rom, "cover.url", "").replace(
|
||||
"t_thumb", "t_1080p"
|
||||
)
|
||||
),
|
||||
"url_screenshots": [
|
||||
self.normalize_cover_url(s.get("url", "")).replace( # type: ignore[attr-defined]
|
||||
"t_thumb", "t_720p"
|
||||
)
|
||||
for s in rom.get("screenshots", [])
|
||||
],
|
||||
"igdb_metadata": extract_metadata_from_igdb_rom(self, rom),
|
||||
}.items()
|
||||
if v
|
||||
}
|
||||
)
|
||||
for rom in matched_roms
|
||||
]
|
||||
preferred_locale = get_igdb_preferred_locale()
|
||||
return [build_igdb_rom(self, rom, preferred_locale) for rom in matched_roms]
|
||||
|
||||
|
||||
class TwitchAuth(MetadataHandler):
|
||||
@@ -675,6 +729,8 @@ class TwitchAuth(MetadataHandler):
|
||||
return token
|
||||
|
||||
|
||||
SEARCH_FIELDS = ("game.id", "name")
|
||||
|
||||
GAMES_FIELDS = (
|
||||
"id",
|
||||
"name",
|
||||
@@ -725,10 +781,13 @@ GAMES_FIELDS = (
|
||||
"similar_games.cover.url",
|
||||
"age_ratings.rating_category",
|
||||
"videos.video_id",
|
||||
"game_localizations.id",
|
||||
"game_localizations.name",
|
||||
"game_localizations.cover.url",
|
||||
"game_localizations.region.identifier",
|
||||
"game_localizations.region.category",
|
||||
)
|
||||
|
||||
SEARCH_FIELDS = ("game.id", "name")
|
||||
|
||||
|
||||
IGDB_PLATFORM_CATEGORIES: dict[int, str] = {
|
||||
0: "Unknown",
|
||||
|
||||
@@ -39,7 +39,10 @@ def get_preferred_regions() -> list[str]:
|
||||
|
||||
|
||||
def get_preferred_languages() -> list[str]:
|
||||
"""Get preferred languages from config"""
|
||||
"""Get preferred languages from config.
|
||||
|
||||
Returns language priority list with default fallbacks.
|
||||
"""
|
||||
config = cm.get_config()
|
||||
return list(dict.fromkeys(config.SCAN_LANGUAGE_PRIORITY + ["en", "fr"]))
|
||||
|
||||
@@ -411,7 +414,9 @@ def build_ss_game(rom: Rom, game: SSGame) -> SSRom:
|
||||
break
|
||||
|
||||
res_summary = ""
|
||||
for lang in get_preferred_languages():
|
||||
preferred_languages = get_preferred_languages()
|
||||
used_lang = None
|
||||
for lang in preferred_languages:
|
||||
res_summary = next(
|
||||
(
|
||||
synopsis["text"]
|
||||
@@ -421,8 +426,15 @@ def build_ss_game(rom: Rom, game: SSGame) -> SSRom:
|
||||
"",
|
||||
)
|
||||
if res_summary:
|
||||
used_lang = lang
|
||||
break
|
||||
|
||||
# Log warning if we had to fall back from the preferred locale
|
||||
if preferred_languages and used_lang and used_lang != preferred_languages[0]:
|
||||
log.warning(
|
||||
f"ScreenScraper locale '{preferred_languages[0]}' not found for '{res_name}', using '{used_lang}'"
|
||||
)
|
||||
|
||||
url_cover = ss_metadata["box2d_url"]
|
||||
url_manual = (
|
||||
ss_metadata["manual_url"]
|
||||
|
||||
@@ -71,13 +71,13 @@ filesystem: {} # { roms_folder: 'roms' } For example if your folder structure is
|
||||
# - "hasheous" # Hasheous
|
||||
# - "flashpoint" # Flashpoint Project
|
||||
# - "hltb" # HowLongToBeat
|
||||
# region: # Cover art and game title (only used by Screenscraper)
|
||||
# region: # Used by IGDB and ScreenScraper for regional variants
|
||||
# - "us"
|
||||
# - "wor"
|
||||
# - "ss"
|
||||
# - "eu"
|
||||
# - "jp"
|
||||
# language: # Cover art and game title (only used by Screenscraper)
|
||||
# language: # Used by ScreenScraper for descriptions
|
||||
# - "en"
|
||||
# - "fr"
|
||||
# # Media assets to download
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import RSection from "@/components/common/RSection.vue";
|
||||
import storeConfig from "@/stores/config";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
|
||||
const { t } = useI18n();
|
||||
const heartbeat = storeHeartbeat();
|
||||
const configStore = storeConfig();
|
||||
|
||||
const heartbeatStatus = ref<Record<string, boolean | undefined>>({
|
||||
igdb: undefined,
|
||||
@@ -131,6 +133,7 @@ function getConnectionStatusTooltip(source: {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
configStore.fetchConfig();
|
||||
fetchAllHeartbeats();
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user