From 63fac787435026ee2e3a73ff9c65fdcbc37790e7 Mon Sep 17 00:00:00 2001 From: zurdi Date: Fri, 25 Oct 2024 01:05:58 +0000 Subject: [PATCH 01/52] screenscraper API test --- backend/config/__init__.py | 4 + backend/endpoints/search.py | 11 +- backend/handler/metadata/__init__.py | 2 + backend/handler/metadata/ss_handler.py | 1036 ++++++++++++++++++++++++ 4 files changed, 1051 insertions(+), 2 deletions(-) create mode 100644 backend/handler/metadata/ss_handler.py diff --git a/backend/config/__init__.py b/backend/config/__init__.py index 388807037..a140edd65 100644 --- a/backend/config/__init__.py +++ b/backend/config/__init__.py @@ -56,6 +56,10 @@ def str_to_bool(value: str) -> bool: "IGDB_CLIENT_SECRET", os.environ.get("CLIENT_SECRET", "") ) +# SCREENSCRAPER +SCREENSCRAPER_USER_ID: Final = os.environ.get("SCREENSCRAPER_USER_ID", "") +SCREENSCRAPER_API_KEY: Final = os.environ.get("SCREENSCRAPER_API_KEY", "") + # STEAMGRIDDB STEAMGRIDDB_API_KEY: Final = os.environ.get("STEAMGRIDDB_API_KEY", "") diff --git a/backend/endpoints/search.py b/backend/endpoints/search.py index 57ee96457..d8fe16eb8 100644 --- a/backend/endpoints/search.py +++ b/backend/endpoints/search.py @@ -3,10 +3,16 @@ from endpoints.responses.search import SearchCoverSchema, SearchRomSchema from fastapi import HTTPException, Request, status from handler.database import db_rom_handler -from handler.metadata import meta_igdb_handler, meta_moby_handler, meta_sgdb_handler +from handler.metadata import ( + meta_igdb_handler, + meta_moby_handler, + meta_sgdb_handler, + meta_ss_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 handler.metadata.ss_handler import SS_API_ENABLED from handler.scan_handler import _get_main_platform_igdb_id from logger.logger import log from utils.router import APIRouter @@ -35,7 +41,7 @@ async def search_rom( list[SearchRomSchema]: List of matched roms """ - if not IGDB_API_ENABLED and not MOBY_API_ENABLED: + if not IGDB_API_ENABLED and not SS_API_ENABLED and not MOBY_API_ENABLED: log.error("Search error: No metadata providers enabled") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -78,6 +84,7 @@ async def search_rom( moby_matched_roms = await meta_moby_handler.get_matched_roms_by_name( search_term, rom.platform.moby_id ) + await meta_ss_handler.search_rom(search_term) merged_dict = { item["name"]: {**item, "igdb_url_cover": item.pop("url_cover", "")} diff --git a/backend/handler/metadata/__init__.py b/backend/handler/metadata/__init__.py index 65bd6ee37..5deff2266 100644 --- a/backend/handler/metadata/__init__.py +++ b/backend/handler/metadata/__init__.py @@ -1,7 +1,9 @@ from .igdb_handler import IGDBBaseHandler from .moby_handler import MobyGamesHandler from .sgdb_handler import SGDBBaseHandler +from .ss_handler import SSBaseHandler meta_igdb_handler = IGDBBaseHandler() +meta_ss_handler = SSBaseHandler() meta_moby_handler = MobyGamesHandler() meta_sgdb_handler = SGDBBaseHandler() diff --git a/backend/handler/metadata/ss_handler.py b/backend/handler/metadata/ss_handler.py new file mode 100644 index 000000000..4c38abb73 --- /dev/null +++ b/backend/handler/metadata/ss_handler.py @@ -0,0 +1,1036 @@ +import functools +import re +import time +from typing import Final, NotRequired, TypedDict + +import httpx +import pydash +from config import SCREENSCRAPER_API_KEY, SCREENSCRAPER_USER_ID +from fastapi import HTTPException, status +from handler.redis_handler import sync_cache +from logger.logger import log +from unidecode import unidecode as uc +from utils.context import ctx_httpx_client + +from .base_hander import MetadataHandler + +# Used to display the IGDB API status in the frontend +SS_API_ENABLED: Final = bool(SCREENSCRAPER_USER_ID) and bool(SCREENSCRAPER_API_KEY) + +MAIN_GAME_CATEGORY: Final = 0 +EXPANDED_GAME_CATEGORY: Final = 10 +N_SCREENSHOTS: Final = 5 +PS1_IGDB_ID: Final = 7 +PS2_IGDB_ID: Final = 8 +PSP_IGDB_ID: Final = 38 +SWITCH_IGDB_ID: Final = 130 +ARCADE_IGDB_IDS: Final = [52, 79, 80] + + +class IGDBPlatform(TypedDict): + slug: str + igdb_id: int | None + name: NotRequired[str] + + +class IGDBAgeRating(TypedDict): + rating: str + category: str + rating_cover_url: str + + +class IGDBMetadataPlatform(TypedDict): + igdb_id: int + name: str + + +class IGDBRelatedGame(TypedDict): + id: int + name: str + slug: str + type: str + cover_url: str + + +class IGDBMetadata(TypedDict): + total_rating: str + aggregated_rating: str + first_release_date: int | None + youtube_video_id: str | None + genres: list[str] + franchises: list[str] + alternative_names: list[str] + collections: list[str] + companies: list[str] + game_modes: list[str] + age_ratings: list[IGDBAgeRating] + platforms: list[IGDBMetadataPlatform] + expansions: list[IGDBRelatedGame] + dlcs: list[IGDBRelatedGame] + remasters: list[IGDBRelatedGame] + remakes: list[IGDBRelatedGame] + expanded_games: list[IGDBRelatedGame] + ports: list[IGDBRelatedGame] + similar_games: list[IGDBRelatedGame] + + +class IGDBRom(TypedDict): + igdb_id: int | None + slug: NotRequired[str] + name: NotRequired[str] + summary: NotRequired[str] + url_cover: NotRequired[str] + url_screenshots: NotRequired[list[str]] + igdb_metadata: NotRequired[IGDBMetadata] + + +def extract_metadata_from_igdb_rom( + rom: dict, video_id: str | None = None +) -> IGDBMetadata: + return IGDBMetadata( + { + "youtube_video_id": video_id, + "total_rating": str(round(rom.get("total_rating", 0.0), 2)), + "aggregated_rating": str(round(rom.get("aggregated_rating", 0.0), 2)), + "first_release_date": rom.get("first_release_date", None), + "genres": pydash.map_(rom.get("genres", []), "name"), + "franchises": pydash.compact( + [rom.get("franchise.name", None)] + + pydash.map_(rom.get("franchises", []), "name") + ), + "alternative_names": pydash.map_(rom.get("alternative_names", []), "name"), + "collections": pydash.map_(rom.get("collections", []), "name"), + "game_modes": pydash.map_(rom.get("game_modes", []), "name"), + "companies": pydash.map_(rom.get("involved_companies", []), "company.name"), + "platforms": [ + IGDBMetadataPlatform(igdb_id=p.get("id", ""), name=p.get("name", "")) + for p in rom.get("platforms", []) + ], + "age_ratings": [ + IGDB_AGE_RATINGS[r["rating"]] + for r in rom.get("age_ratings", []) + if r["rating"] in IGDB_AGE_RATINGS + ], + "expansions": [ + IGDBRelatedGame( + id=e["id"], + slug=e["slug"], + name=e["name"], + cover_url=pydash.get(e, "cover.url", ""), + type="expansion", + ) + for e in rom.get("expansions", []) + ], + "dlcs": [ + IGDBRelatedGame( + id=d["id"], + slug=d["slug"], + name=d["name"], + cover_url=pydash.get(d, "cover.url", ""), + type="dlc", + ) + for d in rom.get("dlcs", []) + ], + "remasters": [ + IGDBRelatedGame( + id=r["id"], + slug=r["slug"], + name=r["name"], + cover_url=pydash.get(r, "cover.url", ""), + type="remaster", + ) + for r in rom.get("remasters", []) + ], + "remakes": [ + IGDBRelatedGame( + id=r["id"], + slug=r["slug"], + name=r["name"], + cover_url=pydash.get(r, "cover.url", ""), + type="remake", + ) + for r in rom.get("remakes", []) + ], + "expanded_games": [ + IGDBRelatedGame( + id=g["id"], + slug=g["slug"], + name=g["name"], + cover_url=pydash.get(g, "cover.url", ""), + type="expanded", + ) + for g in rom.get("expanded_games", []) + ], + "ports": [ + IGDBRelatedGame( + id=p["id"], + slug=p["slug"], + name=p["name"], + cover_url=pydash.get(p, "cover.url", ""), + type="port", + ) + for p in rom.get("ports", []) + ], + "similar_games": [ + IGDBRelatedGame( + id=s["id"], + slug=s["slug"], + name=s["name"], + cover_url=pydash.get(s, "cover.url", ""), + type="similar", + ) + for s in rom.get("similar_games", []) + ], + } + ) + + +class SSBaseHandler(MetadataHandler): + def __init__(self) -> None: + self.BASE_URL = "https://api.screenscraper.fr/api2" + # 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 = f"{self.BASE_URL}/jeuRecherche.php" + self.search_params = SEARCH_FIELDS + # self.video_endpoint = f"{self.BASE_URL}/game_videos" + # self.pagination_limit = 200 + # self.twitch_auth = TwitchAuth() + # self.headers = { + # "Client-ID": IGDB_CLIENT_ID, + # "Accept": "application/json", + # } + self.auth_params = { + "devid": SCREENSCRAPER_USER_ID, + "devpassword": SCREENSCRAPER_API_KEY, + } + self.output_param = {"output": "json"} + + async def _request(self, url: str, search_term: str, timeout: int = 120) -> list: + httpx_client = ctx_httpx_client.get() + try: + params = { + **self.auth_params, + **self.output_param, + "recherche": search_term, + # "systemeid": "1", + } + res = await httpx_client.get( + url, + params=params, + timeout=timeout, + ) + + res.raise_for_status() + matches: list[dict] = [] + for rom in res.json().get("response", []).get("jeux", []): + for name in rom.get("noms", []): + region = name.get("region", None) + text = name.get("text", None) + if region and text: + matches.append({region: text}) + for m in matches: + log.debug(m) + return res.json() + except httpx.NetworkError as exc: + log.critical("Connection error: can't connect to IGDB", exc_info=True) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Can't connect to IGDB, check your internet connection", + ) from exc + + async def search_rom(self, search_term) -> None: + await self._request(self.search_endpoint, search_term) + + # async def _search_rom( + # self, search_term: str, platform_igdb_id: int, with_category: bool = False + # ) -> dict | None: + # if not platform_igdb_id: + # return None + + # search_term = uc(search_term) + # category_filter: str = ( + # f"& (category={MAIN_GAME_CATEGORY} | category={EXPANDED_GAME_CATEGORY})" + # if with_category + # else "" + # ) + + # def is_exact_match(rom: dict, search_term: str) -> bool: + # return ( + # rom["name"].lower() == search_term.lower() + # or rom["slug"].lower() == search_term.lower() + # or ( + # self._normalize_exact_match(rom["name"]) + # == self._normalize_exact_match(search_term) + # ) + # ) + + # roms = await self._request( + # self.games_endpoint, + # data=f'search "{search_term}"; fields {",".join(self.games_fields)}; where platforms=[{platform_igdb_id}] {category_filter};', + # ) + # for rom in roms: + # # Return early if an exact match is found. + # if is_exact_match(rom, search_term): + # return rom + + # roms_expanded = await self._request( + # self.search_endpoint, + # data=f'fields {",".join(self.search_fields)}; where game.platforms=[{platform_igdb_id}] & (name ~ *"{search_term}"* | alternative_name ~ *"{search_term}"*);', + # ) + # if roms_expanded: + # extra_roms = await self._request( + # self.games_endpoint, + # f'fields {",".join(self.games_fields)}; where id={roms_expanded[0]["game"]["id"]};', + # ) + # for rom in extra_roms: + # # Return early if an exact match is found. + # if is_exact_match(rom, search_term): + # return rom + + # roms.extend(extra_roms) + + # return roms[0] if roms else None + + # @check_twitch_token + # async def get_platform(self, slug: str) -> IGDBPlatform: + # if not IGDB_API_ENABLED: + # return IGDBPlatform(igdb_id=None, slug=slug) + + # platforms = await self._request( + # self.platform_endpoint, + # data=f'fields {",".join(self.platforms_fields)}; where slug="{slug.lower()}";', + # ) + + # platform = pydash.get(platforms, "[0]", None) + # if platform: + # return IGDBPlatform( + # igdb_id=platform["id"], + # slug=slug, + # name=platform["name"], + # ) + + # # Check if platform is a version if not found + # platform_versions = await self._request( + # self.platform_version_endpoint, + # data=f'fields {",".join(self.platforms_fields)}; where slug="{slug.lower()}";', + # ) + # version = pydash.get(platform_versions, "[0]", None) + # if version: + # return IGDBPlatform( + # igdb_id=version["id"], + # slug=slug, + # name=version["name"], + # ) + + # return IGDBPlatform(igdb_id=None, slug=slug) + + # @check_twitch_token + # async def get_rom(self, file_name: str, platform_igdb_id: int) -> IGDBRom: + # from handler.filesystem import fs_rom_handler + + # if not IGDB_API_ENABLED: + # return IGDBRom(igdb_id=None) + + # if not platform_igdb_id: + # return IGDBRom(igdb_id=None) + + # search_term = fs_rom_handler.get_file_name_with_no_tags(file_name) + # fallback_rom = IGDBRom(igdb_id=None) + + # # Support for PS2 OPL filename format + # match = PS2_OPL_REGEX.match(file_name) + # if platform_igdb_id == PS2_IGDB_ID and match: + # search_term = await self._ps2_opl_format(match, search_term) + # fallback_rom = IGDBRom(igdb_id=None, name=search_term) + + # # Support for sony serial filename format (PS, PS3, PS3) + # match = SONY_SERIAL_REGEX.search(file_name, re.IGNORECASE) + # if platform_igdb_id == PS1_IGDB_ID and match: + # search_term = await self._ps1_serial_format(match, search_term) + # fallback_rom = IGDBRom(igdb_id=None, name=search_term) + + # if platform_igdb_id == PS2_IGDB_ID and match: + # search_term = await self._ps2_serial_format(match, search_term) + # fallback_rom = IGDBRom(igdb_id=None, name=search_term) + + # if platform_igdb_id == PSP_IGDB_ID and match: + # search_term = await self._psp_serial_format(match, search_term) + # fallback_rom = IGDBRom(igdb_id=None, name=search_term) + + # # Support for switch titleID filename format + # match = SWITCH_TITLEDB_REGEX.search(file_name) + # if platform_igdb_id == SWITCH_IGDB_ID and match: + # search_term, index_entry = await self._switch_titledb_format( + # match, search_term + # ) + # if index_entry: + # fallback_rom = IGDBRom( + # igdb_id=None, + # name=index_entry["name"], + # summary=index_entry.get("description", ""), + # url_cover=index_entry.get("iconUrl", ""), + # url_screenshots=index_entry.get("screenshots", None) or [], + # ) + + # # Support for switch productID filename format + # match = SWITCH_PRODUCT_ID_REGEX.search(file_name) + # if platform_igdb_id == SWITCH_IGDB_ID and match: + # search_term, index_entry = await self._switch_productid_format( + # match, search_term + # ) + # if index_entry: + # fallback_rom = IGDBRom( + # igdb_id=None, + # name=index_entry["name"], + # summary=index_entry.get("description", ""), + # url_cover=index_entry.get("iconUrl", ""), + # url_screenshots=index_entry.get("screenshots", None) or [], + # ) + + # # Support for MAME arcade filename format + # if platform_igdb_id in ARCADE_IGDB_IDS: + # search_term = await self._mame_format(search_term) + # fallback_rom = IGDBRom(igdb_id=None, name=search_term) + + # search_term = self.normalize_search_term(search_term) + + # rom = await self._search_rom(search_term, platform_igdb_id, with_category=True) + # if not rom: + # rom = await self._search_rom(search_term, platform_igdb_id) + + # # Split the search term since igdb struggles with colons + # if not rom and ":" in search_term: + # for term in search_term.split(":")[::-1]: + # rom = await self._search_rom(term, platform_igdb_id) + # if rom: + # break + + # # Some MAME games have two titles split by a slash + # if not rom and "/" in search_term: + # for term in search_term.split("/"): + # rom = await self._search_rom(term.strip(), platform_igdb_id) + # if rom: + # break + + # if not rom: + # return fallback_rom + + # # Get the video ID for the game + # video_ids = await self._request( + # self.video_endpoint, + # f'fields video_id; where game={rom["id"]};', + # ) + # video_id = pydash.get(video_ids, "[0].video_id", None) + + # return IGDBRom( + # igdb_id=rom["id"], + # slug=rom["slug"], + # name=rom["name"], + # summary=rom.get("summary", ""), + # url_cover=self._normalize_cover_url( + # rom.get("cover", {}).get("url", "") + # ).replace("t_thumb", "t_1080p"), + # url_screenshots=[ + # self._normalize_cover_url(s.get("url", "")).replace( + # "t_thumb", "t_screenshot_huge" + # ) + # for s in rom.get("screenshots", []) + # ], + # igdb_metadata=extract_metadata_from_igdb_rom(rom, video_id), + # ) + + # @check_twitch_token + # async def get_rom_by_id(self, igdb_id: int) -> IGDBRom: + # if not IGDB_API_ENABLED: + # return IGDBRom(igdb_id=None) + + # roms = await self._request( + # self.games_endpoint, + # f'fields {",".join(self.games_fields)}; where id={igdb_id};', + # ) + # rom = pydash.get(roms, "[0]", None) + + # if not rom: + # return IGDBRom(igdb_id=None) + + # # Get the video ID for the game + # video_ids = await self._request( + # self.video_endpoint, + # f'fields video_id; where game={rom["id"]};', + # ) + # video_id = pydash.get(video_ids, "[0].video_id", None) + + # return IGDBRom( + # igdb_id=rom["id"], + # slug=rom["slug"], + # name=rom["name"], + # summary=rom.get("summary", ""), + # url_cover=self._normalize_cover_url( + # rom.get("cover", {}).get("url", "") + # ).replace("t_thumb", "t_1080p"), + # url_screenshots=[ + # self._normalize_cover_url(s.get("url", "")).replace( + # "t_thumb", "t_screenshot_huge" + # ) + # for s in rom.get("screenshots", []) + # ], + # igdb_metadata=extract_metadata_from_igdb_rom(rom, video_id), + # ) + + # @check_twitch_token + # async def get_matched_roms_by_id(self, igdb_id: int) -> list[IGDBRom]: + # if not IGDB_API_ENABLED: + # return [] + + # rom = await self.get_rom_by_id(igdb_id) + # return [rom] if rom["igdb_id"] else [] + + # @check_twitch_token + # async def get_matched_roms_by_name( + # self, search_term: str, platform_igdb_id: int + # ) -> list[IGDBRom]: + # if not IGDB_API_ENABLED: + # return [] + + # if not platform_igdb_id: + # return [] + + # search_term = uc(search_term) + # matched_roms = await self._request( + # self.games_endpoint, + # data=f'search "{search_term}"; fields {",".join(self.games_fields)}; where platforms=[{platform_igdb_id}];', + # ) + + # alternative_matched_roms = await self._request( + # self.search_endpoint, + # data=f'fields {",".join(self.search_fields)}; where game.platforms=[{platform_igdb_id}] & (name ~ *"{search_term}"* | alternative_name ~ *"{search_term}"*);', + # ) + + # if alternative_matched_roms: + # alternative_roms_ids = [] + # for rom in alternative_matched_roms: + # alternative_roms_ids.append( + # rom.get("game").get("id", "") + # if "game" in rom.keys() + # else rom.get("id", "") + # ) + # id_filter = " | ".join( + # list( + # map( + # lambda rom: ( + # f'id={rom.get("game").get("id", "")}' + # if "game" in rom.keys() + # else f'id={rom.get("id", "")}' + # ), + # alternative_matched_roms, + # ) + # ) + # ) + # alternative_matched_roms = await self._request( + # self.games_endpoint, + # f'fields {",".join(self.games_fields)}; where {id_filter};', + # ) + # matched_roms.extend(alternative_matched_roms) + + # # Use a dictionary to keep track of unique ids + # unique_ids: dict[str, dict[str, str]] = {} + + # # Use a list comprehension to filter duplicates based on the 'id' key + # matched_roms = [ + # unique_ids.setdefault(rom["id"], rom) + # for rom in matched_roms + # if rom["id"] not in unique_ids + # ] + + # return [ + # IGDBRom( + # { # type: ignore[misc] + # k: v + # for k, v in { + # "igdb_id": rom["id"], + # "slug": rom["slug"], + # "name": rom["name"], + # "summary": rom.get("summary", ""), + # "url_cover": self._normalize_cover_url( + # pydash.get(rom, "cover.url", "").replace( + # "t_thumb", "t_cover_big" + # ) + # ), + # "url_screenshots": [ + # self._normalize_cover_url(s.get("url", "")) # type: ignore[arg-type] + # for s in rom.get("screenshots", []) + # ], + # "igdb_metadata": extract_metadata_from_igdb_rom(rom), + # }.items() + # if v + # } + # ) + # for rom in matched_roms + # ] + + +PLATFORMS_FIELDS = ["id", "name"] + +GAMES_FIELDS = [ + "id", + "name", + "slug", + "summary", + "total_rating", + "aggregated_rating", + "first_release_date", + "artworks.url", + "cover.url", + "screenshots.url", + "platforms.id", + "platforms.name", + "alternative_names.name", + "genres.name", + "franchise.name", + "franchises.name", + "collections.name", + "game_modes.name", + "involved_companies.company.name", + "expansions.id", + "expansions.slug", + "expansions.name", + "expansions.cover.url", + "expanded_games.id", + "expanded_games.slug", + "expanded_games.name", + "expanded_games.cover.url", + "dlcs.id", + "dlcs.name", + "dlcs.slug", + "dlcs.cover.url", + "remakes.id", + "remakes.slug", + "remakes.name", + "remakes.cover.url", + "remasters.id", + "remasters.slug", + "remasters.name", + "remasters.cover.url", + "ports.id", + "ports.slug", + "ports.name", + "ports.cover.url", + "similar_games.id", + "similar_games.slug", + "similar_games.name", + "similar_games.cover.url", + "age_ratings.rating", +] + +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], +# name: a.innerText +# })) + +IGDB_PLATFORM_LIST = [ + {"slug": "visionos", "name": "visionOS"}, + {"slug": "meta-quest-3", "name": "Meta Quest 3"}, + {"slug": "atari2600", "name": "Atari 2600"}, + {"slug": "psvr2", "name": "PlayStation VR2"}, + {"slug": "switch", "name": "Nintendo Switch"}, + {"slug": "evercade", "name": "Evercade"}, + {"slug": "android", "name": "Android"}, + {"slug": "mac", "name": "Mac"}, + {"slug": "win", "name": "PC (Microsoft Windows)"}, + {"slug": "oculus-quest", "name": "Oculus Quest"}, + {"slug": "playdate", "name": "Playdate"}, + {"slug": "series-x", "name": "Xbox Series X"}, + {"slug": "meta-quest-2", "name": "Meta Quest 2"}, + {"slug": "ps5", "name": "PlayStation 5"}, + {"slug": "oculus-rift", "name": "Oculus Rift"}, + {"slug": "xboxone", "name": "Xbox One"}, + {"slug": "leaptv", "name": "LeapTV"}, + {"slug": "new-nintendo-3ds", "name": "New Nintendo 3DS"}, + {"slug": "gear-vr", "name": "Gear VR"}, + {"slug": "psvr", "name": "PlayStation VR"}, + {"slug": "3ds", "name": "Nintendo 3DS"}, + {"slug": "winphone", "name": "Windows Phone"}, + {"slug": "arduboy", "name": "Arduboy"}, + {"slug": "ps4--1", "name": "PlayStation 4"}, + {"slug": "oculus-go", "name": "Oculus Go"}, + {"slug": "psvita", "name": "PlayStation Vita"}, + {"slug": "wiiu", "name": "Wii U"}, + {"slug": "ouya", "name": "Ouya"}, + {"slug": "wii", "name": "Wii"}, + {"slug": "ps3", "name": "PlayStation 3"}, + {"slug": "psp", "name": "PlayStation Portable"}, + {"slug": "nintendo-dsi", "name": "Nintendo DSi"}, + { + "slug": "leapster-explorer-slash-leadpad-explorer", + "name": "Leapster Explorer/LeadPad Explorer", + }, + {"slug": "xbox360", "name": "Xbox 360"}, + {"slug": "nds", "name": "Nintendo DS"}, + {"slug": "ps2", "name": "PlayStation 2"}, + {"slug": "arcade", "name": "Arcade"}, + {"slug": "zeebo", "name": "Zeebo"}, + {"slug": "windows-mobile", "name": "Windows Mobile"}, + {"slug": "ios", "name": "iOS"}, + {"slug": "mobile", "name": "Legacy Mobile Device"}, + {"slug": "blu-ray-player", "name": "Blu-ray Player"}, + {"slug": "hyperscan", "name": "HyperScan"}, + {"slug": "gizmondo", "name": "Gizmondo"}, + {"slug": "gba", "name": "Game Boy Advance"}, + {"slug": "ngage", "name": "N-Gage"}, + {"slug": "vsmile", "name": "V.Smile"}, + {"slug": "n64", "name": "Nintendo 64"}, + {"slug": "leapster", "name": "Leapster"}, + {"slug": "zod", "name": "Tapwave Zodiac"}, + {"slug": "wonderswan-color", "name": "WonderSwan Color"}, + {"slug": "xbox", "name": "Xbox"}, + {"slug": "ngc", "name": "Nintendo GameCube"}, + {"slug": "wonderswan", "name": "WonderSwan"}, + {"slug": "pokemon-mini", "name": "Pokémon mini"}, + {"slug": "nuon", "name": "Nuon"}, + {"slug": "ps", "name": "PlayStation"}, + {"slug": "nintendo-64dd", "name": "Nintendo 64DD"}, + {"slug": "neo-geo-pocket-color", "name": "Neo Geo Pocket Color"}, + {"slug": "dvd-player", "name": "DVD Player"}, + {"slug": "pocketstation", "name": "PocketStation"}, + { + "slug": "visual-memory-unit-slash-visual-memory-system", + "name": "Visual Memory Unit / Visual Memory System", + }, + {"slug": "blackberry", "name": "BlackBerry OS"}, + {"slug": "dc", "name": "Dreamcast"}, + {"slug": "gbc", "name": "Game Boy Color"}, + {"slug": "gb", "name": "Game Boy"}, + {"slug": "neo-geo-pocket", "name": "Neo Geo Pocket"}, + {"slug": "snes", "name": "Super Nintendo Entertainment System"}, + {"slug": "genesis-slash-megadrive", "name": "Sega Mega Drive/Genesis"}, + {"slug": "sfam", "name": "Super Famicom"}, + {"slug": "game-dot-com", "name": "Game.com"}, + {"slug": "hyper-neo-geo-64", "name": "Hyper Neo Geo 64"}, + {"slug": "satellaview", "name": "Satellaview"}, + {"slug": "palm-os", "name": "Palm OS"}, + {"slug": "apple-pippin", "name": "Apple Pippin"}, + {"slug": "sega32", "name": "Sega 32X"}, + {"slug": "neo-geo-cd", "name": "Neo Geo CD"}, + {"slug": "virtualboy", "name": "Virtual Boy"}, + {"slug": "atari-jaguar-cd", "name": "Atari Jaguar CD"}, + {"slug": "saturn", "name": "Sega Saturn"}, + {"slug": "casio-loopy", "name": "Casio Loopy"}, + {"slug": "sega-pico", "name": "Sega Pico"}, + {"slug": "r-zone", "name": "R-Zone"}, + {"slug": "sms", "name": "Sega Master System/Mark III"}, + {"slug": "playdia", "name": "Playdia"}, + {"slug": "pc-fx", "name": "PC-FX"}, + {"slug": "3do", "name": "3DO Interactive Multiplayer"}, + { + "slug": "terebikko-slash-see-n-say-video-phone", + "name": "Terebikko / See 'n Say Video Phone", + }, + {"slug": "jaguar", "name": "Atari Jaguar"}, + {"slug": "segacd", "name": "Sega CD"}, + {"slug": "nes", "name": "Nintendo Entertainment System"}, + {"slug": "amiga-cd32", "name": "Amiga CD32"}, + {"slug": "famicom", "name": "Family Computer"}, + {"slug": "mega-duck-slash-cougar-boy", "name": "Mega Duck/Cougar Boy"}, + {"slug": "amiga", "name": "Amiga"}, + { + "slug": "watara-slash-quickshot-supervision", + "name": "Watara/QuickShot Supervision", + }, + {"slug": "philips-cd-i", "name": "Philips CD-i"}, + {"slug": "gamegear", "name": "Sega Game Gear"}, + {"slug": "neogeoaes", "name": "Neo Geo AES"}, + {"slug": "linux", "name": "Linux"}, + {"slug": "turbografx-16-slash-pc-engine-cd", "name": "Turbografx-16/PC Engine CD"}, + {"slug": "neogeomvs", "name": "Neo Geo MVS"}, + {"slug": "commodore-cdtv", "name": "Commodore CDTV"}, + {"slug": "lynx", "name": "Atari Lynx"}, + {"slug": "gamate", "name": "Gamate"}, + {"slug": "bbcmicro", "name": "BBC Microcomputer System"}, + {"slug": "turbografx16--1", "name": "TurboGrafx-16/PC Engine"}, + {"slug": "supergrafx", "name": "PC Engine SuperGrafx"}, + {"slug": "fm-towns", "name": "FM Towns"}, + {"slug": "pc-9800-series", "name": "PC-9800 Series"}, + {"slug": "apple-iigs", "name": "Apple IIGS"}, + {"slug": "x1", "name": "Sharp X1"}, + {"slug": "sharp-x68000", "name": "Sharp X68000"}, + {"slug": "acorn-archimedes", "name": "Acorn Archimedes"}, + {"slug": "c64", "name": "Commodore C64/128/MAX"}, + {"slug": "fds", "name": "Family Computer Disk System"}, + {"slug": "dragon-32-slash-64", "name": "Dragon 32/64"}, + {"slug": "acorn-electron", "name": "Acorn Electron"}, + {"slug": "acpc", "name": "Amstrad CPC"}, + {"slug": "atari-st", "name": "Atari ST/STE"}, + {"slug": "tatung-einstein", "name": "Tatung Einstein"}, + {"slug": "amstrad-pcw", "name": "Amstrad PCW"}, + {"slug": "epoch-super-cassette-vision", "name": "Epoch Super Cassette Vision"}, + {"slug": "atari7800", "name": "Atari 7800"}, + {"slug": "hp3000", "name": "HP 3000"}, + {"slug": "atari5200", "name": "Atari 5200"}, + {"slug": "c16", "name": "Commodore 16"}, + {"slug": "sinclair-ql", "name": "Sinclair QL"}, + {"slug": "thomson-mo5", "name": "Thomson MO5"}, + {"slug": "c-plus-4", "name": "Commodore Plus/4"}, + {"slug": "sg1000", "name": "SG-1000"}, + {"slug": "vectrex", "name": "Vectrex"}, + {"slug": "sharp-mz-2200", "name": "Sharp MZ-2200"}, + {"slug": "nec-pc-6000-series", "name": "NEC PC-6000 Series"}, + {"slug": "msx2", "name": "MSX2"}, + {"slug": "msx", "name": "MSX"}, + {"slug": "colecovision", "name": "ColecoVision"}, + {"slug": "intellivision", "name": "Intellivision"}, + {"slug": "vic-20", "name": "Commodore VIC-20"}, + {"slug": "zxs", "name": "ZX Spectrum"}, + {"slug": "arcadia-2001", "name": "Arcadia 2001"}, + {"slug": "fm-7", "name": "FM-7"}, + {"slug": "trs-80", "name": "TRS-80"}, + {"slug": "epoch-cassette-vision", "name": "Epoch Cassette Vision"}, + {"slug": "dos", "name": "DOS"}, + {"slug": "ti-99", "name": "Texas Instruments TI-99"}, + {"slug": "sinclair-zx81", "name": "Sinclair ZX81"}, + {"slug": "pc-8800-series", "name": "PC-8800 Series"}, + {"slug": "microvision--1", "name": "Microvision"}, + {"slug": "g-and-w", "name": "Game & Watch"}, + {"slug": "atari8bit", "name": "Atari 8-bit"}, + {"slug": "trs-80-color-computer", "name": "TRS-80 Color Computer"}, + { + "slug": "1292-advanced-programmable-video-system", + "name": "1292 Advanced Programmable Video System", + }, + {"slug": "odyssey-2-slash-videopac-g7000", "name": "Odyssey 2 / Videopac G7000"}, + {"slug": "exidy-sorcerer", "name": "Exidy Sorcerer"}, + {"slug": "pc-50x-family", "name": "PC-50X Family"}, + {"slug": "vc-4000", "name": "VC 4000"}, + {"slug": "appleii", "name": "Apple II"}, + {"slug": "astrocade", "name": "Bally Astrocade"}, + {"slug": "ay-3-8500", "name": "AY-3-8500"}, + {"slug": "cpet", "name": "Commodore PET"}, + {"slug": "fairchild-channel-f", "name": "Fairchild Channel F"}, + {"slug": "ay-3-8610", "name": "AY-3-8610"}, + {"slug": "ay-3-8605", "name": "AY-3-8605"}, + {"slug": "ay-3-8603", "name": "AY-3-8603"}, + {"slug": "ay-3-8710", "name": "AY-3-8710"}, + {"slug": "ay-3-8760", "name": "AY-3-8760"}, + {"slug": "ay-3-8606", "name": "AY-3-8606"}, + {"slug": "ay-3-8607", "name": "AY-3-8607"}, + {"slug": "sol-20", "name": "Sol-20"}, + {"slug": "odyssey--1", "name": "Odyssey"}, + {"slug": "plato--1", "name": "PLATO"}, + {"slug": "cdccyber70", "name": "CDC Cyber 70"}, + {"slug": "sdssigma7", "name": "SDS Sigma 7"}, + {"slug": "pdp11", "name": "PDP-11"}, + {"slug": "hp2100", "name": "HP 2100"}, + {"slug": "pdp10", "name": "PDP-10"}, + { + "slug": "call-a-computer", + "name": "Call-A-Computer time-shared mainframe computer system", + }, + {"slug": "pdp-8--1", "name": "PDP-8"}, + {"slug": "nintendo-playstation", "name": "Nintendo PlayStation"}, + {"slug": "pdp1", "name": "PDP-1"}, + {"slug": "donner30", "name": "Donner Model 30"}, + {"slug": "edsac--1", "name": "EDSAC"}, + {"slug": "nimrod", "name": "Ferranti Nimrod Computer"}, + {"slug": "swancrystal", "name": "SwanCrystal"}, + {"slug": "panasonic-jungle", "name": "Panasonic Jungle"}, + {"slug": "handheld-electronic-lcd", "name": "Handheld Electronic LCD"}, + {"slug": "intellivision-amico", "name": "Intellivision Amico"}, + {"slug": "legacy-computer", "name": "Legacy Computer"}, + {"slug": "panasonic-m2", "name": "Panasonic M2"}, + {"slug": "browser", "name": "Web browser"}, + {"slug": "ooparts", "name": "OOParts"}, + {"slug": "stadia", "name": "Google Stadia"}, + {"slug": "plug-and-play", "name": "Plug & Play"}, + {"slug": "amazon-fire-tv", "name": "Amazon Fire TV"}, + {"slug": "onlive-game-system", "name": "OnLive Game System"}, + {"slug": "vc", "name": "Virtual Console"}, + {"slug": "airconsole", "name": "AirConsole"}, +] + +IGDB_AGE_RATINGS: dict[int, IGDBAgeRating] = { + 1: { + "rating": "Three", + "category": "PEGI", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_3.png", + }, + 2: { + "rating": "Seven", + "category": "PEGI", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_7.png", + }, + 3: { + "rating": "Twelve", + "category": "PEGI", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_12.png", + }, + 4: { + "rating": "Sixteen", + "category": "PEGI", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_16.png", + }, + 5: { + "rating": "Eighteen", + "category": "PEGI", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_18.png", + }, + 6: { + "rating": "RP", + "category": "ESRB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_rp.png", + }, + 7: { + "rating": "EC", + "category": "ESRB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_ec.png", + }, + 8: { + "rating": "E", + "category": "ESRB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_e.png", + }, + 9: { + "rating": "E10", + "category": "ESRB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_e10.png", + }, + 10: { + "rating": "T", + "category": "ESRB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_t.png", + }, + 11: { + "rating": "M", + "category": "ESRB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_m.png", + }, + 12: { + "rating": "AO", + "category": "ESRB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_ao.png", + }, + 13: { + "rating": "CERO_A", + "category": "CERO", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_a.png", + }, + 14: { + "rating": "CERO_B", + "category": "CERO", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_b.png", + }, + 15: { + "rating": "CERO_C", + "category": "CERO", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_c.png", + }, + 16: { + "rating": "CERO_D", + "category": "CERO", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_d.png", + }, + 17: { + "rating": "CERO_Z", + "category": "CERO", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_z.png", + }, + 18: { + "rating": "USK_0", + "category": "USK", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_0.png", + }, + 19: { + "rating": "USK_6", + "category": "USK", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_6.png", + }, + 20: { + "rating": "USK_12", + "category": "USK", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_12.png", + }, + 21: { + "rating": "USK_16", + "category": "USK", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_16.png", + }, + 22: { + "rating": "USK_18", + "category": "USK", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_18.png", + }, + 23: { + "rating": "GRAC_ALL", + "category": "GRAC", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_all.png", + }, + 24: { + "rating": "GRAC_Twelve", + "category": "GRAC", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_twelve.png", + }, + 25: { + "rating": "GRAC_Fifteen", + "category": "GRAC", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_fifteen.png", + }, + 26: { + "rating": "GRAC_Eighteen", + "category": "GRAC", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_eighteen.png", + }, + 27: { + "rating": "GRAC_TESTING", + "category": "GRAC", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_testing.png", + }, + 28: { + "rating": "CLASS_IND_L", + "category": "CLASS_IND", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/classind/classind_l.png", + }, + 29: { + "rating": "CLASS_IND_Ten", + "category": "CLASS_IND", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/classind/classind_ten.png", + }, + 30: { + "rating": "CLASS_IND_Twelve", + "category": "CLASS_IND", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/classind/classind_twelve.png", + }, + 31: { + "rating": "ACB_G", + "category": "ACB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_g.png", + }, + 32: { + "rating": "ACB_PG", + "category": "ACB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_pg.png", + }, + 33: { + "rating": "ACB_M", + "category": "ACB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_m.png", + }, + 34: { + "rating": "ACB_MA15", + "category": "ACB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_ma15.png", + }, + 35: { + "rating": "ACB_R18", + "category": "ACB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_r18.png", + }, + 36: { + "rating": "ACB_RC", + "category": "ACB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_rc.png", + }, +} From 7dfee48eda558ed89644b27d21fa6919e239f93f Mon Sep 17 00:00:00 2001 From: zurdi Date: Fri, 25 Oct 2024 15:22:37 +0000 Subject: [PATCH 02/52] added ss dev token --- .vscode/tasks.json | 2 +- backend/config/__init__.py | 3 +- backend/handler/metadata/ss_handler.py | 138 ++++++++++++------------- docker-compose.yml | 6 +- env.template | 8 +- 5 files changed, 77 insertions(+), 80 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 898a83d09..4b0005420 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -35,7 +35,7 @@ { "label": "Setup testing environment", "type": "shell", - "command": "export $(cat .env | grep DB_ROOT_PASSWD | xargs) && docker exec -i mariadb mariadb -u root -p$DB_ROOT_PASSWD < backend/romm_test/setup.sql", + "command": "export $(cat .env | grep DB_ROOT_PASSWD | xargs) && docker exec -i romm-db-dev mariadb -u root -p$DB_ROOT_PASSWD < backend/romm_test/setup.sql", "problemMatcher": [] }, { diff --git a/backend/config/__init__.py b/backend/config/__init__.py index a140edd65..f21bb6cda 100644 --- a/backend/config/__init__.py +++ b/backend/config/__init__.py @@ -57,7 +57,8 @@ def str_to_bool(value: str) -> bool: ) # SCREENSCRAPER -SCREENSCRAPER_USER_ID: Final = os.environ.get("SCREENSCRAPER_USER_ID", "") +SCREENSCRAPER_USER: Final = os.environ.get("SCREENSCRAPER_USER", "") +SCREENSCRAPER_PASSWORD: Final = os.environ.get("SCREENSCRAPER_PASSWORD", "") SCREENSCRAPER_API_KEY: Final = os.environ.get("SCREENSCRAPER_API_KEY", "") # STEAMGRIDDB diff --git a/backend/handler/metadata/ss_handler.py b/backend/handler/metadata/ss_handler.py index 4c38abb73..0a1514e7b 100644 --- a/backend/handler/metadata/ss_handler.py +++ b/backend/handler/metadata/ss_handler.py @@ -5,7 +5,7 @@ import httpx import pydash -from config import SCREENSCRAPER_API_KEY, SCREENSCRAPER_USER_ID +from config import SCREENSCRAPER_API_KEY, SCREENSCRAPER_PASSWORD, SCREENSCRAPER_USER from fastapi import HTTPException, status from handler.redis_handler import sync_cache from logger.logger import log @@ -15,16 +15,7 @@ from .base_hander import MetadataHandler # Used to display the IGDB API status in the frontend -SS_API_ENABLED: Final = bool(SCREENSCRAPER_USER_ID) and bool(SCREENSCRAPER_API_KEY) - -MAIN_GAME_CATEGORY: Final = 0 -EXPANDED_GAME_CATEGORY: Final = 10 -N_SCREENSHOTS: Final = 5 -PS1_IGDB_ID: Final = 7 -PS2_IGDB_ID: Final = 8 -PSP_IGDB_ID: Final = 38 -SWITCH_IGDB_ID: Final = 130 -ARCADE_IGDB_IDS: Final = [52, 79, 80] +SS_API_ENABLED: Final = bool(SCREENSCRAPER_USER) and bool(SCREENSCRAPER_PASSWORD) class IGDBPlatform(TypedDict): @@ -194,36 +185,37 @@ def __init__(self) -> None: # self.games_endpoint = f"{self.BASE_URL}/games" # self.games_fields = GAMES_FIELDS self.search_endpoint = f"{self.BASE_URL}/jeuRecherche.php" - self.search_params = SEARCH_FIELDS # self.video_endpoint = f"{self.BASE_URL}/game_videos" - # self.pagination_limit = 200 - # self.twitch_auth = TwitchAuth() - # self.headers = { - # "Client-ID": IGDB_CLIENT_ID, - # "Accept": "application/json", - # } self.auth_params = { - "devid": SCREENSCRAPER_USER_ID, + "ssid": SCREENSCRAPER_USER, + "sspassword": SCREENSCRAPER_PASSWORD, + "devid": SCREENSCRAPER_USER, "devpassword": SCREENSCRAPER_API_KEY, } self.output_param = {"output": "json"} + self.LOGIN_ERROR_CHECK: Final = "Erreur de login" async def _request(self, url: str, search_term: str, timeout: int = 120) -> list: httpx_client = ctx_httpx_client.get() try: - params = { - **self.auth_params, - **self.output_param, - "recherche": search_term, - # "systemeid": "1", - } res = await httpx_client.get( url, - params=params, + params={ + **self.auth_params, + **self.output_param, + "recherche": search_term, + }, + headers={}, timeout=timeout, ) res.raise_for_status() + if self.LOGIN_ERROR_CHECK in res.text: + log.error("Invalid screenscraper credentials") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid screenscraper credentials", + ) matches: list[dict] = [] for rom in res.json().get("response", []).get("jeux", []): for name in rom.get("noms", []): @@ -244,55 +236,55 @@ async def _request(self, url: str, search_term: str, timeout: int = 120) -> list async def search_rom(self, search_term) -> None: await self._request(self.search_endpoint, search_term) - # async def _search_rom( - # self, search_term: str, platform_igdb_id: int, with_category: bool = False - # ) -> dict | None: - # if not platform_igdb_id: - # return None - - # search_term = uc(search_term) - # category_filter: str = ( - # f"& (category={MAIN_GAME_CATEGORY} | category={EXPANDED_GAME_CATEGORY})" - # if with_category - # else "" - # ) - - # def is_exact_match(rom: dict, search_term: str) -> bool: - # return ( - # rom["name"].lower() == search_term.lower() - # or rom["slug"].lower() == search_term.lower() - # or ( - # self._normalize_exact_match(rom["name"]) - # == self._normalize_exact_match(search_term) - # ) - # ) - - # roms = await self._request( - # self.games_endpoint, - # data=f'search "{search_term}"; fields {",".join(self.games_fields)}; where platforms=[{platform_igdb_id}] {category_filter};', - # ) - # for rom in roms: - # # Return early if an exact match is found. - # if is_exact_match(rom, search_term): - # return rom + async def _search_rom( + self, search_term: str, platform_igdb_id: int, with_category: bool = False + ) -> dict | None: + if not platform_igdb_id: + return None + + search_term = uc(search_term) + category_filter: str = ( + f"& (category={MAIN_GAME_CATEGORY} | category={EXPANDED_GAME_CATEGORY})" + if with_category + else "" + ) + + def is_exact_match(rom: dict, search_term: str) -> bool: + return ( + rom["name"].lower() == search_term.lower() + or rom["slug"].lower() == search_term.lower() + or ( + self._normalize_exact_match(rom["name"]) + == self._normalize_exact_match(search_term) + ) + ) - # roms_expanded = await self._request( - # self.search_endpoint, - # data=f'fields {",".join(self.search_fields)}; where game.platforms=[{platform_igdb_id}] & (name ~ *"{search_term}"* | alternative_name ~ *"{search_term}"*);', - # ) - # if roms_expanded: - # extra_roms = await self._request( - # self.games_endpoint, - # f'fields {",".join(self.games_fields)}; where id={roms_expanded[0]["game"]["id"]};', - # ) - # for rom in extra_roms: - # # Return early if an exact match is found. - # if is_exact_match(rom, search_term): - # return rom + roms = await self._request( + self.games_endpoint, + data=f'search "{search_term}"; fields {",".join(self.games_fields)}; where platforms=[{platform_igdb_id}] {category_filter};', + ) + for rom in roms: + # Return early if an exact match is found. + if is_exact_match(rom, search_term): + return rom + + roms_expanded = await self._request( + self.search_endpoint, + data=f'fields {",".join(self.search_fields)}; where game.platforms=[{platform_igdb_id}] & (name ~ *"{search_term}"* | alternative_name ~ *"{search_term}"*);', + ) + if roms_expanded: + extra_roms = await self._request( + self.games_endpoint, + f'fields {",".join(self.games_fields)}; where id={roms_expanded[0]["game"]["id"]};', + ) + for rom in extra_roms: + # Return early if an exact match is found. + if is_exact_match(rom, search_term): + return rom - # roms.extend(extra_roms) + roms.extend(extra_roms) - # return roms[0] if roms else None + return roms[0] if roms else None # @check_twitch_token # async def get_platform(self, slug: str) -> IGDBPlatform: @@ -625,8 +617,6 @@ async def search_rom(self, search_term) -> None: "age_ratings.rating", ] -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/docker-compose.yml b/docker-compose.yml index d5d3987a6..47dbc8802 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,8 @@ services: mariadb: - image: mariadb:latest - container_name: mariadb + image: mariadb:11.3.2 + container_name: romm-db-dev restart: unless-stopped environment: - MARIADB_ROOT_PASSWORD=$DB_ROOT_PASSWD @@ -15,7 +15,7 @@ services: valkey: image: valkey/valkey:8 - container_name: valkey + container_name: romm-valkey-dev restart: unless-stopped ports: - $REDIS_PORT:6379 diff --git a/env.template b/env.template index 138da2fcf..89451615d 100644 --- a/env.template +++ b/env.template @@ -2,12 +2,18 @@ ROMM_BASE_PATH=/path/to/romm_mock DEV_MODE=true # Gunicorn (optional) -GUNICORN_WORKERS=4 # (2 × CPU cores) + 1 +# Workers -> (2 × CPU cores) + 1 +GUNICORN_WORKERS=4 # IGDB credentials IGDB_CLIENT_ID= IGDB_CLIENT_SECRET= +# Screenscraper +SCREENSCRAPER_USER= +SCREENSCRAPER_PASSWORD= +SCREENSCRAPER_API_KEY= + # Mobygames MOBYGAMES_API_KEY= From db4001dea0ab8fa251718dbb6f3459f95833b219 Mon Sep 17 00:00:00 2001 From: zurdi Date: Thu, 2 Jan 2025 03:38:01 +0000 Subject: [PATCH 03/52] feat: base structure for ss support --- backend/endpoints/responses/search.py | 2 + backend/endpoints/search.py | 22 +- backend/handler/metadata/base_hander.py | 4 + backend/handler/metadata/igdb_handler.py | 410 ++++---- backend/handler/metadata/ss_handler.py | 881 ++---------------- frontend/assets/scrappers/ss.gif | Bin 0 -> 26119 bytes frontend/assets/scrappers/ss.ico | Bin 0 -> 15086 bytes .../__generated__/models/SearchRomSchema.ts | 2 + frontend/src/components/Settings/Footer.vue | 6 +- .../common/Game/Dialog/MatchRom.vue | 42 +- frontend/src/styles/themes.ts | 16 +- 11 files changed, 353 insertions(+), 1032 deletions(-) create mode 100644 frontend/assets/scrappers/ss.gif create mode 100644 frontend/assets/scrappers/ss.ico diff --git a/backend/endpoints/responses/search.py b/backend/endpoints/responses/search.py index 7f1de633d..2a00e473f 100644 --- a/backend/endpoints/responses/search.py +++ b/backend/endpoints/responses/search.py @@ -4,11 +4,13 @@ class SearchRomSchema(BaseModel): igdb_id: int | None = None moby_id: int | None = None + ss_id: int | None = None slug: str name: str summary: str igdb_url_cover: str = "" moby_url_cover: str = "" + ss_url_cover: str = "" platform_id: int diff --git a/backend/endpoints/search.py b/backend/endpoints/search.py index 4cbfcad68..ee85f3a6d 100644 --- a/backend/endpoints/search.py +++ b/backend/endpoints/search.py @@ -85,12 +85,17 @@ async def search_rom( moby_matched_roms = await meta_moby_handler.get_matched_roms_by_name( search_term, rom.platform.moby_id ) - await meta_ss_handler.search_rom(search_term) + ss_matched_roms = await meta_ss_handler.search_rom(search_term, rom.platform) + + merged_dict: dict[str, dict] = {} + + for item in igdb_matched_roms: + merged_dict[item["name"]] = { + **item, + "igdb_url_cover": item.pop("url_cover", ""), + **merged_dict.get(item.get("name", ""), {}), + } - merged_dict = { - item["name"]: {**item, "igdb_url_cover": item.pop("url_cover", "")} - for item in igdb_matched_roms - } for item in moby_matched_roms: merged_dict[item["name"]] = { **item, @@ -98,6 +103,13 @@ async def search_rom( **merged_dict.get(item.get("name", ""), {}), } + for item in ss_matched_roms: + merged_dict[item["name"]] = { + **item, + "ss_url_cover": item.pop("url_cover", ""), + **merged_dict.get(item.get("name", ""), {}), + } + matched_roms = [ { **{ diff --git a/backend/handler/metadata/base_hander.py b/backend/handler/metadata/base_hander.py index 1dbc48e48..c3f2cebef 100644 --- a/backend/handler/metadata/base_hander.py +++ b/backend/handler/metadata/base_hander.py @@ -216,6 +216,10 @@ def _mask_sensitive_values(self, values: dict[str, str]) -> dict[str, str]: "client_id", "client_secret", "api_key", + "ssid", + "sspassword", + "devid", + "devpassword", } # Leave other keys unchanged else values[key] diff --git a/backend/handler/metadata/igdb_handler.py b/backend/handler/metadata/igdb_handler.py index 814e91e14..ba9b8cf4a 100644 --- a/backend/handler/metadata/igdb_handler.py +++ b/backend/handler/metadata/igdb_handler.py @@ -793,222 +793,228 @@ async def get_oauth_token(self) -> str: # })) IGDB_PLATFORM_LIST = ( - {"slug": "visionos", "name": "visionOS"}, - {"slug": "meta-quest-3", "name": "Meta Quest 3"}, - {"slug": "atari2600", "name": "Atari 2600"}, - {"slug": "psvr2", "name": "PlayStation VR2"}, - {"slug": "switch", "name": "Nintendo Switch"}, - {"slug": "evercade", "name": "Evercade"}, - {"slug": "android", "name": "Android"}, - {"slug": "mac", "name": "Mac"}, - {"slug": "win", "name": "PC (Microsoft Windows)"}, - {"slug": "oculus-quest", "name": "Oculus Quest"}, - {"slug": "playdate", "name": "Playdate"}, - {"slug": "series-x", "name": "Xbox Series X"}, - {"slug": "meta-quest-2", "name": "Meta Quest 2"}, - {"slug": "ps5", "name": "PlayStation 5"}, - {"slug": "oculus-rift", "name": "Oculus Rift"}, - {"slug": "xboxone", "name": "Xbox One"}, - {"slug": "leaptv", "name": "LeapTV"}, - {"slug": "new-nintendo-3ds", "name": "New Nintendo 3DS"}, - {"slug": "gear-vr", "name": "Gear VR"}, - {"slug": "psvr", "name": "PlayStation VR"}, - {"slug": "3ds", "name": "Nintendo 3DS"}, - {"slug": "winphone", "name": "Windows Phone"}, - {"slug": "arduboy", "name": "Arduboy"}, - {"slug": "ps4--1", "name": "PlayStation 4"}, - {"slug": "oculus-go", "name": "Oculus Go"}, - {"slug": "psvita", "name": "PlayStation Vita"}, - {"slug": "wiiu", "name": "Wii U"}, - {"slug": "ouya", "name": "Ouya"}, - {"slug": "wii", "name": "Wii"}, - {"slug": "ps3", "name": "PlayStation 3"}, - {"slug": "psp", "name": "PlayStation Portable"}, - {"slug": "nintendo-dsi", "name": "Nintendo DSi"}, { - "slug": "leapster-explorer-slash-leadpad-explorer", + "name": "1292 Advanced Programmable Video System", + "slug": "1292-advanced-programmable-video-system", + }, + {"name": "3DO Interactive Multiplayer", "slug": "3do"}, + {"name": "Nintendo 3DS", "slug": "3ds"}, + {"name": "Acorn Archimedes", "slug": "acorn-archimedes"}, + {"name": "Acorn Electron", "slug": "acorn-electron"}, + {"name": "Amstrad CPC", "slug": "acpc"}, + {"name": "AirConsole", "slug": "airconsole"}, + {"name": "Amazon Fire TV", "slug": "amazon-fire-tv"}, + {"name": "Amiga", "slug": "amiga"}, + {"name": "Amiga CD32", "slug": "amiga-cd32"}, + {"name": "Amstrad PCW", "slug": "amstrad-pcw"}, + {"name": "Android", "slug": "android"}, + {"name": "Apple IIGS", "slug": "apple-iigs"}, + {"name": "Apple Pippin", "slug": "apple-pippin"}, + {"name": "Apple II", "slug": "appleii"}, + {"name": "Arcade", "slug": "arcade"}, + {"name": "Arcadia 2001", "slug": "arcadia-2001"}, + {"name": "Arduboy", "slug": "arduboy"}, + {"name": "Bally Astrocade", "slug": "astrocade"}, + {"name": "Atari Jaguar CD", "slug": "atari-jaguar-cd"}, + {"name": "Atari ST/STE", "slug": "atari-st"}, + {"name": "Atari 2600", "slug": "atari2600"}, + {"name": "Atari 5200", "slug": "atari5200"}, + {"name": "Atari 7800", "slug": "atari7800"}, + {"name": "Atari 8-bit", "slug": "atari8bit"}, + {"name": "AY-3-8500", "slug": "ay-3-8500"}, + {"name": "AY-3-8603", "slug": "ay-3-8603"}, + {"name": "AY-3-8605", "slug": "ay-3-8605"}, + {"name": "AY-3-8606", "slug": "ay-3-8606"}, + {"name": "AY-3-8607", "slug": "ay-3-8607"}, + {"name": "AY-3-8610", "slug": "ay-3-8610"}, + {"name": "AY-3-8710", "slug": "ay-3-8710"}, + {"name": "AY-3-8760", "slug": "ay-3-8760"}, + {"name": "BBC Microcomputer System", "slug": "bbcmicro"}, + {"name": "BlackBerry OS", "slug": "blackberry"}, + {"name": "Blu-ray Player", "slug": "blu-ray-player"}, + {"name": "Web browser", "slug": "browser"}, + {"name": "Commodore Plus/4", "slug": "c-plus-4"}, + {"name": "Commodore 16", "slug": "c16"}, + {"name": "Commodore C64/128/MAX", "slug": "c64"}, + { + "name": "Call-A-Computer time-shared mainframe computer system", + "slug": "call-a-computer", + }, + {"name": "Casio Loopy", "slug": "casio-loopy"}, + {"name": "CDC Cyber 70", "slug": "cdccyber70"}, + {"name": "ColecoVision", "slug": "colecovision"}, + {"name": "Commodore CDTV", "slug": "commodore-cdtv"}, + {"name": "Commodore PET", "slug": "cpet"}, + {"name": "Dreamcast", "slug": "dc"}, + {"name": "Donner Model 30", "slug": "donner30"}, + {"name": "DOS", "slug": "dos"}, + {"name": "Dragon 32/64", "slug": "dragon-32-slash-64"}, + {"name": "DVD Player", "slug": "dvd-player"}, + {"name": "EDSAC", "slug": "edsac--1"}, + {"name": "Epoch Cassette Vision", "slug": "epoch-cassette-vision"}, + {"name": "Epoch Super Cassette Vision", "slug": "epoch-super-cassette-vision"}, + {"name": "Evercade", "slug": "evercade"}, + {"name": "Exidy Sorcerer", "slug": "exidy-sorcerer"}, + {"name": "Fairchild Channel F", "slug": "fairchild-channel-f"}, + {"name": "Family Computer", "slug": "famicom"}, + {"name": "Family Computer Disk System", "slug": "fds"}, + {"name": "FM-7", "slug": "fm-7"}, + {"name": "FM Towns", "slug": "fm-towns"}, + {"name": "Game & Watch", "slug": "g-and-w"}, + {"name": "Gamate", "slug": "gamate"}, + {"name": "Game.com", "slug": "game-dot-com"}, + {"name": "Sega Game Gear", "slug": "gamegear"}, + {"name": "Game Boy", "slug": "gb"}, + {"name": "Game Boy Advance", "slug": "gba"}, + {"name": "Game Boy Color", "slug": "gbc"}, + {"name": "Gear VR", "slug": "gear-vr"}, + {"name": "Sega Mega Drive/Genesis", "slug": "genesis-slash-megadrive"}, + {"name": "Gizmondo", "slug": "gizmondo"}, + {"name": "Handheld Electronic LCD", "slug": "handheld-electronic-lcd"}, + {"name": "HP 2100", "slug": "hp2100"}, + {"name": "HP 3000", "slug": "hp3000"}, + {"name": "Hyper Neo Geo 64", "slug": "hyper-neo-geo-64"}, + {"name": "HyperScan", "slug": "hyperscan"}, + {"name": "Intellivision", "slug": "intellivision"}, + {"name": "Intellivision Amico", "slug": "intellivision-amico"}, + {"name": "iOS", "slug": "ios"}, + {"name": "Atari Jaguar", "slug": "jaguar"}, + {"name": "Leapster", "slug": "leapster"}, + { "name": "Leapster Explorer/LeadPad Explorer", + "slug": "leapster-explorer-slash-leadpad-explorer", }, - {"slug": "xbox360", "name": "Xbox 360"}, - {"slug": "nds", "name": "Nintendo DS"}, - {"slug": "ps2", "name": "PlayStation 2"}, - {"slug": "arcade", "name": "Arcade"}, - {"slug": "zeebo", "name": "Zeebo"}, - {"slug": "windows-mobile", "name": "Windows Mobile"}, - {"slug": "ios", "name": "iOS"}, - {"slug": "mobile", "name": "Legacy Mobile Device"}, - {"slug": "blu-ray-player", "name": "Blu-ray Player"}, - {"slug": "hyperscan", "name": "HyperScan"}, - {"slug": "gizmondo", "name": "Gizmondo"}, - {"slug": "gba", "name": "Game Boy Advance"}, - {"slug": "ngage", "name": "N-Gage"}, - {"slug": "vsmile", "name": "V.Smile"}, - {"slug": "n64", "name": "Nintendo 64"}, - {"slug": "leapster", "name": "Leapster"}, - {"slug": "zod", "name": "Tapwave Zodiac"}, - {"slug": "wonderswan-color", "name": "WonderSwan Color"}, - {"slug": "xbox", "name": "Xbox"}, - {"slug": "ngc", "name": "Nintendo GameCube"}, - {"slug": "wonderswan", "name": "WonderSwan"}, - {"slug": "pokemon-mini", "name": "Pokémon mini"}, - {"slug": "nuon", "name": "Nuon"}, - {"slug": "ps", "name": "PlayStation"}, - {"slug": "nintendo-64dd", "name": "Nintendo 64DD"}, - {"slug": "neo-geo-pocket-color", "name": "Neo Geo Pocket Color"}, - {"slug": "dvd-player", "name": "DVD Player"}, - {"slug": "pocketstation", "name": "PocketStation"}, + {"name": "LeapTV", "slug": "leaptv"}, + {"name": "Legacy Computer", "slug": "legacy-computer"}, + {"name": "Linux", "slug": "linux"}, + {"name": "Atari Lynx", "slug": "lynx"}, + {"name": "Mac", "slug": "mac"}, + {"name": "Mega Duck/Cougar Boy", "slug": "mega-duck-slash-cougar-boy"}, + {"name": "Meta Quest 2", "slug": "meta-quest-2"}, + {"name": "Meta Quest 3", "slug": "meta-quest-3"}, + {"name": "Microvision", "slug": "microvision--1"}, + {"name": "Legacy Mobile Device", "slug": "mobile"}, + {"name": "MSX", "slug": "msx"}, + {"name": "MSX2", "slug": "msx2"}, + {"name": "Nintendo 64", "slug": "n64"}, + {"name": "Nintendo DS", "slug": "nds"}, + {"name": "NEC PC-6000 Series", "slug": "nec-pc-6000-series"}, + {"name": "Neo Geo CD", "slug": "neo-geo-cd"}, + {"name": "Neo Geo Pocket", "slug": "neo-geo-pocket"}, + {"name": "Neo Geo Pocket Color", "slug": "neo-geo-pocket-color"}, + {"name": "Neo Geo AES", "slug": "neogeoaes"}, + {"name": "Neo Geo MVS", "slug": "neogeomvs"}, + {"name": "Nintendo Entertainment System", "slug": "nes"}, + {"name": "New Nintendo 3DS", "slug": "new-nintendo-3ds"}, + {"name": "N-Gage", "slug": "ngage"}, + {"name": "Nintendo GameCube", "slug": "ngc"}, + {"name": "Ferranti Nimrod Computer", "slug": "nimrod"}, + {"name": "Nintendo 64DD", "slug": "nintendo-64dd"}, + {"name": "Nintendo DSi", "slug": "nintendo-dsi"}, + {"name": "Nintendo PlayStation", "slug": "nintendo-playstation"}, + {"name": "Nuon", "slug": "nuon"}, + {"name": "Oculus Go", "slug": "oculus-go"}, + {"name": "Oculus Quest", "slug": "oculus-quest"}, + {"name": "Oculus Rift", "slug": "oculus-rift"}, + {"name": "Odyssey", "slug": "odyssey--1"}, { - "slug": "visual-memory-unit-slash-visual-memory-system", - "name": "Visual Memory Unit / Visual Memory System", + "name": "Odyssey 2 / Videopac G7000", + "slug": "odyssey-2-slash-videopac-g7000", }, - {"slug": "blackberry", "name": "BlackBerry OS"}, - {"slug": "dc", "name": "Dreamcast"}, - {"slug": "gbc", "name": "Game Boy Color"}, - {"slug": "gb", "name": "Game Boy"}, - {"slug": "neo-geo-pocket", "name": "Neo Geo Pocket"}, - {"slug": "snes", "name": "Super Nintendo Entertainment System"}, - {"slug": "genesis-slash-megadrive", "name": "Sega Mega Drive/Genesis"}, - {"slug": "sfam", "name": "Super Famicom"}, - {"slug": "game-dot-com", "name": "Game.com"}, - {"slug": "hyper-neo-geo-64", "name": "Hyper Neo Geo 64"}, - {"slug": "satellaview", "name": "Satellaview"}, - {"slug": "palm-os", "name": "Palm OS"}, - {"slug": "apple-pippin", "name": "Apple Pippin"}, - {"slug": "sega32", "name": "Sega 32X"}, - {"slug": "neo-geo-cd", "name": "Neo Geo CD"}, - {"slug": "virtualboy", "name": "Virtual Boy"}, - {"slug": "atari-jaguar-cd", "name": "Atari Jaguar CD"}, - {"slug": "saturn", "name": "Sega Saturn"}, - {"slug": "casio-loopy", "name": "Casio Loopy"}, - {"slug": "sega-pico", "name": "Sega Pico"}, - {"slug": "r-zone", "name": "R-Zone"}, - {"slug": "sms", "name": "Sega Master System/Mark III"}, - {"slug": "playdia", "name": "Playdia"}, - {"slug": "pc-fx", "name": "PC-FX"}, - {"slug": "3do", "name": "3DO Interactive Multiplayer"}, + {"name": "OnLive Game System", "slug": "onlive-game-system"}, + {"name": "OOParts", "slug": "ooparts"}, + {"name": "Ouya", "slug": "ouya"}, + {"name": "Palm OS", "slug": "palm-os"}, + {"name": "Panasonic Jungle", "slug": "panasonic-jungle"}, + {"name": "Panasonic M2", "slug": "panasonic-m2"}, + {"name": "PC-50X Family", "slug": "pc-50x-family"}, + {"name": "PC-8800 Series", "slug": "pc-8800-series"}, + {"name": "PC-9800 Series", "slug": "pc-9800-series"}, + {"name": "PC-FX", "slug": "pc-fx"}, + {"name": "PDP-8", "slug": "pdp-8--1"}, + {"name": "PDP-1", "slug": "pdp1"}, + {"name": "PDP-10", "slug": "pdp10"}, + {"name": "PDP-11", "slug": "pdp11"}, + {"name": "Philips CD-i", "slug": "philips-cd-i"}, + {"name": "PLATO", "slug": "plato--1"}, + {"name": "Playdate", "slug": "playdate"}, + {"name": "Playdia", "slug": "playdia"}, + {"name": "Plug & Play", "slug": "plug-and-play"}, + {"name": "PocketStation", "slug": "pocketstation"}, + {"name": "Pokémon mini", "slug": "pokemon-mini"}, + {"name": "PlayStation", "slug": "ps"}, + {"name": "PlayStation 2", "slug": "ps2"}, + {"name": "PlayStation 3", "slug": "ps3"}, + {"name": "PlayStation 4", "slug": "ps4--1"}, + {"name": "PlayStation 5", "slug": "ps5"}, + {"name": "PlayStation Portable", "slug": "psp"}, + {"name": "PlayStation Vita", "slug": "psvita"}, + {"name": "PlayStation VR", "slug": "psvr"}, + {"name": "PlayStation VR2", "slug": "psvr2"}, + {"name": "R-Zone", "slug": "r-zone"}, + {"name": "Satellaview", "slug": "satellaview"}, + {"name": "Sega Saturn", "slug": "saturn"}, + {"name": "SDS Sigma 7", "slug": "sdssigma7"}, + {"name": "Sega Pico", "slug": "sega-pico"}, + {"name": "Sega 32X", "slug": "sega32"}, + {"name": "Sega CD", "slug": "segacd"}, + {"name": "Xbox Series X", "slug": "series-x"}, + {"name": "Super Famicom", "slug": "sfam"}, + {"name": "SG-1000", "slug": "sg1000"}, + {"name": "Sharp MZ-2200", "slug": "sharp-mz-2200"}, + {"name": "Sharp X68000", "slug": "sharp-x68000"}, + {"name": "Sinclair QL", "slug": "sinclair-ql"}, + {"name": "Sinclair ZX81", "slug": "sinclair-zx81"}, + {"name": "Sega Master System/Mark III", "slug": "sms"}, + {"name": "Super Nintendo Entertainment System", "slug": "snes"}, + {"name": "Sol-20", "slug": "sol-20"}, + {"name": "Google Stadia", "slug": "stadia"}, + {"name": "PC Engine SuperGrafx", "slug": "supergrafx"}, + {"name": "SwanCrystal", "slug": "swancrystal"}, + {"name": "Nintendo Switch", "slug": "switch"}, + {"name": "Tatung Einstein", "slug": "tatung-einstein"}, { - "slug": "terebikko-slash-see-n-say-video-phone", "name": "Terebikko / See 'n Say Video Phone", + "slug": "terebikko-slash-see-n-say-video-phone", }, - {"slug": "jaguar", "name": "Atari Jaguar"}, - {"slug": "segacd", "name": "Sega CD"}, - {"slug": "nes", "name": "Nintendo Entertainment System"}, - {"slug": "amiga-cd32", "name": "Amiga CD32"}, - {"slug": "famicom", "name": "Family Computer"}, - {"slug": "mega-duck-slash-cougar-boy", "name": "Mega Duck/Cougar Boy"}, - {"slug": "amiga", "name": "Amiga"}, + {"name": "Thomson MO5", "slug": "thomson-mo5"}, + {"name": "Texas Instruments TI-99", "slug": "ti-99"}, + {"name": "TRS-80", "slug": "trs-80"}, + {"name": "TRS-80 Color Computer", "slug": "trs-80-color-computer"}, { - "slug": "watara-slash-quickshot-supervision", - "name": "Watara/QuickShot Supervision", + "name": "Turbografx-16/PC Engine CD", + "slug": "turbografx-16-slash-pc-engine-cd", }, - {"slug": "philips-cd-i", "name": "Philips CD-i"}, - {"slug": "gamegear", "name": "Sega Game Gear"}, - {"slug": "neogeoaes", "name": "Neo Geo AES"}, - {"slug": "linux", "name": "Linux"}, - {"slug": "turbografx-16-slash-pc-engine-cd", "name": "Turbografx-16/PC Engine CD"}, - {"slug": "neogeomvs", "name": "Neo Geo MVS"}, - {"slug": "commodore-cdtv", "name": "Commodore CDTV"}, - {"slug": "lynx", "name": "Atari Lynx"}, - {"slug": "gamate", "name": "Gamate"}, - {"slug": "bbcmicro", "name": "BBC Microcomputer System"}, - {"slug": "turbografx16--1", "name": "TurboGrafx-16/PC Engine"}, - {"slug": "supergrafx", "name": "PC Engine SuperGrafx"}, - {"slug": "fm-towns", "name": "FM Towns"}, - {"slug": "pc-9800-series", "name": "PC-9800 Series"}, - {"slug": "apple-iigs", "name": "Apple IIGS"}, - {"slug": "x1", "name": "Sharp X1"}, - {"slug": "sharp-x68000", "name": "Sharp X68000"}, - {"slug": "acorn-archimedes", "name": "Acorn Archimedes"}, - {"slug": "c64", "name": "Commodore C64/128/MAX"}, - {"slug": "fds", "name": "Family Computer Disk System"}, - {"slug": "dragon-32-slash-64", "name": "Dragon 32/64"}, - {"slug": "acorn-electron", "name": "Acorn Electron"}, - {"slug": "acpc", "name": "Amstrad CPC"}, - {"slug": "atari-st", "name": "Atari ST/STE"}, - {"slug": "tatung-einstein", "name": "Tatung Einstein"}, - {"slug": "amstrad-pcw", "name": "Amstrad PCW"}, - {"slug": "epoch-super-cassette-vision", "name": "Epoch Super Cassette Vision"}, - {"slug": "atari7800", "name": "Atari 7800"}, - {"slug": "hp3000", "name": "HP 3000"}, - {"slug": "atari5200", "name": "Atari 5200"}, - {"slug": "c16", "name": "Commodore 16"}, - {"slug": "sinclair-ql", "name": "Sinclair QL"}, - {"slug": "thomson-mo5", "name": "Thomson MO5"}, - {"slug": "c-plus-4", "name": "Commodore Plus/4"}, - {"slug": "sg1000", "name": "SG-1000"}, - {"slug": "vectrex", "name": "Vectrex"}, - {"slug": "sharp-mz-2200", "name": "Sharp MZ-2200"}, - {"slug": "nec-pc-6000-series", "name": "NEC PC-6000 Series"}, - {"slug": "msx2", "name": "MSX2"}, - {"slug": "msx", "name": "MSX"}, - {"slug": "colecovision", "name": "ColecoVision"}, - {"slug": "intellivision", "name": "Intellivision"}, - {"slug": "vic-20", "name": "Commodore VIC-20"}, - {"slug": "zxs", "name": "ZX Spectrum"}, - {"slug": "arcadia-2001", "name": "Arcadia 2001"}, - {"slug": "fm-7", "name": "FM-7"}, - {"slug": "trs-80", "name": "TRS-80"}, - {"slug": "epoch-cassette-vision", "name": "Epoch Cassette Vision"}, - {"slug": "dos", "name": "DOS"}, - {"slug": "ti-99", "name": "Texas Instruments TI-99"}, - {"slug": "sinclair-zx81", "name": "Sinclair ZX81"}, - {"slug": "pc-8800-series", "name": "PC-8800 Series"}, - {"slug": "microvision--1", "name": "Microvision"}, - {"slug": "g-and-w", "name": "Game & Watch"}, - {"slug": "atari8bit", "name": "Atari 8-bit"}, - {"slug": "trs-80-color-computer", "name": "TRS-80 Color Computer"}, + {"name": "TurboGrafx-16/PC Engine", "slug": "turbografx16--1"}, + {"name": "Virtual Console", "slug": "vc"}, + {"name": "VC 4000", "slug": "vc-4000"}, + {"name": "Vectrex", "slug": "vectrex"}, + {"name": "Commodore VIC-20", "slug": "vic-20"}, + {"name": "Virtual Boy", "slug": "virtualboy"}, + {"name": "visionOS", "slug": "visionos"}, { - "slug": "1292-advanced-programmable-video-system", - "name": "1292 Advanced Programmable Video System", + "name": "Visual Memory Unit / Visual Memory System", + "slug": "visual-memory-unit-slash-visual-memory-system", }, - {"slug": "odyssey-2-slash-videopac-g7000", "name": "Odyssey 2 / Videopac G7000"}, - {"slug": "exidy-sorcerer", "name": "Exidy Sorcerer"}, - {"slug": "pc-50x-family", "name": "PC-50X Family"}, - {"slug": "vc-4000", "name": "VC 4000"}, - {"slug": "appleii", "name": "Apple II"}, - {"slug": "astrocade", "name": "Bally Astrocade"}, - {"slug": "ay-3-8500", "name": "AY-3-8500"}, - {"slug": "cpet", "name": "Commodore PET"}, - {"slug": "fairchild-channel-f", "name": "Fairchild Channel F"}, - {"slug": "ay-3-8610", "name": "AY-3-8610"}, - {"slug": "ay-3-8605", "name": "AY-3-8605"}, - {"slug": "ay-3-8603", "name": "AY-3-8603"}, - {"slug": "ay-3-8710", "name": "AY-3-8710"}, - {"slug": "ay-3-8760", "name": "AY-3-8760"}, - {"slug": "ay-3-8606", "name": "AY-3-8606"}, - {"slug": "ay-3-8607", "name": "AY-3-8607"}, - {"slug": "sol-20", "name": "Sol-20"}, - {"slug": "odyssey--1", "name": "Odyssey"}, - {"slug": "plato--1", "name": "PLATO"}, - {"slug": "cdccyber70", "name": "CDC Cyber 70"}, - {"slug": "sdssigma7", "name": "SDS Sigma 7"}, - {"slug": "pdp11", "name": "PDP-11"}, - {"slug": "hp2100", "name": "HP 2100"}, - {"slug": "pdp10", "name": "PDP-10"}, + {"name": "V.Smile", "slug": "vsmile"}, { - "slug": "call-a-computer", - "name": "Call-A-Computer time-shared mainframe computer system", + "name": "Watara/QuickShot Supervision", + "slug": "watara-slash-quickshot-supervision", }, - {"slug": "pdp-8--1", "name": "PDP-8"}, - {"slug": "nintendo-playstation", "name": "Nintendo PlayStation"}, - {"slug": "pdp1", "name": "PDP-1"}, - {"slug": "donner30", "name": "Donner Model 30"}, - {"slug": "edsac--1", "name": "EDSAC"}, - {"slug": "nimrod", "name": "Ferranti Nimrod Computer"}, - {"slug": "swancrystal", "name": "SwanCrystal"}, - {"slug": "panasonic-jungle", "name": "Panasonic Jungle"}, - {"slug": "handheld-electronic-lcd", "name": "Handheld Electronic LCD"}, - {"slug": "intellivision-amico", "name": "Intellivision Amico"}, - {"slug": "legacy-computer", "name": "Legacy Computer"}, - {"slug": "panasonic-m2", "name": "Panasonic M2"}, - {"slug": "browser", "name": "Web browser"}, - {"slug": "ooparts", "name": "OOParts"}, - {"slug": "stadia", "name": "Google Stadia"}, - {"slug": "plug-and-play", "name": "Plug & Play"}, - {"slug": "amazon-fire-tv", "name": "Amazon Fire TV"}, - {"slug": "onlive-game-system", "name": "OnLive Game System"}, - {"slug": "vc", "name": "Virtual Console"}, - {"slug": "airconsole", "name": "AirConsole"}, + {"name": "Wii", "slug": "wii"}, + {"name": "Wii U", "slug": "wiiu"}, + {"name": "PC (Microsoft Windows)", "slug": "win"}, + {"name": "Windows Mobile", "slug": "windows-mobile"}, + {"name": "Windows Phone", "slug": "winphone"}, + {"name": "WonderSwan", "slug": "wonderswan"}, + {"name": "WonderSwan Color", "slug": "wonderswan-color"}, + {"name": "Sharp X1", "slug": "x1"}, + {"name": "Xbox", "slug": "xbox"}, + {"name": "Xbox 360", "slug": "xbox360"}, + {"name": "Xbox One", "slug": "xboxone"}, + {"name": "Zeebo", "slug": "zeebo"}, + {"name": "Tapwave Zodiac", "slug": "zod"}, + {"name": "ZX Spectrum", "slug": "zxs"}, ) IGDB_PLATFORM_CATEGORIES: dict[int, str] = { diff --git a/backend/handler/metadata/ss_handler.py b/backend/handler/metadata/ss_handler.py index 0a1514e7b..8be31835b 100644 --- a/backend/handler/metadata/ss_handler.py +++ b/backend/handler/metadata/ss_handler.py @@ -1,20 +1,15 @@ -import functools -import re -import time from typing import Final, NotRequired, TypedDict import httpx import pydash from config import SCREENSCRAPER_API_KEY, SCREENSCRAPER_PASSWORD, SCREENSCRAPER_USER from fastapi import HTTPException, status -from handler.redis_handler import sync_cache from logger.logger import log -from unidecode import unidecode as uc from utils.context import ctx_httpx_client from .base_hander import MetadataHandler -# Used to display the IGDB API status in the frontend +# Used to display the Screenscraper API status in the frontend SS_API_ENABLED: Final = bool(SCREENSCRAPER_USER) and bool(SCREENSCRAPER_PASSWORD) @@ -97,11 +92,7 @@ def extract_metadata_from_igdb_rom( IGDBMetadataPlatform(igdb_id=p.get("id", ""), name=p.get("name", "")) for p in rom.get("platforms", []) ], - "age_ratings": [ - IGDB_AGE_RATINGS[r["rating"]] - for r in rom.get("age_ratings", []) - if r["rating"] in IGDB_AGE_RATINGS - ], + "age_ratings": [], "expansions": [ IGDBRelatedGame( id=e["id"], @@ -179,31 +170,36 @@ def extract_metadata_from_igdb_rom( class SSBaseHandler(MetadataHandler): def __init__(self) -> None: self.BASE_URL = "https://api.screenscraper.fr/api2" - # 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 = f"{self.BASE_URL}/jeuRecherche.php" - # self.video_endpoint = f"{self.BASE_URL}/game_videos" self.auth_params = { "ssid": SCREENSCRAPER_USER, "sspassword": SCREENSCRAPER_PASSWORD, "devid": SCREENSCRAPER_USER, "devpassword": SCREENSCRAPER_API_KEY, + "softname": "romm", } self.output_param = {"output": "json"} self.LOGIN_ERROR_CHECK: Final = "Erreur de login" - async def _request(self, url: str, search_term: str, timeout: int = 120) -> list: + async def _request( + self, url: str, search_term: str, platform_id: int, timeout: int = 120 + ) -> list: httpx_client = ctx_httpx_client.get() try: + masked_params = self._mask_sensitive_values(self.auth_params) + log.debug( + "API request: URL=%s, Params=%s, Timeout=%s", + url, + masked_params, + timeout, + ) res = await httpx_client.get( url, params={ **self.auth_params, **self.output_param, "recherche": search_term, + "systemeid": platform_id, }, headers={}, timeout=timeout, @@ -218,14 +214,50 @@ async def _request(self, url: str, search_term: str, timeout: int = 120) -> list ) matches: list[dict] = [] for rom in res.json().get("response", []).get("jeux", []): - for name in rom.get("noms", []): - region = name.get("region", None) - text = name.get("text", None) - if region and text: - matches.append({region: text}) - for m in matches: - log.debug(m) - return res.json() + noms = rom.get("noms", []) + if len(noms) > 0: + name = "" + for n in noms: + region = n.get("region", "") + text = n.get("text", "") + if region and text: + if region == "ss": + name = text + break + if not name: + continue + synopses = rom.get("synopsis", []) + if len(synopses) > 0: + summary = "" + for s in synopses: + langue = s.get("langue", "") + text = s.get("text", "") + if langue and text: + if langue == "en": + summary = text + break + url_covers = rom.get("medias", []) + if len(url_covers) > 0: + url_cover = "" + for u in url_covers: + if ( + u.get("region", "") == "us" + and u.get("type", "") == "box-2D" + and u.get("parent", "") == "jeu" + ): + url_cover = u.get("url", "") + break + matches.append( + { + "ss_id": rom.get("id", ""), + "name": name, + "slug": name, + "summary": summary, + "url_cover": url_cover, + "ss_metadata": {}, + } + ) + return matches except httpx.NetworkError as exc: log.critical("Connection error: can't connect to IGDB", exc_info=True) raise HTTPException( @@ -233,794 +265,23 @@ async def _request(self, url: str, search_term: str, timeout: int = 120) -> list detail="Can't connect to IGDB, check your internet connection", ) from exc - async def search_rom(self, search_term) -> None: - await self._request(self.search_endpoint, search_term) - - async def _search_rom( - self, search_term: str, platform_igdb_id: int, with_category: bool = False - ) -> dict | None: - if not platform_igdb_id: - return None - - search_term = uc(search_term) - category_filter: str = ( - f"& (category={MAIN_GAME_CATEGORY} | category={EXPANDED_GAME_CATEGORY})" - if with_category - else "" - ) - - def is_exact_match(rom: dict, search_term: str) -> bool: - return ( - rom["name"].lower() == search_term.lower() - or rom["slug"].lower() == search_term.lower() - or ( - self._normalize_exact_match(rom["name"]) - == self._normalize_exact_match(search_term) - ) - ) - - roms = await self._request( - self.games_endpoint, - data=f'search "{search_term}"; fields {",".join(self.games_fields)}; where platforms=[{platform_igdb_id}] {category_filter};', - ) - for rom in roms: - # Return early if an exact match is found. - if is_exact_match(rom, search_term): - return rom - - roms_expanded = await self._request( + async def search_rom(self, search_term, platform) -> list[dict]: + matches = await self._request( self.search_endpoint, - data=f'fields {",".join(self.search_fields)}; where game.platforms=[{platform_igdb_id}] & (name ~ *"{search_term}"* | alternative_name ~ *"{search_term}"*);', + search_term, + SLUG_TO_SS_ID.get(platform.slug, {"id": 1}).get("id", 1), ) - if roms_expanded: - extra_roms = await self._request( - self.games_endpoint, - f'fields {",".join(self.games_fields)}; where id={roms_expanded[0]["game"]["id"]};', - ) - for rom in extra_roms: - # Return early if an exact match is found. - if is_exact_match(rom, search_term): - return rom - - roms.extend(extra_roms) - - return roms[0] if roms else None - - # @check_twitch_token - # async def get_platform(self, slug: str) -> IGDBPlatform: - # if not IGDB_API_ENABLED: - # return IGDBPlatform(igdb_id=None, slug=slug) - - # platforms = await self._request( - # self.platform_endpoint, - # data=f'fields {",".join(self.platforms_fields)}; where slug="{slug.lower()}";', - # ) - - # platform = pydash.get(platforms, "[0]", None) - # if platform: - # return IGDBPlatform( - # igdb_id=platform["id"], - # slug=slug, - # name=platform["name"], - # ) - - # # Check if platform is a version if not found - # platform_versions = await self._request( - # self.platform_version_endpoint, - # data=f'fields {",".join(self.platforms_fields)}; where slug="{slug.lower()}";', - # ) - # version = pydash.get(platform_versions, "[0]", None) - # if version: - # return IGDBPlatform( - # igdb_id=version["id"], - # slug=slug, - # name=version["name"], - # ) - - # return IGDBPlatform(igdb_id=None, slug=slug) - - # @check_twitch_token - # async def get_rom(self, file_name: str, platform_igdb_id: int) -> IGDBRom: - # from handler.filesystem import fs_rom_handler - - # if not IGDB_API_ENABLED: - # return IGDBRom(igdb_id=None) - - # if not platform_igdb_id: - # return IGDBRom(igdb_id=None) - - # search_term = fs_rom_handler.get_file_name_with_no_tags(file_name) - # fallback_rom = IGDBRom(igdb_id=None) - - # # Support for PS2 OPL filename format - # match = PS2_OPL_REGEX.match(file_name) - # if platform_igdb_id == PS2_IGDB_ID and match: - # search_term = await self._ps2_opl_format(match, search_term) - # fallback_rom = IGDBRom(igdb_id=None, name=search_term) - - # # Support for sony serial filename format (PS, PS3, PS3) - # match = SONY_SERIAL_REGEX.search(file_name, re.IGNORECASE) - # if platform_igdb_id == PS1_IGDB_ID and match: - # search_term = await self._ps1_serial_format(match, search_term) - # fallback_rom = IGDBRom(igdb_id=None, name=search_term) - - # if platform_igdb_id == PS2_IGDB_ID and match: - # search_term = await self._ps2_serial_format(match, search_term) - # fallback_rom = IGDBRom(igdb_id=None, name=search_term) - - # if platform_igdb_id == PSP_IGDB_ID and match: - # search_term = await self._psp_serial_format(match, search_term) - # fallback_rom = IGDBRom(igdb_id=None, name=search_term) - - # # Support for switch titleID filename format - # match = SWITCH_TITLEDB_REGEX.search(file_name) - # if platform_igdb_id == SWITCH_IGDB_ID and match: - # search_term, index_entry = await self._switch_titledb_format( - # match, search_term - # ) - # if index_entry: - # fallback_rom = IGDBRom( - # igdb_id=None, - # name=index_entry["name"], - # summary=index_entry.get("description", ""), - # url_cover=index_entry.get("iconUrl", ""), - # url_screenshots=index_entry.get("screenshots", None) or [], - # ) + return matches - # # Support for switch productID filename format - # match = SWITCH_PRODUCT_ID_REGEX.search(file_name) - # if platform_igdb_id == SWITCH_IGDB_ID and match: - # search_term, index_entry = await self._switch_productid_format( - # match, search_term - # ) - # if index_entry: - # fallback_rom = IGDBRom( - # igdb_id=None, - # name=index_entry["name"], - # summary=index_entry.get("description", ""), - # url_cover=index_entry.get("iconUrl", ""), - # url_screenshots=index_entry.get("screenshots", None) or [], - # ) - # # Support for MAME arcade filename format - # if platform_igdb_id in ARCADE_IGDB_IDS: - # search_term = await self._mame_format(search_term) - # fallback_rom = IGDBRom(igdb_id=None, name=search_term) - - # search_term = self.normalize_search_term(search_term) - - # rom = await self._search_rom(search_term, platform_igdb_id, with_category=True) - # if not rom: - # rom = await self._search_rom(search_term, platform_igdb_id) - - # # Split the search term since igdb struggles with colons - # if not rom and ":" in search_term: - # for term in search_term.split(":")[::-1]: - # rom = await self._search_rom(term, platform_igdb_id) - # if rom: - # break - - # # Some MAME games have two titles split by a slash - # if not rom and "/" in search_term: - # for term in search_term.split("/"): - # rom = await self._search_rom(term.strip(), platform_igdb_id) - # if rom: - # break - - # if not rom: - # return fallback_rom - - # # Get the video ID for the game - # video_ids = await self._request( - # self.video_endpoint, - # f'fields video_id; where game={rom["id"]};', - # ) - # video_id = pydash.get(video_ids, "[0].video_id", None) - - # return IGDBRom( - # igdb_id=rom["id"], - # slug=rom["slug"], - # name=rom["name"], - # summary=rom.get("summary", ""), - # url_cover=self._normalize_cover_url( - # rom.get("cover", {}).get("url", "") - # ).replace("t_thumb", "t_1080p"), - # url_screenshots=[ - # self._normalize_cover_url(s.get("url", "")).replace( - # "t_thumb", "t_screenshot_huge" - # ) - # for s in rom.get("screenshots", []) - # ], - # igdb_metadata=extract_metadata_from_igdb_rom(rom, video_id), - # ) - - # @check_twitch_token - # async def get_rom_by_id(self, igdb_id: int) -> IGDBRom: - # if not IGDB_API_ENABLED: - # return IGDBRom(igdb_id=None) - - # roms = await self._request( - # self.games_endpoint, - # f'fields {",".join(self.games_fields)}; where id={igdb_id};', - # ) - # rom = pydash.get(roms, "[0]", None) - - # if not rom: - # return IGDBRom(igdb_id=None) - - # # Get the video ID for the game - # video_ids = await self._request( - # self.video_endpoint, - # f'fields video_id; where game={rom["id"]};', - # ) - # video_id = pydash.get(video_ids, "[0].video_id", None) - - # return IGDBRom( - # igdb_id=rom["id"], - # slug=rom["slug"], - # name=rom["name"], - # summary=rom.get("summary", ""), - # url_cover=self._normalize_cover_url( - # rom.get("cover", {}).get("url", "") - # ).replace("t_thumb", "t_1080p"), - # url_screenshots=[ - # self._normalize_cover_url(s.get("url", "")).replace( - # "t_thumb", "t_screenshot_huge" - # ) - # for s in rom.get("screenshots", []) - # ], - # igdb_metadata=extract_metadata_from_igdb_rom(rom, video_id), - # ) - - # @check_twitch_token - # async def get_matched_roms_by_id(self, igdb_id: int) -> list[IGDBRom]: - # if not IGDB_API_ENABLED: - # return [] - - # rom = await self.get_rom_by_id(igdb_id) - # return [rom] if rom["igdb_id"] else [] - - # @check_twitch_token - # async def get_matched_roms_by_name( - # self, search_term: str, platform_igdb_id: int - # ) -> list[IGDBRom]: - # if not IGDB_API_ENABLED: - # return [] - - # if not platform_igdb_id: - # return [] - - # search_term = uc(search_term) - # matched_roms = await self._request( - # self.games_endpoint, - # data=f'search "{search_term}"; fields {",".join(self.games_fields)}; where platforms=[{platform_igdb_id}];', - # ) - - # alternative_matched_roms = await self._request( - # self.search_endpoint, - # data=f'fields {",".join(self.search_fields)}; where game.platforms=[{platform_igdb_id}] & (name ~ *"{search_term}"* | alternative_name ~ *"{search_term}"*);', - # ) - - # if alternative_matched_roms: - # alternative_roms_ids = [] - # for rom in alternative_matched_roms: - # alternative_roms_ids.append( - # rom.get("game").get("id", "") - # if "game" in rom.keys() - # else rom.get("id", "") - # ) - # id_filter = " | ".join( - # list( - # map( - # lambda rom: ( - # f'id={rom.get("game").get("id", "")}' - # if "game" in rom.keys() - # else f'id={rom.get("id", "")}' - # ), - # alternative_matched_roms, - # ) - # ) - # ) - # alternative_matched_roms = await self._request( - # self.games_endpoint, - # f'fields {",".join(self.games_fields)}; where {id_filter};', - # ) - # matched_roms.extend(alternative_matched_roms) - - # # Use a dictionary to keep track of unique ids - # unique_ids: dict[str, dict[str, str]] = {} - - # # Use a list comprehension to filter duplicates based on the 'id' key - # matched_roms = [ - # unique_ids.setdefault(rom["id"], rom) - # for rom in matched_roms - # if rom["id"] not in unique_ids - # ] - - # return [ - # IGDBRom( - # { # type: ignore[misc] - # k: v - # for k, v in { - # "igdb_id": rom["id"], - # "slug": rom["slug"], - # "name": rom["name"], - # "summary": rom.get("summary", ""), - # "url_cover": self._normalize_cover_url( - # pydash.get(rom, "cover.url", "").replace( - # "t_thumb", "t_cover_big" - # ) - # ), - # "url_screenshots": [ - # self._normalize_cover_url(s.get("url", "")) # type: ignore[arg-type] - # for s in rom.get("screenshots", []) - # ], - # "igdb_metadata": extract_metadata_from_igdb_rom(rom), - # }.items() - # if v - # } - # ) - # for rom in matched_roms - # ] - - -PLATFORMS_FIELDS = ["id", "name"] - -GAMES_FIELDS = [ - "id", - "name", - "slug", - "summary", - "total_rating", - "aggregated_rating", - "first_release_date", - "artworks.url", - "cover.url", - "screenshots.url", - "platforms.id", - "platforms.name", - "alternative_names.name", - "genres.name", - "franchise.name", - "franchises.name", - "collections.name", - "game_modes.name", - "involved_companies.company.name", - "expansions.id", - "expansions.slug", - "expansions.name", - "expansions.cover.url", - "expanded_games.id", - "expanded_games.slug", - "expanded_games.name", - "expanded_games.cover.url", - "dlcs.id", - "dlcs.name", - "dlcs.slug", - "dlcs.cover.url", - "remakes.id", - "remakes.slug", - "remakes.name", - "remakes.cover.url", - "remasters.id", - "remasters.slug", - "remasters.name", - "remasters.cover.url", - "ports.id", - "ports.slug", - "ports.name", - "ports.cover.url", - "similar_games.id", - "similar_games.slug", - "similar_games.name", - "similar_games.cover.url", - "age_ratings.rating", -] - -# 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], -# name: a.innerText -# })) +class SlugToSSId(TypedDict): + id: int + name: str -IGDB_PLATFORM_LIST = [ - {"slug": "visionos", "name": "visionOS"}, - {"slug": "meta-quest-3", "name": "Meta Quest 3"}, - {"slug": "atari2600", "name": "Atari 2600"}, - {"slug": "psvr2", "name": "PlayStation VR2"}, - {"slug": "switch", "name": "Nintendo Switch"}, - {"slug": "evercade", "name": "Evercade"}, - {"slug": "android", "name": "Android"}, - {"slug": "mac", "name": "Mac"}, - {"slug": "win", "name": "PC (Microsoft Windows)"}, - {"slug": "oculus-quest", "name": "Oculus Quest"}, - {"slug": "playdate", "name": "Playdate"}, - {"slug": "series-x", "name": "Xbox Series X"}, - {"slug": "meta-quest-2", "name": "Meta Quest 2"}, - {"slug": "ps5", "name": "PlayStation 5"}, - {"slug": "oculus-rift", "name": "Oculus Rift"}, - {"slug": "xboxone", "name": "Xbox One"}, - {"slug": "leaptv", "name": "LeapTV"}, - {"slug": "new-nintendo-3ds", "name": "New Nintendo 3DS"}, - {"slug": "gear-vr", "name": "Gear VR"}, - {"slug": "psvr", "name": "PlayStation VR"}, - {"slug": "3ds", "name": "Nintendo 3DS"}, - {"slug": "winphone", "name": "Windows Phone"}, - {"slug": "arduboy", "name": "Arduboy"}, - {"slug": "ps4--1", "name": "PlayStation 4"}, - {"slug": "oculus-go", "name": "Oculus Go"}, - {"slug": "psvita", "name": "PlayStation Vita"}, - {"slug": "wiiu", "name": "Wii U"}, - {"slug": "ouya", "name": "Ouya"}, - {"slug": "wii", "name": "Wii"}, - {"slug": "ps3", "name": "PlayStation 3"}, - {"slug": "psp", "name": "PlayStation Portable"}, - {"slug": "nintendo-dsi", "name": "Nintendo DSi"}, - { - "slug": "leapster-explorer-slash-leadpad-explorer", - "name": "Leapster Explorer/LeadPad Explorer", - }, - {"slug": "xbox360", "name": "Xbox 360"}, - {"slug": "nds", "name": "Nintendo DS"}, - {"slug": "ps2", "name": "PlayStation 2"}, - {"slug": "arcade", "name": "Arcade"}, - {"slug": "zeebo", "name": "Zeebo"}, - {"slug": "windows-mobile", "name": "Windows Mobile"}, - {"slug": "ios", "name": "iOS"}, - {"slug": "mobile", "name": "Legacy Mobile Device"}, - {"slug": "blu-ray-player", "name": "Blu-ray Player"}, - {"slug": "hyperscan", "name": "HyperScan"}, - {"slug": "gizmondo", "name": "Gizmondo"}, - {"slug": "gba", "name": "Game Boy Advance"}, - {"slug": "ngage", "name": "N-Gage"}, - {"slug": "vsmile", "name": "V.Smile"}, - {"slug": "n64", "name": "Nintendo 64"}, - {"slug": "leapster", "name": "Leapster"}, - {"slug": "zod", "name": "Tapwave Zodiac"}, - {"slug": "wonderswan-color", "name": "WonderSwan Color"}, - {"slug": "xbox", "name": "Xbox"}, - {"slug": "ngc", "name": "Nintendo GameCube"}, - {"slug": "wonderswan", "name": "WonderSwan"}, - {"slug": "pokemon-mini", "name": "Pokémon mini"}, - {"slug": "nuon", "name": "Nuon"}, - {"slug": "ps", "name": "PlayStation"}, - {"slug": "nintendo-64dd", "name": "Nintendo 64DD"}, - {"slug": "neo-geo-pocket-color", "name": "Neo Geo Pocket Color"}, - {"slug": "dvd-player", "name": "DVD Player"}, - {"slug": "pocketstation", "name": "PocketStation"}, - { - "slug": "visual-memory-unit-slash-visual-memory-system", - "name": "Visual Memory Unit / Visual Memory System", - }, - {"slug": "blackberry", "name": "BlackBerry OS"}, - {"slug": "dc", "name": "Dreamcast"}, - {"slug": "gbc", "name": "Game Boy Color"}, - {"slug": "gb", "name": "Game Boy"}, - {"slug": "neo-geo-pocket", "name": "Neo Geo Pocket"}, - {"slug": "snes", "name": "Super Nintendo Entertainment System"}, - {"slug": "genesis-slash-megadrive", "name": "Sega Mega Drive/Genesis"}, - {"slug": "sfam", "name": "Super Famicom"}, - {"slug": "game-dot-com", "name": "Game.com"}, - {"slug": "hyper-neo-geo-64", "name": "Hyper Neo Geo 64"}, - {"slug": "satellaview", "name": "Satellaview"}, - {"slug": "palm-os", "name": "Palm OS"}, - {"slug": "apple-pippin", "name": "Apple Pippin"}, - {"slug": "sega32", "name": "Sega 32X"}, - {"slug": "neo-geo-cd", "name": "Neo Geo CD"}, - {"slug": "virtualboy", "name": "Virtual Boy"}, - {"slug": "atari-jaguar-cd", "name": "Atari Jaguar CD"}, - {"slug": "saturn", "name": "Sega Saturn"}, - {"slug": "casio-loopy", "name": "Casio Loopy"}, - {"slug": "sega-pico", "name": "Sega Pico"}, - {"slug": "r-zone", "name": "R-Zone"}, - {"slug": "sms", "name": "Sega Master System/Mark III"}, - {"slug": "playdia", "name": "Playdia"}, - {"slug": "pc-fx", "name": "PC-FX"}, - {"slug": "3do", "name": "3DO Interactive Multiplayer"}, - { - "slug": "terebikko-slash-see-n-say-video-phone", - "name": "Terebikko / See 'n Say Video Phone", - }, - {"slug": "jaguar", "name": "Atari Jaguar"}, - {"slug": "segacd", "name": "Sega CD"}, - {"slug": "nes", "name": "Nintendo Entertainment System"}, - {"slug": "amiga-cd32", "name": "Amiga CD32"}, - {"slug": "famicom", "name": "Family Computer"}, - {"slug": "mega-duck-slash-cougar-boy", "name": "Mega Duck/Cougar Boy"}, - {"slug": "amiga", "name": "Amiga"}, - { - "slug": "watara-slash-quickshot-supervision", - "name": "Watara/QuickShot Supervision", - }, - {"slug": "philips-cd-i", "name": "Philips CD-i"}, - {"slug": "gamegear", "name": "Sega Game Gear"}, - {"slug": "neogeoaes", "name": "Neo Geo AES"}, - {"slug": "linux", "name": "Linux"}, - {"slug": "turbografx-16-slash-pc-engine-cd", "name": "Turbografx-16/PC Engine CD"}, - {"slug": "neogeomvs", "name": "Neo Geo MVS"}, - {"slug": "commodore-cdtv", "name": "Commodore CDTV"}, - {"slug": "lynx", "name": "Atari Lynx"}, - {"slug": "gamate", "name": "Gamate"}, - {"slug": "bbcmicro", "name": "BBC Microcomputer System"}, - {"slug": "turbografx16--1", "name": "TurboGrafx-16/PC Engine"}, - {"slug": "supergrafx", "name": "PC Engine SuperGrafx"}, - {"slug": "fm-towns", "name": "FM Towns"}, - {"slug": "pc-9800-series", "name": "PC-9800 Series"}, - {"slug": "apple-iigs", "name": "Apple IIGS"}, - {"slug": "x1", "name": "Sharp X1"}, - {"slug": "sharp-x68000", "name": "Sharp X68000"}, - {"slug": "acorn-archimedes", "name": "Acorn Archimedes"}, - {"slug": "c64", "name": "Commodore C64/128/MAX"}, - {"slug": "fds", "name": "Family Computer Disk System"}, - {"slug": "dragon-32-slash-64", "name": "Dragon 32/64"}, - {"slug": "acorn-electron", "name": "Acorn Electron"}, - {"slug": "acpc", "name": "Amstrad CPC"}, - {"slug": "atari-st", "name": "Atari ST/STE"}, - {"slug": "tatung-einstein", "name": "Tatung Einstein"}, - {"slug": "amstrad-pcw", "name": "Amstrad PCW"}, - {"slug": "epoch-super-cassette-vision", "name": "Epoch Super Cassette Vision"}, - {"slug": "atari7800", "name": "Atari 7800"}, - {"slug": "hp3000", "name": "HP 3000"}, - {"slug": "atari5200", "name": "Atari 5200"}, - {"slug": "c16", "name": "Commodore 16"}, - {"slug": "sinclair-ql", "name": "Sinclair QL"}, - {"slug": "thomson-mo5", "name": "Thomson MO5"}, - {"slug": "c-plus-4", "name": "Commodore Plus/4"}, - {"slug": "sg1000", "name": "SG-1000"}, - {"slug": "vectrex", "name": "Vectrex"}, - {"slug": "sharp-mz-2200", "name": "Sharp MZ-2200"}, - {"slug": "nec-pc-6000-series", "name": "NEC PC-6000 Series"}, - {"slug": "msx2", "name": "MSX2"}, - {"slug": "msx", "name": "MSX"}, - {"slug": "colecovision", "name": "ColecoVision"}, - {"slug": "intellivision", "name": "Intellivision"}, - {"slug": "vic-20", "name": "Commodore VIC-20"}, - {"slug": "zxs", "name": "ZX Spectrum"}, - {"slug": "arcadia-2001", "name": "Arcadia 2001"}, - {"slug": "fm-7", "name": "FM-7"}, - {"slug": "trs-80", "name": "TRS-80"}, - {"slug": "epoch-cassette-vision", "name": "Epoch Cassette Vision"}, - {"slug": "dos", "name": "DOS"}, - {"slug": "ti-99", "name": "Texas Instruments TI-99"}, - {"slug": "sinclair-zx81", "name": "Sinclair ZX81"}, - {"slug": "pc-8800-series", "name": "PC-8800 Series"}, - {"slug": "microvision--1", "name": "Microvision"}, - {"slug": "g-and-w", "name": "Game & Watch"}, - {"slug": "atari8bit", "name": "Atari 8-bit"}, - {"slug": "trs-80-color-computer", "name": "TRS-80 Color Computer"}, - { - "slug": "1292-advanced-programmable-video-system", - "name": "1292 Advanced Programmable Video System", - }, - {"slug": "odyssey-2-slash-videopac-g7000", "name": "Odyssey 2 / Videopac G7000"}, - {"slug": "exidy-sorcerer", "name": "Exidy Sorcerer"}, - {"slug": "pc-50x-family", "name": "PC-50X Family"}, - {"slug": "vc-4000", "name": "VC 4000"}, - {"slug": "appleii", "name": "Apple II"}, - {"slug": "astrocade", "name": "Bally Astrocade"}, - {"slug": "ay-3-8500", "name": "AY-3-8500"}, - {"slug": "cpet", "name": "Commodore PET"}, - {"slug": "fairchild-channel-f", "name": "Fairchild Channel F"}, - {"slug": "ay-3-8610", "name": "AY-3-8610"}, - {"slug": "ay-3-8605", "name": "AY-3-8605"}, - {"slug": "ay-3-8603", "name": "AY-3-8603"}, - {"slug": "ay-3-8710", "name": "AY-3-8710"}, - {"slug": "ay-3-8760", "name": "AY-3-8760"}, - {"slug": "ay-3-8606", "name": "AY-3-8606"}, - {"slug": "ay-3-8607", "name": "AY-3-8607"}, - {"slug": "sol-20", "name": "Sol-20"}, - {"slug": "odyssey--1", "name": "Odyssey"}, - {"slug": "plato--1", "name": "PLATO"}, - {"slug": "cdccyber70", "name": "CDC Cyber 70"}, - {"slug": "sdssigma7", "name": "SDS Sigma 7"}, - {"slug": "pdp11", "name": "PDP-11"}, - {"slug": "hp2100", "name": "HP 2100"}, - {"slug": "pdp10", "name": "PDP-10"}, - { - "slug": "call-a-computer", - "name": "Call-A-Computer time-shared mainframe computer system", - }, - {"slug": "pdp-8--1", "name": "PDP-8"}, - {"slug": "nintendo-playstation", "name": "Nintendo PlayStation"}, - {"slug": "pdp1", "name": "PDP-1"}, - {"slug": "donner30", "name": "Donner Model 30"}, - {"slug": "edsac--1", "name": "EDSAC"}, - {"slug": "nimrod", "name": "Ferranti Nimrod Computer"}, - {"slug": "swancrystal", "name": "SwanCrystal"}, - {"slug": "panasonic-jungle", "name": "Panasonic Jungle"}, - {"slug": "handheld-electronic-lcd", "name": "Handheld Electronic LCD"}, - {"slug": "intellivision-amico", "name": "Intellivision Amico"}, - {"slug": "legacy-computer", "name": "Legacy Computer"}, - {"slug": "panasonic-m2", "name": "Panasonic M2"}, - {"slug": "browser", "name": "Web browser"}, - {"slug": "ooparts", "name": "OOParts"}, - {"slug": "stadia", "name": "Google Stadia"}, - {"slug": "plug-and-play", "name": "Plug & Play"}, - {"slug": "amazon-fire-tv", "name": "Amazon Fire TV"}, - {"slug": "onlive-game-system", "name": "OnLive Game System"}, - {"slug": "vc", "name": "Virtual Console"}, - {"slug": "airconsole", "name": "AirConsole"}, -] -IGDB_AGE_RATINGS: dict[int, IGDBAgeRating] = { - 1: { - "rating": "Three", - "category": "PEGI", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_3.png", - }, - 2: { - "rating": "Seven", - "category": "PEGI", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_7.png", - }, - 3: { - "rating": "Twelve", - "category": "PEGI", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_12.png", - }, - 4: { - "rating": "Sixteen", - "category": "PEGI", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_16.png", - }, - 5: { - "rating": "Eighteen", - "category": "PEGI", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_18.png", - }, - 6: { - "rating": "RP", - "category": "ESRB", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_rp.png", - }, - 7: { - "rating": "EC", - "category": "ESRB", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_ec.png", - }, - 8: { - "rating": "E", - "category": "ESRB", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_e.png", - }, - 9: { - "rating": "E10", - "category": "ESRB", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_e10.png", - }, - 10: { - "rating": "T", - "category": "ESRB", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_t.png", - }, - 11: { - "rating": "M", - "category": "ESRB", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_m.png", - }, - 12: { - "rating": "AO", - "category": "ESRB", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_ao.png", - }, - 13: { - "rating": "CERO_A", - "category": "CERO", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_a.png", - }, - 14: { - "rating": "CERO_B", - "category": "CERO", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_b.png", - }, - 15: { - "rating": "CERO_C", - "category": "CERO", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_c.png", - }, - 16: { - "rating": "CERO_D", - "category": "CERO", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_d.png", - }, - 17: { - "rating": "CERO_Z", - "category": "CERO", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_z.png", - }, - 18: { - "rating": "USK_0", - "category": "USK", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_0.png", - }, - 19: { - "rating": "USK_6", - "category": "USK", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_6.png", - }, - 20: { - "rating": "USK_12", - "category": "USK", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_12.png", - }, - 21: { - "rating": "USK_16", - "category": "USK", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_16.png", - }, - 22: { - "rating": "USK_18", - "category": "USK", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_18.png", - }, - 23: { - "rating": "GRAC_ALL", - "category": "GRAC", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_all.png", - }, - 24: { - "rating": "GRAC_Twelve", - "category": "GRAC", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_twelve.png", - }, - 25: { - "rating": "GRAC_Fifteen", - "category": "GRAC", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_fifteen.png", - }, - 26: { - "rating": "GRAC_Eighteen", - "category": "GRAC", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_eighteen.png", - }, - 27: { - "rating": "GRAC_TESTING", - "category": "GRAC", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_testing.png", - }, - 28: { - "rating": "CLASS_IND_L", - "category": "CLASS_IND", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/classind/classind_l.png", - }, - 29: { - "rating": "CLASS_IND_Ten", - "category": "CLASS_IND", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/classind/classind_ten.png", - }, - 30: { - "rating": "CLASS_IND_Twelve", - "category": "CLASS_IND", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/classind/classind_twelve.png", - }, - 31: { - "rating": "ACB_G", - "category": "ACB", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_g.png", - }, - 32: { - "rating": "ACB_PG", - "category": "ACB", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_pg.png", - }, - 33: { - "rating": "ACB_M", - "category": "ACB", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_m.png", - }, - 34: { - "rating": "ACB_MA15", - "category": "ACB", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_ma15.png", - }, - 35: { - "rating": "ACB_R18", - "category": "ACB", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_r18.png", - }, - 36: { - "rating": "ACB_RC", - "category": "ACB", - "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_rc.png", - }, +SLUG_TO_SS_ID: dict[str, SlugToSSId] = { + "gba": {"id": 12, "name": "Game Boy Advance"}, } + +# Reverse lookup +SS_ID_TO_SLUG = {v["id"]: k for k, v in SLUG_TO_SS_ID.items()} diff --git a/frontend/assets/scrappers/ss.gif b/frontend/assets/scrappers/ss.gif new file mode 100644 index 0000000000000000000000000000000000000000..2cb868726035abfbda8e6b38a01853804c8836d4 GIT binary patch literal 26119 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�=& zooned=4e?tUO zUCQT=PU6k>-rMCCm7#$s^}1!+!8<_nG-1uE^yrHo+@Hc*Yy6{s@=M%cH5MJIc%f{@ zb)0Sz)-W<7cWd8%{QG7_l~mo%dX=cu|3<`h_Fvg9QWtp4q(Pk|A8Bl+k08-6zB%gj zs%Y8j{oPmS4$k!W3||i1^H!u*w&;WYtPvFv`m*M8xTNr28WVMs(v3Y_`>BDO)WOFt zXD+0~#T5=P2&E{6&YJy0tE4w2aFkb6ZLuuK#1LpfI5h~F{Eho#t!{v{gO&2gKcPJf z6?U-pw1Wp{;c>C+$V(~F5~`goVvh{B(hfmaCmL;E@0fz8t?*=1cm`*t*C*Dg^{Cr^ zf@xE1izMq9d`v1x5DIluK@P!Q5BPUY!Il&D&M;Bw4uE=!_~uQ7(ssx9Niml#=I89t zr_vERPyk5dmX9>?u)FlD!T?)(`YycBkyZSnyuc2T!N*feaQ;eVJ!fsZM=LJ^upuW; zpi7x(n54|hL9wO7$g+#5C-o(^H*cn@YY{WBJJ!X)l~YT9N!Mi|C|$BJF zO!L$>r14> zIb?<|95^LOrj>1HFE_u5QFe19&_~plTN<107ZL;`%hFCP<`G>UhfrE7793xhRx_o z!E&a#tIpGwIK=m$4k%FUz2>f%JtWN3pif=p5F)={ zM^mUsRw4o6Kn;1og{!9{nFT}PunUZB?I#CFhC1;RSb$odp?Ix13MLSeN>bwC9inI$ zxuQIulh)5oNS6|Wk07Q?Ni;dUDOE9!RH($Oc!{waLqHqyoyn;(AYY4|Seb5CqSs4d zUJP?=x6EXJPFBUY$gP#ek%hxmf*d;aq}V5i!!Ppp3Yr>3HV%xTlJ<;yBB{SrLQQ?k z1k!d(%k2MijfritLQ}O^+bheLS>lxkW29Pi_N<*%PWZHyJBUlQjGeDVqi`3r%A>Y2 zP0P0kI&GlMR~f(vNBaI@+AUQ5=DojvNb&{Rkq%a+@QwbXQ<+P1$5 zlNPHRHCV45?~L}gh>)JRD{Ve=&6RF^NJ`2XNCQ^w9+F5qYFi8j6nliy?HWAL`H(wE z&$0GSCUxuWA>;c&YKUpd4*#<(?Ou>g2dKV8+$o3|fCg4I)L_=La!V*SSmJ>5dh_hx zBDHl5&p+<%DEx+LdJL~9&97iRfww7C0Ut}-3O_4N0H)gCqSvCZ6P;MHtRY5+fEOZ% zp8PWModiNhw+4&i&?#a88*fJg`!*v(kxHIuwVf8_* zd2Uc$=}ujs4!09JueighKvHP63h$WnyV->Xl2#}Ust9nh;%6T+ecTM@JliZML0Uh& zsjK(PIy_n{-R8%0NQyKh-0HP?qW6GwGW&L#(uUf|#qR4lqG zmNlxc({~k1Ou|Wz<$Vymg%J0iXhwuo;(nDpK;YeDf1KCV zr}mg`yNvL2PfD2wsJjID-t}zp)86JnSc;dO+GB6)=p&l{Ru3@Tl6 zp`B+yLAxsx_%3-!u93Pft?l}a>jOZsh;o;+KNjN~ zc&!#om$QcAxe4^71YfuSV#;khuUkc;V@Eq9FJo&om`CI}V>VP%)bA2Z`vg8&F_~Sy zB<@uU+H>5R?9NX$KeIsXU9|sT(AbjMHZvCBOqBF(THg95L~0GY@j||J$4s{+WS0G~ zMP{ag&d@MbVVh*WVQ*_M(!&A4&gn$&k~y8N6{8mHAq{q8#s?k1~Jgw vk`i9Vj@ethdMwL4ZS)!358i$eaD?0+vpK^bk}jQ(zOjJ|25Bq-#7F-FRf0Q; literal 0 HcmV?d00001 diff --git a/frontend/assets/scrappers/ss.ico b/frontend/assets/scrappers/ss.ico new file mode 100644 index 0000000000000000000000000000000000000000..b73f816b89b39ae73abe34cfc892798c0a6e56cd GIT binary patch literal 15086 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%0DQ7;*gIO6&{5~WC0|U zBdY4pCK0qhsV2_kj!Zsjno9i&!=T=C75KXy`?g-}>bOhK!RrI=e4c_$l9rCa#B0 zXb(glJOJO;hbqoQLA;3}@d6C5AuE{&t19Boc6|+QFRpZgL70hYgOI-z--74(;V&{hV!W-FzG=$b?yP)fO%lAr5-ry zcVpMI5l($h{XtK`rQDwLkg)w-icd$%DgW64e(YFix+K72@I<&=Bby<;>(PDnsix07 zL!YVf)3B?*if;EQhYAmYz%meK5xwB%;&9o<)F7aXd+0h+tzPPAZU2d>chEESMSH2w zX(R$ao&vu&?J&H)1+u+UiGQGZ5Es$aLtt647e05d!h8J|*z_I^nRgb|Ttehc{ES2Hc=%DR)tmBL^UKuFN9!slyLqc0;%oL!hw+{L zQ2zWJF{1`UL&pZP(7x~p41VHjZ#hY!F#KSR#zUFl;$*B}YM9ywyy6qk4oIOocQKq^ z)Ik4SF=TtDs%*&c*mB|}R>8Qe2-Y{4{`6baLvasD2aX|j9`Q~CCc`&r6kG!b!##8e z{JV`p_|Q)fzhpg`cvEyh(o#Q0keG5TR0`Zu*8 zh3Xc8Esb!izD``m_rLYSn9#ix)R)NgbSf%$!{^6l^xC=!rWR)4ImW}DYTlv#?v>7V zmWf(AI{%WtW#$s~$$Vj8TuPp!xpA}l0_qn$NO^lu3X;}QU;WSRFd-h7;Sd?!TV2>r zJ&Z@FFX#-#%1TuqkuUW?1lB%Bc*D<#p`0Uza4(a?1yKFMx4wn?ackg7{lHA$55sLR zJvc_ksL!8xH}i*Q5cTF2`fl3<3mYqN?Ub;N?v3a$zeYzp>%mm_{8Rq?{k4cl?UCL(=R8)SJ-=m*y%}Kd{xg9n`;40@r&NsYl};<@}A5vp!Yf)@YKM z>1Ad{6rD$sk7Yg!pk63%ibpPY&%^rqAy}T>3A@Up@P1m0OuBcUxdl)dn}X|@2;1mB zNKytid$_s&;~Gpv!76@qudGiTJ-u7i7Onxi>>6}N6d-KmWTcgyMD*hpgf!Kt_OILh za(IwU`!+PG`qaV+dn@ax7n^cFW-$48IQdPcKg5Twai^Xy<~vM`SGG2y2h{}ze}5DS zeX^l1wWPpwx`Jud4>9v`JEjviF^zggXFmB=HKtQ- za^{Op%%MKQIh0>~eC`ZJkxxh&I|d%UK`=6NhOtWo(-VWR=(s1{5~6m61^FjC)9)NU zB-1~AFJ*bLFmC8bkC0y3vs?y_t~8pt`i*E8^?VDIp(qllFKO zcstHPt@s$U=k9{Rm=!RlJk}_)Z$J2v^jI-v3ZkxqQ-nQEnw2@6Q0u+KQo=a656xZ zLQ20G(^-2GvK2?5n6eQT{ieeuu^;@E)F&2|gz%UIBqXOGIVGhzCOWz#F+O%eY-FgD zw}-o~hD7s!w14CM`SdP1#m$&JNj*9L6M4$CxnT+OmggnSS+X`Re`;C8&@s1^Ik~kl zy)zpVx~DcKcT22EPK>{v7#DLWG&o?Xx0mOD$na2~sE9CGbX3&8)j#vLABg!)6!Z6g J$N%4Y_%HfV_VWM$ literal 0 HcmV?d00001 diff --git a/frontend/src/__generated__/models/SearchRomSchema.ts b/frontend/src/__generated__/models/SearchRomSchema.ts index efff5ede7..b0c233972 100644 --- a/frontend/src/__generated__/models/SearchRomSchema.ts +++ b/frontend/src/__generated__/models/SearchRomSchema.ts @@ -6,11 +6,13 @@ export type SearchRomSchema = { igdb_id?: (number | null); moby_id?: (number | null); + ss_id?: (number | null); slug: string; name: string; summary: string; igdb_url_cover?: string; moby_url_cover?: string; + ss_url_cover?: string; platform_id: number; }; diff --git a/frontend/src/components/Settings/Footer.vue b/frontend/src/components/Settings/Footer.vue index 3c6cd54ce..39a3a9e54 100644 --- a/frontend/src/components/Settings/Footer.vue +++ b/frontend/src/components/Settings/Footer.vue @@ -21,10 +21,9 @@ const heartbeatStore = storeHeartbeat(); >Github{{ t("settings.join-discord") }} { if ( (rom.igdb_id && isIGDBFiltered.value) || - (rom.moby_id && isMobyFiltered.value) + (rom.moby_id && isMobyFiltered.value) || + rom.ss_id ) { return true; } @@ -100,7 +101,8 @@ async function searchRom() { filteredMatchedRoms.value = matchedRoms.value.filter((rom) => { if ( (rom.igdb_id && isIGDBFiltered.value) || - (rom.moby_id && isMobyFiltered.value) + (rom.moby_id && isMobyFiltered.value) || + rom.ss_id ) { return true; } @@ -138,6 +140,11 @@ function showSources(matchedRom: SearchRomSchema) { name: "Mobygames", logo_path: "/assets/scrappers/moby.png", }); + sources.value.push({ + url_cover: matchedRom.ss_url_cover, + name: "ScreenScraper", + logo_path: "/assets/scrappers/ss.ico", + }); } function selectCover(source: MatchedSource) { @@ -288,6 +295,35 @@ onBeforeUnmount(() => { > + + (); size="30" rounded="1" > - + diff --git a/frontend/src/components/common/Game/Dialog/MatchRom.vue b/frontend/src/components/common/Game/Dialog/MatchRom.vue index 928583478..2653f9d31 100644 --- a/frontend/src/components/common/Game/Dialog/MatchRom.vue +++ b/frontend/src/components/common/Game/Dialog/MatchRom.vue @@ -6,16 +6,17 @@ import romApi from "@/services/api/rom"; import storeGalleryView from "@/stores/galleryView"; import storeHeartbeat from "@/stores/heartbeat"; import storeRoms, { type SimpleRom } from "@/stores/roms"; +import storePlatforms from "@/stores/platforms"; import type { Events } from "@/types/emitter"; import type { Emitter } from "mitt"; -import { inject, onBeforeUnmount, ref } from "vue"; +import { inject, onBeforeUnmount, ref, computed } from "vue"; import { useRoute } from "vue-router"; import { useDisplay, useTheme } from "vuetify"; import { useI18n } from "vue-i18n"; type MatchedSource = { url_cover: string | undefined; - name: "IGDB" | "Mobygames" | "ScreenScraper"; + name: "IGDB" | "Mobygames" | "Screenscraper"; logo_path: string; }; @@ -26,6 +27,7 @@ const show = ref(false); const rom = ref(null); const romsStore = storeRoms(); const galleryViewStore = storeGalleryView(); +const platfotmsStore = storePlatforms(); const searching = ref(false); const route = useRoute(); const searchTerm = ref(""); @@ -42,9 +44,17 @@ const sources = ref([]); const heartbeat = storeHeartbeat(); const isIGDBFiltered = ref(true); const isMobyFiltered = ref(true); +const isSSFiltered = ref(true); +const computedAspectRatio = computed(() => { + const ratio = + platfotmsStore.getAspectRatio(rom.value?.platform_id) || + galleryViewStore.defaultAspectRatioCover; + return parseFloat(ratio.toString()); +}); emitter?.on("showMatchRomDialog", (romToSearch) => { rom.value = romToSearch; show.value = true; + matchedRoms.value = []; // Use name as search term, only when it's matched // Otherwise use the filename without tags and extensions @@ -67,12 +77,17 @@ function toggleSourceFilter(source: MatchedSource["name"]) { heartbeat.value.METADATA_SOURCES.MOBY_API_ENABLED ) { isMobyFiltered.value = !isMobyFiltered.value; + } else if ( + source == "Screenscraper" && + heartbeat.value.METADATA_SOURCES.SS_API_ENABLED + ) { + isSSFiltered.value = !isSSFiltered.value; } filteredMatchedRoms.value = matchedRoms.value.filter((rom) => { if ( (rom.igdb_id && isIGDBFiltered.value) || (rom.moby_id && isMobyFiltered.value) || - rom.ss_id + (rom.ss_id && isSSFiltered.value) ) { return true; } @@ -102,7 +117,7 @@ async function searchRom() { if ( (rom.igdb_id && isIGDBFiltered.value) || (rom.moby_id && isMobyFiltered.value) || - rom.ss_id + (rom.ss_id && isSSFiltered.value) ) { return true; } @@ -130,21 +145,31 @@ function showSources(matchedRom: SearchRomSchema) { } showSelectSource.value = true; selectedMatchRom.value = matchedRom; - sources.value.push({ - url_cover: matchedRom.igdb_url_cover, - name: "IGDB", - logo_path: "/assets/scrappers/igdb.png", - }); - sources.value.push({ - url_cover: matchedRom.moby_url_cover, - name: "Mobygames", - logo_path: "/assets/scrappers/moby.png", - }); - sources.value.push({ - url_cover: matchedRom.ss_url_cover, - name: "ScreenScraper", - logo_path: "/assets/scrappers/ss.ico", - }); + sources.value = []; + if (matchedRom.igdb_url_cover) { + sources.value.push({ + url_cover: matchedRom.igdb_url_cover, + name: "IGDB", + logo_path: "/assets/scrappers/igdb.png", + }); + } + if (matchedRom.moby_url_cover) { + sources.value.push({ + url_cover: matchedRom.moby_url_cover, + name: "Mobygames", + logo_path: "/assets/scrappers/moby.png", + }); + } + if (matchedRom.ss_url_cover) { + sources.value.push({ + url_cover: matchedRom.ss_url_cover, + name: "Screenscraper", + logo_path: "/assets/scrappers/ss.png", + }); + } + if (sources.value.length == 1) { + selectedCover.value = sources.value[0]; + } } function selectCover(source: MatchedSource) { @@ -217,7 +242,6 @@ function closeDialog() { selectedCover.value = undefined; selectedMatchRom.value = undefined; renameAsSource.value = false; - matchedRoms.value = []; } onBeforeUnmount(() => { @@ -300,27 +324,26 @@ onBeforeUnmount(() => { class="tooltip" transition="fade-transition" :text=" - heartbeat.value.METADATA_SOURCES.IGDB_API_ENABLED + heartbeat.value.METADATA_SOURCES.SS_API_ENABLED ? 'Filter Screenscraper matches' : 'Screenscraper source is not enabled' " open-delay="500" > @@ -406,7 +429,7 @@ onBeforeUnmount(() => { - + {{ @@ -437,7 +460,7 @@ onBeforeUnmount(() => { ? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png` : source.url_cover " - :aspect-ratio="galleryViewStore.defaultAspectRatioCover" + :aspect-ratio="computedAspectRatio" cover lazy > diff --git a/frontend/src/services/api/rom.ts b/frontend/src/services/api/rom.ts index 23bfa69b0..35af743eb 100644 --- a/frontend/src/services/api/rom.ts +++ b/frontend/src/services/api/rom.ts @@ -151,6 +151,7 @@ async function updateRom({ const formData = new FormData(); if (rom.igdb_id) formData.append("igdb_id", rom.igdb_id.toString()); if (rom.moby_id) formData.append("moby_id", rom.moby_id.toString()); + if (rom.ss_id) formData.append("ss_id", rom.ss_id.toString()); formData.append("name", rom.name || ""); formData.append("file_name", rom.file_name); formData.append("summary", rom.summary || ""); diff --git a/frontend/src/styles/themes.ts b/frontend/src/styles/themes.ts index d52a75053..0947301fb 100644 --- a/frontend/src/styles/themes.ts +++ b/frontend/src/styles/themes.ts @@ -31,7 +31,7 @@ export const light = { colors: { primary: "#ffffff", secondary: "#ffffff", - terciary: "#f4f4f4f", + terciary: "#f4f4f4", background: "#ffffff", tooltip: "#ffffff", diff --git a/frontend/src/views/Scan.vue b/frontend/src/views/Scan.vue index 8fbd0b818..47456296f 100644 --- a/frontend/src/views/Scan.vue +++ b/frontend/src/views/Scan.vue @@ -30,11 +30,17 @@ const metadataOptions = computed(() => [ disabled: !heartbeat.value.METADATA_SOURCES?.IGDB_API_ENABLED, }, { - name: "MobyGames", + name: "Mobygames", value: "moby", logo_path: "/assets/scrappers/moby.png", disabled: !heartbeat.value.METADATA_SOURCES?.MOBY_API_ENABLED, }, + { + name: "Screenscraper", + value: "ss", + logo_path: "/assets/scrappers/ss.png", + disabled: !heartbeat.value.METADATA_SOURCES?.SS_API_ENABLED, + }, ]); // Use the computed metadataOptions to filter out disabled sources const metadataSources = ref(metadataOptions.value.filter((s) => !s.disabled)); @@ -192,11 +198,7 @@ async function stopScan() { From 5f048a77ff176546eb0a370c538138ad2312d807 Mon Sep 17 00:00:00 2001 From: zurdi Date: Tue, 4 Feb 2025 14:50:51 +0000 Subject: [PATCH 13/52] fix: center background image in Auth layout --- frontend/src/layouts/Auth.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/layouts/Auth.vue b/frontend/src/layouts/Auth.vue index ff1ec4b5f..83f9af008 100644 --- a/frontend/src/layouts/Auth.vue +++ b/frontend/src/layouts/Auth.vue @@ -30,6 +30,7 @@ const heartbeatStore = storeHeartbeat(); #container { background-image: url("/assets/auth_background.svg"); background-size: cover; + background-position: center; max-width: 100vw; } From 78eb3f9ab01afa02dde24524714bf1fb5886ccee Mon Sep 17 00:00:00 2001 From: zurdi Date: Tue, 4 Feb 2025 15:24:20 +0000 Subject: [PATCH 14/52] fix: adapt to new fs_name rom property --- backend/handler/scan_handler.py | 2 +- frontend/src/components/common/Game/Dialog/MatchRom.vue | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/handler/scan_handler.py b/backend/handler/scan_handler.py index 68e2cdc20..3453449d2 100644 --- a/backend/handler/scan_handler.py +++ b/backend/handler/scan_handler.py @@ -303,7 +303,7 @@ async def fetch_ss_rom(): ) ): return await meta_ss_handler.get_rom( - rom_attrs["file_name"], platform_ss_id=platform.ss_id + rom_attrs["fs_name"], platform_ss_id=platform.ss_id ) return SSGamesRom(ss_id=None) diff --git a/frontend/src/components/common/Game/Dialog/MatchRom.vue b/frontend/src/components/common/Game/Dialog/MatchRom.vue index 02d512354..c3b4f2b3b 100644 --- a/frontend/src/components/common/Game/Dialog/MatchRom.vue +++ b/frontend/src/components/common/Game/Dialog/MatchRom.vue @@ -380,6 +380,7 @@ onBeforeUnmount(() => { @click="searchRom()" class="bg-toplayer" variant="text" + rounded="0" icon="mdi-search-web" block :disabled="searching" From 6f909163e26f7b91a209e08d18563dd1785ac754 Mon Sep 17 00:00:00 2001 From: zurdi Date: Tue, 4 Feb 2025 15:31:29 +0000 Subject: [PATCH 15/52] fix: simplify scan stats display logic in Scan.vue --- frontend/src/views/Scan.vue | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/frontend/src/views/Scan.vue b/frontend/src/views/Scan.vue index ed97885b8..4d5793dab 100644 --- a/frontend/src/views/Scan.vue +++ b/frontend/src/views/Scan.vue @@ -369,17 +369,12 @@ async function stopScan() {
- + mdi-controller {{ t("scan.platforms-scanned-n", scanningPlatforms.length) From b55058868d2837a1ab7719dd546e45fb8420ed74 Mon Sep 17 00:00:00 2001 From: zurdi Date: Tue, 4 Feb 2025 17:06:46 +0000 Subject: [PATCH 16/52] added png version of auth background --- frontend/assets/auth_background_static.png | Bin 0 -> 12138 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 frontend/assets/auth_background_static.png diff --git a/frontend/assets/auth_background_static.png b/frontend/assets/auth_background_static.png new file mode 100644 index 0000000000000000000000000000000000000000..92c8838df24c74c0f4c540cdb1205feb2312f435 GIT binary patch literal 12138 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νd#h-&yG?W*>sgqDk_Do?+=jpV+i+uR-WMcK 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 literal 0 HcmV?d00001 From 2ee6026614d6bb4491202ca4883f0665ae4e59a3 Mon Sep 17 00:00:00 2001 From: zurdi Date: Wed, 5 Feb 2025 12:32:16 +0000 Subject: [PATCH 17/52] Remove SCREENSCRAPER_API_KEY from configuration and handler, replace with decoded credentials --- backend/config/__init__.py | 1 - backend/handler/metadata/ss_handler.py | 17 +++++++++++------ env.template | 1 - 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/backend/config/__init__.py b/backend/config/__init__.py index fdf02ead5..c19103196 100644 --- a/backend/config/__init__.py +++ b/backend/config/__init__.py @@ -60,7 +60,6 @@ def str_to_bool(value: str) -> bool: # SCREENSCRAPER SCREENSCRAPER_USER: Final = os.environ.get("SCREENSCRAPER_USER", "") SCREENSCRAPER_PASSWORD: Final = os.environ.get("SCREENSCRAPER_PASSWORD", "") -SCREENSCRAPER_API_KEY: Final = os.environ.get("SCREENSCRAPER_API_KEY", "") # STEAMGRIDDB STEAMGRIDDB_API_KEY: Final = os.environ.get("STEAMGRIDDB_API_KEY", "") diff --git a/backend/handler/metadata/ss_handler.py b/backend/handler/metadata/ss_handler.py index 42f78ae0c..833a6f0d6 100644 --- a/backend/handler/metadata/ss_handler.py +++ b/backend/handler/metadata/ss_handler.py @@ -1,4 +1,5 @@ import asyncio +import base64 import http import re from typing import Final, NotRequired, TypedDict @@ -7,7 +8,7 @@ import httpx import pydash import yarl -from config import SCREENSCRAPER_API_KEY, SCREENSCRAPER_PASSWORD, SCREENSCRAPER_USER +from config import SCREENSCRAPER_PASSWORD, SCREENSCRAPER_USER from fastapi import HTTPException, status from logger.logger import log from unidecode import unidecode as uc @@ -23,6 +24,8 @@ # 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() PS1_SS_ID: Final = 57 PS2_SS_ID: Final = 58 @@ -181,8 +184,8 @@ async def _request(self, url: str, timeout: int = 120) -> dict: authorized_url = yarl.URL(url).update_query( ssid=SCREENSCRAPER_USER, sspassword=SCREENSCRAPER_PASSWORD, - devid=SCREENSCRAPER_USER, - devpassword=SCREENSCRAPER_API_KEY, + devid=SS_DEV_ID, + devpassword=SS_DEV_PASSWORD, softname="romm", output="json", ) @@ -209,14 +212,16 @@ async def _request(self, url: str, timeout: int = 120) -> dict: return {} return res.json() except httpx.NetworkError as exc: - log.critical("Connection error: can't connect to Mobygames", exc_info=True) + log.critical( + "Connection error: can't connect to Screenscrapper", exc_info=True + ) raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Can't connect to Mobygames, check your internet connection", + detail="Can't connect to Screenscrapper, check your internet connection", ) from exc except httpx.HTTPStatusError as err: if err.response.status_code == http.HTTPStatus.UNAUTHORIZED: - # Sometimes Mobygames returns 401 even with a valid API key + # Sometimes Screenscrapper returns 401 even with a valid API key log.error(err) return {} elif err.response.status_code == http.HTTPStatus.TOO_MANY_REQUESTS: diff --git a/env.template b/env.template index 511c9a956..6459f2587 100644 --- a/env.template +++ b/env.template @@ -15,7 +15,6 @@ MOBYGAMES_API_KEY= # Screenscraper SCREENSCRAPER_USER= SCREENSCRAPER_PASSWORD= -SCREENSCRAPER_API_KEY= # SteamGridDB STEAMGRIDDB_API_KEY= From fc63e68a42c0ef59497faf004d3eb2f559de4dc2 Mon Sep 17 00:00:00 2001 From: zurdi Date: Wed, 5 Feb 2025 17:42:32 +0000 Subject: [PATCH 18/52] added grayscale version isotipos --- .../romm_logo_xbox_one_circle_grayscale.png | Bin 0 -> 30950 bytes .../romm_logo_xbox_one_circle_grayscale.svg | 1 + 2 files changed, 1 insertion(+) create mode 100644 frontend/assets/logos/romm_logo_xbox_one_circle_grayscale.png create mode 100644 frontend/assets/logos/romm_logo_xbox_one_circle_grayscale.svg diff --git a/frontend/assets/logos/romm_logo_xbox_one_circle_grayscale.png b/frontend/assets/logos/romm_logo_xbox_one_circle_grayscale.png new file mode 100644 index 0000000000000000000000000000000000000000..24fe4240c63f87ea3780dff4aa78b8c286cfab79 GIT binary patch literal 30950 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 \ No newline at end of file From 8a27d62da2c1dd2710f7bbb36a7f8f49359eac22 Mon Sep 17 00:00:00 2001 From: danblu3 Date: Wed, 5 Feb 2025 23:07:24 +0000 Subject: [PATCH 19/52] Updated IDs for screenscraper --- backend/handler/metadata/ss_handler.py | 183 +++++++++++++++++++++++-- 1 file changed, 175 insertions(+), 8 deletions(-) diff --git a/backend/handler/metadata/ss_handler.py b/backend/handler/metadata/ss_handler.py index 833a6f0d6..4e4937eee 100644 --- a/backend/handler/metadata/ss_handler.py +++ b/backend/handler/metadata/ss_handler.py @@ -464,15 +464,182 @@ class SlugToSSId(TypedDict): name: str -SLUG_TO_SS_ID: dict[str, SlugToSSId] = { +SLUG_TO_SS_ID: dict[str, SlugToSSId] = SLUG_TO_SS_ID: dict[str, SlugToSSId] = { "3do": {"id": 29, "name": "3DO"}, - "acorn-electron": {"id": 85, "name": "Acorn Electron"}, - "dc": {"id": 23, "name": "Dreamcast"}, - "gb": {"id": 9, "name": "Game Boy"}, - "gba": {"id": 12, "name": "Game Boy Advance"}, - "gbc": {"id": 10, "name": "Game Boy Color"}, - "switch": {"id": 225, "name": "Nintendo Switch"}, - "win": {"id": 138, "name": "Microsoft Windows (PC)"}, + "amiga": {"id": 64, "name": "Amiga"}, + "amiga-cd32": {"id": 134, "name": "Amiga CD"}, + "cpc": {"id": 60, "name": "CPC"}, + "acpc": {"id": 60, "name": "CPC"}, # IGDB + "android": {"id": 63, "name": "Android"}, + "apple2": {"id": 86, "name": "Apple II"}, + "appleii": {"id": 86, "name": "Apple II"}, # IGDB + "apple2gs": {"id": 217, "name": "Apple IIGS"}, + "apple-iigs": {"id": 51, "name": "Apple IIGS"}, # IGDB + "arcadia-2001": {"id": 94, "name": "Arcadia 2001"}, + "arduboy": {"id": 263, "name": "Arduboy"}, + "atari-2600": {"id": 26, "name": "Atari 2600"}, + "atari2600": {"id": 26, "name": "Atari 2600"}, # IGDB + "atari-5200": {"id": 40, "name": "Atari 5200"}, + "atari5200": {"id": 40, "name": "Atari 5200"}, # IGDB + "atari-7800": {"id": 41, "name": "Atari 7800"}, + "atari7800": {"id": 41, "name": "Atari 7800"}, # IGDB + "atari-8-bit": {"id": 43, "name": "Atari 8bit"}, + "atari8bit": {"id": 43, "name": "Atari 8bit"}, # IGDB + "atari-st": {"id": 42, "name": "Atari ST"}, + "atom": {"id": 36, "name": "Atom"}, + "bbc-micro": {"id": 37, "name": "BBC Micro"}, + "bbcmicro": {"id": 37, "name": "BBC Micro"}, # IGDB + "bally-astrocade": {"id": 44, "name": "Astrocade"}, + "astrocade": {"id": 44, "name": "Astrocade"}, # IGDB + "cd-i": {"id": 133, "name": "CD-i"}, + "philips-cd-i": {"id": 133, "name": "CD-i"}, # IGDB + "cdtv": {"id": 129, "name": "Amiga CDTV"}, + "commodore-cdtv": {"id": 129, "name": "Amiga CDTV"}, # IGDB + "camputers-lynx": {"id": 88, "name": "Camputers Lynx"}, + "casio-loopy": {"id": 98, "name": "Loopy"}, + "casio-pv-1000": {"id": 74, "name": "PV-1000"}, + "channel-f": {"id": 80, "name": "Channel F"}, + "fairchild-channel-f": {"id": 80, "name": "Channel F"}, # IGDB + "colecoadam": {"id": 89, "name": "Adam"}, + "colecovision": {"id": 48, "name": "Colecovision"}, + "colour-genie": {"id": 92, "name": "EG2000 Colour Genie"}, + "c128": {"id": 66, "name": "Commodore 64"}, + "commodore-16-plus4": {"id": 99, "name": "Plus/4"}, + "c-plus-4": {"id": 99, "name": "Plus/4"}, # IGDB + "c16": {"id": 99, "name": "Plus/4"}, # IGDB + "c64": {"id": 66, "name": "Commodore 64"}, + "pet": {"id": 240, "name": "PET"}, + "cpet": {"id": 240, "name": "PET"}, # IGDB + "creativision": {"id": 241, "name": "CreatiVision"}, + "dos": {"id": 135, "name": "PC Dos"}, + "dragon-3264": {"id": 91, "name": "Dragon 32/64"}, + "dragon-32-slash-64": {"id": 91, "name": "Dragon 32/64"}, # IGDB + "dreamcast": {"id": 23, "name": "Dreamcast"}, + "dc": {"id": 23, "name": "Dreamcast"}, # IGDB + "electron": {"id": 85, "name": "Electron"}, + "acorn-electron": {"id": 85, "name": "Electron"}, # IGDB + "epoch-game-pocket-computer": {"id": 95, "name": "Game Pocket Computer"}, + "epoch-super-cassette-vision": {"id": 67, "name": "Super Cassette Vision"}, + "exelvision": {"id": 96, "name": "EXL 100"}, + "exidy-sorcerer": {"id": 165, "name": "Exidy"}, + "fmtowns": {"id": 253, "name": "FM Towns"}, + "fm-towns": {"id": 253, "name": "FM Towns"}, # IGDB + "fm-7": {"id": 97, "name": "FM-7"}, + "g-and-w": {"id": 52, "name": "Game & Watch"}, # IGDB (Game & Watch) + "gp32": {"id": 101, "name": "GP32"}, + "gameboy": {"id": 9, "name": "Game Boy"}, + "gb": {"id": 9, "name": "Game Boy"}, # IGDB + "gameboy-advance": {"id": 12, "name": "Game Boy Advance"}, + "gba": {"id": 12, "name": "Game Boy Advance"}, # IGDB + "gameboy-color": {"id": 10, "name": "Game Boy Color"}, + "gbc": {"id": 10, "name": "Game Boy Color"}, # IGDB + "game-gear": {"id": 21, "name": "Game Gear"}, + "gamegear": {"id": 21, "name": "Game Gear"}, # IGDB + "game-com": {"id": 121, "name": "Game.com"}, + "game-dot-com": {"id": 121, "name": "Game.com"}, # IGDB + "gamecube": {"id": 13, "name": "GameCube"}, + "ngc": {"id": 13, "name": "GameCube"}, # IGDB + "genesis": {"id": 1, "name": "Megadrive"}, + "genesis-slash-megadrive": {"id": 1, "name": "Megadrive"}, + "intellivision": {"id": 115, "name": "Intellivision"}, + "jaguar": {"id": 27, "name": "Jaguar"}, + "jupiter-ace": {"id": 126, "name": "Jupiter Ace"}, + "linux": {"id": 145, "name": "Linux"}, + "lynx": {"id": 28, "name": "Lynx"}, + "msx": {"id": 113, "name": "MSX"}, + "macintosh": {"id": 146, "name": "Mac OS"}, + "mac": {"id": 146, "name": "Mac OS"}, # IGDB + "ngage": {"id": 30, "name": "N-Gage"}, + "nes": {"id": 3, "name": "NES"}, + "famicom": {"id": 3, "name": "NES"}, + "neo-geo": {"id": 142, "name": "Neo-Geo"}, + "neogeoaes": {"id": 142, "name": "Neo-Geo"}, # IGDB + "neogeomvs": {"id": 68, "name": "Neo-Geo MVS"}, # IGDB + "neo-geo-cd": {"id": 70, "name": "Neo-Geo CD"}, + "neo-geo-pocket": {"id": 25, "name": "Neo-Geo Pocket"}, + "neo-geo-pocket-color": {"id": 82, "name": "Neo-Geo Pocket Color"}, + "3ds": {"id": 17, "name": "Nintendo 3DS"}, + "n64": {"id": 14, "name": "Nintendo 64"}, + "nintendo-ds": {"id": 15, "name": "Nintendo DS"}, + "nds": {"id": 15, "name": "Nintendo DS"}, # IGDB + "nintendo-dsi": {"id": 15, "name": "Nintendo DS"}, + "switch": {"id": 225, "name": "Switch"}, + "odyssey-2": {"id": 104, "name": "Videopac G7000"}, + "odyssey-2-slash-videopac-g7000": {"id": 104, "name": "Videopac G7000"}, + "oric": {"id": 131, "name": "Oric 1 / Atmos"}, + "pc88": {"id": 221, "name": "NEC PC-8801"}, + "pc-8800-series": {"id": 221, "name": "NEC PC-8801"}, # IGDB + "pc98": {"id": 208, "name": "NEC PC-9801"}, + "pc-9800-series": {"id": 208, "name": "NEC PC-9801"}, # IGDB + "pc-fx": {"id": 72, "name": "PC-FX"}, + "pico": {"id": 234, "name": "Pico-8"}, + "ps-vita": {"id": 62, "name": "PS Vita"}, + "psvita": {"id": 62, "name": "PS Vita"}, # IGDB + "psp": {"id": 61, "name": "PSP"}, + "palmos": {"id": 219, "name": "Palm OS"}, + "palm-os": {"id": 219, "name": "Palm OS"}, # IGDB + "philips-vg-5000": {"id": 261, "name": "Philips VG 5000"}, + "playstation": {"id": 57, "name": "Playstation"}, + "ps": {"id": 57, "name": "Playstation"}, # IGDB + "ps2": {"id": 58, "name": "Playstation 2"}, + "ps3": {"id": 59, "name": "Playstation 3"}, + "playstation-4": {"id": 60, "name": "Playstation 4"}, + "ps4--1": {"id": 60, "name": "Playstation 4"}, # IGDB + "playstation-5": {"id": 284, "name": "Playstation 5"}, + "ps5": {"id": 284, "name": "Playstation 5"}, # IGDB + "pokemon-mini": {"id": 211, "name": "Pokémon mini"}, + "sam-coupe": {"id": 213, "name": "MGT SAM Coupé"}, + "sega-32x": {"id": 19, "name": "Megadrive 32X"}, + "sega32": {"id": 19, "name": "Megadrive 32X"}, # IGDB + "sega-cd": {"id": 20, "name": "Mega-CD"}, + "segacd": {"id": 20, "name": "Mega-CD"}, # IGDB + "sega-master-system": {"id": 2, "name": "Master System"}, + "sms": {"id": 2, "name": "Master System"}, # IGDB + "sega-pico": {"id": 250, "name": "Sega Pico"}, + "sega-saturn": {"id": 22, "name": "Saturn"}, + "saturn": {"id": 22, "name": "Saturn"}, # IGDB + "sg-1000": {"id": 109, "name": "SG-1000"}, + "snes": {"id": 4, "name": "Super Nintendo"}, + "sharp-x1": {"id": 220, "name": "Sharp X1"}, + "x1": {"id": 220, "name": "Sharp X1"}, # IGDB + "sharp-x68000": {"id": 79, "name": "Sharp X68000"}, + "spectravideo": {"id": 218, "name": "Spectravideo"}, + "super-acan": {"id": 100, "name": "Super A'can"}, + "supergrafx": {"id": 105, "name": "PC Engine SuperGrafx"}, + "supervision": {"id": 207, "name": "Watara Supervision"}, + "ti-99": {"id": 205, "name": "TI-99/4A"}, # IGDB + "trs-80-coco": {"id": 144, "name": "TRS-80 Color Computer"}, + "trs-80-color-computer": {"id": 144, "name": "TRS-80 Color Computer"}, # IGDB + "taito-x-55": {"id": 112, "name": "Type X"}, + "thomson-mo": {"id": 141, "name": "Thomson MO/TO"}, + "thomson-mo5": {"id": 141, "name": "Thomson MO/TO"}, + "thomson-to": {"id": 141, "name": "Thomson MO/TO"}, + "turbografx-cd": {"id": 114, "name": "PC Engine CD-Rom"}, + "turbografx-16-slash-pc-engine-cd": {"id": 114, "name": "PC Engine CD-Rom"}, + "turbo-grafx": {"id": 31, "name": "PC Engine"}, + "turbografx16--1": {"id": 31, "name": "PC Engine"}, # IGDB + "vsmile": {"id": 120, "name": "V.Smile"}, + "vic-20": {"id": 73, "name": "Vic-20"}, + "vectrex": {"id": 102, "name": "Vectrex"}, + "videopac-g7400": {"id": 104, "name": "Videopac G7000"}, + "virtual-boy": {"id": 11, "name": "Virtual Boy"}, + "virtualboy": {"id": 11, "name": "Virtual Boy"}, + "wii": {"id": 18, "name": "Wii"}, + "wii-u": {"id": 18, "name": "Wii U"}, + "wiiu": {"id": 18, "name": "Wii U"}, + "windows": {"id": 3, "name": "Windows"}, + "win": {"id": 138, "name": "PC Windows"}, # IGDB + "win3x": {"id": 136, "name": "PC Win3.xx"}, + "wonderswan": {"id": 45, "name": "WonderSwan"}, + "wonderswan-color": {"id": 46, "name": "WonderSwan Color"}, + "xbox": {"id": 32, "name": "Xbox"}, + "xbox360": {"id": 33, "name": "Xbox 360"}, + "xbox-one": {"id": 34, "name": "Xbox One"}, + "xboxone": {"id": 34, "name": "Xbox One"}, + "z-machine": {"id": 215, "name": "Z-Machine"}, + "zx-spectrum": {"id": 76, "name": "ZX Spectrum"}, + "zx81": {"id": 77, "name": "ZX81"}, + "sinclair-zx81": {"id": 77, "name": "ZX81"}, # IGDB } # Reverse lookup From ad179a90ebe6e1ac5ae5145f5e2912dee116469a Mon Sep 17 00:00:00 2001 From: zurdi Date: Thu, 6 Feb 2025 01:04:23 +0000 Subject: [PATCH 20/52] Fix SLUG_TO_SS_ID declaration and update avatar style in Scan.vue --- backend/handler/metadata/ss_handler.py | 2 +- frontend/src/views/Scan.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/handler/metadata/ss_handler.py b/backend/handler/metadata/ss_handler.py index 4e4937eee..d71459d9f 100644 --- a/backend/handler/metadata/ss_handler.py +++ b/backend/handler/metadata/ss_handler.py @@ -464,7 +464,7 @@ class SlugToSSId(TypedDict): name: str -SLUG_TO_SS_ID: dict[str, SlugToSSId] = SLUG_TO_SS_ID: dict[str, SlugToSSId] = { +SLUG_TO_SS_ID: dict[str, SlugToSSId] = { "3do": {"id": 29, "name": "3DO"}, "amiga": {"id": 64, "name": "Amiga"}, "amiga-cd32": {"id": 134, "name": "Amiga CD"}, diff --git a/frontend/src/views/Scan.vue b/frontend/src/views/Scan.vue index 4d5793dab..b919ba364 100644 --- a/frontend/src/views/Scan.vue +++ b/frontend/src/views/Scan.vue @@ -317,7 +317,7 @@ async function stopScan() {