[ROMM-1333] Use metadata tag in filename to match game

This commit is contained in:
Georges-Antoine Assi
2025-09-07 14:12:30 -04:00
parent 16c343f6a3
commit d7e85ba633
7 changed files with 182 additions and 1 deletions

View File

@@ -178,3 +178,14 @@ class SteamGridDBService:
url = self.url.joinpath("search/autocomplete", term)
response = await self._request(str(url))
return cast(list[SGDBGame], response.get("data", []))
async def get_game_by_id(self, game_id: int) -> SGDBGame | None:
"""Get game details by ID.
Reference: https://www.steamgriddb.com/api/v2#tag/GAMES/operation/getGameById
"""
url = self.url.joinpath("games", str(game_id))
response = await self._request(str(url))
if not response or "data" not in response:
return None
return cast(SGDBGame, response["data"])

View File

@@ -33,6 +33,9 @@ PSP_IGDB_ID: Final = 38
SWITCH_IGDB_ID: Final = 130
ARCADE_IGDB_IDS: Final = [52, 79, 80]
# Regex to detect IGDB ID tags in filenames like (igdb-12345)
IGDB_TAG_REGEX = re.compile(r"\(igdb-(\d+)\)", re.IGNORECASE)
class IGDBPlatform(TypedDict):
slug: str
@@ -215,6 +218,14 @@ class IGDBHandler(MetadataHandler):
def is_enabled(cls) -> bool:
return bool(IGDB_CLIENT_ID and IGDB_CLIENT_SECRET)
@staticmethod
def extract_igdb_id_from_filename(fs_name: str) -> int | None:
"""Extract IGDB ID from filename tag like (igdb-12345)."""
match = IGDB_TAG_REGEX.search(fs_name)
if match:
return int(match.group(1))
return None
async def _search_rom(
self, search_term: str, platform_igdb_id: int, with_game_type: bool = False
) -> Game | None:
@@ -345,6 +356,21 @@ class IGDBHandler(MetadataHandler):
if not platform_igdb_id:
return IGDBRom(igdb_id=None)
# Check for IGDB ID tag in filename first
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)
if rom_by_id["igdb_id"]:
log.debug(
f"Successfully matched ROM by IGDB ID tag: {fs_name} -> {igdb_id_from_tag}"
)
return rom_by_id
else:
log.warning(
f"IGDB ID {igdb_id_from_tag} from filename tag not found in IGDB"
)
search_term = fs_rom_handler.get_file_name_with_no_tags(fs_name)
fallback_rom = IGDBRom(igdb_id=None)

View File

@@ -1,4 +1,5 @@
import json
import re
from datetime import datetime
from typing import Final, NotRequired, TypedDict
@@ -21,6 +22,9 @@ LAUNCHBOX_METADATA_IMAGE_KEY: Final[str] = "romm:launchbox_metadata_image"
LAUNCHBOX_MAME_KEY: Final[str] = "romm:launchbox_mame"
LAUNCHBOX_FILES_KEY: Final[str] = "romm:launchbox_files"
# Regex to detect LaunchBox ID tags in filenames like (launchbox-12345)
LAUNCHBOX_TAG_REGEX = re.compile(r"\(launchbox-(\d+)\)", re.IGNORECASE)
class LaunchboxPlatform(TypedDict):
slug: str
@@ -123,6 +127,14 @@ class LaunchboxHandler(MetadataHandler):
def is_enabled(cls) -> bool:
return LAUNCHBOX_API_ENABLED
@staticmethod
def extract_launchbox_id_from_filename(fs_name: str) -> int | None:
"""Extract LaunchBox ID from filename tag like (launchbox-12345)."""
match = LAUNCHBOX_TAG_REGEX.search(fs_name)
if match:
return int(match.group(1))
return None
async def _get_rom_from_metadata(
self, file_name: str, platform_slug: str
) -> dict | None:
@@ -231,6 +243,21 @@ class LaunchboxHandler(MetadataHandler):
if not self.is_enabled():
return fallback_rom
# Check for LaunchBox ID tag in filename first
launchbox_id_from_tag = self.extract_launchbox_id_from_filename(fs_name)
if launchbox_id_from_tag:
log.debug(f"Found LaunchBox ID tag in filename: {launchbox_id_from_tag}")
rom_by_id = await self.get_rom_by_id(launchbox_id_from_tag)
if rom_by_id["launchbox_id"]:
log.debug(
f"Successfully matched ROM by LaunchBox ID tag: {fs_name} -> {launchbox_id_from_tag}"
)
return rom_by_id
else:
log.warning(
f"LaunchBox ID {launchbox_id_from_tag} from filename tag not found in LaunchBox"
)
# We replace " - " with ": " to match Launchbox's naming convention
search_term = fs_rom_handler.get_file_name_with_no_tags(fs_name).replace(
" - ", ": "
@@ -277,6 +304,8 @@ class LaunchboxHandler(MetadataHandler):
if not metadata_database_index_entry:
return LaunchboxRom(launchbox_id=None)
# Parse the JSON string from cache
metadata_database_index_entry = json.loads(metadata_database_index_entry)
game_images = await self._get_game_images(
metadata_database_index_entry["DatabaseID"]
)

View File

@@ -26,6 +26,9 @@ PSP_MOBY_ID: Final = 46
SWITCH_MOBY_ID: Final = 203
ARCADE_MOBY_IDS: Final = [143, 36]
# Regex to detect MobyGames ID tags in filenames like (moby-12345)
MOBYGAMES_TAG_REGEX = re.compile(r"\(moby-(\d+)\)", re.IGNORECASE)
class MobyGamesPlatform(TypedDict):
slug: str
@@ -79,6 +82,14 @@ class MobyGamesHandler(MetadataHandler):
def is_enabled(cls) -> bool:
return bool(MOBYGAMES_API_KEY)
@staticmethod
def extract_mobygames_id_from_filename(fs_name: str) -> int | None:
"""Extract MobyGames ID from filename tag like (mobygames-12345)."""
match = MOBYGAMES_TAG_REGEX.search(fs_name)
if match:
return int(match.group(1))
return None
async def _search_rom(
self, search_term: str, platform_moby_id: int, split_game_name: bool = False
) -> MobyGame | None:
@@ -136,6 +147,21 @@ class MobyGamesHandler(MetadataHandler):
if not platform_moby_id:
return MobyGamesRom(moby_id=None)
# Check for MobyGames ID tag in filename first
mobygames_id_from_tag = self.extract_mobygames_id_from_filename(fs_name)
if mobygames_id_from_tag:
log.debug(f"Found MobyGames ID tag in filename: {mobygames_id_from_tag}")
rom_by_id = await self.get_rom_by_id(mobygames_id_from_tag)
if rom_by_id["moby_id"]:
log.debug(
f"Successfully matched ROM by MobyGames ID tag: {fs_name} -> {mobygames_id_from_tag}"
)
return rom_by_id
else:
log.warning(
f"MobyGames ID {mobygames_id_from_tag} from filename tag not found in MobyGames"
)
search_term = fs_rom_handler.get_file_name_with_no_tags(fs_name)
fallback_rom = MobyGamesRom(moby_id=None)

View File

@@ -1,5 +1,6 @@
import json
import os
import re
import time
from datetime import datetime
from typing import NotRequired, TypedDict
@@ -16,11 +17,15 @@ from config import (
RETROACHIEVEMENTS_API_KEY,
)
from handler.filesystem import fs_resource_handler
from logger.logger import log
from models.rom import Rom
from .base_hander import BaseRom, MetadataHandler
from .base_hander import UniversalPlatformSlug as UPS
# Regex to detect RetroAchievements ID tags in filenames like (ra-12345)
RA_TAG_REGEX = re.compile(r"\(ra-(\d+)\)", re.IGNORECASE)
class RAGamesPlatform(TypedDict):
slug: str
@@ -127,6 +132,14 @@ class RAHandler(MetadataHandler):
def is_enabled(cls) -> bool:
return bool(RETROACHIEVEMENTS_API_KEY)
@staticmethod
def extract_ra_id_from_filename(fs_name: str) -> int | None:
"""Extract RetroAchievements ID from filename tag like (ra-12345)."""
match = RA_TAG_REGEX.search(fs_name)
if match:
return int(match.group(1))
return None
def _get_hashes_file_path(self, platform_id: int) -> str:
platform_resources_path = fs_resource_handler.get_platform_resources_path(
platform_id
@@ -201,7 +214,25 @@ class RAHandler(MetadataHandler):
)
async def get_rom(self, rom: Rom, ra_hash: str) -> RAGameRom:
if not rom.platform.ra_id or not ra_hash:
if not rom.platform.ra_id:
return RAGameRom(ra_id=None)
# Check for RetroAchievements ID tag in filename first
ra_id_from_tag = self.extract_ra_id_from_filename(rom.fs_name)
if ra_id_from_tag:
log.debug(f"Found RetroAchievements ID tag in filename: {ra_id_from_tag}")
rom_by_id = await self.get_rom_by_id(rom=rom, ra_id=ra_id_from_tag)
if rom_by_id["ra_id"]:
log.debug(
f"Successfully matched ROM by RetroAchievements ID tag: {rom.fs_name} -> {ra_id_from_tag}"
)
return rom_by_id
else:
log.warning(
f"RetroAchievements ID {ra_id_from_tag} from filename tag not found in RetroAchievements"
)
if not ra_hash:
return RAGameRom(ra_id=None)
ra_game_list_item = await self._search_rom(rom, ra_hash)

View File

@@ -34,6 +34,38 @@ class SGDBBaseHandler(MetadataHandler):
def is_enabled(cls) -> bool:
return bool(STEAMGRIDDB_API_KEY)
async def get_rom_by_id(self, sgdb_id: int) -> SGDBRom:
"""Get ROM details by SteamGridDB ID."""
if not self.is_enabled():
return SGDBRom(sgdb_id=None)
try:
game = await self.sgdb_service.get_game_by_id(sgdb_id)
if not game:
return SGDBRom(sgdb_id=None)
# Get covers for the game
game_details = await self._get_game_covers(
game_id=game["id"],
game_name=game["name"],
types=(SGDBType.STATIC,),
is_nsfw=False,
is_humor=False,
is_epilepsy=False,
)
first_resource = next(
(res for res in game_details["resources"] if res["url"]), None
)
result = SGDBRom(sgdb_id=game["id"])
if first_resource:
result["url_cover"] = first_resource["url"]
return result
except Exception as e:
log.warning(f"Failed to fetch ROM by SteamGridDB ID {sgdb_id}: {e}")
return SGDBRom(sgdb_id=None)
async def get_details(self, search_term: str) -> list[SGDBResult]:
if not self.is_enabled():
return []

View File

@@ -102,6 +102,9 @@ ARCADE_SS_IDS: Final = [
269,
]
# Regex to detect ScreenScraper ID tags in filenames like (ss-12345)
SS_TAG_REGEX = re.compile(r"\(ssfr-(\d+)\)", re.IGNORECASE)
class SSPlatform(TypedDict):
slug: str
@@ -279,6 +282,14 @@ class SSHandler(MetadataHandler):
def is_enabled(cls) -> bool:
return bool(SCREENSCRAPER_USER and SCREENSCRAPER_PASSWORD)
@staticmethod
def extract_ss_id_from_filename(fs_name: str) -> int | None:
"""Extract ScreenScraper ID from filename tag like (ss-12345)."""
match = SS_TAG_REGEX.search(fs_name)
if match:
return int(match.group(1))
return None
async def _search_rom(
self, search_term: str, platform_ss_id: int, split_game_name: bool = False
) -> SSGame | None:
@@ -332,6 +343,21 @@ class SSHandler(MetadataHandler):
if not platform_ss_id:
return SSRom(ss_id=None)
# Check for ScreenScraper ID tag in filename first
ss_id_from_tag = self.extract_ss_id_from_filename(file_name)
if ss_id_from_tag:
log.debug(f"Found ScreenScraper ID tag in filename: {ss_id_from_tag}")
rom_by_id = await self.get_rom_by_id(ss_id_from_tag)
if rom_by_id["ss_id"]:
log.debug(
f"Successfully matched ROM by ScreenScraper ID tag: {file_name} -> {ss_id_from_tag}"
)
return rom_by_id
else:
log.warning(
f"ScreenScraper ID {ss_id_from_tag} from filename tag not found in ScreenScraper"
)
search_term = fs_rom_handler.get_file_name_with_no_tags(file_name)
fallback_rom = SSRom(ss_id=None)