11import json
22from enum import Enum
3- from typing import NotRequired , TypedDict
3+ from typing import Final , NotRequired , TypedDict
44
55import httpx
66import yarl
1616
1717class PlaymatchProvider (str , Enum ):
1818 IGDB = "IGDB"
19+ STEAM_GRID_DB = "SteamGridDB"
20+ SCREEN_SCRAPER = "ScreenScraper"
21+ MOBY_GAMES = "MobyGames"
22+ LAUNCH_BOX = "LaunchBox"
23+ EMU_READY = "EmuReady"
24+ OPEN_VGDB = "OpenVGDB"
25+
26+
27+ # Tag is the uppercased Playmatch MetadataProvider name.
28+ # Playmatch parses it case-insensitively but spacing must match.
29+ # Tags Playmatch doesn't yet know are kept so older RomM clients keep
30+ # submitting the right tag once Playmatch adds support.
31+ PLAYMATCH_TAG_TO_ATTR : Final [dict [str , str ]] = {
32+ "IGDB" : "igdb_id" ,
33+ "MOBYGAMES" : "moby_id" ,
34+ "SCREENSCRAPER" : "ss_id" ,
35+ "RETRO_ACHIEVEMENTS" : "ra_id" ,
36+ "LAUNCHBOX" : "launchbox_id" ,
37+ "HASHEOUS" : "hasheous_id" ,
38+ "TGDB" : "tgdb_id" ,
39+ "FLASHPOINT" : "flashpoint_id" ,
40+ "HOWLONGTOBEAT" : "hltb_id" ,
41+ "LIBRETRO" : "libretro_id" ,
42+ "STEAMGRIDDB" : "sgdb_id" ,
43+ "GAMELIST" : "gamelist_id" ,
44+ }
45+
46+ # Rom attrs the scan handler actually consumes from a Playmatch lookup.
47+ # Other tags exist only for outbound suggestions.
48+ PLAYMATCH_LOOKUP_ROM_ATTRS : frozenset [str ] = frozenset (
49+ {"igdb_id" , "moby_id" , "ss_id" , "launchbox_id" , "sgdb_id" }
50+ )
1951
20-
21- # (rom attribute, provider tag). Playmatch drops unknown tags server-side.
22- _PLAYMATCH_PROVIDER_TAGS : tuple [tuple [str , str ], ...] = (
23- ("igdb_id" , "IGDB" ),
24- ("moby_id" , "MOBY_GAMES" ),
25- ("ss_id" , "SCREENSCRAPER" ),
26- ("ra_id" , "RETRO_ACHIEVEMENTS" ),
27- ("launchbox_id" , "LAUNCHBOX" ),
28- ("hasheous_id" , "HASHEOUS" ),
29- ("tgdb_id" , "TGDB" ),
30- ("flashpoint_id" , "FLASHPOINT" ),
31- ("hltb_id" , "HOWLONGTOBEAT" ),
32- ("libretro_id" , "LIBRETRO" ),
33- ("sgdb_id" , "STEAMGRIDDB" ),
34- ("gamelist_id" , "GAMELIST" ),
52+ # MetadataSource values (StrEnum) for which Playmatch can return ids. Typed as
53+ # strings so this module stays free of scan_handler imports. EmuReady and
54+ # OpenVGDB are in Playmatch's enum but have no RomM counterpart yet.
55+ PLAYMATCH_SUPPORTED_SOURCES : frozenset [str ] = frozenset (
56+ {"igdb" , "moby" , "ss" , "launchbox" , "sgdb" }
3557)
3658
3759
3860class GameMatchType (str , Enum ):
3961 SHA256 = "SHA256"
4062 SHA1 = "SHA1"
4163 MD5 = "MD5"
42- FileNameAndSize = "FileNameAndSize"
43- NoMatch = "NoMatch"
64+ FILE_NAME_AND_SIZE = "FileNameAndSize"
65+ NO_MATCH = "NoMatch"
4466
4567
4668class PlaymatchExternalMetadata (TypedDict ):
@@ -55,6 +77,17 @@ class PlaymatchExternalMetadata(TypedDict):
5577
5678class PlaymatchRomMatch (TypedDict ):
5779 igdb_id : int | None
80+ moby_id : int | None
81+ ss_id : int | None
82+ launchbox_id : int | None
83+ sgdb_id : int | None
84+ ra_id : int | None
85+ hasheous_id : int | None
86+ tgdb_id : int | None
87+ flashpoint_id : str | None
88+ hltb_id : int | None
89+ gamelist_id : str | None
90+ libretro_id : str | None
5891
5992
6093class PlaymatchHandler (MetadataHandler ):
@@ -124,7 +157,7 @@ async def _request(self, url: str, query: dict) -> dict:
124157 detail = "Can't connect to Playmatch, check your internet connection" ,
125158 ) from exc
126159 except json .JSONDecodeError as exc :
127- log .error ("Error decoding JSON response from ScreenScraper : %s" , exc )
160+ log .error ("Error decoding JSON response from Playmatch : %s" , exc )
128161 return {}
129162
130163 async def lookup_rom (self , files : list [RomFile ]) -> PlaymatchRomMatch :
@@ -135,15 +168,30 @@ async def lookup_rom(self, files: list[RomFile]) -> PlaymatchRomMatch:
135168 :return: A PlaymatchRomMatch objects containing the matched ROM information.
136169 :raises HTTPException: If the request fails or the service is unavailable.
137170 """
171+ fallback_rom = PlaymatchRomMatch (
172+ igdb_id = None ,
173+ moby_id = None ,
174+ ss_id = None ,
175+ launchbox_id = None ,
176+ sgdb_id = None ,
177+ ra_id = None ,
178+ hasheous_id = None ,
179+ tgdb_id = None ,
180+ flashpoint_id = None ,
181+ hltb_id = None ,
182+ gamelist_id = None ,
183+ libretro_id = None ,
184+ )
185+
138186 if not self .is_enabled ():
139- return PlaymatchRomMatch ( igdb_id = None )
187+ return fallback_rom
140188
141189 first_file = next (
142190 (file for file in files if file .file_size_bytes > 0 ),
143191 None ,
144192 )
145193 if first_file is None :
146- return PlaymatchRomMatch ( igdb_id = None )
194+ return fallback_rom
147195
148196 try :
149197 response = await self ._request (
@@ -157,45 +205,61 @@ async def lookup_rom(self, files: list[RomFile]) -> PlaymatchRomMatch:
157205 )
158206 except httpx .HTTPStatusError :
159207 # We silently fail if the service is unavailable as this should not block the rest of RomM.
160- return PlaymatchRomMatch ( igdb_id = None )
208+ return fallback_rom
161209
162210 game_match_type = response .get ("gameMatchType" , None )
163- if game_match_type == GameMatchType .NoMatch :
211+ if game_match_type == GameMatchType .NO_MATCH :
164212 log .debug ("No match found for the provided ROM file." )
165- return PlaymatchRomMatch ( igdb_id = None )
213+ return fallback_rom
166214
167215 externalMetadata = response .get ("externalMetadata" , [])
168216 if len (externalMetadata ) == 0 :
169217 log .debug ("No external metadata found for the matched ROM file." )
170- return PlaymatchRomMatch (igdb_id = None )
171-
172- igdb_id = None
218+ return fallback_rom
173219
220+ result = fallback_rom
174221 for metadata in externalMetadata :
175222 provider_name = metadata .get ("providerName" , None )
176223 provider_game_id = metadata .get ("providerId" , None )
177- if provider_name == PlaymatchProvider .IGDB and provider_game_id is not None :
224+ if not provider_name or provider_game_id is None :
225+ continue
226+
227+ attr = PLAYMATCH_TAG_TO_ATTR .get (provider_name .upper ())
228+ if not attr or attr not in PLAYMATCH_LOOKUP_ROM_ATTRS :
229+ continue
230+
231+ try :
232+ parsed_id = int (provider_game_id )
233+ except (TypeError , ValueError ):
178234 log .debug (
179- "Playmatch found IGDB match with IGDB ID: %s" , provider_game_id
235+ "Playmatch returned non-int ID for %s: %r" ,
236+ provider_name ,
237+ provider_game_id ,
180238 )
181- igdb_id = int (provider_game_id )
239+ continue
240+
241+ log .debug ("Playmatch found %s match with id: %s" , provider_name , parsed_id )
242+ result [attr ] = parsed_id # trunk-ignore(mypy/literal-required)
182243
183- return PlaymatchRomMatch ( igdb_id = igdb_id )
244+ return result
184245
185246 @staticmethod
186247 def is_manual_match (form_fields_set : set [str ]) -> bool :
187248 """True if the submitted form contains any Playmatch-tracked provider id field."""
188- return any (attr in form_fields_set for attr , _ in _PLAYMATCH_PROVIDER_TAGS )
249+ return any (attr in form_fields_set for attr in PLAYMATCH_TAG_TO_ATTR . values () )
189250
190251 async def submit_manual_match_suggestion (self , rom : Rom ) -> None :
191- """Fire-and-forget suggestion POST. No-ops if disabled or no provider IDs are set; never raises."""
252+ """
253+ Fire-and-forget suggestion POST.
254+ No-ops if disabled or no provider IDs are set.
255+ """
192256 try :
193257 if not self .is_enabled ():
194258 return
195259
196260 mappings = [
197261 {"provider" : tag , "providerId" : str (getattr (rom , attr ))}
198- for attr , tag in _PLAYMATCH_PROVIDER_TAGS
262+ for tag , attr in PLAYMATCH_TAG_TO_ATTR . items ()
199263 if getattr (rom , attr , None )
200264 ]
201265 if not mappings :
0 commit comments