diff --git a/backend/alembic/versions/0044_hasheous_id.py b/backend/alembic/versions/0044_hasheous_id.py new file mode 100644 index 000000000..1a27d40ec --- /dev/null +++ b/backend/alembic/versions/0044_hasheous_id.py @@ -0,0 +1,39 @@ +"""empty message + +Revision ID: 0044_hasheous_id +Revises: 0043_launchbox_id +Create Date: 2025-06-16 03:15:42.692551 + +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "0044_hasheous_id" +down_revision = "0043_launchbox_id" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + with op.batch_alter_table("roms", schema=None) as batch_op: + batch_op.add_column(sa.Column("hasheous_id", sa.Integer(), nullable=True)) + batch_op.add_column( + sa.Column( + "hasheous_metadata", + sa.JSON().with_variant( + postgresql.JSONB(astext_type=sa.Text()), "postgresql" + ), + nullable=True, + ) + ) + batch_op.create_index("idx_roms_hasheous_id", ["hasheous_id"], unique=False) + + +def downgrade() -> None: + with op.batch_alter_table("roms", schema=None) as batch_op: + batch_op.drop_index("idx_roms_hasheous_id") + batch_op.drop_column("hasheous_metadata") + batch_op.drop_column("hasheous_id") diff --git a/backend/config/__init__.py b/backend/config/__init__.py index bdb3ab2ef..123099f22 100644 --- a/backend/config/__init__.py +++ b/backend/config/__init__.py @@ -85,6 +85,11 @@ PLAYMATCH_API_ENABLED: Final = str_to_bool( os.environ.get("PLAYMATCH_API_ENABLED", "false") ) +# HASHEOUS +HASHEOUS_API_ENABLED: Final = str_to_bool( + os.environ.get("HASHEOUS_API_ENABLED", "false") +) + # AUTH ROMM_AUTH_SECRET_KEY: Final = os.environ.get( "ROMM_AUTH_SECRET_KEY", secrets.token_hex(32) diff --git a/backend/endpoints/heartbeat.py b/backend/endpoints/heartbeat.py index 27460e1a0..09fcc684f 100644 --- a/backend/endpoints/heartbeat.py +++ b/backend/endpoints/heartbeat.py @@ -6,6 +6,7 @@ from config import ( ENABLE_SCHEDULED_RESCAN, ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA, ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB, + HASHEOUS_API_ENABLED, LAUNCHBOX_API_ENABLED, OIDC_ENABLED, OIDC_PROVIDER, @@ -51,13 +52,15 @@ def heartbeat() -> HeartbeatResponse: or MOBY_API_ENABLED or RA_API_ENABLED or LAUNCHBOX_API_ENABLED - or PLAYMATCH_API_ENABLED, + or PLAYMATCH_API_ENABLED + or HASHEOUS_API_ENABLED, "IGDB_API_ENABLED": IGDB_API_ENABLED, "SS_API_ENABLED": SS_API_ENABLED, "MOBY_API_ENABLED": MOBY_API_ENABLED, "STEAMGRIDDB_API_ENABLED": STEAMGRIDDB_API_ENABLED, "RA_API_ENABLED": RA_API_ENABLED, "LAUNCHBOX_API_ENABLED": LAUNCHBOX_API_ENABLED, + "HASHEOUS_API_ENABLED": HASHEOUS_API_ENABLED, # Platmatch requires use of the IGDB API "PLAYMATCH_API_ENABLED": PLAYMATCH_API_ENABLED and IGDB_API_ENABLED, }, diff --git a/backend/endpoints/responses/heartbeat.py b/backend/endpoints/responses/heartbeat.py index fae3e2d29..04fc79e67 100644 --- a/backend/endpoints/responses/heartbeat.py +++ b/backend/endpoints/responses/heartbeat.py @@ -31,6 +31,7 @@ class MetadataSourcesDict(TypedDict): RA_API_ENABLED: bool LAUNCHBOX_API_ENABLED: bool PLAYMATCH_API_ENABLED: bool + HASHEOUS_API_ENABLED: bool class FilesystemDict(TypedDict): diff --git a/backend/handler/metadata/__init__.py b/backend/handler/metadata/__init__.py index 66b184951..03201e62b 100644 --- a/backend/handler/metadata/__init__.py +++ b/backend/handler/metadata/__init__.py @@ -1,3 +1,4 @@ +from .hasheous_handler import HasheousHandler from .igdb_handler import IGDBHandler from .launchbox_handler import LaunchboxHandler from .moby_handler import MobyGamesHandler @@ -13,3 +14,4 @@ meta_sgdb_handler = SGDBBaseHandler() meta_ra_handler = RAHandler() meta_pm_handler = PlaymatchHandler() meta_launchbox_handler = LaunchboxHandler() +meta_hasheous_handler = HasheousHandler() diff --git a/backend/handler/metadata/hasheous_handler.py b/backend/handler/metadata/hasheous_handler.py new file mode 100644 index 000000000..d2c5045a1 --- /dev/null +++ b/backend/handler/metadata/hasheous_handler.py @@ -0,0 +1,1321 @@ +import json +from typing import TypedDict + +import httpx +from config import HASHEOUS_API_ENABLED +from fastapi import HTTPException, status +from logger.logger import log +from utils.context import ctx_httpx_client + +from .base_hander import MetadataHandler + + +class HasheousMetadata(TypedDict): + pass + + +class HasheousPlatform(TypedDict): + id: int + + +class HasheousRom(TypedDict): + igdb_id: int | None + + +class HasheousHandler(MetadataHandler): + def __init__(self) -> None: + self.BASE_URL = "https://hasheous.org/api/v1/Lookup" + self.platform_endpoint = f"{self.BASE_URL}/Platforms" + self.games_endpoint = f"{self.BASE_URL}/ByHash" + + async def _request(self, url: str, params: dict, timeout: int = 120) -> dict: + httpx_client = ctx_httpx_client.get() + + try: + log.debug( + "API request: URL=%s, Params=%s, Timeout=%s", + url, + params, + timeout, + ) + res = await httpx_client.get( + url, + params=params, + timeout=timeout, + ) + + res.raise_for_status() + return res.json() + 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 list if the response is not valid JSON + log.error(exc) + return {} + except httpx.TimeoutException: + pass + + return {} + + # async def _build_platforms(self) -> None: + # from .igdb_handler import IGDB_PLATFORMS_BY_SLUG + + # if not HASHEOUS_API_ENABLED: + # return + + # platforms = await self._request( + # self.platform_endpoint, + # params={ + # "PageSize": 500, + # }, + # ) + + # iplats = {} + # mplats = {} + + # for platform in platforms["objects"]: + # metadata = platform["metadata"] + + # igdb_id = None + # tgdb_id = None + # ra_id = None + + # for meta in metadata: + # if meta["source"] == "IGDB": + # igdb_id = meta["id"] + # elif meta["source"] == "TheGamesDB": + # tgdb_id = meta["id"] + # elif meta["source"] == "RetroAchievements": + # ra_id = meta["id"] + + # platform_data = { + # "id": platform["id"], + # "name": platform["name"], + # "igdb_id": igdb_id, + # "tgdb_id": int(tgdb_id) if tgdb_id else None, + # "ra_id": int(ra_id) if ra_id else None, + # } + + # if igdb_id in IGDB_PLATFORMS_BY_SLUG: + # iplats[igdb_id] = platform_data + # else: + # mplats[platform["id"]] = platform_data + + async def get_rom(self, rom_attrs: dict) -> HasheousRom: + if not HASHEOUS_API_ENABLED: + return HasheousRom(igdb_id=None) + + hasheous_game = await self._request( + self.games_endpoint, + params={ + "mD5": rom_attrs["md5_hash"], + "shA1": rom_attrs["sha1_hash"], + "crc": rom_attrs["crc_hash"], + "returnAllSources": True, + "returnFields": "All", + }, + ) + + import ipdb + + ipdb.set_trace() + + return HasheousRom(igdb_id=None) + + +class SlugToHasheousId(TypedDict): + id: int + name: str + igdb_id: str | None + tgdb_id: int | None + ra_id: int | None + + +HASHEOUS_PLATFORM_LIST: dict[str, SlugToHasheousId] = { + "3do": { + "id": 161825, + "name": "3DO Interactive Multiplayer", + "igdb_id": "3do", + "tgdb_id": None, + "ra_id": 43, + }, + "acorn-archimedes": { + "id": 24, + "name": "Acorn Archimedes", + "igdb_id": "acorn-archimedes", + "tgdb_id": None, + "ra_id": None, + }, + "acorn-electron": { + "id": 25, + "name": "Acorn Electron", + "igdb_id": "acorn-electron", + "tgdb_id": None, + "ra_id": None, + }, + "acpc": { + "id": 28, + "name": "Amstrad CPC", + "igdb_id": "acpc", + "tgdb_id": None, + "ra_id": 37, + }, + "amstrad-pcw": { + "id": 29, + "name": "Amstrad PCW", + "igdb_id": "amstrad-pcw", + "tgdb_id": None, + "ra_id": None, + }, + "appleii": { + "id": 20, + "name": "Apple II", + "igdb_id": "appleii", + "tgdb_id": None, + "ra_id": 38, + }, + "apple-iigs": { + "id": 21, + "name": "Apple IIGS", + "igdb_id": "apple-iigs", + "tgdb_id": None, + "ra_id": None, + }, + "mac": { + "id": 30, + "name": "Apple Mac", + "igdb_id": "mac", + "tgdb_id": None, + "ra_id": None, + }, + "apple-pippin": { + "id": 22, + "name": "Apple Pippin", + "igdb_id": "apple-pippin", + "tgdb_id": None, + "ra_id": None, + }, + "arcade": { + "id": 178, + "name": "Arcade", + "igdb_id": "arcade", + "tgdb_id": None, + "ra_id": 27, + }, + "atari2600": { + "id": 12, + "name": "Atari 2600", + "igdb_id": "atari2600", + "tgdb_id": None, + "ra_id": 25, + }, + "atari5200": { + "id": 17, + "name": "Atari 5200", + "igdb_id": "atari5200", + "tgdb_id": None, + "ra_id": 50, + }, + "atari7800": { + "id": 16, + "name": "Atari 7800", + "igdb_id": "atari7800", + "tgdb_id": None, + "ra_id": 51, + }, + "atari8bit": { + "id": 18, + "name": "Atari 8-bit", + "igdb_id": "atari8bit", + "tgdb_id": None, + "ra_id": None, + }, + "jaguar": { + "id": 13, + "name": "Atari Jaguar", + "igdb_id": "jaguar", + "tgdb_id": None, + "ra_id": 17, + }, + "lynx": { + "id": 14, + "name": "Atari Lynx", + "igdb_id": "lynx", + "tgdb_id": None, + "ra_id": 13, + }, + "atari-st": { + "id": 15, + "name": "Atari ST/STE", + "igdb_id": "atari-st", + "tgdb_id": None, + "ra_id": 36, + }, + "astrocade": { + "id": 31, + "name": "Bally Astrocade", + "igdb_id": "astrocade", + "tgdb_id": None, + "ra_id": None, + }, + "wonderswan": { + "id": 34, + "name": "Bandai WonderSwan", + "igdb_id": "wonderswan", + "tgdb_id": None, + "ra_id": 53, + }, + "wonderswan-color": { + "id": 35, + "name": "Bandai WonderSwan Color", + "igdb_id": "wonderswan-color", + "tgdb_id": None, + "ra_id": None, + }, + "bbcmicro": { + "id": 26, + "name": "BBC Micro", + "igdb_id": "bbcmicro", + "tgdb_id": None, + "ra_id": None, + }, + "casio-loopy": { + "id": 37, + "name": "Casio Loopy", + "igdb_id": "casio-loopy", + "tgdb_id": None, + "ra_id": None, + }, + "colecovision": { + "id": 39, + "name": "ColecoVision", + "igdb_id": "colecovision", + "tgdb_id": None, + "ra_id": 44, + }, + "c16": { + "id": 6, + "name": "Commodore 16", + "igdb_id": "c16", + "tgdb_id": None, + "ra_id": None, + }, + "c64": { + "id": 5, + "name": "Commodore MAX", + "igdb_id": "c64", + "tgdb_id": None, + "ra_id": None, + }, + "amiga": { + "id": 3, + "name": "Commodore Amiga", + "igdb_id": "amiga", + "tgdb_id": None, + "ra_id": 35, + }, + "amiga-cd32": { + "id": 161823, + "name": "Commodore CD32", + "igdb_id": "amiga-cd32", + "tgdb_id": None, + "ra_id": None, + }, + "commodore-cdtv": { + "id": 9, + "name": "Commodore CDTV", + "igdb_id": "commodore-cdtv", + "tgdb_id": None, + "ra_id": None, + }, + "cpet": { + "id": 10, + "name": "Commodore PET", + "igdb_id": "cpet", + "tgdb_id": None, + "ra_id": None, + }, + "c-plus-4": { + "id": 7, + "name": "Commodore Plus/4", + "igdb_id": "c-plus-4", + "tgdb_id": None, + "ra_id": None, + }, + "vic-20": { + "id": 4, + "name": "Commodore VIC20", + "igdb_id": "vic-20", + "tgdb_id": None, + "ra_id": None, + }, + "fairchild-channel-f": { + "id": 43, + "name": "Fairchild Channel F", + "igdb_id": "fairchild-channel-f", + "tgdb_id": None, + "ra_id": 57, + }, + "fds": { + "id": 54692, + "name": "Nintendo Famicom Disk System", + "igdb_id": "fds", + "tgdb_id": None, + "ra_id": None, + }, + "linux": { + "id": 233076, + "name": "Linux", + "igdb_id": "linux", + "tgdb_id": None, + "ra_id": None, + }, + "odyssey--1": { + "id": 48, + "name": "Magnavox Odyssey", + "igdb_id": "odyssey--1", + "tgdb_id": None, + "ra_id": None, + }, + "odyssey-2-slash-videopac-g7000": { + "id": 49, + "name": "Magnavox Odyssey 2", + "igdb_id": "odyssey-2-slash-videopac-g7000", + "tgdb_id": None, + "ra_id": 23, + }, + "intellivision": { + "id": 52, + "name": "Mattel Intellivision", + "igdb_id": "intellivision", + "tgdb_id": None, + "ra_id": 45, + }, + "dos": { + "id": 233075, + "name": "Microsoft DOS", + "igdb_id": "dos", + "tgdb_id": None, + "ra_id": None, + }, + "win": { + "id": 233074, + "name": "Microsoft Windows", + "igdb_id": "win", + "tgdb_id": None, + "ra_id": None, + }, + "xbox": { + "id": 54696, + "name": "Microsoft Xbox", + "igdb_id": "xbox", + "tgdb_id": None, + "ra_id": None, + }, + "xbox360": { + "id": 54697, + "name": "Microsoft Xbox 360", + "igdb_id": "xbox360", + "tgdb_id": None, + "ra_id": None, + }, + "xboxone": { + "id": 161824, + "name": "Microsoft Xbox One", + "igdb_id": "xboxone", + "tgdb_id": None, + "ra_id": None, + }, + "msx": {"id": 53, "name": "MSX", "igdb_id": "msx", "tgdb_id": None, "ra_id": 29}, + "msx2": { + "id": 54, + "name": "MSX 2", + "igdb_id": "msx2", + "tgdb_id": None, + "ra_id": None, + }, + "nec-pc-6000-series": { + "id": 58, + "name": "NEC PC-6000", + "igdb_id": "nec-pc-6000-series", + "tgdb_id": None, + "ra_id": None, + }, + "pc-8800-series": { + "id": 57, + "name": "NEC PC-8800", + "igdb_id": "pc-8800-series", + "tgdb_id": None, + "ra_id": None, + }, + "pc-9800-series": { + "id": 59, + "name": "NEC PC-9000", + "igdb_id": "pc-9800-series", + "tgdb_id": None, + "ra_id": None, + }, + "neo-geo-cd": { + "id": 161829, + "name": "Neo Geo CD", + "igdb_id": "neo-geo-cd", + "tgdb_id": None, + "ra_id": 56, + }, + "neo-geo-pocket": { + "id": 97, + "name": "Neo Geo Pocket", + "igdb_id": "neo-geo-pocket", + "tgdb_id": None, + "ra_id": 14, + }, + "neo-geo-pocket-color": { + "id": 98, + "name": "Neo Geo Pocket Color", + "igdb_id": "neo-geo-pocket-color", + "tgdb_id": None, + "ra_id": None, + }, + "3ds": { + "id": 62, + "name": "Nintendo 3DS", + "igdb_id": "3ds", + "tgdb_id": None, + "ra_id": 62, + }, + "n64": { + "id": 64, + "name": "Nintendo 64", + "igdb_id": "n64", + "tgdb_id": None, + "ra_id": 2, + }, + "nds": { + "id": 66, + "name": "Nintendo DS", + "igdb_id": "nds", + "tgdb_id": None, + "ra_id": 18, + }, + "nintendo-dsi": { + "id": 67, + "name": "Nintendo DSi", + "igdb_id": "nintendo-dsi", + "tgdb_id": None, + "ra_id": 78, + }, + "nes": { + "id": 68, + "name": "Nintendo Entertainment System", + "igdb_id": "nes", + "tgdb_id": None, + "ra_id": 7, + }, + "gba": { + "id": 71, + "name": "Nintendo Game Boy Advance", + "igdb_id": "gba", + "tgdb_id": None, + "ra_id": 5, + }, + "gbc": { + "id": 72, + "name": "Nintendo Game Boy Color", + "igdb_id": "gbc", + "tgdb_id": None, + "ra_id": 6, + }, + "gb": { + "id": 70, + "name": "Nintendo GameBoy", + "igdb_id": "gb", + "tgdb_id": None, + "ra_id": 4, + }, + "ngc": { + "id": 73, + "name": "Nintendo GameCube", + "igdb_id": "ngc", + "tgdb_id": None, + "ra_id": 16, + }, + "new-nintendo-3ds": { + "id": 63, + "name": "Nintendo New 3DS", + "igdb_id": "new-nintendo-3ds", + "tgdb_id": None, + "ra_id": None, + }, + "switch": { + "id": 233067, + "name": "Nintendo Switch", + "igdb_id": "switch", + "tgdb_id": None, + "ra_id": None, + }, + "virtualboy": { + "id": 75, + "name": "Nintendo Virtual Boy", + "igdb_id": "virtualboy", + "tgdb_id": None, + "ra_id": 28, + }, + "wii": { + "id": 76, + "name": "Nintendo Wii", + "igdb_id": "wii", + "tgdb_id": None, + "ra_id": None, + }, + "wiiu": { + "id": 77, + "name": "Nintendo WiiU", + "igdb_id": "wiiu", + "tgdb_id": None, + "ra_id": None, + }, + "philips-cd-i": { + "id": 161827, + "name": "Philips CD-i", + "igdb_id": "philips-cd-i", + "tgdb_id": None, + "ra_id": 42, + }, + "sega32": { + "id": 80, + "name": "Sega 32X", + "igdb_id": "sega32", + "tgdb_id": None, + "ra_id": 10, + }, + "dc": { + "id": 54694, + "name": "Sega Dreamcast", + "igdb_id": "dc", + "tgdb_id": None, + "ra_id": 40, + }, + "gamegear": { + "id": 84, + "name": "Sega Game Gear", + "igdb_id": "gamegear", + "tgdb_id": None, + "ra_id": 15, + }, + "sms": { + "id": 85, + "name": "Sega Master System", + "igdb_id": "sms", + "tgdb_id": None, + "ra_id": 11, + }, + "segacd": { + "id": 161828, + "name": "Sega Mega CD / Sega CD", + "igdb_id": "segacd", + "tgdb_id": None, + "ra_id": 9, + }, + "genesis-slash-megadrive": { + "id": 86, + "name": "Sega Mega Drive / Genesis", + "igdb_id": "genesis-slash-megadrive", + "tgdb_id": None, + "ra_id": 1, + }, + "sega-pico": { + "id": 81, + "name": "Sega Pico", + "igdb_id": "sega-pico", + "tgdb_id": None, + "ra_id": 68, + }, + "saturn": { + "id": 54695, + "name": "Sega Saturn", + "igdb_id": "saturn", + "tgdb_id": None, + "ra_id": 39, + }, + "sg1000": { + "id": 244470, + "name": "SG-1000", + "igdb_id": "sg1000", + "tgdb_id": None, + "ra_id": 33, + }, + "x1": {"id": 89, "name": "Sharp X1", "igdb_id": "x1", "tgdb_id": None, "ra_id": 64}, + "sharp-x68000": { + "id": 90, + "name": "Sharp X68000", + "igdb_id": "sharp-x68000", + "tgdb_id": None, + "ra_id": 52, + }, + "sinclair-ql": { + "id": 92, + "name": "Sinclair QL", + "igdb_id": "sinclair-ql", + "tgdb_id": None, + "ra_id": None, + }, + "zxs": { + "id": 93, + "name": "Sinclair ZX Spectrum", + "igdb_id": "zxs", + "tgdb_id": None, + "ra_id": None, + }, + "sinclair-zx81": { + "id": 94, + "name": "Sinclair ZX81", + "igdb_id": "sinclair-zx81", + "tgdb_id": None, + "ra_id": None, + }, + "ps": { + "id": 100, + "name": "Sony PlayStation", + "igdb_id": "ps", + "tgdb_id": None, + "ra_id": 12, + }, + "ps2": { + "id": 101, + "name": "Sony PlayStation 2", + "igdb_id": "ps2", + "tgdb_id": None, + "ra_id": 21, + }, + "ps3": { + "id": 161830, + "name": "Sony Playstation 3", + "igdb_id": "ps3", + "tgdb_id": None, + "ra_id": None, + }, + "ps4--1": { + "id": 232986, + "name": "Sony Playstation 4", + "igdb_id": "ps4--1", + "tgdb_id": None, + "ra_id": None, + }, + "ps5": { + "id": 232987, + "name": "Sony Playstation 5", + "igdb_id": "ps5", + "tgdb_id": None, + "ra_id": None, + }, + "psp": { + "id": 161831, + "name": "Sony Playstation Portable", + "igdb_id": "psp", + "tgdb_id": None, + "ra_id": 41, + }, + "psvita": { + "id": 102, + "name": "Sony PlayStation Vita", + "igdb_id": "psvita", + "tgdb_id": None, + "ra_id": None, + }, + "pocketstation": { + "id": 103, + "name": "Sony PocketStation", + "igdb_id": "pocketstation", + "tgdb_id": None, + "ra_id": None, + }, + "sfam": { + "id": 233081, + "name": "Super Famicom", + "igdb_id": "sfam", + "tgdb_id": None, + "ra_id": None, + }, + "snes": { + "id": 74, + "name": "Super Nintendo Entertainment System", + "igdb_id": "snes", + "tgdb_id": None, + "ra_id": 3, + }, + "trs-80": { + "id": 105, + "name": "Tandy/RadioShack TRS-80", + "igdb_id": "trs-80", + "tgdb_id": None, + "ra_id": None, + }, + "trs-80-color-computer": { + "id": 106, + "name": "Tandy/RadioShack TRS-80 Color Computer", + "igdb_id": "trs-80-color-computer", + "tgdb_id": None, + "ra_id": None, + }, + "turbografx-16-slash-pc-engine-cd": { + "id": 247350, + "name": "Turbografx-16/PC Engine CD", + "igdb_id": "turbografx-16-slash-pc-engine-cd", + "tgdb_id": None, + "ra_id": None, + }, + "vectrex": { + "id": 45, + "name": "Vectrex", + "igdb_id": "vectrex", + "tgdb_id": None, + "ra_id": 46, + }, + "watara-slash-quickshot-supervision": { + "id": 244828, + "name": "Watara Supervision", + "igdb_id": "watara-slash-quickshot-supervision", + "tgdb_id": None, + "ra_id": 63, + }, +} + +MISSING_PLATFORMS = { + 54769: { + "id": 54769, + "name": "8-Bit Productions Commander X16", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 55099: { + "id": 55099, + "name": "Acorn Atom", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 59891: { + "id": 59891, + "name": "Acorn FileStore E01", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 59894: { + "id": 59894, + "name": "Acorn Risc PC", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 59898: { + "id": 59898, + "name": "ACT Apricot PC-Xi", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 232983: { + "id": 232983, + "name": "Action Max", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 59904: { + "id": 59904, + "name": "Advanced Computer Design PDQ-3", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 59919: { + "id": 59919, + "name": "AEG Olympia Olytext 20", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 59924: { + "id": 59924, + "name": "AlphaSmart Pro", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 59927: { + "id": 59927, + "name": "Alspa Computers ALSPA", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 59930: { + "id": 59930, + "name": "Altos Computer Systems ACS-186, 586 & 986", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 59947: { + "id": 59947, + "name": "Altos Computer Systems ACS-186, 586, 686 & 986", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 59968: { + "id": 59968, + "name": "Altos Computer Systems ACS-8000", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 59971: { + "id": 59971, + "name": "Altos Computer Systems ACS-8600", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 59974: { + "id": 59974, + "name": "Altos Computer Systems Series 5", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 59989: { + "id": 59989, + "name": "AMPRO Computers LittleBoard Z80", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 61540: { + "id": 61540, + "name": "Amstrad GX4000", + "igdb_id": "amstrad-gx4000", + "tgdb_id": None, + "ra_id": None, + }, + 61550: { + "id": 61550, + "name": "Amstrad NC-100", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 61552: { + "id": 61552, + "name": "Amstrad NC-200", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 61562: { + "id": 61562, + "name": "Analogue Mega Sg", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 61564: { + "id": 61564, + "name": "Analogue Nt Mini Noir", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 61567: { + "id": 61567, + "name": "Analogue Pocket", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 61735: { + "id": 61735, + "name": "Analogue Super Nt", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 61738: { + "id": 61738, + "name": "APF Imagination Machine", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 61862: { + "id": 61862, + "name": "APF M-1000", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 61885: { + "id": 61885, + "name": "Apple 1", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 63154: { + "id": 63154, + "name": "Apple III", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 69659: { + "id": 69659, + "name": "Apple Lisa", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 69714: { + "id": 69714, + "name": "Applied Technology MicroBee", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 69734: { + "id": 69734, + "name": "Aquaplus P-ECE", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 244294: { + "id": 244294, + "name": "Arduboy Inc - Arduboy", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 69743: { + "id": 69743, + "name": "AT&T 3B20", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97255: { + "id": 97255, + "name": "Bandai Design Master Denshi Mangajuku", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97264: { + "id": 97264, + "name": "Bandai Gundam RX-78", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97267: { + "id": 97267, + "name": "Bandai Super Vision 8000", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97550: { + "id": 97550, + "name": "Benesse Pocket Challenge V2", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97577: { + "id": 97577, + "name": "Benesse Pocket Challenge W", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97608: { + "id": 97608, + "name": "Bernd Huebler & Klaus-Peter Evert Huebler-Evert-MC", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97610: { + "id": 97610, + "name": "Bernd Huebler Huebler-Grafik-MC", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97612: { + "id": 97612, + "name": "BGR Computers Excalibur 64", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97614: { + "id": 97614, + "name": "Bit Corporation BIT 90", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97616: { + "id": 97616, + "name": "Bit Corporation Gamate", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97681: { + "id": 97681, + "name": "Bondwell 12", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97685: { + "id": 97685, + "name": "Bondwell 14", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97694: { + "id": 97694, + "name": "Bondwell 2", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97705: { + "id": 97705, + "name": "Burroughs B1000 Series", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97712: { + "id": 97712, + "name": "Burroughs B20", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97718: { + "id": 97718, + "name": "Cambridge Computer Z88", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97720: { + "id": 97720, + "name": "Camputers Lynx", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 97839: { + "id": 97839, + "name": "Casio CFX-9850", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 98757: { + "id": 98757, + "name": "Casio FP-1000 & FP-1100", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 98771: { + "id": 98771, + "name": "Casio PB-1000", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 98793: { + "id": 98793, + "name": "Casio PV-1000", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 98811: { + "id": 98811, + "name": "Casio PV-2000", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 98822: { + "id": 98822, + "name": "CCE MC-1000", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 8: { + "id": 8, + "name": "Commodore 128", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 139611: { + "id": 139611, + "name": "Commodore BX256-80HP", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 234388: { + "id": 234388, + "name": "Entex Adventure Vision", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 238902: { + "id": 238902, + "name": "Fujitsu - FM Towns (Flux)", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 246662: { + "id": 246662, + "name": "HomeLab BraiLab", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 233269: { + "id": 233269, + "name": "IBM PCjr", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 51: { + "id": 51, + "name": "Mattel Aquarius", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 232984: { + "id": 232984, + "name": "Microsoft Xbox Series X", + "igdb_id": "series-x-s", + "tgdb_id": None, + "ra_id": None, + }, + 234456: { + "id": 234456, + "name": "MITS Altair 8800", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 96: {"id": 96, "name": "Neo Geo", "igdb_id": "", "tgdb_id": None, "ra_id": None}, + 65: { + "id": 65, + "name": "Nintendo 64DD", + "igdb_id": "nintendo-64dd", + "tgdb_id": None, + "ra_id": None, + }, + 244733: { + "id": 244733, + "name": "Nintendo Pokemon Mini", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 236532: { + "id": 236532, + "name": "Radica Arcade Legends & Play TV Legends", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 234745: { + "id": 234745, + "name": "RCA Studio II", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 82: { + "id": 82, + "name": "Sega Advanced Pico Beena", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 52165: { + "id": 52165, + "name": "Sega Computer 3000", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 51951: { + "id": 51951, + "name": "Sega Super Control Station", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 232985: { + "id": 232985, + "name": "Sinclair ZX80", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 47973: { + "id": 47973, + "name": "Texas Instruments TI-82", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 243852: { + "id": 243852, + "name": "Texas Instruments TI-83", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 52922: { + "id": 52922, + "name": "Tsukuda Original Othello Multivision", + "igdb_id": "", + "tgdb_id": None, + "ra_id": None, + }, + 245372: { + "id": 245372, + "name": "TurboGrafx-16/PC Engine", + "igdb_id": "turbograpfx16--1", + "tgdb_id": None, + "ra_id": 8, + }, +} diff --git a/backend/handler/metadata/igdb_handler.py b/backend/handler/metadata/igdb_handler.py index 8e3a0523e..659d432de 100644 --- a/backend/handler/metadata/igdb_handler.py +++ b/backend/handler/metadata/igdb_handler.py @@ -819,7 +819,13 @@ SEARCH_FIELDS = ("game.id", "name") # name: a.innerText # })) -IGDB_PLATFORM_LIST = ( + +class SlugToIGDB(TypedDict): + slug: str + name: str + + +IGDB_PLATFORM_LIST: list[SlugToIGDB] = [ {"slug": "visionos", "name": "visionOS"}, {"slug": "meta-quest-3", "name": "Meta Quest 3"}, {"slug": "atari2600", "name": "Atari 2600"}, @@ -1036,7 +1042,11 @@ IGDB_PLATFORM_LIST = ( {"slug": "onlive-game-system", "name": "OnLive Game System"}, {"slug": "vc", "name": "Virtual Console"}, {"slug": "airconsole", "name": "AirConsole"}, -) +] + +IGDB_PLATFORMS_BY_SLUG: dict[str, SlugToIGDB] = { + platform["slug"]: platform for platform in IGDB_PLATFORM_LIST +} IGDB_PLATFORM_CATEGORIES: dict[int, str] = { 0: "Unknown", diff --git a/backend/handler/scan_handler.py b/backend/handler/scan_handler.py index 7ab801019..fba720815 100644 --- a/backend/handler/scan_handler.py +++ b/backend/handler/scan_handler.py @@ -8,6 +8,7 @@ from handler.database import db_platform_handler from handler.filesystem import fs_asset_handler, fs_firmware_handler, fs_rom_handler from handler.filesystem.roms_handler import FSRom from handler.metadata import ( + meta_hasheous_handler, meta_igdb_handler, meta_launchbox_handler, meta_moby_handler, @@ -15,6 +16,7 @@ from handler.metadata import ( meta_ra_handler, meta_ss_handler, ) +from handler.metadata.hasheous_handler import HasheousRom from handler.metadata.igdb_handler import IGDBPlatform, IGDBRom from handler.metadata.launchbox_handler import LaunchboxPlatform, LaunchboxRom from handler.metadata.moby_handler import MobyGamesPlatform, MobyGamesRom @@ -49,6 +51,7 @@ class MetadataSource: RA = "ra" # RetroAchivements LB = "lb" # Launchbox PM = "pm" # Playmatch + HASHEOUS = "hasheous" # Hasheous async def _get_main_platform_igdb_id(platform: Platform): @@ -150,6 +153,8 @@ async def scan_platform( else LaunchboxPlatform(launchbox_id=None, slug=platform_attrs["slug"]) ) + await meta_hasheous_handler.get_platforms() + platform_attrs["name"] = platform_attrs["slug"].replace("-", " ").title() platform_attrs.update( { @@ -423,6 +428,21 @@ async def scan_rom( return IGDBRom(igdb_id=None) + async def fetch_hasheous_rom() -> HasheousRom: + if ( + MetadataSource.HASHEOUS in metadata_sources + and platform.fs_slug + and ( + newly_added + or scan_type == ScanType.COMPLETE + or (scan_type == ScanType.PARTIAL and not rom.hasheous_id) + or (scan_type == ScanType.UNIDENTIFIED and not rom.hasheous_id) + ) + ): + return await meta_hasheous_handler.get_rom(rom_attrs) + + return HasheousRom(igdb_id=None) + # Run both metadata fetches concurrently ( igdb_handler_rom, @@ -431,6 +451,7 @@ async def scan_rom( ra_handler_rom, launchbox_handler_rom, playmatch_handler_rom, + hasheous_handler_rom, ) = await asyncio.gather( fetch_igdb_rom(), fetch_moby_rom(), @@ -438,6 +459,7 @@ async def scan_rom( fetch_ra_rom(), fetch_launchbox_rom(platform.slug), fetch_playmatch_rom(), + fetch_hasheous_rom(), ) # Only update fields if match is found diff --git a/backend/models/rom.py b/backend/models/rom.py index f29a31136..7c5d69f43 100644 --- a/backend/models/rom.py +++ b/backend/models/rom.py @@ -120,6 +120,7 @@ class Rom(BaseModel): ss_id: Mapped[int | None] ra_id: Mapped[int | None] launchbox_id: Mapped[int | None] + hasheous_id: Mapped[int | None] __table_args__ = ( Index("idx_roms_igdb_id", "igdb_id"), @@ -128,6 +129,7 @@ class Rom(BaseModel): Index("idx_roms_ra_id", "ra_id"), Index("idx_roms_sgdb_id", "sgdb_id"), Index("idx_roms_launchbox_id", "launchbox_id"), + Index("idx_roms_hasheous_id", "hasheous_id"), ) fs_name: Mapped[str] = mapped_column(String(length=450)) @@ -154,6 +156,9 @@ class Rom(BaseModel): launchbox_metadata: Mapped[dict[str, Any] | None] = mapped_column( CustomJSON(), default=dict ) + hasheous_metadata: Mapped[dict[str, Any] | None] = mapped_column( + CustomJSON(), default=dict + ) path_cover_s: Mapped[str | None] = mapped_column(Text, default="") path_cover_l: Mapped[str | None] = mapped_column(Text, default="") diff --git a/env.template b/env.template index 723d1130e..888ba41b9 100644 --- a/env.template +++ b/env.template @@ -30,6 +30,9 @@ PLAYMATCH_API_ENABLED= # LaunchBox LAUNCHBOX_API_ENABLED= +# Hasheous +HASHEOUS_API_ENABLED= + # Database config DB_HOST=127.0.0.1 DB_PORT=3306 diff --git a/frontend/assets/scrappers/hasheous.png b/frontend/assets/scrappers/hasheous.png new file mode 100644 index 000000000..6ddcf2c77 Binary files /dev/null and b/frontend/assets/scrappers/hasheous.png differ diff --git a/frontend/src/__generated__/models/MetadataSourcesDict.ts b/frontend/src/__generated__/models/MetadataSourcesDict.ts index 281fde932..e2a9cc526 100644 --- a/frontend/src/__generated__/models/MetadataSourcesDict.ts +++ b/frontend/src/__generated__/models/MetadataSourcesDict.ts @@ -11,5 +11,6 @@ export type MetadataSourcesDict = { RA_API_ENABLED: boolean; LAUNCHBOX_API_ENABLED: boolean; PLAYMATCH_API_ENABLED: boolean; + HASHEOUS_API_ENABLED: boolean; }; diff --git a/frontend/src/stores/heartbeat.ts b/frontend/src/stores/heartbeat.ts index fcbaec63b..5c7f3fda5 100644 --- a/frontend/src/stores/heartbeat.ts +++ b/frontend/src/stores/heartbeat.ts @@ -43,6 +43,7 @@ const defaultHeartbeat: Heartbeat = { STEAMGRIDDB_API_ENABLED: false, LAUNCHBOX_API_ENABLED: false, PLAYMATCH_API_ENABLED: false, + HASHEOUS_API_ENABLED: false, }, FILESYSTEM: { FS_PLATFORMS: [], diff --git a/frontend/src/views/Scan.vue b/frontend/src/views/Scan.vue index 2ae7ba32a..8eec42ae9 100644 --- a/frontend/src/views/Scan.vue +++ b/frontend/src/views/Scan.vue @@ -63,6 +63,14 @@ const metadataOptions = computed(() => [ ? t("scan.disabled-by-admin") : "", }, + { + name: "Hasheous", + value: "hasheous", + logo_path: "/assets/scrappers/hasheous.png", + disabled: !heartbeat.value.METADATA_SOURCES?.HASHEOUS_API_ENABLED + ? t("scan.disabled-by-admin") + : "", + }, { name: "Playmatch", value: "pm",