2025-09-20 17:57:38 -04:00

1400 lines
41 KiB
Python

import re
from typing import Final, NotRequired, TypedDict
from urllib.parse import quote
import pydash
from unidecode import unidecode as uc
from adapters.services.mobygames import MobyGamesService
from adapters.services.mobygames_types import MobyGame
from config import MOBYGAMES_API_KEY
from logger.logger import log
from .base_handler import (
PS2_OPL_REGEX,
SONY_SERIAL_REGEX,
SWITCH_PRODUCT_ID_REGEX,
SWITCH_TITLEDB_REGEX,
BaseRom,
MetadataHandler,
)
from .base_handler import UniversalPlatformSlug as UPS
PS1_MOBY_ID: Final = 6
PS2_MOBY_ID: Final = 7
PSP_MOBY_ID: Final = 46
SWITCH_MOBY_ID: Final = 203
ARCADE_MOBY_IDS: Final = [143, 36]
# Regex to detect MobyGames ID tags in filenames like (moby-12345)
MOBYGAMES_TAG_REGEX = re.compile(r"\(moby-(\d+)\)", re.IGNORECASE)
class MobyGamesPlatform(TypedDict):
slug: str
moby_id: int | None
moby_slug: NotRequired[str]
name: NotRequired[str]
class MobyMetadataPlatform(TypedDict):
moby_id: int
name: str
class MobyMetadata(TypedDict):
moby_score: str
genres: list[str]
alternate_titles: list[str]
platforms: list[MobyMetadataPlatform]
class MobyGamesRom(BaseRom):
moby_id: int | None
moby_metadata: NotRequired[MobyMetadata]
def extract_metadata_from_moby_rom(rom: MobyGame) -> MobyMetadata:
return MobyMetadata(
{
"moby_score": str(rom.get("moby_score", "")),
"genres": [genre["genre_name"] for genre in rom.get("genres", [])],
"alternate_titles": [
alt["title"] for alt in rom.get("alternate_titles", [])
],
"platforms": [
{
"moby_id": p["platform_id"],
"name": p["platform_name"],
}
for p in rom.get("platforms", [])
],
}
)
class MobyGamesHandler(MetadataHandler):
def __init__(self) -> None:
self.moby_service = MobyGamesService()
self.min_similarity_score = 0.6
@classmethod
def is_enabled(cls) -> bool:
return bool(MOBYGAMES_API_KEY)
async def heartbeat(self) -> bool:
if not self.is_enabled():
return False
try:
response = await self.moby_service.list_groups(limit=1)
except Exception as e:
log.error("Error checking MobyGames API: %s", e)
return False
return bool(response)
@staticmethod
def extract_mobygames_id_from_filename(fs_name: str) -> int | None:
"""Extract MobyGames ID from filename tag like (moby-12345)."""
match = MOBYGAMES_TAG_REGEX.search(fs_name)
if match:
return int(match.group(1))
return None
async def _search_rom(
self, search_term: str, platform_moby_id: int, split_game_name: bool = False
) -> MobyGame | None:
if not platform_moby_id:
return None
roms = await self.moby_service.list_games(
platform_ids=[platform_moby_id],
title=quote(uc(search_term), safe="/ "),
)
if not roms:
return None
games_by_name: dict[str, MobyGame] = {}
for game in roms:
if (
game["title"] not in games_by_name
or game["game_id"] < games_by_name[game["title"]]["game_id"]
):
games_by_name[game["title"]] = game
best_match, best_score = self.find_best_match(
search_term,
list(games_by_name.keys()),
self.min_similarity_score,
split_game_name=split_game_name,
)
if best_match:
log.debug(
f"Found match for '{search_term}' -> '{best_match}' (score: {best_score:.3f})"
)
return games_by_name[best_match]
return None
def get_platform(self, slug: str) -> MobyGamesPlatform:
if slug not in MOBYGAMES_PLATFORM_LIST:
return MobyGamesPlatform(moby_id=None, slug=slug)
platform = MOBYGAMES_PLATFORM_LIST[UPS(slug)]
return MobyGamesPlatform(
moby_id=platform["id"],
slug=slug,
moby_slug=platform["slug"],
name=platform["name"],
)
async def get_rom(self, fs_name: str, platform_moby_id: int) -> MobyGamesRom:
from handler.filesystem import fs_rom_handler
if not self.is_enabled():
return MobyGamesRom(moby_id=None)
if not platform_moby_id:
return MobyGamesRom(moby_id=None)
# Check for MobyGames ID tag in filename first
mobygames_id_from_tag = self.extract_mobygames_id_from_filename(fs_name)
if mobygames_id_from_tag:
log.debug(f"Found MobyGames ID tag in filename: {mobygames_id_from_tag}")
rom_by_id = await self.get_rom_by_id(mobygames_id_from_tag)
if rom_by_id["moby_id"]:
log.debug(
f"Successfully matched ROM by MobyGames ID tag: {fs_name} -> {mobygames_id_from_tag}"
)
return rom_by_id
else:
log.warning(
f"MobyGames ID {mobygames_id_from_tag} from filename tag not found in MobyGames"
)
search_term = fs_rom_handler.get_file_name_with_no_tags(fs_name)
fallback_rom = MobyGamesRom(moby_id=None)
# Support for PS2 OPL filename format
match = PS2_OPL_REGEX.match(fs_name)
if platform_moby_id == PS2_MOBY_ID and match:
search_term = await self._ps2_opl_format(match, search_term)
fallback_rom = MobyGamesRom(moby_id=None, name=search_term)
# Support for sony serial filename format (PS, PS3, PS3)
match = SONY_SERIAL_REGEX.search(fs_name, re.IGNORECASE)
if platform_moby_id == PS1_MOBY_ID and match:
search_term = await self._ps1_serial_format(match, search_term)
fallback_rom = MobyGamesRom(moby_id=None, name=search_term)
if platform_moby_id == PS2_MOBY_ID and match:
search_term = await self._ps2_serial_format(match, search_term)
fallback_rom = MobyGamesRom(moby_id=None, name=search_term)
if platform_moby_id == PSP_MOBY_ID and match:
search_term = await self._psp_serial_format(match, search_term)
fallback_rom = MobyGamesRom(moby_id=None, name=search_term)
# Support for switch titleID filename format
match = SWITCH_TITLEDB_REGEX.search(fs_name)
if platform_moby_id == SWITCH_MOBY_ID and match:
search_term, index_entry = await self._switch_titledb_format(
match, search_term
)
if index_entry:
fallback_rom = MobyGamesRom(
moby_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(fs_name)
if platform_moby_id == SWITCH_MOBY_ID and match:
search_term, index_entry = await self._switch_productid_format(
match, search_term
)
if index_entry:
fallback_rom = MobyGamesRom(
moby_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_moby_id in ARCADE_MOBY_IDS:
search_term = await self._mame_format(search_term)
fallback_rom = MobyGamesRom(moby_id=None, name=search_term)
normalized_search_term = self.normalize_search_term(
search_term, remove_punctuation=False
)
res = await self._search_rom(
self.SEARCH_TERM_NORMALIZER.sub(": ", normalized_search_term),
platform_moby_id,
)
# Moby API doesn't handle some special characters well
if not res:
terms = re.split(self.SEARCH_TERM_SPLIT_PATTERN, search_term)
res = await self._search_rom(
terms[-1], platform_moby_id, split_game_name=True
)
if not res:
return fallback_rom
rom = {
"moby_id": res["game_id"],
"name": res["title"],
"summary": res.get("description", ""),
"url_cover": pydash.get(res, "sample_cover.image", None),
"url_screenshots": [s["image"] for s in res.get("sample_screenshots", [])],
"moby_metadata": extract_metadata_from_moby_rom(res),
}
return MobyGamesRom({k: v for k, v in rom.items() if v}) # type: ignore[misc]
async def get_rom_by_id(self, moby_id: int) -> MobyGamesRom:
if not self.is_enabled():
return MobyGamesRom(moby_id=None)
roms = await self.moby_service.list_games(game_id=moby_id)
if not roms:
return MobyGamesRom(moby_id=None)
res = roms[0]
rom = {
"moby_id": res["game_id"],
"name": res["title"],
"summary": res.get("description", None),
"url_cover": pydash.get(res, "sample_cover.image", None),
"url_screenshots": [s["image"] for s in res.get("sample_screenshots", [])],
"moby_metadata": extract_metadata_from_moby_rom(res),
}
return MobyGamesRom({k: v for k, v in rom.items() if v}) # type: ignore[misc]
async def get_matched_rom_by_id(self, moby_id: int) -> MobyGamesRom | None:
if not self.is_enabled():
return None
rom = await self.get_rom_by_id(moby_id)
return rom if rom["moby_id"] else None
async def get_matched_roms_by_name(
self, search_term: str, platform_moby_id: int | None
) -> list[MobyGamesRom]:
if not self.is_enabled():
return []
if not platform_moby_id:
return []
matched_roms = await self.moby_service.list_games(
platform_ids=[platform_moby_id],
title=quote(uc(search_term), safe="/ "),
)
return [
MobyGamesRom(
{ # type: ignore[misc]
k: v
for k, v in {
"moby_id": rom["game_id"],
"name": rom["title"],
"summary": rom.get("description", ""),
"url_cover": pydash.get(rom, "sample_cover.image", None),
"url_screenshots": [
s["image"] for s in rom.get("sample_screenshots", [])
],
"moby_metadata": extract_metadata_from_moby_rom(rom),
}.items()
if v
}
)
for rom in matched_roms
]
class SlugToMobyId(TypedDict):
id: int
name: str
slug: str
MOBYGAMES_PLATFORM_LIST: dict[UPS, SlugToMobyId] = {
UPS.APVS: {
"id": 253,
"name": "1292 Advanced Programmable Video System",
"slug": "1292-advanced-programmable-video-system",
},
UPS._3DO: {"id": 35, "name": "3DO", "slug": "3do"},
UPS.N3DS: {"id": 101, "name": "Nintendo 3DS", "slug": "3ds"},
UPS.ABC_80: {"id": 318, "name": "ABC 80", "slug": "abc-80"},
UPS.ACORN_ARCHIMEDES: {
"id": 117,
"name": "Acorn Archimedes",
"slug": "acorn-32-bit",
},
UPS.ACORN_ELECTRON: {
"id": 93,
"name": "Electron",
"slug": "electron",
},
UPS.ACPC: {"id": 60, "name": "Amstrad CPC", "slug": "cpc"},
UPS.ADVENTURE_VISION: {
"id": 210,
"name": "Adventure Vision",
"slug": "adventure-vision",
},
UPS.AIRCONSOLE: {
"id": 305,
"name": "AirConsole",
"slug": "airconsole",
},
UPS.ALICE_3290: {
"id": 194,
"name": "Alice 32/90",
"slug": "alice-3290",
},
UPS.ALTAIR_680: {
"id": 265,
"name": "Altair 680",
"slug": "altair-680",
},
UPS.ALTAIR_8800: {
"id": 222,
"name": "Altair 8800",
"slug": "altair-8800",
},
UPS.AMAZON_ALEXA: {
"id": 237,
"name": "Amazon Alexa",
"slug": "amazon-alexa",
},
UPS.AMAZON_FIRE_TV: {
"id": 159,
"name": "Fire TV",
"slug": "fire-os",
},
UPS.AMIGA: {"id": 19, "name": "Amiga", "slug": "amiga"},
UPS.AMIGA_CD32: {
"id": 56,
"name": "Amiga CD32",
"slug": "amiga-cd32",
},
UPS.AMSTRAD_PCW: {
"id": 136,
"name": "Amstrad PCW",
"slug": "amstrad-pcw",
},
UPS.ANDROID: {"id": 91, "name": "Android", "slug": "android"},
UPS.ANTSTREAM: {
"id": 286,
"name": "Antstream",
"slug": "antstream",
},
UPS.APF: {
"id": 213,
"name": "APF MP1000/Imagination Machine",
"slug": "apf",
},
UPS.APPLE: {"id": 245, "name": "Apple I", "slug": "apple-i"},
UPS.APPLE_IIGS: {
"id": 51,
"name": "Apple IIGD",
"slug": "apple2gs",
},
UPS.APPLEII: {"id": 31, "name": "Apple II", "slug": "apple2"},
UPS.ARCADE: {"id": 143, "name": "Arcade", "slug": "arcade"},
UPS.ARCADIA_2001: {
"id": 162,
"name": "Arcadia 2001",
"slug": "arcadia-2001",
},
UPS.ARDUBOY: {"id": 215, "name": "Arduboy", "slug": "arduboy"},
UPS.ASTRAL_2000: {
"id": 241,
"name": "Astral 2000",
"slug": "astral-2000",
},
UPS.ASTROCADE: {
"id": 160,
"name": "Bally Astrocade",
"slug": "bally-astrocade",
},
UPS.ATARI_ST: {"id": 24, "name": "Atari ST", "slug": "atari-st"},
UPS.ATARI_VCS: {
"id": 319,
"name": "Atari VCS",
"slug": "atari-vcs",
},
UPS.ATARI2600: {
"id": 28,
"name": "Atari 2600",
"slug": "atari-2600",
},
UPS.ATARI5200: {
"id": 33,
"name": "Atari 5200",
"slug": "atari-5200",
},
UPS.ATARI7800: {
"id": 34,
"name": "Atari 7800",
"slug": "atari-7800",
},
UPS.ATARI8BIT: {
"id": 39,
"name": "Atari 8-bit",
"slug": "atari-8-bit",
},
UPS.ATOM: {"id": 129, "name": "Atom", "slug": "atom"},
UPS.BADA: {"id": 99, "name": "Bada", "slug": "bada"},
UPS.BBCMICRO: {
"id": 92,
"name": "BBC Micro",
"slug": "bbc-micro",
},
UPS.BEOS: {"id": 165, "name": "BeOS", "slug": "beos"},
UPS.BLACKBERRY: {
"id": 90,
"name": "BlackBerry",
"slug": "blackberry",
},
UPS.BLACKNUT: {"id": 290, "name": "Blacknut", "slug": "blacknut"},
UPS.BLU_RAY_PLAYER: {
"id": 169,
"name": "Blu-ray Player",
"slug": "blu-ray-disc-player",
},
UPS.BREW: {"id": 63, "name": "BREW", "slug": "brew"},
UPS.BROWSER: {"id": 84, "name": "Browser", "slug": "browser"},
UPS.BUBBLE: {"id": 231, "name": "Bubble", "slug": "bubble"},
UPS.C_PLUS_4: {
"id": 115,
"name": "Commodore Plus/4",
"slug": "commodore-16-plus4",
},
UPS.C128: {"id": 61, "name": "Commodore 128", "slug": "c128"},
UPS.C16: {
"id": 115,
"name": "Commodore 16",
"slug": "commodore-16-plus4",
},
UPS.C64: {"id": 27, "name": "Commodore 64", "slug": "c64"},
UPS.CAMPUTERS_LYNX: {
"id": 154,
"name": "Camputers Lynx",
"slug": "camputers-lynx",
},
UPS.CASIO_LOOPY: {
"id": 124,
"name": "Casio Loopy",
"slug": "casio-loopy",
},
UPS.CASIO_PROGRAMMABLE_CALCULATOR: {
"id": 306,
"name": "Casio Programmable Calculator",
"slug": "casio-programmable-calculator",
},
UPS.CASIO_PV_1000: {
"id": 125,
"name": "Casio PV-1000",
"slug": "casio-pv-1000",
},
UPS.CHAMPION_2711: {
"id": 298,
"name": "Champion 2711",
"slug": "champion-2711",
},
UPS.CLICKSTART: {
"id": 188,
"name": "ClickStart",
"slug": "clickstart",
},
UPS.COLECOADAM: {
"id": 156,
"name": "Coleco Adam",
"slug": "colecoadam",
},
UPS.COLECOVISION: {
"id": 29,
"name": "ColecoVision",
"slug": "colecovision",
},
UPS.COLOUR_GENIE: {
"id": 197,
"name": "Colour Genie",
"slug": "colour-genie",
},
UPS.COMMODORE_CDTV: {"id": 83, "name": "CDTV", "slug": "cdtv"},
UPS.COMPAL_80: {
"id": 277,
"name": "Compal 80",
"slug": "compal-80",
},
UPS.COMPUCOLOR_I: {
"id": 243,
"name": "Compucolor I",
"slug": "compucolor-i",
},
UPS.COMPUCOLOR_II: {
"id": 198,
"name": "Compucolor II",
"slug": "compucolor-ii",
},
UPS.COMPUCORP_PROGRAMMABLE_CALCULATOR: {
"id": 238,
"name": "Compucorp Programmable Calculator",
"slug": "compucorp-programmable-calculator",
},
UPS.CPET: {"id": 77, "name": "Commodore PET/CBM", "slug": "pet"},
UPS.CPM: {"id": 261, "name": "CP/M", "slug": "cpm"},
UPS.CREATIVISION: {
"id": 212,
"name": "CreatiVision",
"slug": "creativision",
},
UPS.CYBERVISION: {
"id": 301,
"name": "Cybervision",
"slug": "cybervision",
},
UPS.DANGER_OS: {
"id": 285,
"name": "Danger OS",
"slug": "danger-os",
},
UPS.DC: {"id": 8, "name": "Dreamcast", "slug": "dc"},
UPS.DEDICATED_CONSOLE: {
"id": 204,
"name": "Dedicated console",
"slug": "dedicated-console",
},
UPS.DEDICATED_HANDHELD: {
"id": 205,
"name": "Dedicated handheld",
"slug": "dedicated-handheld",
},
UPS.DIDJ: {"id": 184, "name": "Didj", "slug": "didj"},
UPS.DIGIBLAST: {
"id": 187,
"name": "digiBlast",
"slug": "digiblast",
},
UPS.DOJA: {"id": 72, "name": "DoJa", "slug": "doja"},
UPS.DOS: {"id": 2, "name": "DOS", "slug": "dos"},
UPS.DRAGON_32_SLASH_64: {
"id": 79,
"name": "Dragon 32/64",
"slug": "dragon-3264",
},
UPS.DVD_PLAYER: {
"id": 166,
"name": "DVD Player",
"slug": "dvd-player",
},
UPS.ECD_MICROMIND: {
"id": 269,
"name": "ECD Micromind",
"slug": "ecd-micromind",
},
UPS.ENTERPRISE: {
"id": 161,
"name": "Enterprise",
"slug": "enterprise",
},
UPS.EPOCH_CASSETTE_VISION: {
"id": 137,
"name": "Epoch Cassette Vision",
"slug": "epoch-cassette-vision",
},
UPS.EPOCH_GAME_POCKET_COMPUTER: {
"id": 139,
"name": "Epoch Game Pocket Computer",
"slug": "epoch-game-pocket-computer",
},
UPS.EPOCH_SUPER_CASSETTE_VISION: {
"id": 138,
"name": "Epoch Super Cassette Vision",
"slug": "epoch-super-cassette-vision",
},
UPS.EVERCADE: {"id": 284, "name": "Evercade", "slug": "evercade"},
UPS.EXELVISION: {
"id": 195,
"name": "Exelvision",
"slug": "exelvision",
},
UPS.EXEN: {"id": 70, "name": "ExEn", "slug": "exen"},
UPS.EXIDY_SORCERER: {
"id": 176,
"name": "Exidy Sorcerer",
"slug": "exidy-sorcerer",
},
UPS.FAIRCHILD_CHANNEL_F: {
"id": 76,
"name": "Channel F",
"slug": "channel-f",
},
UPS.FAMICOM: {
"id": 22,
"name": "Family Computer",
"slug": "famicom",
},
UPS.FM_7: {"id": 126, "name": "FM-7", "slug": "fm-7"},
UPS.FM_TOWNS: {"id": 102, "name": "FM Towns", "slug": "fmtowns"},
UPS.FRED_COSMAC: {
"id": 216,
"name": "COSMAC",
"slug": "fred-cosmac",
},
UPS.FREEBOX: {"id": 268, "name": "Freebox", "slug": "freebox"},
UPS.G_AND_W: {
"id": 205,
"name": "Dedicated handheld",
"slug": "dedicated-handheld",
},
UPS.G_CLUSTER: {
"id": 302,
"name": "G-cluster",
"slug": "g-cluster",
},
UPS.GALAKSIJA: {
"id": 236,
"name": "Galaksija",
"slug": "galaksija",
},
UPS.GAME_DOT_COM: {
"id": 50,
"name": "Game.Com",
"slug": "game-com",
},
UPS.GAME_WAVE: {
"id": 104,
"name": "Game Wave",
"slug": "game-wave",
},
UPS.GAMEGEAR: {
"id": 25,
"name": "Game Gear",
"slug": "game-gear",
},
UPS.GAMESTICK: {
"id": 155,
"name": "GameStick",
"slug": "gamestick",
},
UPS.GB: {"id": 10, "name": "Game Boy", "slug": "gameboy"},
UPS.GBA: {
"id": 12,
"name": "Game Boy Advance",
"slug": "gameboy-advance",
},
UPS.GBC: {
"id": 11,
"name": "Game Boy Color",
"slug": "gameboy-color",
},
UPS.GENESIS: {
"id": 16,
"name": "Genesis/Mega Drive",
"slug": "genesis",
},
UPS.GIMINI: {"id": 251, "name": "GIMINI", "slug": "gimini"},
UPS.GIZMONDO: {"id": 55, "name": "Gizmondo", "slug": "gizmondo"},
UPS.GLOUD: {"id": 292, "name": "Gloud", "slug": "gloud"},
UPS.GLULX: {"id": 172, "name": "Glulx", "slug": "glulx"},
UPS.GNEX: {"id": 258, "name": "GNEX", "slug": "gnex"},
UPS.GP2X: {"id": 122, "name": "GP2X", "slug": "gp2x"},
UPS.GP2X_WIZ: {"id": 123, "name": "GP2X Wiz", "slug": "gp2x-wiz"},
UPS.GP32: {"id": 108, "name": "GP32", "slug": "gp32"},
UPS.GVM: {"id": 257, "name": "GVM", "slug": "gvm"},
UPS.HD_DVD_PLAYER: {
"id": 167,
"name": "HD DVD Player",
"slug": "hd-dvd-player",
},
UPS.HEATHKIT_H11: {
"id": 248,
"name": "Heathkit H11",
"slug": "heathkit-h11",
},
UPS.HEATHZENITH: {
"id": 262,
"name": "Heath/Zenith H8/H89",
"slug": "heathzenith",
},
UPS.HITACHI_S1: {
"id": 274,
"name": "Hitachi S1",
"slug": "hitachi-s1",
},
UPS.HP_9800: {"id": 219, "name": "HP 9800", "slug": "hp-9800"},
UPS.HP_PROGRAMMABLE_CALCULATOR: {
"id": 234,
"name": "HP Programmable Calculator",
"slug": "hp-programmable-calculator",
},
UPS.HUGO: {"id": 170, "name": "Hugo", "slug": "hugo"},
UPS.HYPERSCAN: {
"id": 192,
"name": "HyperScan",
"slug": "hyperscan",
},
UPS.IBM_5100: {"id": 250, "name": "IBM 5100", "slug": "ibm-5100"},
UPS.IDEAL_COMPUTER: {
"id": 252,
"name": "Ideal-Computer",
"slug": "ideal-computer",
},
UPS.IIRCADE: {"id": 314, "name": "iiRcade", "slug": "iircade"},
UPS.INTEL_8008: {
"id": 224,
"name": "Intel 8008",
"slug": "intel-8008",
},
UPS.INTEL_8080: {
"id": 225,
"name": "Intel 8080",
"slug": "intel-8080",
},
UPS.INTEL_8086: {
"id": 317,
"name": "Intel 8086 / 8088",
"slug": "intel-8086",
},
UPS.INTELLIVISION: {
"id": 30,
"name": "Intellivision",
"slug": "intellivision",
},
UPS.INTERACT_MODEL_ONE: {
"id": 295,
"name": "Interact Model One",
"slug": "interact-model-one",
},
UPS.INTERTON_VIDEO_2000: {
"id": 221,
"name": "Interton Video 2000",
"slug": "interton-video-2000",
},
UPS.IOS: {"id": 86, "name": "iOS", "slug": "iphone"},
UPS.IPAD: {"id": 96, "name": "iPad", "slug": "ipad"},
UPS.IPOD_CLASSIC: {
"id": 80,
"name": "iPod Classic",
"slug": "ipod-classic",
},
UPS.J2ME: {"id": 64, "name": "J2ME", "slug": "j2me"},
UPS.JAGUAR: {"id": 17, "name": "Jaguar", "slug": "jaguar"},
UPS.JOLT: {"id": 247, "name": "Jolt", "slug": "jolt"},
UPS.JUPITER_ACE: {
"id": 153,
"name": "Jupiter Ace",
"slug": "jupiter-ace",
},
UPS.KAIOS: {"id": 313, "name": "KaiOS", "slug": "kaios"},
UPS.KIM_1: {"id": 226, "name": "KIM-1", "slug": "kim-1"},
UPS.KINDLE: {
"id": 145,
"name": "Kindle Classic",
"slug": "kindle",
},
UPS.LASER200: {
"id": 264,
"name": "Laser 200",
"slug": "laser200",
},
UPS.LASERACTIVE: {
"id": 163,
"name": "LaserActive",
"slug": "laseractive",
},
UPS.LEAPFROG_EXPLORER: {
"id": 185,
"name": "LeapFrog Explorer",
"slug": "leapfrog-explorer",
},
UPS.LEAPSTER: {"id": 183, "name": "Leapster", "slug": "leapster"},
UPS.LEAPSTER_EXPLORER_SLASH_LEADPAD_EXPLORER: {
"id": 183,
"name": "Leapster Explorer/LeapPad Explorer",
"slug": "leapster",
},
UPS.LEAPTV: {"id": 186, "name": "LeapTV", "slug": "leaptv"},
UPS.LINUX: {"id": 1, "name": "Linux", "slug": "linux"},
UPS.LUNA: {"id": 297, "name": "Luna", "slug": "luna"},
UPS.LYNX: {"id": 18, "name": "Lynx", "slug": "lynx"},
UPS.MAC: {"id": 74, "name": "Macintosh", "slug": "macintosh"},
UPS.MAEMO: {"id": 157, "name": "Maemo", "slug": "maemo"},
UPS.MAINFRAME: {
"id": 208,
"name": "Mainframe",
"slug": "mainframe",
},
UPS.MATSUSHITAPANASONIC_JR: {
"id": 307,
"name": "Matsushita/Panasonic JR",
"slug": "matsushitapanasonic-jr",
},
UPS.AQUARIUS: {
"id": 135,
"name": "Mattel Aquarius",
"slug": "mattel-aquarius",
},
UPS.MEEGO: {"id": 158, "name": "MeeGo", "slug": "meego"},
UPS.MEMOTECH_MTX: {
"id": 148,
"name": "Memotech MTX",
"slug": "memotech-mtx",
},
UPS.MERITUM: {"id": 311, "name": "Meritum", "slug": "meritum"},
UPS.MICROBEE: {"id": 200, "name": "Microbee", "slug": "microbee"},
UPS.MICROTAN_65: {
"id": 232,
"name": "Microtan 65",
"slug": "microtan-65",
},
UPS.MICROVISION: {
"id": 97,
"name": "Microvision",
"slug": "microvision",
},
UPS.MOBILE_CUSTOM: {
"id": 315,
"name": "Feature phone",
"slug": "mobile-custom",
},
UPS.MOPHUN: {"id": 71, "name": "Mophun", "slug": "mophun"},
UPS.MOS_TECHNOLOGY_6502: {
"id": 240,
"name": "MOS Technology 6502",
"slug": "mos-technology-6502",
},
UPS.MOTOROLA_6800: {
"id": 235,
"name": "Motorola 6800",
"slug": "motorola-6800",
},
UPS.MOTOROLA_68K: {
"id": 275,
"name": "Motorola 68k",
"slug": "motorola-68k",
},
UPS.MRE: {"id": 229, "name": "MRE", "slug": "mre"},
UPS.MSX: {"id": 57, "name": "MSX", "slug": "msx"},
UPS.N64: {"id": 9, "name": "Nintendo 64", "slug": "n64"},
UPS.NASCOM: {"id": 175, "name": "Nascom", "slug": "nascom"},
UPS.NDS: {"id": 44, "name": "Nintendo DS", "slug": "nintendo-ds"},
UPS.NEO_GEO_CD: {
"id": 54,
"name": "Neo Geo CD",
"slug": "neo-geo-cd",
},
UPS.NEO_GEO_POCKET: {
"id": 52,
"name": "Neo Geo Pocket",
"slug": "neo-geo-pocket",
},
UPS.NEO_GEO_POCKET_COLOR: {
"id": 53,
"name": "Neo Geo Pocket Color",
"slug": "neo-geo-pocket-color",
},
UPS.NEO_GEO_X: {
"id": 279,
"name": "Neo Geo X",
"slug": "neo-geo-x",
},
UPS.NEOGEOAES: {"id": 36, "name": "Neo Geo", "slug": "neo-geo"},
UPS.NEOGEOMVS: {"id": 36, "name": "Neo Geo", "slug": "neo-geo"},
UPS.NES: {"id": 22, "name": "NES", "slug": "nes"},
UPS.NEW_NINTENDON3DS: {
"id": 174,
"name": "New Nintendo 3DS",
"slug": "new-nintendo-3ds",
},
UPS.NEWBRAIN: {"id": 177, "name": "NewBrain", "slug": "newbrain"},
UPS.NEWTON: {"id": 207, "name": "Newton", "slug": "newton"},
UPS.NGAGE: {"id": 32, "name": "N-Gage", "slug": "ngage"},
UPS.NGAGE2: {
"id": 89,
"name": "N-Gage (service)",
"slug": "ngage2",
},
UPS.NGC: {"id": 14, "name": "GameCube", "slug": "gamecube"},
UPS.NINTENDO_DSI: {
"id": 87,
"name": "Nintendo DSi",
"slug": "nintendo-dsi",
},
UPS.NORTHSTAR: {
"id": 266,
"name": "North Star",
"slug": "northstar",
},
UPS.NOVAL_760: {
"id": 244,
"name": "Noval 760",
"slug": "noval-760",
},
UPS.NUON: {"id": 116, "name": "Nuon", "slug": "nuon"},
UPS.OCULUS_GO: {
"id": 218,
"name": "Oculus Go",
"slug": "oculus-go",
},
UPS.OCULUS_QUEST: {
"id": 271,
"name": "Quest",
"slug": "oculus-quest",
},
UPS.ODYSSEY: {"id": 75, "name": "Odyssey", "slug": "odyssey"},
UPS.ODYSSEY_2: {
"id": 78,
"name": "Odyssey 2",
"slug": "odyssey-2",
},
UPS.ODYSSEY_2_SLASH_VIDEOPAC_G7000: {
"id": 78,
"name": "Odyssey 2/Videopac G7000",
"slug": "odyssey-2",
},
UPS.OHIO_SCIENTIFIC: {
"id": 178,
"name": "Ohio Scientific",
"slug": "ohio-scientific",
},
UPS.ONLIVE_GAME_SYSTEM: {
"id": 282,
"name": "OnLive Game System",
"slug": "onlive",
},
UPS.OOPARTS: {"id": 300, "name": "OOParts", "slug": "ooparts"},
UPS.ORAO: {"id": 270, "name": "Orao", "slug": "orao"},
UPS.ORIC: {"id": 111, "name": "Oric", "slug": "oric"},
UPS.OS2: {"id": 146, "name": "OS/2", "slug": "os2"},
UPS.OUYA: {"id": 144, "name": "Ouya", "slug": "ouya"},
UPS.PALM_OS: {"id": 65, "name": "Palm OS", "slug": "palmos"},
UPS.PANDORA: {"id": 308, "name": "Pandora", "slug": "pandora"},
UPS.PC_6001: {"id": 149, "name": "PC-6001", "slug": "pc-6001"},
UPS.PC_8000: {"id": 201, "name": "PC-8000", "slug": "pc-8000"},
UPS.PC_8800_SERIES: {
"id": 94,
"name": "PC-8800 Series",
"slug": "pc88",
},
UPS.PC_9800_SERIES: {
"id": 95,
"name": "PC-9800 Series",
"slug": "pc98",
},
UPS.PC_BOOTER: {
"id": 4,
"name": "PC Booter",
"slug": "pc-booter",
},
UPS.PC_FX: {"id": 59, "name": "PC-FX", "slug": "pc-fx"},
UPS.PEBBLE: {"id": 304, "name": "Pebble", "slug": "pebble"},
UPS.PHILIPS_CD_I: {"id": 73, "name": "CD-i", "slug": "cd-i"},
UPS.PHILIPS_VG_5000: {
"id": 133,
"name": "Philips VG 5000",
"slug": "philips-vg-5000",
},
UPS.PHOTOCD: {"id": 272, "name": "Photo CD", "slug": "photocd"},
UPS.PICO: {"id": 316, "name": "PICO", "slug": "pico"},
UPS.PIPPIN: {"id": 112, "name": "Pippin", "slug": "pippin"},
UPS.PLAYDATE: {"id": 303, "name": "Playdate", "slug": "playdate"},
UPS.PLAYDIA: {"id": 107, "name": "Playdia", "slug": "playdia"},
UPS.PLAYSTATION_NOW: {
"id": 294,
"name": "PlayStation Now",
"slug": "playstation-now",
},
UPS.PLEX_ARCADE: {
"id": 291,
"name": "Plex Arcade",
"slug": "plex-arcade",
},
UPS.POKEMON_MINI: {
"id": 152,
"name": "Pokémon Mini",
"slug": "pokemon-mini",
},
UPS.POKITTO: {"id": 230, "name": "Pokitto", "slug": "pokitto"},
UPS.POLY_88: {"id": 249, "name": "Poly-88", "slug": "poly-88"},
UPS.PS2: {
"id": 7,
"name": "PlayStation 2",
"slug": "playstation-2",
},
UPS.PS3: {
"id": 81,
"name": "PlayStation 3",
"slug": "playstation-3",
},
UPS.PS4: {
"id": 141,
"name": "PlayStation 4",
"slug": "playstation-4",
},
UPS.PS5: {
"id": 288,
"name": "PlayStation 5",
"slug": "playstation-5",
},
UPS.PSP: {"id": 46, "name": "PSP", "slug": "psp"},
UPS.PSVITA: {"id": 105, "name": "PS Vita", "slug": "ps-vita"},
UPS.PSX: {"id": 6, "name": "PlayStation", "slug": "playstation"},
UPS.RCA_STUDIO_II: {
"id": 113,
"name": "RCA Studio II",
"slug": "rca-studio-ii",
},
UPS.RESEARCH_MACHINES_380Z: {
"id": 309,
"name": "Research Machines 380Z",
"slug": "research-machines-380z",
},
UPS.ROKU: {"id": 196, "name": "Roku", "slug": "roku"},
UPS.SAM_COUPE: {
"id": 120,
"name": "SAM Coupé",
"slug": "sam-coupe",
},
UPS.SATURN: {
"id": 23,
"name": "SEGA Saturn",
"slug": "sega-saturn",
},
UPS.SCMP: {"id": 255, "name": "SC/MP", "slug": "scmp"},
UPS.SD_200270290: {
"id": 267,
"name": "SD-200/270/290",
"slug": "sd-200270290",
},
UPS.SEGA_PICO: {
"id": 103,
"name": "SEGA Pico",
"slug": "sega-pico",
},
UPS.SEGA32: {"id": 21, "name": "SEGA 32X", "slug": "sega-32x"},
UPS.SEGACD: {"id": 20, "name": "SEGA CD", "slug": "sega-cd"},
UPS.SERIES_X_S: {
"id": 289,
"name": "Xbox Series X/S",
"slug": "xbox-series",
},
UPS.SFAM: {"id": 15, "name": "Super Famicom", "slug": "snes"},
UPS.SG1000: {"id": 114, "name": "SG-1000", "slug": "sg-1000"},
UPS.SHARP_MZ_80B20002500: {
"id": 182,
"name": "Sharp MZ-80B/2000/2500",
"slug": "sharp-mz-80b20002500",
},
UPS.SHARP_MZ_80K7008001500: {
"id": 181,
"name": "Sharp MZ-80K/700/800/1500",
"slug": "sharp-mz-80k7008001500",
},
UPS.SHARP_X68000: {
"id": 106,
"name": "Sharp X68000",
"slug": "sharp-x68000",
},
UPS.SHARP_ZAURUS: {
"id": 202,
"name": "Sharp Zaurus",
"slug": "sharp-zaurus",
},
UPS.SIGNETICS_2650: {
"id": 278,
"name": "Signetics 2650",
"slug": "signetics-2650",
},
UPS.SINCLAIR_QL: {
"id": 131,
"name": "Sinclair QL",
"slug": "sinclair-ql",
},
UPS.SK_VM: {"id": 259, "name": "SK-VM", "slug": "sk-vm"},
UPS.SMC_777: {"id": 273, "name": "SMC-777", "slug": "smc-777"},
UPS.SMS: {
"id": 26,
"name": "SEGA Master System",
"slug": "sega-master-system",
},
UPS.SNES: {"id": 15, "name": "SNES", "slug": "snes"},
UPS.SOCRATES: {"id": 190, "name": "Socrates", "slug": "socrates"},
UPS.SOL_20: {"id": 199, "name": "Sol-20", "slug": "sol-20"},
UPS.SORD_M5: {"id": 134, "name": "Sord M5", "slug": "sord-m5"},
UPS.SPECTRAVIDEO: {
"id": 85,
"name": "Spectravideo",
"slug": "spectravideo",
},
UPS.SRI_5001000: {
"id": 242,
"name": "SRI-500/1000",
"slug": "sri-5001000",
},
UPS.STADIA: {"id": 281, "name": "Stadia", "slug": "stadia"},
UPS.SUPER_ACAN: {
"id": 110,
"name": "Super A'can",
"slug": "super-acan",
},
UPS.SUPER_VISION_8000: {
"id": 296,
"name": "Super Vision 8000",
"slug": "super-vision-8000",
},
UPS.SUPERGRAFX: {
"id": 127,
"name": "SuperGrafx",
"slug": "supergrafx",
},
UPS.SUPERVISION: {
"id": 109,
"name": "Supervision",
"slug": "supervision",
},
UPS.SURE_SHOT_HD: {
"id": 287,
"name": "Sure Shot HD",
"slug": "sure-shot-hd",
},
UPS.SWITCH: {
"id": 203,
"name": "Nintendo Switch",
"slug": "switch",
},
UPS.SWITCH_2: {
"id": -1,
"name": "Nintendo Switch 2",
"slug": "switch-2",
},
UPS.SWTPC_6800: {
"id": 228,
"name": "SWTPC 6800",
"slug": "swtpc-6800",
},
UPS.SYMBIAN: {"id": 67, "name": "Symbian", "slug": "symbian"},
UPS.TADS: {"id": 171, "name": "TADS", "slug": "tads"},
UPS.TAITO_X_55: {
"id": 283,
"name": "Taito X-55",
"slug": "taito-x-55",
},
UPS.TATUNG_EINSTEIN: {
"id": 150,
"name": "Tatung Einstein",
"slug": "tatung-einstein",
},
UPS.TEKTRONIX_4050: {
"id": 223,
"name": "Tektronix 4050",
"slug": "tektronix-4050",
},
UPS.TELE_SPIEL: {
"id": 220,
"name": "Tele-Spiel ES-2201",
"slug": "tele-spiel",
},
UPS.TELSTAR_ARCADE: {
"id": 233,
"name": "Telstar Arcade",
"slug": "telstar-arcade",
},
UPS.TERMINAL: {"id": 209, "name": "Terminal", "slug": "terminal"},
UPS.TG16: {
"id": 40,
"name": "TurboGrafx-16",
"slug": "turbo-grafx",
},
UPS.THOMSON_MO5: {
"id": 147,
"name": "Thomson MO5",
"slug": "thomson-mo",
},
UPS.THOMSON_TO: {
"id": 130,
"name": "Thomson TO",
"slug": "thomson-to",
},
UPS.TI_99: {"id": 47, "name": "TI-99/4A", "slug": "ti-994a"},
UPS.TI_994A: {"id": 47, "name": "TI-99/4A", "slug": "ti-994a"},
UPS.TI_PROGRAMMABLE_CALCULATOR: {
"id": 239,
"name": "TI Programmable Calculator",
"slug": "ti-programmable-calculator",
},
UPS.TIKI_100: {"id": 263, "name": "Tiki 100", "slug": "tiki-100"},
UPS.TIM: {"id": 246, "name": "TIM", "slug": "tim"},
UPS.TIMEX_SINCLAIR_2068: {
"id": 173,
"name": "Timex Sinclair 2068",
"slug": "timex-sinclair-2068",
},
UPS.TIZEN: {"id": 206, "name": "Tizen", "slug": "tizen"},
UPS.TOMAHAWK_F1: {
"id": 256,
"name": "Tomahawk F1",
"slug": "tomahawk-f1",
},
UPS.TOMY_TUTOR: {
"id": 151,
"name": "Tomy Tutor",
"slug": "tomy-tutor",
},
UPS.TRITON: {"id": 310, "name": "Triton", "slug": "triton"},
UPS.TRS_80: {"id": 58, "name": "TRS-80", "slug": "trs-80"},
UPS.TRS_80_COLOR_COMPUTER: {
"id": 62,
"name": "TRS-80 Color Computer",
"slug": "trs-80-coco",
},
UPS.TRS_80_MC_10: {
"id": 193,
"name": "TRS-80 MC-10",
"slug": "trs-80-mc-10",
},
UPS.TRS_80_MODEL_100: {
"id": 312,
"name": "TRS-80 Model 100",
"slug": "trs-80-model-100",
},
UPS.TURBOGRAFX_CD: {
"id": 45,
"name": "TurboGrafx CD",
"slug": "turbografx-cd",
},
UPS.TVOS: {"id": 179, "name": "tvOS", "slug": "tvos"},
UPS.VECTREX: {"id": 37, "name": "Vectrex", "slug": "vectrex"},
UPS.VERSATILE: {
"id": 299,
"name": "Versatile",
"slug": "versatile",
},
UPS.VFLASH: {"id": 189, "name": "V.Flash", "slug": "vflash"},
UPS.VIC_20: {"id": 43, "name": "VIC-20", "slug": "vic-20"},
UPS.VIDEOBRAIN: {
"id": 214,
"name": "VideoBrain",
"slug": "videobrain",
},
UPS.VIDEOPAC_G7400: {
"id": 128,
"name": "Videopac+ G7400",
"slug": "videopac-g7400",
},
UPS.VIRTUALBOY: {
"id": 38,
"name": "Virtual Boy",
"slug": "virtual-boy",
},
UPS.VIS: {"id": 164, "name": "VIS", "slug": "vis"},
UPS.VSMILE: {"id": 42, "name": "V.Smile", "slug": "vsmile"},
UPS.WANG2200: {
"id": 217,
"name": "Wang 2200",
"slug": "wang2200",
},
UPS.WATCHOS: {"id": 180, "name": "watchOS", "slug": "watchos"},
UPS.WEBOS: {"id": 100, "name": "webOS", "slug": "webos"},
UPS.WII: {"id": 82, "name": "Wii", "slug": "wii"},
UPS.WIIU: {"id": 132, "name": "Wii U", "slug": "wii-u"},
UPS.WIN: {"id": 3, "name": "Windows", "slug": "windows"},
UPS.WIN3X: {"id": 5, "name": "Windows 3.x", "slug": "win3x"},
UPS.WINDOWS_APPS: {
"id": 140,
"name": "Windows Apps",
"slug": "windows-apps",
},
UPS.WINDOWS_MOBILE: {
"id": 66,
"name": "Windows Mobile",
"slug": "windowsmobile",
},
UPS.WINPHONE: {
"id": 98,
"name": "Windows Phone",
"slug": "windows-phone",
},
UPS.WIPI: {"id": 260, "name": "WIPI", "slug": "wipi"},
UPS.WONDERSWAN: {
"id": 48,
"name": "WonderSwan",
"slug": "wonderswan",
},
UPS.WONDERSWAN_COLOR: {
"id": 49,
"name": "WonderSwan Color",
"slug": "wonderswan-color",
},
UPS.X1: {"id": 121, "name": "Sharp X1", "slug": "sharp-x1"},
UPS.XAVIXPORT: {
"id": 191,
"name": "XaviXPORT",
"slug": "xavixport",
},
UPS.XBOX: {"id": 13, "name": "Xbox", "slug": "xbox"},
UPS.XBOX360: {"id": 69, "name": "Xbox 360", "slug": "xbox360"},
UPS.XBOXCLOUDGAMING: {
"id": 293,
"name": "Xbox Cloud Gaming",
"slug": "xboxcloudgaming",
},
UPS.XBOXONE: {"id": 142, "name": "Xbox One", "slug": "xbox-one"},
UPS.XEROX_ALTO: {
"id": 254,
"name": "Xerox Alto",
"slug": "xerox-alto",
},
UPS.Z_MACHINE: {
"id": 169,
"name": "Z-machine",
"slug": "z-machine",
},
UPS.Z80: {"id": 227, "name": "Zilog Z80", "slug": "z80"},
UPS.ZEEBO: {"id": 88, "name": "Zeebo", "slug": "zeebo"},
UPS.ZILOG_Z8000: {
"id": 276,
"name": "Zilog Z8000",
"slug": "zilog-z8000",
},
UPS.ZODIAC: {"id": 68, "name": "Zodiac", "slug": "zodiac"},
UPS.ZUNE: {"id": 211, "name": "Zune", "slug": "zune"},
UPS.ZXS: {
"id": 41,
"name": "ZX Spectrum",
"slug": "zx-spectrum",
},
UPS.ZX_SPECTRUM_NEXT: {
"id": 280,
"name": "ZX Spectrum Next",
"slug": "zx-spectrum-next",
},
UPS.ZX80: {"id": 118, "name": "ZX80", "slug": "zx80"},
UPS.ZX81: {"id": 119, "name": "ZX81", "slug": "zx81"},
}
# Reverse lookup
MOBY_ID_TO_SLUG = {v["id"]: k for k, v in MOBYGAMES_PLATFORM_LIST.items()}
# These platforms are ignored due to lack of data:
# arb, casiofp, dai, hitachibasicmaster3, hposcilloscope, hpseries80
# Need the IDs for these platforms before we can add them:
# p2000, visionos