use env & reuse request

This commit is contained in:
SaraVieira
2024-09-10 21:00:20 +01:00
parent 7f09e317ed
commit 9b298d46eb
16 changed files with 54 additions and 108 deletions

View File

@@ -49,6 +49,9 @@ IGDB_CLIENT_SECRET: Final = os.environ.get(
# STEAMGRIDDB
STEAMGRIDDB_API_KEY: Final = os.environ.get("STEAMGRIDDB_API_KEY", "")
# STEAMGRIDDB
RETROACHIEVEMENTS_USERNAME: Final = os.environ.get("RETROACHIEVEMENTS_USERNAME", "")
RETROACHIEVEMENTS_API_KEY: Final = os.environ.get("RETROACHIEVEMENTS_API_KEY", "")
# MOBYGAMES
MOBYGAMES_API_KEY: Final = os.environ.get("MOBYGAMES_API_KEY", "")

View File

@@ -13,6 +13,7 @@ from handler.database import db_user_handler
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.ra_handler import RETROACHIEVEMENTS_API_ENABLED
from handler.metadata.sgdb_handler import STEAMGRIDDB_API_ENABLED
from utils import get_version
from utils.router import APIRouter
@@ -36,6 +37,7 @@ def heartbeat() -> HeartbeatResponse:
"IGDB_API_ENABLED": IGDB_API_ENABLED,
"MOBY_API_ENABLED": MOBY_API_ENABLED,
"STEAMGRIDDB_ENABLED": STEAMGRIDDB_API_ENABLED,
"RETROACHIEVEMENTS_ENABLED": RETROACHIEVEMENTS_API_ENABLED,
},
"FS_PLATFORMS": fs_platform_handler.get_platforms(),
"WATCHER": {

View File

@@ -20,6 +20,7 @@ class MetadataSourcesDict(TypedDict):
IGDB_API_ENABLED: bool
MOBY_API_ENABLED: bool
STEAMGRIDDB_ENABLED: bool
RETROACHIEVEMENTS_ENABLED: bool
class EmulationDict(TypedDict):

View File

@@ -1,8 +1,10 @@
from __future__ import annotations
from fastapi import HTTPException, Query, Request, UploadFile, status
from typing import Any
from pydantic import BaseModel, Field
from fastapi import Request
from pydantic import BaseModel
class Achievements(BaseModel):
ID: int
@@ -22,8 +24,6 @@ class Achievements(BaseModel):
type: Any
class RetroAchievementsGameSchema(BaseModel):
ID: int
Title: str
@@ -57,8 +57,9 @@ class RetroAchievementsGameSchema(BaseModel):
HighestAwardDate: str | None = None
@classmethod
def from_orm_with_request(cls, db_rom: RetroAchievementsGameSchema, request: Request) -> RetroAchievementsGameSchema:
def from_orm_with_request(
cls, db_rom: RetroAchievementsGameSchema, request: Request
) -> RetroAchievementsGameSchema:
rom = cls.model_validate(db_rom)
return rom

View File

@@ -1,68 +1,18 @@
from anyio import Path
import yarl
from decorators.auth import protected_route
from fastapi import Request
from utils.router import APIRouter
from handler.database import db_rom_handler
from endpoints.responses.retroachievements import RetroAchievementsGameSchema
from exceptions.endpoint_exceptions import RomNotFoundInRetroAchievementsException
import asyncio
import http
import httpx
import yarl
from fastapi import HTTPException, status
from logger.logger import log
from utils.context import ctx_httpx_client
from fastapi import Request
from handler.metadata.ra_handler import RetroAchievementsHandler
from utils.router import APIRouter
router = APIRouter()
async def _request(url: str, timeout: int = 120) -> dict:
httpx_client = ctx_httpx_client.get()
authorized_url = yarl.URL(url)
try:
res = await httpx_client.get(str(authorized_url), timeout=timeout)
res.raise_for_status()
return res.json()
except httpx.NetworkError as exc:
log.critical(
"Connection error: can't connect to RetroAchievements", exc_info=True
)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Can't connect to RetroAchievements, check your internet connection",
) from exc
except httpx.HTTPStatusError as err:
if err.response.status_code == http.HTTPStatus.TOO_MANY_REQUESTS:
# Retry after 2 seconds if rate limit hit
await asyncio.sleep(2)
else:
# Log the error and return an empty dict if the request fails with a different code
log.error(err)
return {}
except httpx.TimeoutException:
# Retry the request once if it times out
pass
try:
res = await httpx_client.get(url, timeout=timeout)
res.raise_for_status()
except (httpx.HTTPStatusError, httpx.TimeoutException) as err:
if (
isinstance(err, httpx.HTTPStatusError)
and err.response.status_code == http.HTTPStatus.UNAUTHORIZED
):
# Sometimes Mobygames returns 401 even with a valid API key
return {}
# Log the error and return an empty dict if the request fails with a different code
log.error(err)
return {}
return res.json()
@protected_route(router.get, "/retroachievements/{id}", ["roms.read"])
async def get_rom_retroachievements(request: Request, id: int) -> RetroAchievementsGameSchema:
async def get_rom_retroachievements(
request: Request, id: int
) -> RetroAchievementsGameSchema:
"""Get rom endpoint
Args:
@@ -73,7 +23,9 @@ async def get_rom_retroachievements(request: Request, id: int) -> RetroAchieveme
RetroAchievementsGameSchema: User and Game info from retro achivements
"""
url = yarl.URL("https://retroachievements.org/API/API_GetGameInfoAndUserProgress.php").with_query(
url = yarl.URL(
"https://retroachievements.org/API/API_GetGameInfoAndUserProgress.php"
).with_query(
g=[id],
a=["1"],
u=[request.user.ra_username],
@@ -81,10 +33,11 @@ async def get_rom_retroachievements(request: Request, id: int) -> RetroAchieveme
y=[request.user.ra_api_key],
)
game_with_details = await _request(str(url))
game_with_details = await RetroAchievementsHandler._request(
RetroAchievementsHandler, str(url)
)
if not game_with_details:
raise RomNotFoundInRetroAchievementsException(id)
return RetroAchievementsGameSchema.from_orm_with_request(game_with_details, request)
return RetroAchievementsGameSchema.model_validate(game_with_details)

View File

@@ -106,7 +106,6 @@ async def scan_platforms(
scan_type: ScanType = ScanType.QUICK,
roms_ids: list[str] | None = None,
metadata_sources: list[str] | None = None,
retroAchievements_info: dict | None = None,
):
"""Scan all the listed platforms and fetch metadata from different sources
@@ -164,7 +163,6 @@ async def scan_platforms(
roms_ids=roms_ids,
metadata_sources=metadata_sources,
socket_manager=sm,
retroAchievements_info=retroAchievements_info,
)
# Same protection for platforms
@@ -190,7 +188,6 @@ async def _identify_platform(
roms_ids: list[str],
metadata_sources: list[str],
socket_manager: socketio.AsyncRedisManager,
retroAchievements_info: dict | None = None,
) -> ScanStats:
# Stop the scan if the flag is set
if redis_client.get(STOP_SCAN_FLAG):
@@ -264,7 +261,6 @@ async def _identify_platform(
roms_ids=roms_ids,
metadata_sources=metadata_sources,
socket_manager=socket_manager,
retroAchievements_info=retroAchievements_info,
)
# Only purge entries if there are some file remaining in the library
@@ -313,7 +309,6 @@ async def _identify_rom(
roms_ids: list[str],
metadata_sources: list[str],
socket_manager: socketio.AsyncRedisManager,
retroAchievements_info: dict | None = None,
) -> ScanStats:
scan_stats = ScanStats()
@@ -338,7 +333,6 @@ async def _identify_rom(
scan_type=scan_type,
rom=rom,
metadata_sources=metadata_sources,
retroAchievements_info=retroAchievements_info,
)
scan_stats.scanned_roms += 1
@@ -401,7 +395,6 @@ async def scan_handler(_sid: str, options: dict):
scan_type = ScanType[options.get("type", "quick").upper()]
roms_ids = options.get("roms_ids", [])
metadata_sources = options.get("apis", [])
retroAchievements_info = options.get("retroAchievementsInfo", [])
# Uncomment this to run scan in the current process
# await scan_platforms(
@@ -417,7 +410,6 @@ async def scan_handler(_sid: str, options: dict):
scan_type,
roms_ids,
metadata_sources,
retroAchievements_info,
job_timeout=SCAN_TIMEOUT, # Timeout (default of 4 hours)
)

View File

@@ -186,7 +186,7 @@ async def update_user(
@protected_route(router.put, "/users/{id}/settings", ["me.write"])
async def update_user(
async def update_user_settings(
request: Request, id: int, form_data: Annotated[UserForm, Depends()]
) -> UserSchema:
"""Update user settings endpoint

View File

@@ -58,6 +58,7 @@ class CollectionAlreadyExistsException(Exception):
def __repr__(self) -> str:
return self.message
class RomNotFoundInRetroAchievementsException(Exception):
def __init__(self, id):
self.message = f"Rom with id '{id}' does not exist on RetroAchievements"
@@ -66,4 +67,4 @@ class RomNotFoundInRetroAchievementsException(Exception):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=self.message)
def __repr__(self) -> str:
return self.message
return self.message

View File

@@ -1,15 +1,21 @@
import asyncio
import http
from typing import NotRequired, TypedDict
from typing import Final, NotRequired, TypedDict
import httpx
import yarl
from config import RETROACHIEVEMENTS_API_KEY, RETROACHIEVEMENTS_USERNAME
from fastapi import HTTPException, status
from logger.logger import log
from utils.context import ctx_httpx_client
from .base_hander import MetadataHandler
# Used to display the Mobygames API status in the frontend
RETROACHIEVEMENTS_API_ENABLED: Final = bool(RETROACHIEVEMENTS_API_KEY) and bool(
RETROACHIEVEMENTS_USERNAME
)
class RAGamesPlatform(TypedDict):
slug: str
@@ -70,9 +76,7 @@ class RetroAchievementsHandler(MetadataHandler):
return res.json()
async def _search_rom(
self, md5_hash: str, platform_ra_id: int, retroAchievements_info: dict
) -> dict | None:
async def _search_rom(self, md5_hash: str, platform_ra_id: int) -> dict | None:
if not platform_ra_id:
return None
@@ -81,8 +85,8 @@ class RetroAchievementsHandler(MetadataHandler):
i=[platform_ra_id],
h=["1"],
f=["1"],
z=[retroAchievements_info.get("username")],
y=[retroAchievements_info.get("api_key")],
z=[RETROACHIEVEMENTS_USERNAME],
y=[RETROACHIEVEMENTS_API_KEY],
)
roms = await self._request(str(url))
@@ -104,20 +108,13 @@ class RetroAchievementsHandler(MetadataHandler):
name=platform["name"],
)
async def get_rom(
self, md5_hash: str, retroAchievements_info: dict, platform_ra_id: int
) -> RAGameRom:
if not retroAchievements_info.get("api_key") or not retroAchievements_info.get(
"username"
):
return RAGameRom(ra_id=None)
async def get_rom(self, md5_hash: str, platform_ra_id: int) -> RAGameRom:
if not platform_ra_id:
return RAGameRom(ra_id=None)
fallback_rom = RAGameRom(ra_id=None)
res = await self._search_rom(md5_hash, platform_ra_id, retroAchievements_info)
res = await self._search_rom(md5_hash, platform_ra_id)
if not res:
return fallback_rom

View File

@@ -50,7 +50,6 @@ async def _get_main_platform_igdb_id(platform: Platform):
async def scan_platform(
fs_slug: str,
fs_platforms: list[str],
retroAchievements_info: dict | None = None,
metadata_sources: list[str] | None = None,
) -> Platform:
"""Get platform details
@@ -172,7 +171,6 @@ async def scan_rom(
scan_type: ScanType,
rom: Rom | None = None,
metadata_sources: list[str] | None = None,
retroAchievements_info: dict | None = None,
) -> Rom:
if not metadata_sources:
metadata_sources = ["igdb", "moby", "retro_achievements"]
@@ -193,9 +191,6 @@ async def scan_rom(
"name": fs_rom["file_name"],
"url_cover": "",
"url_screenshots": [],
"crc_hash": rom.crc_hash if rom else None,
"md5_hash": rom.md5_hash if rom else None,
"sha1_hash": rom.sha1_hash if rom else None,
}
# Update properties from existing rom if not a complete rescan
@@ -301,7 +296,6 @@ async def scan_rom(
):
return await meta_ra_handler.get_rom(
rom_attrs["md5_hash"],
retroAchievements_info=retroAchievements_info or {},
platform_ra_id=platform.ra_id,
)

View File

@@ -19,9 +19,9 @@ from endpoints import (
feeds,
firmware,
heartbeat,
user,
platform,
raw,
retroachievements,
rom,
saves,
screenshots,
@@ -29,7 +29,7 @@ from endpoints import (
states,
stats,
tasks,
retroachievements
user,
)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

View File

@@ -14,6 +14,11 @@ MOBYGAMES_API_KEY=
# SteamGridDB
STEAMGRIDDB_API_KEY=
# RetroAchievements
RETROACHIEVEMENTS_API_KEY=
RETROACHIEVEMENTS_USERNAME=
# Database config
DB_HOST=127.0.0.1
DB_PORT=3306

View File

@@ -20,6 +20,8 @@ services:
- IGDB_CLIENT_SECRET= # https://api-docs.igdb.com/#account-creation
- MOBYGAMES_API_KEY= # https://www.mobygames.com/info/api/
- STEAMGRIDDB_API_KEY # https://github.com/rommapp/romm/wiki/Generate-API-Keys#steamgriddb
- RETROACHIEVEMENTS_API_KEY # https://api-docs.retroachievements.org/#api-access
- RETROACHIEVEMENTS_USERNAME # https://api-docs.retroachievements.org/#api-access
volumes:
- romm_resources:/romm/resources # Resources fetched from IGDB (covers, screenshots, etc.)
- romm_redis_data:/redis-data # Cached data for background tasks

View File

@@ -7,5 +7,6 @@ export type MetadataSourcesDict = {
IGDB_API_ENABLED: boolean;
MOBY_API_ENABLED: boolean;
STEAMGRIDDB_ENABLED: boolean;
RETROACHIEVEMENTS_ENABLED: boolean;
};

View File

@@ -94,7 +94,7 @@ watch(
() => route.fullPath,
async () => {
await fetchDetails();
}
},
);
</script>

View File

@@ -24,7 +24,7 @@ const retroAchievements = computed(() => ({
name: "RetroAchievements",
value: "retro_achievements",
logo_path: "/assets/scrappers/ra.webp",
disabled: !auth.user?.ra_api_key || !auth.user.ra_username,
disabled: !heartbeat.value.METADATA_SOURCES?.RETROACHIEVEMENTS_ENABLED,
}));
// Use a computed property to reactively update metadataOptions based on heartbeat
const metadataOptions = computed(() => [
@@ -89,12 +89,6 @@ async function scan() {
socket.emit("scan", {
platforms: platformsToScan.value.map((p) => p.id),
type: scanType.value,
retroAchievementsInfo: retroAchievements.value.disabled
? {}
: {
api_key: auth.user?.ra_api_key,
username: auth.user?.ra_username,
},
apis: [
...metadataSources.value.map((s) => s.value),
retroAchievements.value.value,