diff --git a/backend/config/__init__.py b/backend/config/__init__.py index 7092cf908..dd81f9d40 100644 --- a/backend/config/__init__.py +++ b/backend/config/__init__.py @@ -55,46 +55,46 @@ REDIS_URL: Final = yarl.URL.build( ) # IGDB -IGDB_CLIENT_ID: Final = os.environ.get( +IGDB_CLIENT_ID: Final[str] = os.environ.get( "IGDB_CLIENT_ID", os.environ.get("CLIENT_ID", "") ).strip() -IGDB_CLIENT_SECRET: Final = os.environ.get( +IGDB_CLIENT_SECRET: Final[str] = os.environ.get( "IGDB_CLIENT_SECRET", os.environ.get("CLIENT_SECRET", "") ).strip() # MOBYGAMES -MOBYGAMES_API_KEY: Final = os.environ.get("MOBYGAMES_API_KEY", "").strip() +MOBYGAMES_API_KEY: Final[str] = os.environ.get("MOBYGAMES_API_KEY", "").strip() # SCREENSCRAPER -SCREENSCRAPER_USER: Final = os.environ.get("SCREENSCRAPER_USER", "") -SCREENSCRAPER_PASSWORD: Final = os.environ.get("SCREENSCRAPER_PASSWORD", "") +SCREENSCRAPER_USER: Final[str] = os.environ.get("SCREENSCRAPER_USER", "") +SCREENSCRAPER_PASSWORD: Final[str] = os.environ.get("SCREENSCRAPER_PASSWORD", "") # STEAMGRIDDB -STEAMGRIDDB_API_KEY: Final = os.environ.get("STEAMGRIDDB_API_KEY", "").strip() +STEAMGRIDDB_API_KEY: Final[str] = os.environ.get("STEAMGRIDDB_API_KEY", "").strip() # RETROACHIEVEMENTS -RETROACHIEVEMENTS_API_KEY: Final = os.environ.get("RETROACHIEVEMENTS_API_KEY", "") -REFRESH_RETROACHIEVEMENTS_CACHE_DAYS: Final = int( +RETROACHIEVEMENTS_API_KEY: Final[str] = os.environ.get("RETROACHIEVEMENTS_API_KEY", "") +REFRESH_RETROACHIEVEMENTS_CACHE_DAYS: Final[int] = int( os.environ.get("REFRESH_RETROACHIEVEMENTS_CACHE_DAYS", 30) ) # LAUNCHBOX -LAUNCHBOX_API_ENABLED: Final = str_to_bool( +LAUNCHBOX_API_ENABLED: Final[bool] = str_to_bool( os.environ.get("LAUNCHBOX_API_ENABLED", "false") ) # PLAYMATCH -PLAYMATCH_API_ENABLED: Final = str_to_bool( +PLAYMATCH_API_ENABLED: Final[bool] = str_to_bool( os.environ.get("PLAYMATCH_API_ENABLED", "false") ) # HASHEOUS -HASHEOUS_API_ENABLED: Final = str_to_bool( +HASHEOUS_API_ENABLED: Final[bool] = str_to_bool( os.environ.get("HASHEOUS_API_ENABLED", "false") ) # THEGAMESDB -TGDB_API_ENABLED: Final = str_to_bool(os.environ.get("TGDB_API_ENABLED", "false")) +TGDB_API_ENABLED: Final[bool] = str_to_bool(os.environ.get("TGDB_API_ENABLED", "false")) # AUTH ROMM_AUTH_SECRET_KEY: Final = os.environ.get("ROMM_AUTH_SECRET_KEY") diff --git a/backend/endpoints/heartbeat.py b/backend/endpoints/heartbeat.py index 0c87937a9..048828ffc 100644 --- a/backend/endpoints/heartbeat.py +++ b/backend/endpoints/heartbeat.py @@ -6,27 +6,29 @@ from config import ( ENABLE_SCHEDULED_RESCAN, ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA, ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB, - HASHEOUS_API_ENABLED, - LAUNCHBOX_API_ENABLED, OIDC_ENABLED, OIDC_PROVIDER, - PLAYMATCH_API_ENABLED, SCHEDULED_CONVERT_IMAGES_TO_WEBP_CRON, SCHEDULED_RESCAN_CRON, SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON, SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON, - TGDB_API_ENABLED, UPLOAD_TIMEOUT, YOUTUBE_BASE_URL, ) from endpoints.responses.heartbeat import HeartbeatResponse 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 RA_API_ENABLED -from handler.metadata.sgdb_handler import STEAMGRIDDB_API_ENABLED -from handler.metadata.ss_handler import SS_API_ENABLED +from handler.metadata import ( + meta_hasheous_handler, + meta_igdb_handler, + meta_launchbox_handler, + meta_moby_handler, + meta_playmatch_handler, + meta_ra_handler, + meta_sgdb_handler, + meta_ss_handler, + meta_tgdb_handler, +) from utils import get_version from utils.router import APIRouter @@ -49,22 +51,24 @@ async def heartbeat() -> HeartbeatResponse: "SHOW_SETUP_WIZARD": len(db_user_handler.get_admin_users()) == 0, }, "METADATA_SOURCES": { - "ANY_SOURCE_ENABLED": IGDB_API_ENABLED - or SS_API_ENABLED - or MOBY_API_ENABLED - or RA_API_ENABLED - or LAUNCHBOX_API_ENABLED - or HASHEOUS_API_ENABLED - or TGDB_API_ENABLED, - "IGDB_API_ENABLED": IGDB_API_ENABLED, - "SS_API_ENABLED": SS_API_ENABLED, - "MOBY_API_ENABLED": MOBY_API_ENABLED, - "STEAMGRIDDB_API_ENABLED": STEAMGRIDDB_API_ENABLED, - "RA_API_ENABLED": RA_API_ENABLED, - "LAUNCHBOX_API_ENABLED": LAUNCHBOX_API_ENABLED, - "HASHEOUS_API_ENABLED": HASHEOUS_API_ENABLED, - "PLAYMATCH_API_ENABLED": PLAYMATCH_API_ENABLED, - "TGDB_API_ENABLED": TGDB_API_ENABLED, + "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() + ), + "IGDB_API_ENABLED": meta_igdb_handler.is_enabled(), + "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(), }, "FILESYSTEM": { "FS_PLATFORMS": await fs_platform_handler.get_platforms(), diff --git a/backend/endpoints/search.py b/backend/endpoints/search.py index a22e1f9fa..62638f159 100644 --- a/backend/endpoints/search.py +++ b/backend/endpoints/search.py @@ -12,10 +12,10 @@ from handler.metadata import ( meta_sgdb_handler, meta_ss_handler, ) -from handler.metadata.igdb_handler import IGDB_API_ENABLED, IGDBRom -from handler.metadata.moby_handler import MOBY_API_ENABLED, MobyGamesRom -from handler.metadata.sgdb_handler import STEAMGRIDDB_API_ENABLED, SGDBRom -from handler.metadata.ss_handler import SS_API_ENABLED, SSRom +from handler.metadata.igdb_handler import IGDBRom +from handler.metadata.moby_handler import MobyGamesRom +from handler.metadata.sgdb_handler import SGDBRom +from handler.metadata.ss_handler import SSRom from handler.scan_handler import get_main_platform_igdb_id from logger.formatter import BLUE, CYAN from logger.formatter import highlight as hl @@ -50,7 +50,11 @@ async def search_rom( list[SearchRomSchema]: List of matched roms """ - if not IGDB_API_ENABLED and not SS_API_ENABLED and not MOBY_API_ENABLED: + if ( + not meta_igdb_handler.is_enabled() + and not meta_ss_handler.is_enabled() + and not meta_moby_handler.is_enabled() + ): log.error("Search error: No metadata providers enabled") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -182,7 +186,7 @@ async def search_cover( search_term: str = "", ) -> list[SearchCoverSchema]: - if not STEAMGRIDDB_API_ENABLED: + if not meta_sgdb_handler.is_enabled(): log.error("Search error: No SteamGridDB enabled") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, diff --git a/backend/handler/metadata/base_hander.py b/backend/handler/metadata/base_hander.py index 104c3f2e8..d2521cd0d 100644 --- a/backend/handler/metadata/base_hander.py +++ b/backend/handler/metadata/base_hander.py @@ -1,3 +1,4 @@ +import abc import enum import json import re @@ -79,10 +80,15 @@ def _normalize_search_term( return name.strip() -class MetadataHandler: +class MetadataHandler(abc.ABC): SEARCH_TERM_SPLIT_PATTERN = re.compile(r"[\:\-\/]") SEARCH_TERM_NORMALIZER = re.compile(r"\s*[:-]\s*") + @classmethod + @abc.abstractmethod + def is_enabled(cls) -> bool: + """Return whether this metadata handler is enabled.""" + def normalize_cover_url(self, url: str) -> str: return url if not url else f"https:{url.replace('https:', '')}" diff --git a/backend/handler/metadata/hasheous_handler.py b/backend/handler/metadata/hasheous_handler.py index e1c316071..74756c343 100644 --- a/backend/handler/metadata/hasheous_handler.py +++ b/backend/handler/metadata/hasheous_handler.py @@ -123,6 +123,11 @@ class HasheousHandler(MetadataHandler): else "JNoFBA-jEh4HbxuxEHM6MVzydKoAXs9eCcp2dvcg5LRCnpp312voiWmjuaIssSzS" ) + @classmethod + def is_enabled(cls) -> bool: + """Return whether this metadata handler is enabled.""" + return HASHEOUS_API_ENABLED + async def _request( self, url: str, @@ -213,7 +218,7 @@ class HasheousHandler(MetadataHandler): hasheous_id=None, igdb_id=None, tgdb_id=None, ra_id=None ) - if not HASHEOUS_API_ENABLED: + if not self.is_enabled(): return fallback_rom filtered_files = [ @@ -314,7 +319,7 @@ class HasheousHandler(MetadataHandler): ) async def get_igdb_game(self, hasheous_rom: HasheousRom) -> HasheousRom: - if not HASHEOUS_API_ENABLED: + if not self.is_enabled(): return hasheous_rom igdb_id = hasheous_rom.get("igdb_id", None) @@ -358,7 +363,7 @@ class HasheousHandler(MetadataHandler): ) async def get_ra_game(self, hasheous_rom: HasheousRom) -> HasheousRom: - if not HASHEOUS_API_ENABLED: + if not self.is_enabled(): return hasheous_rom ra_id = hasheous_rom.get("ra_id", None) diff --git a/backend/handler/metadata/igdb_handler.py b/backend/handler/metadata/igdb_handler.py index c75436325..22e27e91a 100644 --- a/backend/handler/metadata/igdb_handler.py +++ b/backend/handler/metadata/igdb_handler.py @@ -26,9 +26,6 @@ from .base_hander import ( ) from .base_hander import UniversalPlatformSlug as UPS -# Used to display the IGDB API status in the frontend -IGDB_API_ENABLED: Final = bool(IGDB_CLIENT_ID) and bool(IGDB_CLIENT_SECRET) - PS1_IGDB_ID: Final = 7 PS2_IGDB_ID: Final = 8 PSP_IGDB_ID: Final = 38 @@ -213,6 +210,10 @@ class IGDBHandler(MetadataHandler): self.igdb_service = IGDBService(twitch_auth=TwitchAuth()) self.pagination_limit = 200 + @classmethod + def is_enabled(cls) -> bool: + return bool(IGDB_CLIENT_ID and IGDB_CLIENT_SECRET) + async def _search_rom( self, search_term: str, platform_igdb_id: int, with_game_type: bool = False ) -> Game | None: @@ -337,7 +338,7 @@ class IGDBHandler(MetadataHandler): async def get_rom(self, fs_name: str, platform_igdb_id: int) -> IGDBRom: from handler.filesystem import fs_rom_handler - if not IGDB_API_ENABLED: + if not self.is_enabled(): return IGDBRom(igdb_id=None) if not platform_igdb_id: @@ -432,7 +433,7 @@ class IGDBHandler(MetadataHandler): ) async def get_rom_by_id(self, igdb_id: int) -> IGDBRom: - if not IGDB_API_ENABLED: + if not self.is_enabled(): return IGDBRom(igdb_id=None) roms = await self.igdb_service.list_games( @@ -463,7 +464,7 @@ class IGDBHandler(MetadataHandler): ) async def get_matched_rom_by_id(self, igdb_id: int) -> IGDBRom | None: - if not IGDB_API_ENABLED: + if not self.is_enabled(): return None rom = await self.get_rom_by_id(igdb_id) @@ -472,7 +473,7 @@ class IGDBHandler(MetadataHandler): async def get_matched_roms_by_name( self, search_term: str, platform_igdb_id: int | None ) -> list[IGDBRom]: - if not IGDB_API_ENABLED: + if not self.is_enabled(): return [] if not platform_igdb_id: @@ -561,8 +562,12 @@ class TwitchAuth(MetadataHandler): self.masked_params = self._mask_sensitive_values(self.params) self.timeout = 10 + @classmethod + def is_enabled(cls) -> bool: + return IGDBHandler.is_enabled() + async def _update_twitch_token(self) -> str: - if not IGDB_API_ENABLED: + if not self.is_enabled(): return "" token = None @@ -608,7 +613,7 @@ class TwitchAuth(MetadataHandler): if IS_PYTEST_RUN: return "test_token" - if not IGDB_API_ENABLED: + if not self.is_enabled(): return "" # Fetch the token cache diff --git a/backend/handler/metadata/launchbox_handler.py b/backend/handler/metadata/launchbox_handler.py index fdc40aa1d..bf848b3f8 100644 --- a/backend/handler/metadata/launchbox_handler.py +++ b/backend/handler/metadata/launchbox_handler.py @@ -1,22 +1,25 @@ import json from datetime import datetime -from typing import NotRequired, TypedDict +from typing import Final, NotRequired, TypedDict import pydash from config import LAUNCHBOX_API_ENABLED, str_to_bool from handler.redis_handler import async_cache from logger.logger import log -from tasks.scheduled.update_launchbox_metadata import ( # LAUNCHBOX_MAME_KEY, - LAUNCHBOX_METADATA_ALTERNATE_NAME_KEY, - LAUNCHBOX_METADATA_DATABASE_ID_KEY, - LAUNCHBOX_METADATA_IMAGE_KEY, - LAUNCHBOX_METADATA_NAME_KEY, - update_launchbox_metadata_task, -) from .base_hander import BaseRom, MetadataHandler from .base_hander import UniversalPlatformSlug as UPS +LAUNCHBOX_PLATFORMS_KEY: Final[str] = "romm:launchbox_platforms" +LAUNCHBOX_METADATA_DATABASE_ID_KEY: Final[str] = "romm:launchbox_metadata_database_id" +LAUNCHBOX_METADATA_NAME_KEY: Final[str] = "romm:launchbox_metadata_name" +LAUNCHBOX_METADATA_ALTERNATE_NAME_KEY: Final[str] = ( + "romm:launchbox_metadata_alternate_name" +) +LAUNCHBOX_METADATA_IMAGE_KEY: Final[str] = "romm:launchbox_metadata_image" +LAUNCHBOX_MAME_KEY: Final[str] = "romm:launchbox_mame" +LAUNCHBOX_FILES_KEY: Final[str] = "romm:launchbox_files" + class LaunchboxPlatform(TypedDict): slug: str @@ -115,11 +118,20 @@ def extract_metadata_from_launchbox_rom( class LaunchboxHandler(MetadataHandler): + @classmethod + def is_enabled(cls) -> bool: + return LAUNCHBOX_API_ENABLED + async def _get_rom_from_metadata( self, file_name: str, platform_slug: str ) -> dict | None: if not (await async_cache.exists(LAUNCHBOX_METADATA_NAME_KEY)): log.info("Fetching the Launchbox Metadata.xml file...") + + from tasks.scheduled.update_launchbox_metadata import ( + update_launchbox_metadata_task, + ) + await update_launchbox_metadata_task.run(force=True) if not (await async_cache.exists(LAUNCHBOX_METADATA_NAME_KEY)): @@ -215,7 +227,7 @@ class LaunchboxHandler(MetadataHandler): fallback_rom = LaunchboxRom(launchbox_id=None) - if not LAUNCHBOX_API_ENABLED: + if not self.is_enabled(): return fallback_rom # We replace " - " with ": " to match Launchbox's naming convention @@ -254,7 +266,7 @@ class LaunchboxHandler(MetadataHandler): return LaunchboxRom({k: v for k, v in rom.items() if v}) # type: ignore[misc] async def get_rom_by_id(self, database_id: int) -> LaunchboxRom: - if not LAUNCHBOX_API_ENABLED: + if not self.is_enabled(): return LaunchboxRom(launchbox_id=None) metadata_database_index_entry = await async_cache.hget( @@ -281,7 +293,7 @@ class LaunchboxHandler(MetadataHandler): return LaunchboxRom({k: v for k, v in rom.items() if v}) # type: ignore[misc] async def get_matched_rom_by_id(self, database_id: int) -> LaunchboxRom | None: - if not LAUNCHBOX_API_ENABLED: + if not self.is_enabled(): return None return await self.get_rom_by_id(database_id) diff --git a/backend/handler/metadata/moby_handler.py b/backend/handler/metadata/moby_handler.py index 61b171c50..9420cbd47 100644 --- a/backend/handler/metadata/moby_handler.py +++ b/backend/handler/metadata/moby_handler.py @@ -19,9 +19,6 @@ from .base_hander import ( ) from .base_hander import UniversalPlatformSlug as UPS -# Used to display the Mobygames API status in the frontend -MOBY_API_ENABLED: Final = bool(MOBYGAMES_API_KEY) - PS1_MOBY_ID: Final = 6 PS2_MOBY_ID: Final = 7 PSP_MOBY_ID: Final = 46 @@ -77,6 +74,10 @@ class MobyGamesHandler(MetadataHandler): self.moby_service = MobyGamesService() self.min_similarity_score = 0.6 + @classmethod + def is_enabled(cls) -> bool: + return bool(MOBYGAMES_API_KEY) + async def _search_rom( self, search_term: str, platform_moby_id: int, split_game_name: bool = False ) -> MobyGame | None: @@ -128,7 +129,7 @@ class MobyGamesHandler(MetadataHandler): async def get_rom(self, fs_name: str, platform_moby_id: int) -> MobyGamesRom: from handler.filesystem import fs_rom_handler - if not MOBY_API_ENABLED: + if not self.is_enabled(): return MobyGamesRom(moby_id=None) if not platform_moby_id: @@ -222,7 +223,7 @@ class MobyGamesHandler(MetadataHandler): return MobyGamesRom({k: v for k, v in rom.items() if v}) # type: ignore[misc] async def get_rom_by_id(self, moby_id: int) -> MobyGamesRom: - if not MOBY_API_ENABLED: + if not self.is_enabled(): return MobyGamesRom(moby_id=None) roms = await self.moby_service.list_games(game_id=moby_id) @@ -242,7 +243,7 @@ class MobyGamesHandler(MetadataHandler): return MobyGamesRom({k: v for k, v in rom.items() if v}) # type: ignore[misc] async def get_matched_rom_by_id(self, moby_id: int) -> MobyGamesRom | None: - if not MOBY_API_ENABLED: + if not self.is_enabled(): return None rom = await self.get_rom_by_id(moby_id) @@ -251,7 +252,7 @@ class MobyGamesHandler(MetadataHandler): async def get_matched_roms_by_name( self, search_term: str, platform_moby_id: int | None ) -> list[MobyGamesRom]: - if not MOBY_API_ENABLED: + if not self.is_enabled(): return [] if not platform_moby_id: diff --git a/backend/handler/metadata/playmatch_handler.py b/backend/handler/metadata/playmatch_handler.py index 05f444483..708329754 100644 --- a/backend/handler/metadata/playmatch_handler.py +++ b/backend/handler/metadata/playmatch_handler.py @@ -6,6 +6,7 @@ import httpx import yarl from config import PLAYMATCH_API_ENABLED from fastapi import HTTPException, status +from handler.metadata.base_hander import MetadataHandler from logger.logger import log from models.rom import RomFile from utils import get_version @@ -38,7 +39,7 @@ class PlaymatchRomMatch(TypedDict): igdb_id: int | None -class PlaymatchHandler: +class PlaymatchHandler(MetadataHandler): """ Handler for [Playmatch](https://github.com/RetroRealm/playmatch), a service for matching Roms by Hashes. """ @@ -47,6 +48,10 @@ class PlaymatchHandler: self.base_url = "https://playmatch.retrorealm.dev/api" self.identify_url = f"{self.base_url}/identify/ids" + @classmethod + def is_enabled(cls) -> bool: + return PLAYMATCH_API_ENABLED + async def _request(self, url: str, query: dict) -> dict: """ Sends a Request to Playmatch API. @@ -100,7 +105,7 @@ class PlaymatchHandler: :return: A PlaymatchRomMatch objects containing the matched ROM information. :raises HTTPException: If the request fails or the service is unavailable. """ - if not PLAYMATCH_API_ENABLED: + if not self.is_enabled(): return PlaymatchRomMatch(igdb_id=None) first_file = next( diff --git a/backend/handler/metadata/ra_handler.py b/backend/handler/metadata/ra_handler.py index 1504892e5..10e13bd3f 100644 --- a/backend/handler/metadata/ra_handler.py +++ b/backend/handler/metadata/ra_handler.py @@ -2,7 +2,7 @@ import json import os import time from datetime import datetime -from typing import Final, NotRequired, TypedDict +from typing import NotRequired, TypedDict import pydash from adapters.services.retroachievements import RetroAchievementsService @@ -20,9 +20,6 @@ from models.rom import Rom from .base_hander import BaseRom, MetadataHandler from .base_hander import UniversalPlatformSlug as UPS -# Used to display the Retroachievements API status in the frontend -RA_API_ENABLED: Final = bool(RETROACHIEVEMENTS_API_KEY) - class RAGamesPlatform(TypedDict): slug: str @@ -125,6 +122,10 @@ class RAHandler(MetadataHandler): self.ra_service = RetroAchievementsService() self.HASHES_FILE_NAME = "ra_hashes.json" + @classmethod + def is_enabled(cls) -> bool: + return bool(RETROACHIEVEMENTS_API_KEY) + def _get_hashes_file_path(self, platform_id: int) -> str: platform_resources_path = fs_resource_handler.get_platform_resources_path( platform_id diff --git a/backend/handler/metadata/sgdb_handler.py b/backend/handler/metadata/sgdb_handler.py index 63aa226d7..d4d67c12c 100644 --- a/backend/handler/metadata/sgdb_handler.py +++ b/backend/handler/metadata/sgdb_handler.py @@ -8,9 +8,6 @@ from logger.logger import log from .base_hander import MetadataHandler -# Used to display the Mobygames API status in the frontend -STEAMGRIDDB_API_ENABLED: Final = bool(STEAMGRIDDB_API_KEY) - class SGDBResource(TypedDict): thumb: str @@ -33,8 +30,12 @@ class SGDBBaseHandler(MetadataHandler): self.sgdb_service = SteamGridDBService() self.min_similarity_score: Final = 0.98 + @classmethod + def is_enabled(cls) -> bool: + return bool(STEAMGRIDDB_API_KEY) + async def get_details(self, search_term: str) -> list[SGDBResult]: - if not STEAMGRIDDB_API_ENABLED: + if not self.is_enabled(): return [] games = await self.sgdb_service.search_games(term=search_term) @@ -51,7 +52,7 @@ class SGDBBaseHandler(MetadataHandler): return list(filter(None, results)) async def get_details_by_names(self, game_names: list[str]) -> SGDBRom: - if not STEAMGRIDDB_API_ENABLED: + if not self.is_enabled(): return SGDBRom(sgdb_id=None) for game_name in game_names: diff --git a/backend/handler/metadata/ss_handler.py b/backend/handler/metadata/ss_handler.py index d0bcb3001..604c56540 100644 --- a/backend/handler/metadata/ss_handler.py +++ b/backend/handler/metadata/ss_handler.py @@ -21,8 +21,6 @@ from .base_hander import ( ) from .base_hander import UniversalPlatformSlug as UPS -# Used to display the Screenscraper API status in the frontend -SS_API_ENABLED: Final = bool(SCREENSCRAPER_USER) and bool(SCREENSCRAPER_PASSWORD) SS_DEV_ID: Final = base64.b64decode("enVyZGkxNQ==").decode() SS_DEV_PASSWORD: Final = base64.b64decode("eFRKd29PRmpPUUc=").decode() @@ -276,6 +274,10 @@ class SSHandler(MetadataHandler): def __init__(self) -> None: self.ss_service = ScreenScraperService() + @classmethod + def is_enabled(cls) -> bool: + return bool(SCREENSCRAPER_USER and SCREENSCRAPER_PASSWORD) + async def _search_rom( self, search_term: str, platform_ss_id: int, split_game_name: bool = False ) -> SSGame | None: @@ -323,7 +325,7 @@ class SSHandler(MetadataHandler): async def get_rom(self, file_name: str, platform_ss_id: int) -> SSRom: from handler.filesystem import fs_rom_handler - if not SS_API_ENABLED: + if not self.is_enabled(): return SSRom(ss_id=None) if not platform_ss_id: @@ -411,7 +413,7 @@ class SSHandler(MetadataHandler): return build_ss_rom(res) async def get_rom_by_id(self, ss_id: int) -> SSRom: - if not SS_API_ENABLED: + if not self.is_enabled(): return SSRom(ss_id=None) res = await self.ss_service.get_game_info(game_id=ss_id) @@ -421,7 +423,7 @@ class SSHandler(MetadataHandler): return build_ss_rom(res) async def get_matched_rom_by_id(self, ss_id: int) -> SSRom | None: - if not SS_API_ENABLED: + if not self.is_enabled(): return None rom = await self.get_rom_by_id(ss_id) @@ -430,7 +432,7 @@ class SSHandler(MetadataHandler): async def get_matched_roms_by_name( self, search_term: str, platform_ss_id: int | None ) -> list[SSRom]: - if not SS_API_ENABLED: + if not self.is_enabled(): return [] if not platform_ss_id: diff --git a/backend/handler/metadata/tgdb_handler.py b/backend/handler/metadata/tgdb_handler.py index d2b4e89b5..3166f7e5d 100644 --- a/backend/handler/metadata/tgdb_handler.py +++ b/backend/handler/metadata/tgdb_handler.py @@ -1,5 +1,7 @@ from typing import NotRequired, TypedDict +from config import TGDB_API_ENABLED + from .base_hander import MetadataHandler from .base_hander import UniversalPlatformSlug as UPS @@ -22,6 +24,10 @@ class TGDBHandler(MetadataHandler): self.platform_endpoint = f"{self.BASE_URL}/Lookup/Platforms" self.games_endpoint = f"{self.BASE_URL}/Lookup/ByHash" + @classmethod + def is_enabled(cls) -> bool: + return TGDB_API_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/tasks/scheduled/scan_library.py b/backend/tasks/scheduled/scan_library.py index 25c0efae5..fe7c81875 100644 --- a/backend/tasks/scheduled/scan_library.py +++ b/backend/tasks/scheduled/scan_library.py @@ -1,15 +1,17 @@ from config import ( ENABLE_SCHEDULED_RESCAN, - HASHEOUS_API_ENABLED, - LAUNCHBOX_API_ENABLED, SCHEDULED_RESCAN_CRON, ) from endpoints.sockets.scan import scan_platforms -from handler.metadata.igdb_handler import IGDB_API_ENABLED -from handler.metadata.moby_handler import MOBY_API_ENABLED -from handler.metadata.ra_handler import RA_API_ENABLED -from handler.metadata.sgdb_handler import STEAMGRIDDB_API_ENABLED -from handler.metadata.ss_handler import SS_API_ENABLED +from handler.metadata import ( + meta_hasheous_handler, + meta_igdb_handler, + meta_launchbox_handler, + meta_moby_handler, + meta_ra_handler, + meta_sgdb_handler, + meta_ss_handler, +) from handler.scan_handler import MetadataSource, ScanType from logger.logger import log from tasks.tasks import PeriodicTask @@ -33,13 +35,13 @@ class ScanLibraryTask(PeriodicTask): return None source_mapping: dict[str, bool] = { - MetadataSource.IGDB: IGDB_API_ENABLED, - MetadataSource.SS: SS_API_ENABLED, - MetadataSource.MOBY: MOBY_API_ENABLED, - MetadataSource.RA: RA_API_ENABLED, - MetadataSource.LB: LAUNCHBOX_API_ENABLED, - MetadataSource.HASHEOUS: HASHEOUS_API_ENABLED, - MetadataSource.SGDB: STEAMGRIDDB_API_ENABLED, + MetadataSource.IGDB: meta_igdb_handler.is_enabled(), + MetadataSource.SS: meta_ss_handler.is_enabled(), + MetadataSource.MOBY: meta_moby_handler.is_enabled(), + MetadataSource.RA: meta_ra_handler.is_enabled(), + MetadataSource.LB: meta_launchbox_handler.is_enabled(), + MetadataSource.HASHEOUS: meta_hasheous_handler.is_enabled(), + MetadataSource.SGDB: meta_sgdb_handler.is_enabled(), } metadata_sources = [source for source, flag in source_mapping.items() if flag] diff --git a/backend/tasks/scheduled/sync_retroachievements_progress.py b/backend/tasks/scheduled/sync_retroachievements_progress.py index 6a260fa1f..3a3fc7f7b 100644 --- a/backend/tasks/scheduled/sync_retroachievements_progress.py +++ b/backend/tasks/scheduled/sync_retroachievements_progress.py @@ -6,7 +6,7 @@ from config import ( ) from handler.database import db_user_handler from handler.metadata import meta_ra_handler -from handler.metadata.ra_handler import RA_API_ENABLED, RAUserProgression +from handler.metadata.ra_handler import RAUserProgression from logger.logger import log from tasks.tasks import PeriodicTask from utils.context import initialize_context @@ -25,7 +25,7 @@ class SyncRetroAchievementsProgressTask(PeriodicTask): @initialize_context() async def run(self) -> None: - if not RA_API_ENABLED: + if not meta_ra_handler.is_enabled(): log.warning("RetroAchievements API is not enabled, skipping progress sync") return None diff --git a/backend/tasks/scheduled/update_launchbox_metadata.py b/backend/tasks/scheduled/update_launchbox_metadata.py index 6ca1d0905..42c2f2aa9 100644 --- a/backend/tasks/scheduled/update_launchbox_metadata.py +++ b/backend/tasks/scheduled/update_launchbox_metadata.py @@ -1,27 +1,28 @@ import json import zipfile from io import BytesIO -from typing import Any, Final +from typing import Any from config import ( ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA, - LAUNCHBOX_API_ENABLED, SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON, ) from defusedxml import ElementTree as ET +from handler.metadata import meta_launchbox_handler +from handler.metadata.launchbox_handler import ( + LAUNCHBOX_FILES_KEY, + LAUNCHBOX_MAME_KEY, + LAUNCHBOX_METADATA_ALTERNATE_NAME_KEY, + LAUNCHBOX_METADATA_DATABASE_ID_KEY, + LAUNCHBOX_METADATA_IMAGE_KEY, + LAUNCHBOX_METADATA_NAME_KEY, + LAUNCHBOX_PLATFORMS_KEY, +) from handler.redis_handler import async_cache from logger.logger import log from tasks.tasks import RemoteFilePullTask from utils.context import initialize_context -LAUNCHBOX_PLATFORMS_KEY: Final = "romm:launchbox_platforms" -LAUNCHBOX_METADATA_DATABASE_ID_KEY: Final = "romm:launchbox_metadata_database_id" -LAUNCHBOX_METADATA_NAME_KEY: Final = "romm:launchbox_metadata_name" -LAUNCHBOX_METADATA_ALTERNATE_NAME_KEY: Final = "romm:launchbox_metadata_alternate_name" -LAUNCHBOX_METADATA_IMAGE_KEY: Final = "romm:launchbox_metadata_image" -LAUNCHBOX_MAME_KEY: Final = "romm:launchbox_mame" -LAUNCHBOX_FILES_KEY: Final = "romm:launchbox_files" - class UpdateLaunchboxMetadataTask(RemoteFilePullTask): def __init__(self): @@ -37,7 +38,7 @@ class UpdateLaunchboxMetadataTask(RemoteFilePullTask): @initialize_context() async def run(self, force: bool = False) -> None: - if not LAUNCHBOX_API_ENABLED: + if not meta_launchbox_handler.is_enabled(): log.warning("Launchbox API is not enabled, skipping metadata update") return None diff --git a/backend/tests/handler/metadata/test_base_handler.py b/backend/tests/handler/metadata/test_base_handler.py index ce4ecedcf..992746118 100644 --- a/backend/tests/handler/metadata/test_base_handler.py +++ b/backend/tests/handler/metadata/test_base_handler.py @@ -24,6 +24,12 @@ from handler.metadata.base_hander import ( from handler.redis_handler import async_cache +class ExampleMetadataHandler(MetadataHandler): + @classmethod + def is_enabled(cls) -> bool: + return True + + class TestNormalizeSearchTerm: """Test the _normalize_search_term function.""" @@ -102,7 +108,7 @@ class TestMetadataHandlerMethods: @pytest.fixture def handler(self): - return MetadataHandler() + return ExampleMetadataHandler() def test_normalize_cover_url_with_url(self, handler: MetadataHandler): """Test URL normalization with valid URL.""" diff --git a/backend/tests/tasks/test_scan_library.py b/backend/tests/tasks/test_scan_library.py index 6e4eb2dcc..2ae050c1a 100644 --- a/backend/tests/tasks/test_scan_library.py +++ b/backend/tests/tasks/test_scan_library.py @@ -1,6 +1,13 @@ -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock import pytest +from handler.metadata.hasheous_handler import HasheousHandler +from handler.metadata.igdb_handler import IGDBHandler +from handler.metadata.launchbox_handler import LaunchboxHandler +from handler.metadata.moby_handler import MobyGamesHandler +from handler.metadata.ra_handler import RAHandler +from handler.metadata.sgdb_handler import SGDBBaseHandler +from handler.metadata.ss_handler import SSHandler from handler.scan_handler import MetadataSource, ScanType from tasks.scheduled.scan_library import ScanLibraryTask, scan_library_task @@ -15,18 +22,20 @@ class TestScanLibraryTask: assert task.func == "tasks.scheduled.scan_library.scan_library_task.run" assert task.description == "Rescans the entire library" - @patch("tasks.scheduled.scan_library.ENABLE_SCHEDULED_RESCAN", True) - @patch("tasks.scheduled.scan_library.IGDB_API_ENABLED", False) - @patch("tasks.scheduled.scan_library.SS_API_ENABLED", False) - @patch("tasks.scheduled.scan_library.MOBY_API_ENABLED", False) - @patch("tasks.scheduled.scan_library.RA_API_ENABLED", True) - @patch("tasks.scheduled.scan_library.LAUNCHBOX_API_ENABLED", True) - @patch("tasks.scheduled.scan_library.HASHEOUS_API_ENABLED", False) - @patch("tasks.scheduled.scan_library.STEAMGRIDDB_API_ENABLED", False) - @patch("tasks.scheduled.scan_library.scan_platforms") - @patch("tasks.scheduled.scan_library.log") - async def test_run_enabled(self, mock_log, mock_scan_platforms, task): + async def test_run_enabled(self, task, mocker): """Test run when scheduled rescan is enabled""" + mocker.patch.object(HasheousHandler, "is_enabled", return_value=False) + mocker.patch.object(IGDBHandler, "is_enabled", return_value=False) + mocker.patch.object(LaunchboxHandler, "is_enabled", return_value=True) + mocker.patch.object(MobyGamesHandler, "is_enabled", return_value=False) + mocker.patch.object(RAHandler, "is_enabled", return_value=True) + mocker.patch.object(SGDBBaseHandler, "is_enabled", return_value=False) + mocker.patch.object(SSHandler, "is_enabled", return_value=False) + mocker.patch("tasks.scheduled.scan_library.ENABLE_SCHEDULED_RESCAN", True) + mock_scan_platforms = mocker.patch( + "tasks.scheduled.scan_library.scan_platforms" + ) + mock_log = mocker.patch("tasks.scheduled.scan_library.log") mock_scan_platforms.return_value = AsyncMock() await task.run() @@ -39,11 +48,13 @@ class TestScanLibraryTask: ) mock_log.info.assert_any_call("Scheduled library scan done") - @patch("tasks.scheduled.scan_library.ENABLE_SCHEDULED_RESCAN", False) - @patch("tasks.scheduled.scan_library.scan_platforms") - @patch("tasks.scheduled.scan_library.log") - async def test_run_disabled(self, mock_log, mock_scan_platforms, task): + async def test_run_disabled(self, task, mocker): """Test run when scheduled rescan is disabled""" + mocker.patch("tasks.scheduled.scan_library.ENABLE_SCHEDULED_RESCAN", False) + mock_scan_platforms = mocker.patch( + "tasks.scheduled.scan_library.scan_platforms" + ) + mock_log = mocker.patch("tasks.scheduled.scan_library.log") task.unschedule = MagicMock() await task.run() diff --git a/backend/tests/tasks/test_sync_retroachievements_progress.py b/backend/tests/tasks/test_sync_retroachievements_progress.py index c815e5b91..126d0b465 100644 --- a/backend/tests/tasks/test_sync_retroachievements_progress.py +++ b/backend/tests/tasks/test_sync_retroachievements_progress.py @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest from handler.database.users_handler import DBUsersHandler @@ -25,10 +25,11 @@ class TestSyncRetroAchievementsProgressTask: ) assert task.description == "Updates RetroAchievements progress for all users" - @patch("tasks.scheduled.sync_retroachievements_progress.RA_API_ENABLED", False) - @patch("tasks.scheduled.sync_retroachievements_progress.log") - async def test_run_when_retroachievements_api_disabled(self, mock_log, task): + async def test_run_when_retroachievements_api_disabled(self, task, mocker): """Test run method when RetroAchievements API is disabled.""" + mocker.patch.object(RAHandler, "is_enabled", return_value=False) + mock_log = mocker.patch("tasks.scheduled.sync_retroachievements_progress.log") + await task.run() mock_log.warning.assert_called_once_with( diff --git a/backend/tests/tasks/test_update_launchbox_metadata.py b/backend/tests/tasks/test_update_launchbox_metadata.py index a1e9a93e1..16281594b 100644 --- a/backend/tests/tasks/test_update_launchbox_metadata.py +++ b/backend/tests/tasks/test_update_launchbox_metadata.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import anyio import pytest -from tasks.scheduled.update_launchbox_metadata import ( +from handler.metadata.launchbox_handler import ( LAUNCHBOX_FILES_KEY, LAUNCHBOX_MAME_KEY, LAUNCHBOX_METADATA_ALTERNATE_NAME_KEY, @@ -11,6 +11,9 @@ from tasks.scheduled.update_launchbox_metadata import ( LAUNCHBOX_METADATA_IMAGE_KEY, LAUNCHBOX_METADATA_NAME_KEY, LAUNCHBOX_PLATFORMS_KEY, + LaunchboxHandler, +) +from tasks.scheduled.update_launchbox_metadata import ( UpdateLaunchboxMetadataTask, update_launchbox_metadata_task, ) @@ -61,10 +64,11 @@ class TestUpdateLaunchboxMetadataTask: mock_super_run.assert_called_once_with(True) - @patch("tasks.scheduled.update_launchbox_metadata.LAUNCHBOX_API_ENABLED", False) - @patch("tasks.scheduled.update_launchbox_metadata.log") - async def test_run_when_launchbox_api_disabled(self, mock_log, task): + async def test_run_when_launchbox_api_disabled(self, task, mocker): """Test run method when Launchbox API is disabled""" + mocker.patch.object(LaunchboxHandler, "is_enabled", return_value=False) + mock_log = mocker.patch("tasks.scheduled.update_launchbox_metadata.log") + await task.run(force=True) mock_log.warning.assert_called_once_with( diff --git a/backend/watcher.py b/backend/watcher.py index 828158fcd..ddd1f8d70 100644 --- a/backend/watcher.py +++ b/backend/watcher.py @@ -8,8 +8,6 @@ from typing import cast import sentry_sdk from config import ( ENABLE_RESCAN_ON_FILESYSTEM_CHANGE, - HASHEOUS_API_ENABLED, - LAUNCHBOX_API_ENABLED, LIBRARY_BASE_PATH, RESCAN_ON_FILESYSTEM_CHANGE_DELAY, SCAN_TIMEOUT, @@ -18,11 +16,15 @@ from config import ( from config.config_manager import config_manager as cm from endpoints.sockets.scan import scan_platforms from handler.database import db_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 RA_API_ENABLED -from handler.metadata.sgdb_handler import STEAMGRIDDB_API_ENABLED -from handler.metadata.ss_handler import SS_API_ENABLED +from handler.metadata import ( + meta_hasheous_handler, + meta_igdb_handler, + meta_launchbox_handler, + meta_moby_handler, + meta_ra_handler, + meta_sgdb_handler, + meta_ss_handler, +) from handler.scan_handler import MetadataSource, ScanType from logger.formatter import CYAN from logger.formatter import highlight as hl @@ -96,13 +98,13 @@ def process_changes(changes: Sequence[Change]) -> None: # Check whether any metadata source is enabled. source_mapping: dict[str, bool] = { - MetadataSource.IGDB: IGDB_API_ENABLED, - MetadataSource.SS: SS_API_ENABLED, - MetadataSource.MOBY: MOBY_API_ENABLED, - MetadataSource.RA: RA_API_ENABLED, - MetadataSource.LB: LAUNCHBOX_API_ENABLED, - MetadataSource.HASHEOUS: HASHEOUS_API_ENABLED, - MetadataSource.SGDB: STEAMGRIDDB_API_ENABLED, + MetadataSource.IGDB: meta_igdb_handler.is_enabled(), + MetadataSource.SS: meta_ss_handler.is_enabled(), + MetadataSource.MOBY: meta_moby_handler.is_enabled(), + MetadataSource.RA: meta_ra_handler.is_enabled(), + MetadataSource.LB: meta_launchbox_handler.is_enabled(), + MetadataSource.HASHEOUS: meta_hasheous_handler.is_enabled(), + MetadataSource.SGDB: meta_sgdb_handler.is_enabled(), } metadata_sources = [source for source, flag in source_mapping.items() if flag] if not metadata_sources: