fix: use region-aware release dates for SS and IGDB metadata

Co-authored-by: gantoine <3247106+gantoine@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-05-26 13:21:39 +00:00
committed by GitHub
parent 9f2e8da964
commit 536f6ac815
4 changed files with 194 additions and 15 deletions

View File

@@ -133,8 +133,48 @@ def build_related_game(
)
def select_preferred_release_date(
rom: Game,
preferred_regions: list[int],
platform_igdb_id: int | None,
) -> int | None:
release_dates = rom.get("release_dates", [])
assert mark_list_expanded(release_dates)
candidates: list[tuple[int | None, int]] = []
for release_date in release_dates:
date = release_date.get("date")
if not isinstance(date, int):
continue
platform = release_date.get("platform")
if platform_igdb_id is not None:
if isinstance(platform, dict):
if platform.get("id") != platform_igdb_id:
continue
elif isinstance(platform, int) and platform != platform_igdb_id:
continue
region = release_date.get("region")
region_id = region if isinstance(region, int) else None
candidates.append((region_id, date))
if not candidates:
return rom.get("first_release_date", None)
for preferred_region in preferred_regions:
preferred_dates = [date for region_id, date in candidates if region_id == preferred_region]
if preferred_dates:
return min(preferred_dates)
return min(date for _, date in candidates)
def extract_metadata_from_igdb_rom(
self: MetadataHandler, rom: Game, platform_igdb_id: int | None
self: MetadataHandler,
rom: Game,
platform_igdb_id: int | None,
preferred_release_regions: list[int],
) -> IGDBMetadata:
age_ratings = rom.get("age_ratings", [])
alternative_names = rom.get("alternative_names", [])
@@ -210,7 +250,9 @@ def extract_metadata_from_igdb_rom(
"youtube_video_id": videos[0].get("video_id") if videos else None,
"total_rating": str(round(rom.get("total_rating", 0.0), 2)),
"aggregated_rating": str(round(rom.get("aggregated_rating", 0.0), 2)),
"first_release_date": rom.get("first_release_date", None),
"first_release_date": select_preferred_release_date(
rom, preferred_release_regions, platform_igdb_id
),
"genres": [g.get("name", "") for g in genres if g.get("name")],
"franchises": pydash.compact(
[franchise.get("name") if franchise else None]
@@ -326,6 +368,17 @@ REGION_TO_IGDB_LOCALE: dict[str, str | None] = {
"tw": "zh-TW", # Taiwan (Traditional Chinese)
}
REGION_TO_IGDB_RELEASE_DATE: dict[str, int] = {
"eu": 1, # Europe
"us": 2, # North America
"jp": 5, # Japan
"cn": 6, # China
"tw": 7, # Asia
"wor": 8, # Worldwide
"kr": 9, # Korea
"br": 10, # Brazil
}
def get_igdb_preferred_locale(rom: Rom | None = None) -> str | None:
"""Get IGDB locale, preferring the rom's own region tag when available.
@@ -350,6 +403,30 @@ def get_igdb_preferred_locale(rom: Rom | None = None) -> str | None:
return None
def get_igdb_preferred_release_regions(rom: Rom | None = None) -> list[int]:
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)
)
ordered_codes = list(
dict.fromkeys(rom_codes + priority + ["us", "wor", "eu", "jp", "kr", "cn", "tw"])
)
return [
REGION_TO_IGDB_RELEASE_DATE[code]
for code in ordered_codes
if code in REGION_TO_IGDB_RELEASE_DATE
]
def extract_localized_data(rom: Game, preferred_locale: str | None) -> tuple[str, str]:
"""Extract localized name and cover URL based on preferred locale.
@@ -398,6 +475,7 @@ def build_igdb_rom(
handler: "IGDBHandler",
rom: Game,
preferred_locale: str | None,
preferred_release_regions: list[int],
platform_igdb_id: int | None,
) -> "IGDBRom":
"""Build an IGDBRom from IGDB game data with localization support.
@@ -428,7 +506,9 @@ def build_igdb_rom(
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, platform_igdb_id),
igdb_metadata=extract_metadata_from_igdb_rom(
handler, rom, platform_igdb_id, preferred_release_regions
),
)
@@ -707,7 +787,11 @@ class IGDBHandler(MetadataHandler):
return fallback_rom
return build_igdb_rom(
self, res, get_igdb_preferred_locale(rom=rom), platform_igdb_id
self,
res,
get_igdb_preferred_locale(rom=rom),
get_igdb_preferred_release_regions(rom=rom),
platform_igdb_id,
)
async def get_rom_by_id(self, rom: Rom, igdb_id: int) -> IGDBRom:
@@ -722,7 +806,13 @@ class IGDBHandler(MetadataHandler):
if not roms:
return IGDBRom(igdb_id=None)
return build_igdb_rom(self, roms[0], get_igdb_preferred_locale(rom=rom), None)
return build_igdb_rom(
self,
roms[0],
get_igdb_preferred_locale(rom=rom),
get_igdb_preferred_release_regions(rom=rom),
None,
)
async def get_matched_rom_by_id(self, rom: Rom, igdb_id: int) -> IGDBRom | None:
if not self.is_enabled():
@@ -784,8 +874,11 @@ class IGDBHandler(MetadataHandler):
]
preferred_locale = get_igdb_preferred_locale(rom=rom)
preferred_release_regions = get_igdb_preferred_release_regions(rom=rom)
return [
build_igdb_rom(self, rom, preferred_locale, platform_igdb_id)
build_igdb_rom(
self, rom, preferred_locale, preferred_release_regions, platform_igdb_id
)
for rom in matched_roms
]
@@ -874,6 +967,9 @@ GAMES_FIELDS = (
"total_rating",
"aggregated_rating",
"first_release_date",
"release_dates.date",
"release_dates.region",
"release_dates.platform.id",
"artworks.url",
"cover.url",
"screenshots.url",

View File

@@ -374,18 +374,32 @@ def extract_metadata_from_ss_rom(rom: Rom, game: SSGame) -> SSMetadata:
except (ValueError, TypeError):
return ""
def _parse_date(date_text: str) -> int | None:
try:
return int(datetime.strptime(date_text, "%Y-%m-%d").timestamp())
except ValueError:
try:
return int(datetime.strptime(date_text, "%Y").timestamp())
except ValueError:
return None
def _get_lowest_date(dates: list[SSGameDate]) -> int | None:
if not dates:
return None
for region in get_preferred_regions(rom):
region_dates = [d for d in dates if d.get("region", "unk") == region]
lowest_region_date = min(
region_dates, default=None, key=lambda v: v.get("text", "")
)
if lowest_region_date:
return _parse_date(lowest_region_date.get("text", ""))
lowest_date = min(dates, default=None, key=lambda v: v.get("text", ""))
if not lowest_date:
return None
try:
return int(datetime.strptime(lowest_date["text"], "%Y-%m-%d").timestamp())
except ValueError:
try:
return int(datetime.strptime(lowest_date["text"], "%Y").timestamp())
except ValueError:
return None
return _parse_date(lowest_date.get("text", ""))
def _get_genres(game: SSGame) -> list[str]:
return [

View File

@@ -1,11 +1,15 @@
"""Tests for the IGDB metadata handler."""
from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from adapters.services.igdb_types import GameType
from handler.metadata.igdb_handler import IGDBHandler
from handler.metadata.igdb_handler import (
IGDBHandler,
build_igdb_rom,
get_igdb_preferred_release_regions,
)
GENESIS_IGDB_ID = 29
@@ -42,6 +46,7 @@ def _make_game(game_id: int, name: str) -> dict:
"age_ratings": [],
"multiplayer_modes": [],
"game_localizations": [],
"release_dates": [],
}
@@ -155,3 +160,37 @@ class TestSearchRomGameTypeFilter:
f"Expected Ecco: The Tides of Time (id=5379), got {result.get('name')} (id={result.get('id')}). "
"The expanded search must consider ALL results, not just the first."
)
class TestIgdbReleaseDates:
def test_build_igdb_rom_prefers_region_specific_release_date(self):
handler = IGDBHandler()
game = _make_game(5379, "Ecco: The Tides of Time")
game["first_release_date"] = 672537600 # 1991-04-16
game["release_dates"] = [
{"date": 632448000, "region": 2, "platform": {"id": GENESIS_IGDB_ID}}, # US
{"date": 593568000, "region": 5, "platform": {"id": GENESIS_IGDB_ID}}, # JP
]
rom = build_igdb_rom(
handler=handler,
rom=game,
preferred_locale="ja-JP",
preferred_release_regions=[5, 2, 1],
platform_igdb_id=GENESIS_IGDB_ID,
)
assert rom["igdb_metadata"]["first_release_date"] == 593568000
def test_get_igdb_preferred_release_regions_prefers_rom_tags(self):
rom = MagicMock()
rom.regions = ["Japan", "USA"]
with patch(
"handler.metadata.igdb_handler.cm.get_config",
return_value=MagicMock(SCAN_REGION_PRIORITY=["us", "eu"]),
):
regions = get_igdb_preferred_release_regions(rom=rom)
assert regions[0] == 2 # North America (US)
assert regions[1] == 5 # Japan

View File

@@ -14,6 +14,7 @@ from handler.metadata.ss_handler import (
_is_notgame,
add_ss_auth_to_url,
extract_media_from_ss_game,
extract_metadata_from_ss_rom,
get_preferred_regions,
)
@@ -203,6 +204,35 @@ class TestExtractMediaFromSsGame:
assert "box-2D(us)" in result["box2d_url"]
class TestExtractMetadataFromSsRom:
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 test_release_date_prefers_tagged_region(self):
config = _make_config(region_priority=[])
rom = self._make_rom(regions=["Japan", "USA"])
game = cast(
SSGame,
{
"dates": [
{"region": "us", "text": "1990-02-12"},
{"region": "jp", "text": "1988-10-23"},
{"region": "eu", "text": "1991-08-29"},
],
"medias": [],
},
)
with patch("handler.metadata.ss_handler.cm.get_config", return_value=config):
metadata = extract_metadata_from_ss_rom(rom, game)
assert metadata["first_release_date"] == 593568000
class TestIsNotgame:
def _game(self, notgame: str = "false", names: list[str] | None = None) -> SSGame:
return cast(