From 180c373e1efb4cf73783ac245e10e6245408eabb Mon Sep 17 00:00:00 2001 From: zurdi Date: Wed, 26 Jun 2024 17:36:02 +0200 Subject: [PATCH 01/15] steamgridDB integration added --- backend/endpoints/heartbeat.py | 2 + backend/endpoints/responses/heartbeat.py | 1 + backend/endpoints/responses/search.py | 5 + backend/endpoints/search.py | 13 +- backend/handler/metadata/igdb_handler.py | 17 +- backend/handler/metadata/sgdb_handler.py | 63 ++-- frontend/assets/scrappers/sgdb.svg | 1 + frontend/src/__generated__/index.ts | 1 + .../__generated__/models/HeartbeatResponse.ts | 1 + .../__generated__/models/SearchCoverSchema.ts | 9 + .../src/components/common/Game/AdminMenu.vue | 41 +-- .../common/Game/Dialog/SearchCoverRom.vue | 282 ++++++++++++++++++ frontend/src/services/api/rom.ts | 10 + frontend/src/types/emitter.d.ts | 1 + frontend/src/views/Home.vue | 2 + 15 files changed, 402 insertions(+), 47 deletions(-) create mode 100644 frontend/assets/scrappers/sgdb.svg create mode 100644 frontend/src/__generated__/models/SearchCoverSchema.ts create mode 100644 frontend/src/components/common/Game/Dialog/SearchCoverRom.vue diff --git a/backend/endpoints/heartbeat.py b/backend/endpoints/heartbeat.py index 3f90f822e..10e06a11b 100644 --- a/backend/endpoints/heartbeat.py +++ b/backend/endpoints/heartbeat.py @@ -11,6 +11,7 @@ from handler.filesystem import fs_platform_handler from handler.metadata.igdb_handler import IGDB_API_ENABLED from handler.metadata.moby_handler import MOBY_API_ENABLED +from handler.metadata.sgdb_handler import STEAMGRIDDB_API_ENABLED from utils import get_version router = APIRouter() @@ -27,6 +28,7 @@ def heartbeat() -> HeartbeatResponse: return { "VERSION": get_version(), "ANY_SOURCE_ENABLED": IGDB_API_ENABLED or MOBY_API_ENABLED, + "STEAMGRIDDB_ENABLED": STEAMGRIDDB_API_ENABLED, "METADATA_SOURCES": { "IGDB_API_ENABLED": IGDB_API_ENABLED, "MOBY_API_ENABLED": MOBY_API_ENABLED, diff --git a/backend/endpoints/responses/heartbeat.py b/backend/endpoints/responses/heartbeat.py index cacdf55a2..208f15367 100644 --- a/backend/endpoints/responses/heartbeat.py +++ b/backend/endpoints/responses/heartbeat.py @@ -27,4 +27,5 @@ class HeartbeatResponse(TypedDict): SCHEDULER: SchedulerDict ANY_SOURCE_ENABLED: bool METADATA_SOURCES: MetadataSourcesDict + STEAMGRIDDB_ENABLED: bool FS_PLATFORMS: list diff --git a/backend/endpoints/responses/search.py b/backend/endpoints/responses/search.py index 4219410da..0e3011ad9 100644 --- a/backend/endpoints/responses/search.py +++ b/backend/endpoints/responses/search.py @@ -9,3 +9,8 @@ class SearchRomSchema(BaseModel): summary: str igdb_url_cover: str = "" moby_url_cover: str = "" + + +class SearchCoverSchema(BaseModel): + name: str + resources: list diff --git a/backend/endpoints/search.py b/backend/endpoints/search.py index 26fa5ec07..63653cc0e 100644 --- a/backend/endpoints/search.py +++ b/backend/endpoints/search.py @@ -1,9 +1,9 @@ import emoji from decorators.auth import protected_route -from endpoints.responses.search import SearchRomSchema +from endpoints.responses.search import SearchCoverSchema, SearchRomSchema from fastapi import APIRouter, HTTPException, Request, status from handler.database import db_rom_handler -from handler.metadata import meta_igdb_handler, meta_moby_handler +from handler.metadata import meta_igdb_handler, meta_moby_handler, meta_sgdb_handler from handler.metadata.igdb_handler import IGDB_API_ENABLED from handler.metadata.moby_handler import MOBY_API_ENABLED from handler.scan_handler import _get_main_platform_igdb_id @@ -107,3 +107,12 @@ async def search_rom( log.info(f"\t - {m_rom['name']}") return matched_roms + + +@protected_route(router.get, "/search/cover", ["roms.read"]) +async def search_cover( + request: Request, + search_term: str | None = None, +) -> list[SearchCoverSchema]: + + return meta_sgdb_handler.get_details(search_term) diff --git a/backend/handler/metadata/igdb_handler.py b/backend/handler/metadata/igdb_handler.py index 98488b978..59b36c35d 100644 --- a/backend/handler/metadata/igdb_handler.py +++ b/backend/handler/metadata/igdb_handler.py @@ -179,13 +179,14 @@ def extract_metadata_from_igdb_rom(rom: dict) -> IGDBMetadata: class IGDBBaseHandler(MetadataHandler): def __init__(self) -> None: - self.platform_endpoint = "https://api.igdb.com/v4/platforms" - self.platform_version_endpoint = "https://api.igdb.com/v4/platform_versions" - self.platforms_fields = ["id", "name"] - self.games_endpoint = "https://api.igdb.com/v4/games" + self.BASE_URL = "https://api.igdb.com/v4" + self.platform_endpoint = f"{self.BASE_URL}/platforms" + self.platform_version_endpoint = f"{self.BASE_URL}/platform_versions" + self.platforms_fields = PLATFORMS_FIELDS + self.games_endpoint = f"{self.BASE_URL}/games" self.games_fields = GAMES_FIELDS - self.search_endpoint = "https://api.igdb.com/v4/search" - self.search_fields = ["game.id", "name"] + self.search_endpoint = f"{self.BASE_URL}/search" + self.search_fields = SEARCH_FIELDS self.pagination_limit = 200 self.twitch_auth = TwitchAuth() self.headers = { @@ -617,6 +618,8 @@ def get_oauth_token(self) -> str: return token +PLATFORMS_FIELDS = ["id", "name"] + GAMES_FIELDS = [ "id", "name", @@ -667,6 +670,8 @@ def get_oauth_token(self) -> str: "similar_games.cover.url", ] +SEARCH_FIELDS = ["game.id", "name"] + # Generated from the following code on https://www.igdb.com/platforms/: # Array.from(document.querySelectorAll(".media-body a")).map(a => ({ # slug: a.href.split("/")[4], diff --git a/backend/handler/metadata/sgdb_handler.py b/backend/handler/metadata/sgdb_handler.py index c75ff29fb..b16867180 100644 --- a/backend/handler/metadata/sgdb_handler.py +++ b/backend/handler/metadata/sgdb_handler.py @@ -1,42 +1,63 @@ +from typing import Final + import requests from config import STEAMGRIDDB_API_KEY from logger.logger import log +# Used to display the Mobygames API status in the frontend +STEAMGRIDDB_API_ENABLED: Final = bool(STEAMGRIDDB_API_KEY) + +# SteamGridDB dimensions +STEAMVERTICAL = "600x900" +GALAXY342 = "342x482" +GALAXY660 = "660x930" +SQUARE512 = "512x512" +SQUARE1024 = "1024x1024" + class SGDBBaseHandler: def __init__(self) -> None: + self.BASE_URL = "https://www.steamgriddb.com/api/v2" + self.search_endpoint = f"{self.BASE_URL}/search/autocomplete" + self.grid_endpoint = f"{self.BASE_URL}/grids/game" self.headers = { "Authorization": f"Bearer {STEAMGRIDDB_API_KEY}", "Accept": "*/*", } - self.BASE_URL = "https://www.steamgriddb.com/api/v2" - self.DEFAULT_IMAGE_URL = "https://www.steamgriddb.com/static/img/logo-512.png" - def get_details(self, term): + def get_details(self, search_term): search_response = requests.get( - f"{self.BASE_URL}/search/autocomplete/{term}", + f"{self.search_endpoint}/{search_term}", headers=self.headers, timeout=120, ).json() if len(search_response["data"]) == 0: - log.info(f"Could not find {term} on SteamGridDB") - return ("", "", self.DEFAULT_IMAGE_URL) - - game_id = search_response["data"][0]["id"] - game_name = search_response["data"][0]["name"] - - game_response = requests.get( - f"{self.BASE_URL}/grid/game/{game_id}", headers=self.headers, timeout=120 - ).json() - - if len(game_response["data"]) == 0: - log.info(f"Could not find {game_name} image on SteamGridDB") - return (game_id, game_name, self.DEFAULT_IMAGE_URL) - - game_image_url = game_response["data"][0]["url"] - - return (game_id, game_name, game_image_url) + log.warning(f"Could not find '{search_term}' on SteamGridDB") + return "" + + games = [] + for game in search_response["data"]: + covers_response = requests.get( + f"{self.grid_endpoint}/{game['id']}", + headers=self.headers, + timeout=120, + params={ + "dimensions": f"{STEAMVERTICAL},{GALAXY342},{GALAXY660},{SQUARE512},{SQUARE1024}", + }, + ).json() + + games.append( + { + "name": game["name"], + "resources": [ + {"thumb": cover["thumb"], "url": cover["url"]} + for cover in covers_response["data"] + ], + } + ) + + return games sgdb_handler = SGDBBaseHandler() diff --git a/frontend/assets/scrappers/sgdb.svg b/frontend/assets/scrappers/sgdb.svg new file mode 100644 index 000000000..b1084e1a4 --- /dev/null +++ b/frontend/assets/scrappers/sgdb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/__generated__/index.ts b/frontend/src/__generated__/index.ts index 68dd6dcc6..b4c5aafa9 100644 --- a/frontend/src/__generated__/index.ts +++ b/frontend/src/__generated__/index.ts @@ -32,6 +32,7 @@ export type { RomSchema } from "./models/RomSchema"; export type { SaveSchema } from "./models/SaveSchema"; export type { SchedulerDict } from "./models/SchedulerDict"; export type { ScreenshotSchema } from "./models/ScreenshotSchema"; +export type { SearchCoverSchema } from "./models/SearchCoverSchema"; export type { SearchRomSchema } from "./models/SearchRomSchema"; export type { StateSchema } from "./models/StateSchema"; export type { StatsReturn } from "./models/StatsReturn"; diff --git a/frontend/src/__generated__/models/HeartbeatResponse.ts b/frontend/src/__generated__/models/HeartbeatResponse.ts index 01a3e2c49..15f70be8b 100644 --- a/frontend/src/__generated__/models/HeartbeatResponse.ts +++ b/frontend/src/__generated__/models/HeartbeatResponse.ts @@ -13,5 +13,6 @@ export type HeartbeatResponse = { SCHEDULER: SchedulerDict; ANY_SOURCE_ENABLED: boolean; METADATA_SOURCES: MetadataSourcesDict; + STEAMGRIDDB_ENABLED: boolean; FS_PLATFORMS: Array; }; diff --git a/frontend/src/__generated__/models/SearchCoverSchema.ts b/frontend/src/__generated__/models/SearchCoverSchema.ts new file mode 100644 index 000000000..7915dcb20 --- /dev/null +++ b/frontend/src/__generated__/models/SearchCoverSchema.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type SearchCoverSchema = { + name: string; + resources: Array; +}; diff --git a/frontend/src/components/common/Game/AdminMenu.vue b/frontend/src/components/common/Game/AdminMenu.vue index c5b4c7701..3b525a810 100644 --- a/frontend/src/components/common/Game/AdminMenu.vue +++ b/frontend/src/components/common/Game/AdminMenu.vue @@ -11,38 +11,46 @@ const heartbeat = storeHeartbeat();