import json from datetime import datetime from typing import Any, NotRequired, TypedDict import httpx import pydash from fastapi import HTTPException, status from config import DEV_MODE, HASHEOUS_API_ENABLED from logger.logger import log from models.rom import RomFile from utils import get_version from utils.context import ctx_httpx_client from .base_handler import BaseRom, MetadataHandler from .base_handler import UniversalPlatformSlug as UPS from .igdb_handler import ( IGDB_AGE_RATINGS, IGDBMetadata, IGDBMetadataPlatform, ) from .ra_handler import RAMetadata class HasheousMetadata(TypedDict): tosec_match: bool mame_arcade_match: bool mame_mess_match: bool nointro_match: bool redump_match: bool whdload_match: bool ra_match: bool fbneo_match: bool puredos_match: bool class HasheousPlatform(TypedDict): slug: str hasheous_id: int | None name: NotRequired[str] igdb_id: NotRequired[int | None] tgdb_id: NotRequired[int | None] ra_id: NotRequired[int | None] class HasheousRom(BaseRom): hasheous_id: int | None igdb_id: NotRequired[int | None] slug: NotRequired[str] igdb_metadata: NotRequired[IGDBMetadata] ra_id: NotRequired[int | None] ra_metadata: NotRequired[RAMetadata] tgdb_id: NotRequired[int | None] hasheous_metadata: NotRequired[HasheousMetadata] ACCEPTABLE_FILE_EXTENSIONS_BY_PLATFORM_SLUG = {UPS.DC: ["cue", "bin"]} def extract_metadata_from_igdb_rom(rom: dict[str, Any]) -> IGDBMetadata: return IGDBMetadata( { "youtube_video_id": ( list(rom["videos"].values())[0]["video_id"] if rom.get("videos") else None ), "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": ( int( datetime.fromisoformat( rom["first_release_date"].replace("Z", "+00:00") ).timestamp() ) if rom.get("first_release_date") else 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.compact( pydash.map_(rom.get("involved_companies", {}), "company.name") ), "platforms": [ IGDBMetadataPlatform(igdb_id=p.get("id", ""), name=p.get("name", "")) for p in pydash.map_(rom.get("platforms", {})) ], "age_ratings": [ IGDB_AGE_RATINGS[r] for r in pydash.map_(rom.get("age_ratings", {}), "rating_category") if r in IGDB_AGE_RATINGS ], "expansions": [], "dlcs": [], "ports": [], "remakes": [], "remasters": [], "similar_games": [], "expanded_games": [], } ) class HasheousHandler(MetadataHandler): def __init__(self) -> None: self.BASE_URL = ( "https://beta.hasheous.org/api/v1" if DEV_MODE else "https://hasheous.org/api/v1" ) self.healthcheck_endpoint = f"{self.BASE_URL}/HealthCheck" self.platform_endpoint = f"{self.BASE_URL}/Lookup/Platforms" self.games_endpoint = f"{self.BASE_URL}/Lookup/ByHash" self.proxy_igdb_game_endpoint = f"{self.BASE_URL}/MetadataProxy/IGDB/Game" self.proxy_igdb_cover_endpoint = f"{self.BASE_URL}/MetadataProxy/IGDB/Cover" self.proxy_ra_game_endpoint = f"{self.BASE_URL}/MetadataProxy/RA/Game" self.app_api_key = ( "UUvh9ef_CddMM4xXO1iqxl9FqEt764v33LU-UiGFc0P34odXjMP9M6MTeE4JZRxZ" if DEV_MODE else "JNoFBA-jEh4HbxuxEHM6MVzydKoAXs9eCcp2dvcg5LRCnpp312voiWmjuaIssSzS" ) @classmethod def is_enabled(cls) -> bool: """Return whether this metadata handler is enabled.""" return HASHEOUS_API_ENABLED async def heartbeat(self) -> bool: if not self.is_enabled(): return False httpx_client = ctx_httpx_client.get() try: response = await httpx_client.get(self.healthcheck_endpoint) response.raise_for_status() except Exception as e: log.error("Error checking Hasheous API: %s", e) return False return bool(response) async def _request( self, url: str, method: str = "POST", params: dict | None = None, data: dict | None = None, ) -> dict: httpx_client = ctx_httpx_client.get() # Normalize method to uppercase method = method.upper() if method not in ["GET", "POST"]: raise ValueError(f"Unsupported HTTP method: {method}") try: log.debug( "API request: Method=%s, URL=%s, Params=%s, Data=%s", method, url, params, data, ) # Prepare request kwargs request_kwargs = { "url": url, "params": params, "headers": { "Content-Type": "application/json-patch+json", "User-Agent": f"RomM/{get_version()}", "X-Client-API-Key": self.app_api_key, }, "timeout": 120, } # Add method-specific parameters if method == "POST": request_kwargs["json"] = data res = await httpx_client.request(method, **request_kwargs) res.raise_for_status() return res.json() except httpx.HTTPStatusError as exc: # Check if its a 404 error if exc.response.status_code == status.HTTP_404_NOT_FOUND: log.debug("Game not found in Hasheous API") return {} log.error( "Hasheous API returned an error: %s %s", exc.response.status_code, exc.response.text, ) pass except httpx.NetworkError as exc: log.critical("Connection error: can't connect to Hasheous") raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Can't connect to Hasheous, check your internet connection", ) from exc except json.decoder.JSONDecodeError as exc: # Log the error and return an empty dict if the response is not valid JSON log.error(exc) return {} except httpx.TimeoutException: pass return {} def get_platform(self, slug: str) -> HasheousPlatform: if slug not in HASHEOUS_PLATFORM_LIST: return HasheousPlatform(hasheous_id=None, slug=slug) platform = HASHEOUS_PLATFORM_LIST[UPS(slug)] return HasheousPlatform( hasheous_id=platform["id"], slug=slug, name=platform["name"], igdb_id=platform["igdb_id"], tgdb_id=platform["tgdb_id"], ra_id=platform["ra_id"], ) async def lookup_rom(self, platform_slug: str, files: list[RomFile]) -> HasheousRom: fallback_rom = HasheousRom( hasheous_id=None, igdb_id=None, tgdb_id=None, ra_id=None ) if not self.is_enabled(): return fallback_rom filtered_files = [ file for file in files if file.file_size_bytes > 0 and file.is_top_level and ( UPS(platform_slug) not in ACCEPTABLE_FILE_EXTENSIONS_BY_PLATFORM_SLUG or file.file_extension in ACCEPTABLE_FILE_EXTENSIONS_BY_PLATFORM_SLUG[UPS(platform_slug)] ) ] # Select the largest file by size, as it is most likely to be the main ROM file. # This increases the accuracy of metadata lookups, since the largest file is # expected to have the correct and complete hash values for external services. first_file = max(filtered_files, key=lambda f: f.file_size_bytes, default=None) if first_file is None: return fallback_rom md5_hash = first_file.md5_hash sha1_hash = first_file.sha1_hash crc_hash = first_file.crc_hash if not (md5_hash or sha1_hash or crc_hash): log.warning( "No hashes provided for Hasheous lookup. " "At least one of md5_hash, sha1_hash, or crc_hash is required." ) return fallback_rom data = {} if md5_hash: data["mD5"] = md5_hash if sha1_hash: data["shA1"] = sha1_hash if crc_hash: data["crc"] = crc_hash hasheous_game = await self._request( self.games_endpoint, params={ "returnAllSources": "true", "returnFields": "Signatures, Metadata, Attributes", }, data=data, ) if not hasheous_game: return fallback_rom metadata = hasheous_game.get("metadata", []) attributes = hasheous_game.get("attributes", []) signatures = hasheous_game.get("signatures", {}).keys() igdb_id = None tgdb_id = None ra_id = None for meta in metadata: if meta["source"] == "IGDB": try: # TEMP: Hasheous is slowly replacing slugs with IDs igdb_id = int(meta["immutableId"]) except (ValueError, TypeError): log.debug( f"Found an IGDB slug instead of an ID: {meta['immutableId']}" ) pass elif meta["source"] == "TheGamesDB": tgdb_id = meta["immutableId"] elif meta["source"] == "RetroAchievements": ra_id = meta["immutableId"] url_cover = "" for attr in attributes: if attr["attributeName"] == "Logo": url_cover = f"https://hasheous.org{attr['link']}" break return HasheousRom( hasheous_id=hasheous_game["id"], name=hasheous_game.get("name", ""), igdb_id=int(igdb_id) if igdb_id else None, tgdb_id=int(tgdb_id) if tgdb_id else None, ra_id=int(ra_id) if ra_id else None, url_cover=url_cover, hasheous_metadata=HasheousMetadata( tosec_match="TOSEC" in signatures, mame_arcade_match="MAMEArcade" in signatures, mame_mess_match="MAMEMess" in signatures, nointro_match="NoIntros" in signatures, redump_match="Redump" in signatures, whdload_match="WHDLoad" in signatures, ra_match="RetroAchievements" in signatures, fbneo_match="FBNeo" in signatures, puredos_match="PureDOS" in signatures, ), ) async def get_igdb_game(self, hasheous_rom: HasheousRom) -> HasheousRom: if not self.is_enabled(): return hasheous_rom igdb_id = hasheous_rom.get("igdb_id", None) if igdb_id is None: log.info("No IGDB ID provided for Hasheous IGDB game lookup.") return hasheous_rom igdb_game = await self._request( self.proxy_igdb_game_endpoint, params={ "Id": igdb_id, "expandColumns": "age_ratings, alternative_names, collections, cover, dlcs, expanded_games, franchise, franchises, game_modes, genres, involved_companies, platforms, ports, remakes, screenshots, similar_games, videos", }, method="GET", ) if not igdb_game: log.debug(f"No Hasheous game found for IGDB ID {igdb_id}.") return hasheous_rom return HasheousRom( { **hasheous_rom, "slug": igdb_game.get("slug") or hasheous_rom.get("slug") or "", "name": igdb_game.get("name") or hasheous_rom.get("name") or "", "summary": igdb_game.get("summary", ""), "url_cover": self.normalize_cover_url( pydash.get(igdb_game, "cover.url", "") ).replace("t_thumb", "t_1080p") or hasheous_rom.get("url_cover", ""), "url_screenshots": [ self.normalize_cover_url(s.get("url", "")).replace( "t_thumb", "t_720p" ) for s in pydash.get(igdb_game, "screenshots", {}).values() ] or hasheous_rom.get("url_screenshots", []), "igdb_metadata": extract_metadata_from_igdb_rom(igdb_game), } ) async def get_ra_game(self, hasheous_rom: HasheousRom) -> HasheousRom: if not self.is_enabled(): return hasheous_rom ra_id = hasheous_rom.get("ra_id", None) if ra_id is None: log.info("No RA ID provided for Hasheous RA game lookup.") return hasheous_rom ra_game = await self._request( self.proxy_ra_game_endpoint, params={"Id": ra_id}, method="GET", ) if not ra_game: log.debug(f"No Hasheous game found for RA ID {ra_id}.") return hasheous_rom return hasheous_rom class SlugToHasheousId(TypedDict): id: int name: str igdb_id: int | None igdb_slug: str | None tgdb_id: int | None ra_id: int | None HASHEOUS_PLATFORM_LIST: dict[UPS, SlugToHasheousId] = { UPS._3DO: { "id": 161825, "igdb_id": 50, "igdb_slug": "3do", "name": "3DO Interactive Multiplayer", "ra_id": 43, "tgdb_id": None, }, UPS.N3DS: { "id": 62, "igdb_id": 37, "igdb_slug": "3ds", "name": "Nintendo 3DS", "ra_id": 62, "tgdb_id": None, }, UPS.N64DD: { "id": 65, "igdb_id": 416, "igdb_slug": "64dd", "name": "Nintendo 64DD", "ra_id": None, "tgdb_id": None, }, UPS.ACORN_ARCHIMEDES: { "id": 24, "igdb_id": 116, "igdb_slug": "acorn-archimedes", "name": "Acorn Archimedes", "ra_id": None, "tgdb_id": None, }, UPS.ACORN_ELECTRON: { "id": 25, "igdb_id": 134, "igdb_slug": "acorn-electron", "name": "Acorn Electron", "ra_id": None, "tgdb_id": None, }, UPS.ACPC: { "id": 28, "igdb_id": 25, "igdb_slug": "acpc", "name": "Amstrad CPC", "ra_id": 37, "tgdb_id": None, }, UPS.ACTION_MAX: { "id": 232983, "igdb_id": None, "igdb_slug": "", "name": "Action Max", "ra_id": None, "tgdb_id": None, }, UPS.ADVENTURE_VISION: { "id": 234388, "igdb_id": None, "igdb_slug": "", "name": "Entex Adventure Vision", "ra_id": None, "tgdb_id": None, }, UPS.ALTAIR_8800: { "id": 234456, "igdb_id": None, "igdb_slug": "", "name": "MITS Altair 8800", "ra_id": None, "tgdb_id": None, }, UPS.AMIGA: { "id": 3, "igdb_id": 16, "igdb_slug": "amiga", "name": "Commodore Amiga", "ra_id": 35, "tgdb_id": None, }, UPS.AMIGA_CD32: { "id": 161823, "igdb_id": 114, "igdb_slug": "amiga-cd32", "name": "Commodore CD32", "ra_id": None, "tgdb_id": None, }, UPS.AMSTRAD_GX4000: { "id": 61540, "igdb_id": 506, "igdb_slug": "amstrad-gx4000", "name": "Amstrad GX4000", "ra_id": None, "tgdb_id": None, }, UPS.AMSTRAD_PCW: { "id": 29, "igdb_id": 154, "igdb_slug": "amstrad-pcw", "name": "Amstrad PCW", "ra_id": None, "tgdb_id": None, }, UPS.APF: { "id": 61738, "igdb_id": None, "igdb_slug": "", "name": "APF Imagination Machine", "ra_id": None, "tgdb_id": None, }, UPS.APPLE: { "id": 61885, "igdb_id": None, "igdb_slug": "", "name": "Apple I", "ra_id": None, "tgdb_id": None, }, UPS.APPLE_IIGS: { "id": 21, "igdb_id": 115, "igdb_slug": "apple-iigs", "name": "Apple IIGS", "ra_id": None, "tgdb_id": None, }, UPS.APPLE_LISA: { "id": 69659, "igdb_id": None, "igdb_slug": "", "name": "Apple Lisa", "ra_id": None, "tgdb_id": None, }, UPS.APPLE_PIPPIN: { "id": 22, "igdb_id": 476, "igdb_slug": "apple-pippin", "name": "Apple Pippin", "ra_id": None, "tgdb_id": None, }, UPS.APPLEII: { "id": 20, "igdb_id": 75, "igdb_slug": "appleii", "name": "Apple II", "ra_id": 38, "tgdb_id": None, }, UPS.APPLEIII: { "id": 63154, "igdb_id": None, "igdb_slug": "", "name": "Apple III", "ra_id": None, "tgdb_id": None, }, UPS.ARCADE: { "id": 178, "igdb_id": 52, "igdb_slug": "arcade", "name": "Arcade", "ra_id": 27, "tgdb_id": None, }, UPS.ARDUBOY: { "id": 244294, "igdb_id": 438, "igdb_slug": "arduboy", "name": "Arduboy", "ra_id": None, "tgdb_id": None, }, UPS.ASTROCADE: { "id": 31, "igdb_id": 91, "igdb_slug": "astrocade", "name": "Bally Astrocade", "ra_id": None, "tgdb_id": None, }, UPS.ATARI_ST: { "id": 15, "igdb_id": 63, "igdb_slug": "atari-st", "name": "Atari ST/STE", "ra_id": 36, "tgdb_id": None, }, UPS.ATARI2600: { "id": 12, "igdb_id": 59, "igdb_slug": "atari2600", "name": "Atari 2600", "ra_id": 25, "tgdb_id": None, }, UPS.ATARI5200: { "id": 17, "igdb_id": 66, "igdb_slug": "atari5200", "name": "Atari 5200", "ra_id": 50, "tgdb_id": None, }, UPS.ATARI7800: { "id": 16, "igdb_id": 60, "igdb_slug": "atari7800", "name": "Atari 7800", "ra_id": 51, "tgdb_id": None, }, UPS.ATARI8BIT: { "id": 18, "igdb_id": 65, "igdb_slug": "atari8bit", "name": "Atari 8-bit", "ra_id": None, "tgdb_id": None, }, UPS.ATOM: { "id": 55099, "igdb_id": None, "igdb_slug": "", "name": "Acorn Atom", "ra_id": None, "tgdb_id": None, }, UPS.BBCMICRO: { "id": 26, "igdb_id": 69, "igdb_slug": "bbcmicro", "name": "BBC Micro", "ra_id": None, "tgdb_id": None, }, UPS.BEENA: { "id": 82, "igdb_id": None, "igdb_slug": "", "name": "Sega Advanced Pico Beena", "ra_id": None, "tgdb_id": None, }, UPS.BIT_90: { "id": 97614, "igdb_id": None, "igdb_slug": "", "name": "Bit Corporation BIT 90", "ra_id": None, "tgdb_id": None, }, UPS.C_PLUS_4: { "id": 7, "igdb_id": 94, "igdb_slug": "c-plus-4", "name": "Commodore Plus/4", "ra_id": None, "tgdb_id": None, }, UPS.C128: { "id": 8, "igdb_id": None, "igdb_slug": "", "name": "Commodore 128", "ra_id": None, "tgdb_id": None, }, UPS.C16: { "id": 6, "igdb_id": 93, "igdb_slug": "c16", "name": "Commodore 16", "ra_id": None, "tgdb_id": None, }, UPS.C64: { "id": 5, "igdb_id": 15, "igdb_slug": "c64", "name": "Commodore MAX", "ra_id": None, "tgdb_id": None, }, UPS.CAMPUTERS_LYNX: { "id": 97720, "igdb_id": None, "igdb_slug": "", "name": "Camputers Lynx", "ra_id": None, "tgdb_id": None, }, UPS.CASIO_CFX_9850: { "id": 97839, "igdb_id": None, "igdb_slug": "", "name": "Casio CFX-9850", "ra_id": None, "tgdb_id": None, }, UPS.CASIO_FP_1000: { "id": 98757, "igdb_id": None, "igdb_slug": "", "name": "Casio FP-1000 & FP-1100", "ra_id": None, "tgdb_id": None, }, UPS.CASIO_LOOPY: { "id": 37, "igdb_id": 380, "igdb_slug": "casio-loopy", "name": "Casio Loopy", "ra_id": None, "tgdb_id": None, }, UPS.CASIO_PB_1000: { "id": 98771, "igdb_id": None, "igdb_slug": "", "name": "Casio PB-1000", "ra_id": None, "tgdb_id": None, }, UPS.CASIO_PV_1000: { "id": 98793, "igdb_id": None, "igdb_slug": "", "name": "Casio PV-1000", "ra_id": None, "tgdb_id": None, }, UPS.CASIO_PV_2000: { "id": 98811, "igdb_id": None, "igdb_slug": "", "name": "Casio PV-2000", "ra_id": None, "tgdb_id": None, }, UPS.COLECOVISION: { "id": 39, "igdb_id": 68, "igdb_slug": "colecovision", "name": "ColecoVision", "ra_id": 44, "tgdb_id": None, }, UPS.COMMANDER_X16: { "id": 54769, "igdb_id": None, "igdb_slug": "", "name": "8-Bit Productions Commander X16", "ra_id": None, "tgdb_id": None, }, UPS.COMMODORE_CDTV: { "id": 9, "igdb_id": 158, "igdb_slug": "commodore-cdtv", "name": "Commodore CDTV", "ra_id": None, "tgdb_id": None, }, UPS.CPET: { "id": 10, "igdb_id": 90, "igdb_slug": "cpet", "name": "Commodore PET", "ra_id": None, "tgdb_id": None, }, UPS.DC: { "id": 54694, "igdb_id": 23, "igdb_slug": "dc", "name": "Sega Dreamcast", "ra_id": 40, "tgdb_id": None, }, UPS.DOS: { "id": 233075, "igdb_id": 13, "igdb_slug": "dos", "name": "Microsoft DOS", "ra_id": None, "tgdb_id": None, }, UPS.EXCALIBUR_64: { "id": 97612, "igdb_id": None, "igdb_slug": "", "name": "BGR Computers Excalibur 64", "ra_id": None, "tgdb_id": None, }, UPS.FAIRCHILD_CHANNEL_F: { "id": 43, "igdb_id": 127, "igdb_slug": "fairchild-channel-f", "name": "Fairchild Channel F", "ra_id": 57, "tgdb_id": None, }, UPS.FDS: { "id": 54692, "igdb_id": 51, "igdb_slug": "fds", "name": "Nintendo Famicom Disk System", "ra_id": None, "tgdb_id": None, }, UPS.FM_TOWNS: { "id": 238902, "igdb_id": 118, "igdb_slug": "fm-towns", "name": "Fujitsu - FM Towns", "ra_id": None, "tgdb_id": None, }, UPS.GAMATE: { "id": 97616, "igdb_id": 378, "igdb_slug": "gamate", "name": "Bit Corporation Gamate", "ra_id": None, "tgdb_id": None, }, UPS.GAMEGEAR: { "id": 84, "igdb_id": 35, "igdb_slug": "gamegear", "name": "Sega Game Gear", "ra_id": 15, "tgdb_id": None, }, UPS.GB: { "id": 70, "igdb_id": 33, "igdb_slug": "gb", "name": "Nintendo GameBoy", "ra_id": 4, "tgdb_id": None, }, UPS.GBA: { "id": 71, "igdb_id": 24, "igdb_slug": "gba", "name": "Nintendo Game Boy Advance", "ra_id": 5, "tgdb_id": None, }, UPS.GBC: { "id": 72, "igdb_id": 22, "igdb_slug": "gbc", "name": "Nintendo Game Boy Color", "ra_id": 6, "tgdb_id": None, }, UPS.GENESIS: { "id": 86, "igdb_id": 29, "igdb_slug": "genesis-slash-megadrive", "name": "Sega Mega Drive / Genesis", "ra_id": 1, "tgdb_id": None, }, UPS.INTELLIVISION: { "id": 52, "igdb_id": 67, "igdb_slug": "intellivision", "name": "Mattel Intellivision", "ra_id": 45, "tgdb_id": None, }, UPS.JAGUAR: { "id": 13, "igdb_id": 62, "igdb_slug": "jaguar", "name": "Atari Jaguar", "ra_id": 17, "tgdb_id": None, }, UPS.LINUX: { "id": 233076, "igdb_id": 3, "igdb_slug": "linux", "name": "Linux", "ra_id": None, "tgdb_id": None, }, UPS.LYNX: { "id": 14, "igdb_id": 61, "igdb_slug": "lynx", "name": "Atari Lynx", "ra_id": 13, "tgdb_id": None, }, UPS.MAC: { "id": 30, "igdb_id": 14, "igdb_slug": "mac", "name": "Apple Mac", "ra_id": None, "tgdb_id": None, }, UPS.AQUARIUS: { "id": 51, "igdb_id": None, "igdb_slug": "", "name": "Mattel Aquarius", "ra_id": None, "tgdb_id": None, }, UPS.MICROBEE: { "id": 69714, "igdb_id": None, "igdb_slug": "", "name": "Applied Technology MicroBee", "ra_id": None, "tgdb_id": None, }, UPS.MSX: { "id": 53, "igdb_id": 27, "igdb_slug": "msx", "name": "MSX", "ra_id": 29, "tgdb_id": None, }, UPS.MSX2: { "id": 54, "igdb_id": 53, "igdb_slug": "msx2", "name": "MSX 2", "ra_id": None, "tgdb_id": None, }, UPS.MULTIVISION: { "id": 52922, "igdb_id": None, "igdb_slug": "", "name": "Tsukuda Original Othello Multivision", "ra_id": None, "tgdb_id": None, }, UPS.N64: { "id": 64, "igdb_id": 4, "igdb_slug": "n64", "name": "Nintendo 64", "ra_id": 2, "tgdb_id": None, }, UPS.NDS: { "id": 66, "igdb_id": 20, "igdb_slug": "nds", "name": "Nintendo DS", "ra_id": 18, "tgdb_id": None, }, UPS.NEC_PC_6000_SERIES: { "id": 58, "igdb_id": 157, "igdb_slug": "nec-pc-6000-series", "name": "NEC PC-6000", "ra_id": None, "tgdb_id": None, }, UPS.NEO_GEO_CD: { "id": 161829, "igdb_id": 136, "igdb_slug": "neo-geo-cd", "name": "Neo Geo CD", "ra_id": 56, "tgdb_id": None, }, UPS.NEO_GEO_POCKET: { "id": 97, "igdb_id": 119, "igdb_slug": "neo-geo-pocket", "name": "Neo Geo Pocket", "ra_id": 14, "tgdb_id": None, }, UPS.NEO_GEO_POCKET_COLOR: { "id": 98, "igdb_id": 120, "igdb_slug": "neo-geo-pocket-color", "name": "Neo Geo Pocket Color", "ra_id": None, "tgdb_id": None, }, UPS.NEOGEOAES: { "id": 96, "igdb_id": 80, "igdb_slug": "neogeoaes", "name": "Neo Geo", "ra_id": None, "tgdb_id": None, }, UPS.NES: { "id": 68, "igdb_id": 18, "igdb_slug": "nes", "name": "Nintendo Entertainment System", "ra_id": 7, "tgdb_id": None, }, UPS.NEW_NINTENDON3DS: { "id": 63, "igdb_id": 137, "igdb_slug": "new-nintendo-3ds", "name": "Nintendo New 3DS", "ra_id": None, "tgdb_id": None, }, UPS.NGC: { "id": 73, "igdb_id": 21, "igdb_slug": "ngc", "name": "Nintendo GameCube", "ra_id": 16, "tgdb_id": None, }, UPS.NINTENDO_DSI: { "id": 67, "igdb_id": 159, "igdb_slug": "nintendo-dsi", "name": "Nintendo DSi", "ra_id": 78, "tgdb_id": None, }, UPS.ODYSSEY: { "id": 48, "igdb_id": 88, "igdb_slug": "odyssey--1", "name": "Magnavox Odyssey", "ra_id": None, "tgdb_id": None, }, UPS.ODYSSEY_2: { "id": 49, "igdb_id": 133, "igdb_slug": "odyssey-2-slash-videopac-g7000", "name": "Magnavox Odyssey 2", "ra_id": 23, "tgdb_id": None, }, UPS.PC_8800_SERIES: { "id": 57, "igdb_id": 125, "igdb_slug": "pc-8800-series", "name": "NEC PC-8800", "ra_id": None, "tgdb_id": None, }, UPS.PC_9800_SERIES: { "id": 59, "igdb_id": 149, "igdb_slug": "pc-9800-series", "name": "NEC PC-9000", "ra_id": None, "tgdb_id": None, }, UPS.PC_JR: { "id": 233269, "igdb_id": None, "igdb_slug": "", "name": "IBM PCjr", "ra_id": None, "tgdb_id": None, }, UPS.PHILIPS_CD_I: { "id": 161827, "igdb_id": 117, "igdb_slug": "philips-cd-i", "name": "Philips CD-i", "ra_id": 42, "tgdb_id": None, }, UPS.POCKET_CHALLENGE_V2: { "id": 97550, "igdb_id": None, "igdb_slug": "", "name": "Benesse Pocket Challenge V2", "ra_id": None, "tgdb_id": None, }, UPS.POCKET_CHALLENGE_W: { "id": 97577, "igdb_id": None, "igdb_slug": "", "name": "Benesse Pocket Challenge W", "ra_id": None, "tgdb_id": None, }, UPS.POCKETSTATION: { "id": 103, "igdb_id": 441, "igdb_slug": "pocketstation", "name": "Sony PocketStation", "ra_id": None, "tgdb_id": None, }, UPS.POKEMON_MINI: { "id": 244733, "igdb_id": 166, "igdb_slug": "pokemon-mini", "name": "Nintendo Pokemon Mini", "ra_id": 24, "tgdb_id": None, }, UPS.PS2: { "id": 101, "igdb_id": 8, "igdb_slug": "ps2", "name": "Sony PlayStation 2", "ra_id": 21, "tgdb_id": None, }, UPS.PS3: { "id": 161830, "igdb_id": 9, "igdb_slug": "ps3", "name": "Sony Playstation 3", "ra_id": None, "tgdb_id": None, }, UPS.PS4: { "id": 232986, "igdb_id": 48, "igdb_slug": "ps4--1", "name": "Sony Playstation 4", "ra_id": None, "tgdb_id": None, }, UPS.PS5: { "id": 232987, "igdb_id": 167, "igdb_slug": "ps5", "name": "Sony Playstation 5", "ra_id": None, "tgdb_id": None, }, UPS.PSP: { "id": 161831, "igdb_id": 38, "igdb_slug": "psp", "name": "Sony Playstation Portable", "ra_id": 41, "tgdb_id": None, }, UPS.PSVITA: { "id": 102, "igdb_id": 46, "igdb_slug": "psvita", "name": "Sony PlayStation Vita", "ra_id": None, "tgdb_id": None, }, UPS.PSX: { "id": 100, "igdb_id": 7, "igdb_slug": "ps", "name": "Sony PlayStation", "ra_id": 12, "tgdb_id": None, }, UPS.RCA_STUDIO_II: { "id": 234745, "igdb_id": None, "igdb_slug": "", "name": "RCA Studio II", "ra_id": None, "tgdb_id": None, }, UPS.SATURN: { "id": 54695, "igdb_id": 32, "igdb_slug": "saturn", "name": "Sega Saturn", "ra_id": 39, "tgdb_id": None, }, UPS.SC3000: { "id": 52165, "igdb_id": None, "igdb_slug": "", "name": "Sega Computer 3000", "ra_id": None, "tgdb_id": None, }, UPS.SEGA_PICO: { "id": 81, "igdb_id": 339, "igdb_slug": "sega-pico", "name": "Sega Pico", "ra_id": 68, "tgdb_id": None, }, UPS.SEGA32: { "id": 80, "igdb_id": 30, "igdb_slug": "sega32", "name": "Sega 32X", "ra_id": 10, "tgdb_id": None, }, UPS.SEGACD: { "id": 161828, "igdb_id": None, "igdb_slug": "", "name": "Sega Mega CD / Sega CD", "ra_id": 9, "tgdb_id": None, }, UPS.SERIES_X_S: { "id": 232984, "igdb_id": 169, "igdb_slug": "series-x-s", "name": "Microsoft Xbox Series X", "ra_id": None, "tgdb_id": None, }, UPS.SFAM: { "id": 233081, "igdb_id": 58, "igdb_slug": "sfam", "name": "Super Famicom", "ra_id": None, "tgdb_id": None, }, UPS.SG1000: { "id": 244470, "igdb_id": 84, "igdb_slug": "sg1000", "name": "SG-1000", "ra_id": 33, "tgdb_id": None, }, UPS.SHARP_X68000: { "id": 90, "igdb_id": 121, "igdb_slug": "sharp-x68000", "name": "Sharp X68000", "ra_id": 52, "tgdb_id": None, }, UPS.SINCLAIR_QL: { "id": 92, "igdb_id": 406, "igdb_slug": "sinclair-ql", "name": "Sinclair QL", "ra_id": None, "tgdb_id": None, }, UPS.SMS: { "id": 85, "igdb_id": 64, "igdb_slug": "sms", "name": "Sega Master System", "ra_id": 11, "tgdb_id": None, }, UPS.SNES: { "id": 74, "igdb_id": 19, "igdb_slug": "snes", "name": "Super Nintendo Entertainment System", "ra_id": 3, "tgdb_id": None, }, UPS.SUPER_VISION_8000: { "id": 97267, "igdb_id": None, "igdb_slug": "", "name": "Bandai Super Vision 8000", "ra_id": None, "tgdb_id": None, }, UPS.SWITCH: { "id": 233067, "igdb_id": 130, "igdb_slug": "switch", "name": "Nintendo Switch", "ra_id": None, "tgdb_id": None, }, UPS.TG16: { "id": 245372, "igdb_id": 86, "igdb_slug": "turbografx16--1", "name": "TurboGrafx-16/PC Engine", "ra_id": 8, "tgdb_id": None, }, UPS.TI_82: { "id": 47973, "igdb_id": None, "igdb_slug": "", "name": "Texas Instruments TI-82", "ra_id": None, "tgdb_id": None, }, UPS.TI_83: { "id": 243852, "igdb_id": None, "igdb_slug": "", "name": "Texas Instruments TI-83", "ra_id": None, "tgdb_id": None, }, UPS.TRS_80: { "id": 105, "igdb_id": 126, "igdb_slug": "trs-80", "name": "Tandy/RadioShack TRS-80", "ra_id": None, "tgdb_id": None, }, UPS.TRS_80_COLOR_COMPUTER: { "id": 106, "igdb_id": 151, "igdb_slug": "trs-80-color-computer", "name": "Tandy/RadioShack TRS-80 Color Computer", "ra_id": None, "tgdb_id": None, }, UPS.TURBOGRAFX_CD: { "id": 247350, "igdb_id": 150, "igdb_slug": "turbografx-16-slash-pc-engine-cd", "name": "Turbografx-16/PC Engine CD", "ra_id": None, "tgdb_id": None, }, UPS.VECTREX: { "id": 45, "igdb_id": 70, "igdb_slug": "vectrex", "name": "Vectrex", "ra_id": 46, "tgdb_id": None, }, UPS.VIC_20: { "id": 4, "igdb_id": 71, "igdb_slug": "vic-20", "name": "Commodore VIC20", "ra_id": None, "tgdb_id": None, }, UPS.VIRTUALBOY: { "id": 75, "igdb_id": 87, "igdb_slug": "virtualboy", "name": "Nintendo Virtual Boy", "ra_id": 28, "tgdb_id": None, }, UPS.SUPERVISION: { "id": 244828, "igdb_id": 415, "igdb_slug": "watara-slash-quickshot-supervision", "name": "Watara Supervision", "ra_id": 63, "tgdb_id": None, }, UPS.WII: { "id": 76, "igdb_id": 5, "igdb_slug": "wii", "name": "Nintendo Wii", "ra_id": None, "tgdb_id": None, }, UPS.WIIU: { "id": 77, "igdb_id": 41, "igdb_slug": "wiiu", "name": "Nintendo WiiU", "ra_id": None, "tgdb_id": None, }, UPS.WIN: { "id": 233074, "igdb_id": 6, "igdb_slug": "win", "name": "Microsoft Windows", "ra_id": None, "tgdb_id": None, }, UPS.WONDERSWAN: { "id": 34, "igdb_id": 57, "igdb_slug": "wonderswan", "name": "Bandai WonderSwan", "ra_id": 53, "tgdb_id": None, }, UPS.WONDERSWAN_COLOR: { "id": 35, "igdb_id": 123, "igdb_slug": "wonderswan-color", "name": "Bandai WonderSwan Color", "ra_id": None, "tgdb_id": None, }, UPS.X1: { "id": 89, "igdb_id": 77, "igdb_slug": "x1", "name": "Sharp X1", "ra_id": 64, "tgdb_id": None, }, UPS.XBOX: { "id": 54696, "igdb_id": 11, "igdb_slug": "xbox", "name": "Microsoft Xbox", "ra_id": None, "tgdb_id": None, }, UPS.XBOX360: { "id": 54697, "igdb_id": 12, "igdb_slug": "xbox360", "name": "Microsoft Xbox 360", "ra_id": None, "tgdb_id": None, }, UPS.XBOXONE: { "id": 161824, "igdb_id": 49, "igdb_slug": "xboxone", "name": "Microsoft Xbox One", "ra_id": None, "tgdb_id": None, }, UPS.Z88: { "id": 97718, "igdb_id": None, "igdb_slug": "", "name": "Cambridge Computer Z88", "ra_id": None, "tgdb_id": None, }, UPS.ZX80: { "id": 232985, "igdb_id": None, "igdb_slug": "", "name": "Sinclair ZX80", "ra_id": None, "tgdb_id": None, }, UPS.ZX81: { "id": 94, "igdb_id": 373, "igdb_slug": "sinclair-zx81", "name": "Sinclair ZX81", "ra_id": None, "tgdb_id": None, }, UPS.ZXS: { "id": 93, "igdb_id": 26, "igdb_slug": "zxs", "name": "Sinclair ZX Spectrum", "ra_id": None, "tgdb_id": None, }, }