Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SteamGridDB integration #960

Merged
merged 16 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/endpoints/heartbeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions backend/endpoints/responses/heartbeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ class HeartbeatResponse(TypedDict):
SCHEDULER: SchedulerDict
ANY_SOURCE_ENABLED: bool
METADATA_SOURCES: MetadataSourcesDict
STEAMGRIDDB_ENABLED: bool
FS_PLATFORMS: list
5 changes: 5 additions & 0 deletions backend/endpoints/responses/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ class SearchRomSchema(BaseModel):
summary: str
igdb_url_cover: str = ""
moby_url_cover: str = ""


class SearchCoverSchema(BaseModel):
name: str
resources: list
13 changes: 11 additions & 2 deletions backend/endpoints/search.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
8 changes: 6 additions & 2 deletions backend/handler/filesystem/resources_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ def _cover_exists(rom: Rom, size: CoverSize):
def resize_cover_to_small(cover_path: str):
"""Path of the cover image to resize"""
cover = Image.open(cover_path)
small_width = int(cover.width * 0.1)
small_height = int(cover.height * 0.1)
if cover.height >= 1000:
ratio = 0.2
else:
ratio = 0.4
small_width = int(cover.width * ratio)
small_height = int(cover.height * ratio)
small_size = (small_width, small_height)
small_img = cover.resize(small_size)
small_img.save(cover_path)
Expand Down
17 changes: 11 additions & 6 deletions backend/handler/metadata/igdb_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -617,6 +618,8 @@ def get_oauth_token(self) -> str:
return token


PLATFORMS_FIELDS = ["id", "name"]

GAMES_FIELDS = [
"id",
"name",
Expand Down Expand Up @@ -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],
Expand Down
63 changes: 42 additions & 21 deletions backend/handler/metadata/sgdb_handler.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions frontend/assets/scrappers/sgdb.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/src/__generated__/index.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/src/__generated__/models/HeartbeatResponse.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions frontend/src/__generated__/models/SearchCoverSchema.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions frontend/src/components/Details/BackgroundHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const theme = useTheme();
: `/assets/romm/resources/${rom.path_cover_l}`
"
lazy
cover
>
<template #error>
<v-img
Expand All @@ -38,9 +39,7 @@ const theme = useTheme();
</template>
<style scoped>
#background-header {
width: 100%;
height: 300px;
transform: scale(10);
filter: blur(5px);
filter: blur(30px);
}
</style>
41 changes: 23 additions & 18 deletions frontend/src/components/common/Game/AdminMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,46 @@ const heartbeat = storeHeartbeat();
</script>

<template>
<v-list
rounded="0"
class="pa-0"
>
<v-list rounded="0" class="pa-0">
<v-list-item
:disabled="!heartbeat.value.ANY_SOURCE_ENABLED"
class="py-4 pr-5"
@click="emitter?.emit('showMatchRomDialog', rom)"
>
<v-list-item-title class="d-flex">
<v-icon
icon="mdi-search-web"
class="mr-2"
/>Manual
search
<v-icon icon="mdi-search-web" class="mr-2" />Manual match
</v-list-item-title>
<v-list-item-subtitle>
{{
!heartbeat.value.ANY_SOURCE_ENABLED ? "No metadata source enabled" : ""
!heartbeat.value.ANY_SOURCE_ENABLED
? "No metadata source enabled"
: ""
}}
</v-list-item-subtitle>
</v-list-item>

<v-list-item
:disabled="!heartbeat.value.STEAMGRIDDB_ENABLED"
class="py-4 pr-5"
@click="emitter?.emit('showSearchCoverDialog', rom)"
>
<v-list-item-title class="d-flex">
<v-icon icon="mdi-image-outline" class="mr-2" />Cover search
</v-list-item-title>
<v-list-item-subtitle>
{{
!heartbeat.value.STEAMGRIDDB_ENABLED ? "No SteamgridDB enabled" : ""
}}
</v-list-item-subtitle>
</v-list-item>

<v-divider />
<v-list-item
class="py-4 pr-5"
@click="emitter?.emit('showEditRomDialog', { ...rom })"
>
<v-list-item-title class="d-flex">
<v-icon
icon="mdi-pencil-box"
class="mr-2"
/>Edit
<v-icon icon="mdi-pencil-box" class="mr-2" />Edit
</v-list-item-title>
</v-list-item>
<v-divider />
Expand All @@ -51,10 +59,7 @@ const heartbeat = storeHeartbeat();
@click="emitter?.emit('showDeleteRomDialog', [rom])"
>
<v-list-item-title class="d-flex">
<v-icon
icon="mdi-delete"
class="mr-2"
/>Delete
<v-icon icon="mdi-delete" class="mr-2" />Delete
</v-list-item-title>
</v-list-item>
</v-list>
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/common/Game/Card/Base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ onMounted(() => {
v-bind="hoverProps"
class="pointer"
ref="card"
cover
:src="
src
? src
Expand Down Expand Up @@ -110,7 +111,7 @@ onMounted(() => {
? rom.igdb_url_cover
: rom.moby_url_cover
"
:aspect-ratio="3 / 4"
:aspect-ratio="2 / 3"
>
<div v-bind="props" style="position: absolute; top: 0; width: 100%">
<template v-if="titleOnHover">
Expand Down Expand Up @@ -143,7 +144,7 @@ onMounted(() => {
<template #error>
<v-img
:src="`/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`"
:aspect-ratio="3 / 4"
cover
></v-img>
</template>
<template #placeholder>
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/common/Game/Card/Related.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ const theme = useTheme();
? `https:${rom.cover_url.replace('t_thumb', 't_cover_big')}`
: `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`
"
:aspect-ratio="3 / 4"
:aspect-ratio="2 / 3"
cover
lazy
><v-chip
class="px-2 position-absolute chip-type text-white translucent-dark"
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/common/Game/Dialog/MatchRom.vue
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,8 @@ onBeforeUnmount(() => {
? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`
: source.url_cover
"
:aspect-ratio="3 / 4"
:aspect-ratio="2 / 3"
cover
lazy
>
<template #placeholder>
Expand Down
Loading
Loading