mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 00:27:41 +01:00
166 lines
5.4 KiB
Python
166 lines
5.4 KiB
Python
import json
|
|
from enum import Enum
|
|
from typing import NotRequired, TypedDict
|
|
|
|
import httpx
|
|
import yarl
|
|
from fastapi import HTTPException, status
|
|
|
|
from config import PLAYMATCH_API_ENABLED
|
|
from handler.metadata.base_handler import MetadataHandler
|
|
from logger.logger import log
|
|
from models.rom import RomFile
|
|
from utils import get_version
|
|
from utils.context import ctx_httpx_client
|
|
|
|
|
|
class PlaymatchProvider(str, Enum):
|
|
IGDB = "IGDB"
|
|
|
|
|
|
class GameMatchType(str, Enum):
|
|
SHA256 = "SHA256"
|
|
SHA1 = "SHA1"
|
|
MD5 = "MD5"
|
|
FileNameAndSize = "FileNameAndSize"
|
|
NoMatch = "NoMatch"
|
|
|
|
|
|
class PlaymatchExternalMetadata(TypedDict):
|
|
automaticMatchReason: NotRequired[str]
|
|
comment: NotRequired[str]
|
|
failedMatchReason: NotRequired[str]
|
|
manualMatchType: NotRequired[str]
|
|
matchType: NotRequired[str]
|
|
providerId: NotRequired[str]
|
|
providerName: NotRequired[str]
|
|
|
|
|
|
class PlaymatchRomMatch(TypedDict):
|
|
igdb_id: int | None
|
|
|
|
|
|
class PlaymatchHandler(MetadataHandler):
|
|
"""
|
|
Handler for [Playmatch](https://github.com/RetroRealm/playmatch), a service for matching Roms by Hashes.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.base_url = "https://playmatch.retrorealm.dev/api"
|
|
self.identify_url = f"{self.base_url}/identify/ids"
|
|
self.healthcheck_url = f"{self.base_url}/health"
|
|
|
|
@classmethod
|
|
def is_enabled(cls) -> bool:
|
|
return PLAYMATCH_API_ENABLED
|
|
|
|
async def heartbeat(self) -> bool:
|
|
if not self.is_enabled():
|
|
return False
|
|
|
|
try:
|
|
response = await self._request(self.healthcheck_url, {})
|
|
except Exception as e:
|
|
log.error("Error checking Playmatch API: %s", e)
|
|
return False
|
|
|
|
return bool(response)
|
|
|
|
async def _request(self, url: str, query: dict) -> dict:
|
|
"""
|
|
Sends a Request to Playmatch API.
|
|
|
|
:param url: The API endpoint URL.
|
|
:param query: A dictionary containing the query parameters.
|
|
:return: A dictionary with the json result.
|
|
:raises HTTPException: If the request fails or the service is unavailable.
|
|
"""
|
|
httpx_client = ctx_httpx_client.get()
|
|
|
|
filtered_query = {
|
|
key: value
|
|
for key, value in query.items()
|
|
if value is not None and value != "" # drop None and ""
|
|
}
|
|
|
|
url_with_query = yarl.URL(url).update_query(**filtered_query)
|
|
|
|
log.debug(
|
|
"API request: URL=%s, Timeout=%s",
|
|
url_with_query,
|
|
60,
|
|
)
|
|
|
|
headers = {"user-agent": f"RomM/{get_version()}"}
|
|
|
|
try:
|
|
res = await httpx_client.get(
|
|
str(url_with_query), headers=headers, timeout=60
|
|
)
|
|
res.raise_for_status()
|
|
return res.json()
|
|
except (httpx.HTTPStatusError, httpx.ConnectError, httpx.ReadTimeout) as exc:
|
|
log.warning("Connection error: can't connect to Playmatch", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="Can't connect to Playmatch, check your internet connection",
|
|
) from exc
|
|
except json.JSONDecodeError as exc:
|
|
log.error("Error decoding JSON response from ScreenScraper: %s", exc)
|
|
return {}
|
|
|
|
async def lookup_rom(self, files: list[RomFile]) -> PlaymatchRomMatch:
|
|
"""
|
|
Identify a ROM file using Playmatch API.
|
|
|
|
:param rom_attrs: A dictionary containing the ROM attributes.
|
|
:return: A PlaymatchRomMatch objects containing the matched ROM information.
|
|
:raises HTTPException: If the request fails or the service is unavailable.
|
|
"""
|
|
if not self.is_enabled():
|
|
return PlaymatchRomMatch(igdb_id=None)
|
|
|
|
first_file = next(
|
|
(file for file in files if file.file_size_bytes > 0),
|
|
None,
|
|
)
|
|
if first_file is None:
|
|
return PlaymatchRomMatch(igdb_id=None)
|
|
|
|
try:
|
|
response = await self._request(
|
|
self.identify_url,
|
|
{
|
|
"fileName": first_file.file_name,
|
|
"fileSize": first_file.file_size_bytes,
|
|
"md5": first_file.md5_hash,
|
|
"sha1": first_file.sha1_hash,
|
|
},
|
|
)
|
|
except httpx.HTTPStatusError:
|
|
# We silently fail if the service is unavailable as this should not block the rest of RomM.
|
|
return PlaymatchRomMatch(igdb_id=None)
|
|
|
|
game_match_type = response.get("gameMatchType", None)
|
|
if game_match_type == GameMatchType.NoMatch:
|
|
log.debug("No match found for the provided ROM file.")
|
|
return PlaymatchRomMatch(igdb_id=None)
|
|
|
|
externalMetadata = response.get("externalMetadata", [])
|
|
if len(externalMetadata) == 0:
|
|
log.debug("No external metadata found for the matched ROM file.")
|
|
return PlaymatchRomMatch(igdb_id=None)
|
|
|
|
igdb_id = None
|
|
|
|
for metadata in externalMetadata:
|
|
provider_name = metadata.get("providerName", None)
|
|
provider_game_id = metadata.get("providerId", None)
|
|
if provider_name == PlaymatchProvider.IGDB and provider_game_id is not None:
|
|
log.debug(
|
|
"Playmatch found IGDB match with IGDB ID: %s", provider_game_id
|
|
)
|
|
igdb_id = int(provider_game_id)
|
|
|
|
return PlaymatchRomMatch(igdb_id=igdb_id)
|