Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 14 additions & 19 deletions backend/adapters/services/igdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -4808,6 +4808,13 @@ def _igdb_family(slug: UPS) -> PlatformFamily:
)


# IGDB platform versions whose IGDB slug differs from RomM's.
IGDB_PLATFORM_VERSIONS |= {
UPS.HIKARU: IGDB_PLATFORM_VERSIONS["sega-hikaru"],
UPS.NAOMI2: IGDB_PLATFORM_VERSIONS["naomi-2"],
UPS.STV: IGDB_PLATFORM_VERSIONS["sega-titan-video"],
}

# Platforms IGDB doesn't list, mapped to an IGDB platform they share a family
# and category with.
IGDB_SIBLING_PLATFORMS: dict[UPS, UPS] = {
Expand Down Expand Up @@ -4843,23 +4850,11 @@ def _igdb_family(slug: UPS) -> PlatformFamily:
UPS.WATCHOS: UPS.IOS,
UPS.TELSTAR_ARCADE: UPS.COLECOVISION,
UPS.CASIO_PV_1000: UPS.CASIO_LOOPY,
UPS.MODEL1: UPS.ARCADE,
UPS.MODEL2: UPS.ARCADE,
UPS.MODEL3: UPS.ARCADE,
UPS.SYSTEM16: UPS.ARCADE,
UPS.SYSTEM32: UPS.ARCADE,
UPS.STV: UPS.ARCADE,
UPS.HIKARU: UPS.ARCADE,
UPS.CPS1: UPS.ARCADE,
UPS.CPS2: UPS.ARCADE,
UPS.CPS3: UPS.ARCADE,
UPS.TYPE_X: UPS.ARCADE,
UPS.TAITO_X_55: UPS.ARCADE,
}


# Families IGDB doesn't list, or that no IGDB platform of the same category has.
_CAPCOM_FAMILY = PlatformFamily(family_name="Capcom", family_slug="capcom")
_TAITO_FAMILY = PlatformFamily(family_name="Taito", family_slug="taito")
_TEXAS_INSTRUMENTS_FAMILY = PlatformFamily(
family_name="Texas Instruments", family_slug="texas-instruments"
Expand All @@ -4881,19 +4876,19 @@ def _igdb_family(slug: UPS) -> PlatformFamily:
family_name="MOS Technology", family_slug="mos-technology"
)

# Platforms IGDB doesn't list, mapped to their family. Overrides a sibling's.
# Platforms IGDB lists under no platform of their own, mapped to their family.
# Overrides the family of a sibling or of the platform IGDB files them under.
IGDB_PLATFORM_FAMILIES: dict[UPS, PlatformFamily] = {
UPS.MODEL1: _igdb_family(UPS.DC),
UPS.MODEL2: _igdb_family(UPS.DC),
UPS.MODEL3: _igdb_family(UPS.DC),
UPS.SYSTEM16: _igdb_family(UPS.DC),
UPS.SYSTEM32: _igdb_family(UPS.DC),
UPS.STV: _igdb_family(UPS.DC),
UPS.HIKARU: _igdb_family(UPS.DC),
UPS.NAOMI: _igdb_family(UPS.DC),
UPS.NAOMI2: _igdb_family(UPS.DC),
UPS.NAOMIGD: _igdb_family(UPS.DC),
UPS.CHIHIRO: _igdb_family(UPS.DC),
UPS.LINDBERGH: _igdb_family(UPS.DC),
UPS.SC3000: _igdb_family(UPS.DC),
UPS.CPS1: _CAPCOM_FAMILY,
UPS.CPS2: _CAPCOM_FAMILY,
UPS.CPS3: _CAPCOM_FAMILY,
UPS.TYPE_X: _TAITO_FAMILY,
UPS.TAITO_X_55: _TAITO_FAMILY,
UPS.ZUNE: _igdb_family(UPS.WIN),
Expand Down
44 changes: 44 additions & 0 deletions backend/alembic/versions/0154_retired_arcade_slugs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""move platforms off the retired MAME-only arcade board slugs

Revision ID: 0154_retired_arcade_slugs
Revises: 0153_play_permissions
Create Date: 2026-10-19 00:00:00.000000

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "0154_retired_arcade_slugs"
down_revision = "0153_play_permissions"
branch_labels = None
depends_on = None

RETIRED_SLUGS = (
"cps1",
"cps2",
"cps3",
"model1",
"system-32",
"system16",
"system32",
)


def upgrade() -> None:
op.execute(
sa.text(
"UPDATE platforms SET slug = 'arcade' WHERE LOWER(slug) IN :slugs"
).bindparams(sa.bindparam("slugs", value=RETIRED_SLUGS, expanding=True))
)


def downgrade() -> None:
# The old slug is gone, so only a folder named after it can restore it.
op.execute(
sa.text(
"UPDATE platforms SET slug = LOWER(fs_slug) "
"WHERE slug = 'arcade' AND LOWER(fs_slug) IN :slugs"
).bindparams(sa.bindparam("slugs", value=RETIRED_SLUGS, expanding=True))
)
5 changes: 3 additions & 2 deletions backend/endpoints/responses/feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
from pydantic import BaseModel, BeforeValidator, Field, field_validator

from tasks.scheduled.update_switch_titledb import TITLEDB_REGION_LIST
from utils.arcade_platforms import PLAYABLE_ARCADE_SET_PLATFORMS
from utils.database import safe_int
from utils.platform_slugs import UniversalPlatformSlug as UPS

WEBRCADE_SUPPORTED_PLATFORM_SLUGS = frozenset(
WEBRCADE_SUPPORTED_PLATFORM_SLUGS = PLAYABLE_ARCADE_SET_PLATFORMS | frozenset(
(
UPS._3DO,
UPS.ARCADE,
UPS.ATARI2600,
UPS.ATARI5200,
UPS.ATARI7800,
Expand Down Expand Up @@ -45,6 +45,7 @@
)

WEBRCADE_SLUG_TO_TYPE_MAP: dict[str, str] = {
**dict.fromkeys(PLAYABLE_ARCADE_SET_PLATFORMS, "arcade"),
UPS.ATARI2600: "2600",
UPS.ATARI5200: "5200",
UPS.ATARI7800: "7800",
Expand Down
3 changes: 2 additions & 1 deletion backend/handler/database/roms_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
)
from utils import get_version
from utils.age_ratings import MIN_AGE_SOURCE_COLUMNS, compute_min_age
from utils.arcade_platforms import PLAYABLE_ARCADE_SET_PLATFORMS
from utils.database import (
ROMS_SEARCH_FULLTEXT_COLUMNS,
SORTABLE_NULLABLE_ROM_COLUMNS,
Expand Down Expand Up @@ -133,7 +134,7 @@
UPS.AMIGA,
UPS.AMIGA_CD,
UPS.AMIGA_CD32,
UPS.ARCADE,
*sorted(PLAYABLE_ARCADE_SET_PLATFORMS),
UPS.NEOGEOAES,
UPS.NEOGEOMVS,
UPS.ATARI2600,
Expand Down
24 changes: 13 additions & 11 deletions backend/handler/metadata/base_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import json
import re
import unicodedata
from collections.abc import Awaitable, Callable, Collection
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
Expand All @@ -23,6 +23,7 @@
SWITCH_TITLEDB_STORE,
)
from utils import get_version, int_or_none
from utils.arcade_platforms import MAME_SHORTNAME_PLATFORMS
from utils.cache import is_cache_store_ready
from utils.context import ctx_httpx_client
from utils.switch import derive_base_title_id
Expand Down Expand Up @@ -96,7 +97,6 @@ class IndexedFormatPlatforms:
ps2: int
psp: int
switch: int
arcade: Collection[int]
scummvm: int | None = None


Expand All @@ -107,6 +107,12 @@ def _fill_from_switch_entry(fallback_rom: BaseRom, index_entry: dict[str, Any])
fallback_rom["url_screenshots"] = index_entry.get("screenshots", None) or []


async def mame_title(shortname: str) -> str | None:
"""MAME's full title for a set, region and revision tags included."""
entry = await async_cache.hget(MAME_XML_KEY, shortname)
return json.loads(entry).get("description") if entry else None


class CoverResource(TypedDict):
"""One piece of artwork the manual cover search offers, in the shape
SteamGridDB grids have, so every provider fills the same picker."""
Expand Down Expand Up @@ -368,7 +374,7 @@ async def _resolve_indexed_title(
if index_entry:
_fill_from_switch_entry(fallback_rom, index_entry)

if platform_id in platforms.arcade:
if rom.platform and rom.platform.slug in MAME_SHORTNAME_PLATFORMS:
search_term = await self._mame_format(search_term)
fallback_rom["name"] = search_term

Expand Down Expand Up @@ -483,14 +489,10 @@ async def _switch_product_id_entry(cls, product_id: str) -> dict[str, Any] | Non
async def _mame_format(self, search_term: str) -> str:
from handler.filesystem import fs_rom_handler

index_entry = await async_cache.hget(MAME_XML_KEY, search_term)
if index_entry:
index_entry = json.loads(index_entry)
search_term = fs_rom_handler.get_file_name_with_no_tags(
index_entry.get("description", search_term)
)

return search_term
title = await mame_title(search_term)
return (
fs_rom_handler.get_file_name_with_no_tags(title) if title else search_term
)

async def _scummvm_format(self, search_term: str) -> str:
from handler.filesystem import fs_rom_handler
Expand Down
7 changes: 7 additions & 0 deletions backend/handler/metadata/hasheous_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from logger.logger import log
from models.rom import RomFile
from utils import get_version
from utils.arcade_platforms import ARCADE_SET_PLATFORMS, arcade_fallbacks
from utils.context import ctx_httpx_client
from utils.datetime import parse_utc_timestamp
from utils.platform_slugs import UniversalPlatformSlug as UPS
Expand Down Expand Up @@ -1631,3 +1632,9 @@ class SlugToHasheousId(TypedDict):
"tgdb_id": None,
},
}
# Hasheous's Arcade carries RetroAchievements' Arcade, which only hashes MAME
# and FinalBurn sets.
HASHEOUS_PLATFORM_LIST |= {
slug: platform if slug in ARCADE_SET_PLATFORMS else {**platform, "ra_id": None}
for slug, platform in arcade_fallbacks(HASHEOUS_PLATFORM_LIST).items()
}
2 changes: 2 additions & 0 deletions backend/handler/metadata/hltb_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from adapters.services.response_validation import parse_response
from config import HLTB_API_ENABLED
from logger.logger import log
from utils.arcade_platforms import arcade_fallbacks
from utils.context import ctx_httpx_client
from utils.hltb_search import (
HLTB_API_URL_FIXTURE,
Expand Down Expand Up @@ -879,3 +880,4 @@ class SlugToHLTBPlatform(TypedDict):
UPS.ZX81: {"name": "ZX81", "slug": "zx81", "count": 41},
UPS.ZEEBO: {"name": "Zeebo", "slug": "zeebo", "count": 9},
}
HLTB_PLATFORM_LIST |= arcade_fallbacks(HLTB_PLATFORM_LIST)
27 changes: 17 additions & 10 deletions backend/handler/metadata/igdb_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from handler.redis_handler import async_cache
from logger.logger import log
from models.rom import Rom
from utils.arcade_platforms import ARCADE_PLATFORMS
from utils.context import ctx_httpx_client
from utils.platform_slugs import UniversalPlatformSlug as UPS

Expand All @@ -40,17 +41,11 @@
PS2_IGDB_ID: Final = IGDB_PLATFORM_LIST[UPS.PS2]["id"]
PSP_IGDB_ID: Final = IGDB_PLATFORM_LIST[UPS.PSP]["id"]
SWITCH_IGDB_ID: Final = IGDB_PLATFORM_LIST[UPS.SWITCH]["id"]
ARCADE_IGDB_IDS: Final = [
IGDB_PLATFORM_LIST[UPS.ARCADE]["id"],
IGDB_PLATFORM_LIST[UPS.NEOGEOAES]["id"],
IGDB_PLATFORM_LIST[UPS.NEOGEOMVS]["id"],
]
IGDB_INDEXED_FORMAT_PLATFORMS: Final = IndexedFormatPlatforms(
ps1=PS1_IGDB_ID,
ps2=PS2_IGDB_ID,
psp=PSP_IGDB_ID,
switch=SWITCH_IGDB_ID,
arcade=ARCADE_IGDB_IDS,
scummvm=IGDB_PLATFORM_LIST[UPS.SCUMMVM]["id"],
)

Expand Down Expand Up @@ -777,6 +772,14 @@ async def probe() -> bool:
return await self._heartbeat("IGDB API", probe)

def get_platform(self, slug: str) -> IGDBPlatform:
platform = self._base_platform(slug)
if platform["slug"] in IGDB_PLATFORM_FAMILIES:
family = IGDB_PLATFORM_FAMILIES[UPS(platform["slug"])]
platform["family_name"] = family["family_name"]
platform["family_slug"] = family["family_slug"]
return platform

def _base_platform(self, slug: str) -> IGDBPlatform:
if slug in IGDB_PLATFORM_LIST:
platform = IGDB_PLATFORM_LIST[UPS(slug)]

Expand Down Expand Up @@ -812,16 +815,20 @@ def get_platform(self, slug: str) -> IGDBPlatform:
),
)

# IGDB files every arcade game under Arcade. A provider that knows the
# board supplies the name, so it doesn't read "Arcade".
if slug in ARCADE_PLATFORMS:
arcade = self._base_platform(UPS.ARCADE)
arcade["slug"] = slug
arcade.pop("name", None)
return arcade

unlisted_platform = IGDBPlatform(igdb_id=None, slug=slug)
if slug in IGDB_SIBLING_PLATFORMS:
sibling = IGDB_PLATFORM_LIST[IGDB_SIBLING_PLATFORMS[UPS(slug)]]
unlisted_platform["category"] = sibling["category"]
unlisted_platform["family_name"] = sibling["family_name"]
unlisted_platform["family_slug"] = sibling["family_slug"]
if slug in IGDB_PLATFORM_FAMILIES:
family = IGDB_PLATFORM_FAMILIES[UPS(slug)]
unlisted_platform["family_name"] = family["family_name"]
unlisted_platform["family_slug"] = family["family_slug"]

return unlisted_platform

Expand Down
3 changes: 2 additions & 1 deletion backend/handler/metadata/launchbox_handler/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from handler.filesystem import fs_rom_handler
from handler.redis_handler import async_cache
from logger.logger import log
from utils.arcade_platforms import MAME_SHORTNAME_PLATFORMS
from utils.cache import is_cache_store_ready
from utils.database import safe_int
from utils.platform_slugs import UniversalPlatformSlug as UPS
Expand Down Expand Up @@ -168,7 +169,7 @@ async def get_rom(

# Resolve MAME arcade filename (e.g. wrlok_l3.zip) to its full title
# via LaunchBox's Mame.xml before name-based lookup.
if platform_slug == UPS.ARCADE:
if platform_slug in MAME_SHORTNAME_PLATFORMS:
mame_entry = await self._remote.get_mame_entry(fs_name)
if mame_entry:
name = (mame_entry.get("Name") or "").strip()
Expand Down
15 changes: 6 additions & 9 deletions backend/handler/metadata/launchbox_handler/platforms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import TypedDict

from utils.arcade_platforms import arcade_fallbacks
from utils.platform_slugs import UniversalPlatformSlug as UPS

from .types import LaunchboxPlatform
Expand Down Expand Up @@ -32,6 +33,10 @@ class SlugToLaunchboxId(TypedDict):
UPS.APPLE_IIGS: {"id": 112, "name": "Apple IIGS"},
UPS.APPLEII: {"id": 110, "name": "Apple II"},
UPS.ARCADE: {"id": 5, "name": "Arcade"},
UPS.ATOMISWAVE: {"id": 98, "name": "Sammy Atomiswave"},
UPS.NAOMI: {"id": 99, "name": "Sega Naomi"},
UPS.NAOMI2: {"id": 100, "name": "Sega Naomi 2"},
UPS.TRIFORCE: {"id": 207, "name": "Sega Triforce"},
UPS.ARCADIA_2001: {"id": 79, "name": "Emerson Arcadia 2001"},
UPS.ASTROCADE: {"id": 77, "name": "Bally Astrocade"},
UPS.ATARI_JAGUAR_CD: {"id": 10, "name": "Atari Jaguar CD"},
Expand Down Expand Up @@ -103,7 +108,6 @@ class SlugToLaunchboxId(TypedDict):
UPS.MAC: {"id": 17, "name": "Apple Mac OS"},
UPS.AQUARIUS: {"id": 69, "name": "Mattel Aquarius"},
UPS.MEGA_DUCK_SLASH_COUGAR_BOY: {"id": 127, "name": "Mega Duck"},
UPS.MODEL1: {"id": 104, "name": "Sega Model 1"},
UPS.MODEL2: {"id": 88, "name": "Sega Model 2"},
UPS.MODEL3: {"id": 94, "name": "Sega Model 3"},
UPS.MSX: {"id": 82, "name": "Microsoft MSX"},
Expand Down Expand Up @@ -182,9 +186,6 @@ class SlugToLaunchboxId(TypedDict):
UPS.SUPERGRAFX: {"id": 162, "name": "PC Engine SuperGrafx"},
UPS.SWITCH: {"id": 211, "name": "Nintendo Switch"},
UPS.SWITCH_2: {"id": 224, "name": "Nintendo Switch 2"},
UPS.SYSTEM_32: {"id": 93, "name": "Namco System 22"},
UPS.SYSTEM16: {"id": 97, "name": "Sega System 16"},
UPS.SYSTEM32: {"id": 96, "name": "Sega System 32"},
UPS.TG16: {"id": 54, "name": "NEC TurboGrafx-16"},
UPS.TI_994A: {"id": 149, "name": "Texas Instruments TI 99/4A"},
UPS.TOMY_TUTOR: {"id": 200, "name": "Tomy Tutor"},
Expand Down Expand Up @@ -219,11 +220,7 @@ class SlugToLaunchboxId(TypedDict):
UPS.ZX81: {"id": 147, "name": "Sinclair ZX-81"},
UPS.ZXS: {"id": 46, "name": "Sinclair ZX Spectrum"},
}

# Reverse lookup: LaunchBox platform ID → UPS slug
LAUNCHBOX_PLATFORM_NAME_TO_SLUG = {
v["id"]: k for k, v in LAUNCHBOX_PLATFORM_LIST.items()
}
LAUNCHBOX_PLATFORM_LIST |= arcade_fallbacks(LAUNCHBOX_PLATFORM_LIST)


def get_platform(slug: str) -> LaunchboxPlatform:
Expand Down
Loading
Loading