diff --git a/backend/adapters/services/mobygames.py b/backend/adapters/services/mobygames.py index 0ea0aec05..2f2a8a750 100644 --- a/backend/adapters/services/mobygames.py +++ b/backend/adapters/services/mobygames.py @@ -106,6 +106,19 @@ class MobyGamesService: log.error("Error decoding JSON response from ScreenScraper: %s", exc) return {} + async def list_groups(self, limit: int | None = None) -> list[dict]: + """Retrieve a list of groups. + + Reference: https://www.mobygames.com/info/api/#groups + """ + params: dict[str, list[str]] = {} + if limit is not None: + params["limit"] = [str(limit)] + + url = self.url.joinpath("groups").with_query(**params) + response = await self._request(str(url)) + return response.get("groups", []) + @overload async def list_games( self, diff --git a/backend/adapters/services/screenscraper.py b/backend/adapters/services/screenscraper.py index c275d9b89..29c157494 100644 --- a/backend/adapters/services/screenscraper.py +++ b/backend/adapters/services/screenscraper.py @@ -129,6 +129,14 @@ class ScreenScraperService: log.error("Error decoding JSON response from ScreenScraper: %s", exc) return {} + async def get_infra_info(self) -> dict: + """Retrieve information about the infrastructure. + + Reference: https://api.screenscraper.fr/webapi2.php#infraInfos + """ + url = self.url.joinpath("ssinfraInfos.php") + return await self._request(str(url)) + async def get_game_info( self, *, diff --git a/backend/endpoints/heartbeat.py b/backend/endpoints/heartbeat.py index 4dc42bf8c..e0b8009f9 100644 --- a/backend/endpoints/heartbeat.py +++ b/backend/endpoints/heartbeat.py @@ -1,4 +1,4 @@ -import asyncio +from fastapi import HTTPException from config import ( DISABLE_EMULATOR_JS, @@ -33,6 +33,7 @@ from handler.metadata import ( meta_ss_handler, meta_tgdb_handler, ) +from handler.scan_handler import MetadataSource from utils import get_version from utils.router import APIRouter @@ -48,13 +49,17 @@ async def heartbeat() -> HeartbeatResponse: Returns: HeartbeatReturn: TypedDict structure with all the defined values in the HeartbeatReturn class. """ - - # Run async operations in parallel - igdb_heartbeat, flashpoint_heartbeat, fs_platforms = await asyncio.gather( - meta_igdb_handler.heartbeat(), - meta_flashpoint_handler.heartbeat(), - fs_platform_handler.get_platforms(), - ) + igdb_enabled = meta_igdb_handler.is_enabled() + flashpoint_enabled = meta_flashpoint_handler.is_enabled() + ss_enabled = meta_ss_handler.is_enabled() + moby_enabled = meta_moby_handler.is_enabled() + ra_enabled = meta_ra_handler.is_enabled() + sgdb_enabled = meta_sgdb_handler.is_enabled() + launchbox_enabled = meta_launchbox_handler.is_enabled() + hasheous_enabled = meta_hasheous_handler.is_enabled() + playmatch_enabled = meta_playmatch_handler.is_enabled() + hltb_enabled = meta_hltb_handler.is_enabled() + tgdb_enabled = meta_tgdb_handler.is_enabled() return { "SYSTEM": { @@ -63,32 +68,30 @@ async def heartbeat() -> HeartbeatResponse: }, "METADATA_SOURCES": { "ANY_SOURCE_ENABLED": ( - meta_igdb_handler.is_enabled() - or meta_ss_handler.is_enabled() - or meta_moby_handler.is_enabled() - or meta_ra_handler.is_enabled() - or meta_launchbox_handler.is_enabled() - or meta_hasheous_handler.is_enabled() - or meta_tgdb_handler.is_enabled() - or meta_flashpoint_handler.is_enabled() - or meta_hltb_handler.is_enabled() + igdb_enabled + or ss_enabled + or moby_enabled + or ra_enabled + or launchbox_enabled + or hasheous_enabled + or tgdb_enabled + or flashpoint_enabled + or hltb_enabled ), - "IGDB_API_ENABLED": meta_igdb_handler.is_enabled(), - "IGDB_API_HEARTBEAT": igdb_heartbeat, - "SS_API_ENABLED": meta_ss_handler.is_enabled(), - "MOBY_API_ENABLED": meta_moby_handler.is_enabled(), - "STEAMGRIDDB_API_ENABLED": meta_sgdb_handler.is_enabled(), - "RA_API_ENABLED": meta_ra_handler.is_enabled(), - "LAUNCHBOX_API_ENABLED": meta_launchbox_handler.is_enabled(), - "HASHEOUS_API_ENABLED": meta_hasheous_handler.is_enabled(), - "PLAYMATCH_API_ENABLED": meta_playmatch_handler.is_enabled(), - "TGDB_API_ENABLED": meta_tgdb_handler.is_enabled(), - "FLASHPOINT_API_ENABLED": meta_flashpoint_handler.is_enabled(), - "FLASHPOINT_API_HEARTBEAT": flashpoint_heartbeat, - "HLTB_API_ENABLED": meta_hltb_handler.is_enabled(), + "IGDB_API_ENABLED": igdb_enabled, + "SS_API_ENABLED": ss_enabled, + "MOBY_API_ENABLED": moby_enabled, + "STEAMGRIDDB_API_ENABLED": sgdb_enabled, + "RA_API_ENABLED": ra_enabled, + "LAUNCHBOX_API_ENABLED": launchbox_enabled, + "HASHEOUS_API_ENABLED": hasheous_enabled, + "PLAYMATCH_API_ENABLED": playmatch_enabled, + "TGDB_API_ENABLED": tgdb_enabled, + "FLASHPOINT_API_ENABLED": flashpoint_enabled, + "HLTB_API_ENABLED": hltb_enabled, }, "FILESYSTEM": { - "FS_PLATFORMS": fs_platforms, + "FS_PLATFORMS": await fs_platform_handler.get_platforms(), }, "EMULATION": { "DISABLE_EMULATOR_JS": DISABLE_EMULATOR_JS, @@ -114,3 +117,34 @@ async def heartbeat() -> HeartbeatResponse: "SCHEDULED_CONVERT_IMAGES_TO_WEBP_CRON": SCHEDULED_CONVERT_IMAGES_TO_WEBP_CRON, }, } + + +@router.get("/heartbeat/metadata") +async def metadata_heartbeat(metadata_source: str) -> bool: + """Endpoint to return the heartbeat of the metadata sources""" + try: + metadata_source = MetadataSource(metadata_source) + except ValueError as e: + raise HTTPException(status_code=400, detail="Invalid metadata source") from e + + match metadata_source: + case MetadataSource.IGDB: + return await meta_igdb_handler.heartbeat() + case MetadataSource.MOBY: + return await meta_moby_handler.heartbeat() + case MetadataSource.SS: + return await meta_ss_handler.heartbeat() + case MetadataSource.RA: + return await meta_ra_handler.heartbeat() + case MetadataSource.LB: + return await meta_launchbox_handler.heartbeat() + case MetadataSource.HASHEOUS: + return await meta_hasheous_handler.heartbeat() + case MetadataSource.TGDB: + return await meta_tgdb_handler.heartbeat() + case MetadataSource.SGDB: + return await meta_sgdb_handler.heartbeat() + case MetadataSource.FLASHPOINT: + return await meta_flashpoint_handler.heartbeat() + case MetadataSource.HLTB: + return await meta_hltb_handler.heartbeat() diff --git a/backend/endpoints/responses/heartbeat.py b/backend/endpoints/responses/heartbeat.py index cbbc2c2e5..924ccc3b0 100644 --- a/backend/endpoints/responses/heartbeat.py +++ b/backend/endpoints/responses/heartbeat.py @@ -9,17 +9,15 @@ class SystemDict(TypedDict): class MetadataSourcesDict(TypedDict): ANY_SOURCE_ENABLED: bool IGDB_API_ENABLED: bool - IGDB_API_HEARTBEAT: bool - MOBY_API_ENABLED: bool SS_API_ENABLED: bool + MOBY_API_ENABLED: bool STEAMGRIDDB_API_ENABLED: bool RA_API_ENABLED: bool LAUNCHBOX_API_ENABLED: bool - PLAYMATCH_API_ENABLED: bool HASHEOUS_API_ENABLED: bool + PLAYMATCH_API_ENABLED: bool TGDB_API_ENABLED: bool FLASHPOINT_API_ENABLED: bool - FLASHPOINT_API_HEARTBEAT: bool HLTB_API_ENABLED: bool diff --git a/backend/handler/metadata/flashpoint_handler.py b/backend/handler/metadata/flashpoint_handler.py index 08b876fc8..355a495f3 100644 --- a/backend/handler/metadata/flashpoint_handler.py +++ b/backend/handler/metadata/flashpoint_handler.py @@ -153,7 +153,12 @@ class FlashpointHandler(MetadataHandler): return False # make a request to the Flashpoint API to check if the API is working - response = await self._request(self.platforms_url, {}) + try: + response = await self._request(self.platforms_url, {}) + except Exception as e: + log.error("Error checking Flashpoint API: %s", e) + return False + return bool(response) async def search_games(self, search_term: str) -> list[FlashpointGame]: diff --git a/backend/handler/metadata/hasheous_handler.py b/backend/handler/metadata/hasheous_handler.py index 9e96275ac..2ad1e1d92 100644 --- a/backend/handler/metadata/hasheous_handler.py +++ b/backend/handler/metadata/hasheous_handler.py @@ -129,6 +129,21 @@ class HasheousHandler(MetadataHandler): """Return whether this metadata handler is enabled.""" return HASHEOUS_API_ENABLED + async def heartbeat(self) -> bool: + return True + + # if not self.is_enabled(): + # return False + + # # make a request to the Hasheous API to check if the API is working + # try: + # response = await self._request(self.platform_endpoint, "GET") + # except Exception as e: + # log.error("Error checking Hasheous API: %s", e) + # return False + + # return bool(response) + async def _request( self, url: str, diff --git a/backend/handler/metadata/hltb_handler.py b/backend/handler/metadata/hltb_handler.py index 89a267015..052b3322c 100644 --- a/backend/handler/metadata/hltb_handler.py +++ b/backend/handler/metadata/hltb_handler.py @@ -178,6 +178,23 @@ class HowLongToBeatHandler(MetadataHandler): def is_enabled(cls) -> bool: return HLTB_API_ENABLED + async def heartbeat(self) -> bool: + return True + + # if not self.is_enabled(): + # return False + + # # make a request to the HLTB API to check if the API is working + # try: + # async with ctx_httpx_client() as client: + # response = await client.get(self.search_url, params={"q": "test"}) + # response.raise_for_status() + # except Exception as e: + # log.error("Error checking HLTB API: %s", e) + # return False + + # return True + @staticmethod def extract_hltb_id_from_filename(fs_name: str) -> int | None: """Extract HLTB ID from filename tag like (hltb-12345).""" diff --git a/backend/handler/metadata/igdb_handler.py b/backend/handler/metadata/igdb_handler.py index 5455ab951..b51bc0bb8 100644 --- a/backend/handler/metadata/igdb_handler.py +++ b/backend/handler/metadata/igdb_handler.py @@ -314,10 +314,14 @@ class IGDBHandler(MetadataHandler): return False # make a request to the IGDB API to check if the API is working - roms = await self.igdb_service.list_games( - fields=["id"], - limit=1, - ) + try: + roms = await self.igdb_service.list_games( + fields=["id"], + limit=1, + ) + except Exception as e: + log.error("Error checking IGDB API: %s", e) + return False return bool(roms) diff --git a/backend/handler/metadata/launchbox_handler.py b/backend/handler/metadata/launchbox_handler.py index af1eeb1d0..824b0f519 100644 --- a/backend/handler/metadata/launchbox_handler.py +++ b/backend/handler/metadata/launchbox_handler.py @@ -127,6 +127,9 @@ class LaunchboxHandler(MetadataHandler): def is_enabled(cls) -> bool: return LAUNCHBOX_API_ENABLED + async def heartbeat(self) -> bool: + return self.is_enabled() + @staticmethod def extract_launchbox_id_from_filename(fs_name: str) -> int | None: """Extract LaunchBox ID from filename tag like (launchbox-12345).""" diff --git a/backend/handler/metadata/moby_handler.py b/backend/handler/metadata/moby_handler.py index 4605132c4..5f193cef1 100644 --- a/backend/handler/metadata/moby_handler.py +++ b/backend/handler/metadata/moby_handler.py @@ -82,6 +82,18 @@ class MobyGamesHandler(MetadataHandler): def is_enabled(cls) -> bool: return bool(MOBYGAMES_API_KEY) + async def heartbeat(self) -> bool: + if not self.is_enabled(): + return False + + try: + response = await self.moby_service.list_groups(limit=1) + except Exception as e: + log.error("Error checking MobyGames API: %s", e) + return False + + return bool(response) + @staticmethod def extract_mobygames_id_from_filename(fs_name: str) -> int | None: """Extract MobyGames ID from filename tag like (moby-12345).""" diff --git a/backend/handler/metadata/playmatch_handler.py b/backend/handler/metadata/playmatch_handler.py index 5c80c2fd3..ed4df6204 100644 --- a/backend/handler/metadata/playmatch_handler.py +++ b/backend/handler/metadata/playmatch_handler.py @@ -53,6 +53,21 @@ class PlaymatchHandler(MetadataHandler): def is_enabled(cls) -> bool: return PLAYMATCH_API_ENABLED + async def heartbeat(self) -> bool: + return True + + # if not self.is_enabled(): + # return False + + # # make a request to the Playmatch API to check if the API is working + # try: + # response = await self._request(self.identify_url, {"hashes": ["test"]}) + # 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. diff --git a/backend/handler/metadata/ra_handler.py b/backend/handler/metadata/ra_handler.py index 1367dbfb4..ee8e08a3c 100644 --- a/backend/handler/metadata/ra_handler.py +++ b/backend/handler/metadata/ra_handler.py @@ -132,6 +132,21 @@ class RAHandler(MetadataHandler): def is_enabled(cls) -> bool: return bool(RETROACHIEVEMENTS_API_KEY) + async def heartbeat(self) -> bool: + return True + + # if not self.is_enabled(): + # return False + + # # make a request to the RetroAchievements API to check if the API is working + # try: + # response = await self.ra_service.get_console_ids() + # except Exception as e: + # log.error("Error checking RetroAchievements API: %s", e) + # return False + + # return bool(response) + @staticmethod def extract_ra_id_from_filename(fs_name: str) -> int | None: """Extract RetroAchievements ID from filename tag like (ra-12345).""" diff --git a/backend/handler/metadata/sgdb_handler.py b/backend/handler/metadata/sgdb_handler.py index 3199f1c66..11507622e 100644 --- a/backend/handler/metadata/sgdb_handler.py +++ b/backend/handler/metadata/sgdb_handler.py @@ -34,6 +34,21 @@ class SGDBBaseHandler(MetadataHandler): def is_enabled(cls) -> bool: return bool(STEAMGRIDDB_API_KEY) + async def heartbeat(self) -> bool: + return True + + # if not self.is_enabled(): + # return False + + # # make a request to the SteamGridDB API to check if the API is working + # try: + # response = await self.sgdb_service.get_platforms() + # except Exception as e: + # log.error("Error checking SteamGridDB API: %s", e) + # return False + + # return bool(response) + async def get_rom_by_id(self, sgdb_id: int) -> SGDBRom: """Get ROM details by SteamGridDB ID.""" if not self.is_enabled(): diff --git a/backend/handler/metadata/ss_handler.py b/backend/handler/metadata/ss_handler.py index 265778b6b..03babc5ae 100644 --- a/backend/handler/metadata/ss_handler.py +++ b/backend/handler/metadata/ss_handler.py @@ -282,6 +282,18 @@ class SSHandler(MetadataHandler): def is_enabled(cls) -> bool: return bool(SCREENSCRAPER_USER and SCREENSCRAPER_PASSWORD) + async def heartbeat(self) -> bool: + if not self.is_enabled(): + return False + + try: + response = await self.ss_service.get_infra_info() + except Exception as e: + log.error("Error checking ScreenScraper API: %s", e) + return False + + return bool(response.get("response", {})) + @staticmethod def extract_ss_id_from_filename(fs_name: str) -> int | None: """Extract ScreenScraper ID from filename tag like (ss-12345).""" diff --git a/backend/handler/metadata/tgdb_handler.py b/backend/handler/metadata/tgdb_handler.py index 453aee89d..9166b157b 100644 --- a/backend/handler/metadata/tgdb_handler.py +++ b/backend/handler/metadata/tgdb_handler.py @@ -28,6 +28,9 @@ class TGDBHandler(MetadataHandler): def is_enabled(cls) -> bool: return TGDB_API_ENABLED + async def heartbeat(self) -> bool: + return self.is_enabled() + def get_platform(self, slug: str) -> TGDBPlatform: if slug not in TGDB_PLATFORM_LIST: return TGDBPlatform(tgdb_id=None, slug=slug) diff --git a/backend/tests/endpoints/test_heartbeat.py b/backend/tests/endpoints/test_heartbeat.py index 8ccaee291..548694ce6 100644 --- a/backend/tests/endpoints/test_heartbeat.py +++ b/backend/tests/endpoints/test_heartbeat.py @@ -27,7 +27,6 @@ def test_heartbeat(client): metadata = heartbeat["METADATA_SOURCES"] assert isinstance(metadata["ANY_SOURCE_ENABLED"], bool) assert isinstance(metadata["IGDB_API_ENABLED"], bool) - assert isinstance(metadata["IGDB_API_HEARTBEAT"], bool) assert isinstance(metadata["MOBY_API_ENABLED"], bool) assert isinstance(metadata["SS_API_ENABLED"], bool) assert isinstance(metadata["STEAMGRIDDB_API_ENABLED"], bool) @@ -37,7 +36,6 @@ def test_heartbeat(client): assert isinstance(metadata["HASHEOUS_API_ENABLED"], bool) assert isinstance(metadata["TGDB_API_ENABLED"], bool) assert isinstance(metadata["FLASHPOINT_API_ENABLED"], bool) - assert isinstance(metadata["FLASHPOINT_API_HEARTBEAT"], bool) assert "FILESYSTEM" in heartbeat filesystem = heartbeat["FILESYSTEM"] diff --git a/frontend/src/__generated__/models/MetadataSourcesDict.ts b/frontend/src/__generated__/models/MetadataSourcesDict.ts index 02dd14a2b..ea4eab7c0 100644 --- a/frontend/src/__generated__/models/MetadataSourcesDict.ts +++ b/frontend/src/__generated__/models/MetadataSourcesDict.ts @@ -5,7 +5,6 @@ export type MetadataSourcesDict = { ANY_SOURCE_ENABLED: boolean; IGDB_API_ENABLED: boolean; - IGDB_API_HEARTBEAT: boolean; MOBY_API_ENABLED: boolean; SS_API_ENABLED: boolean; STEAMGRIDDB_API_ENABLED: boolean; @@ -15,7 +14,6 @@ export type MetadataSourcesDict = { HASHEOUS_API_ENABLED: boolean; TGDB_API_ENABLED: boolean; FLASHPOINT_API_ENABLED: boolean; - FLASHPOINT_API_HEARTBEAT: boolean; HLTB_API_ENABLED: boolean; }; diff --git a/frontend/src/stores/heartbeat.ts b/frontend/src/stores/heartbeat.ts index c3fe6682b..004a73437 100644 --- a/frontend/src/stores/heartbeat.ts +++ b/frontend/src/stores/heartbeat.ts @@ -19,7 +19,6 @@ const defaultHeartbeat: Heartbeat = { METADATA_SOURCES: { ANY_SOURCE_ENABLED: false, IGDB_API_ENABLED: false, - IGDB_API_HEARTBEAT: false, SS_API_ENABLED: false, MOBY_API_ENABLED: false, RA_API_ENABLED: false, @@ -29,7 +28,6 @@ const defaultHeartbeat: Heartbeat = { HASHEOUS_API_ENABLED: false, TGDB_API_ENABLED: false, FLASHPOINT_API_ENABLED: false, - FLASHPOINT_API_HEARTBEAT: false, HLTB_API_ENABLED: false, }, FILESYSTEM: { diff --git a/frontend/src/views/Settings/MetadataSources.vue b/frontend/src/views/Settings/MetadataSources.vue index 1b8c0eac8..b28fd6522 100644 --- a/frontend/src/views/Settings/MetadataSources.vue +++ b/frontend/src/views/Settings/MetadataSources.vue @@ -1,8 +1,7 @@