Files
romm/backend/handler/metadata/moby_handler.py
Georges-Antoine Assi b2085f87a8 bunch of fixes for trunk
2024-05-21 17:10:11 -04:00

722 lines
31 KiB
Python

import re
import time
from typing import Final, Optional
from urllib.parse import quote
import pydash
import requests
import yarl
from config import MOBYGAMES_API_KEY
from fastapi import HTTPException, status
from logger.logger import log
from requests.exceptions import HTTPError, Timeout
from typing_extensions import NotRequired, TypedDict
from unidecode import unidecode as uc
from .base_hander import (
PS2_OPL_REGEX,
SONY_SERIAL_REGEX,
SWITCH_PRODUCT_ID_REGEX,
SWITCH_TITLEDB_REGEX,
MetadataHandler,
)
# Used to display the Mobygames API status in the frontend
MOBY_API_ENABLED: Final = bool(MOBYGAMES_API_KEY)
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]
class MobyGamesPlatform(TypedDict):
moby_id: int
name: NotRequired[str]
class MobyMetadata(TypedDict):
moby_score: str
genres: list[str]
alternate_titles: list[str]
platforms: list[MobyGamesPlatform]
class MobyGamesRom(TypedDict):
moby_id: int | None
slug: NotRequired[str]
name: NotRequired[str]
summary: NotRequired[str]
url_cover: NotRequired[str]
url_screenshots: NotRequired[list[str]]
moby_metadata: Optional[MobyMetadata]
def extract_metadata_from_moby_rom(rom: dict) -> MobyMetadata:
return MobyMetadata(
{
"moby_score": str(rom.get("moby_score", "")),
"genres": rom.get("genres.genre_name", []),
"alternate_titles": rom.get("alternate_titles.title", []),
"platforms": [
{
"moby_id": p["platform_id"],
"name": p["platform_name"],
}
for p in rom.get("platforms", [])
],
}
)
class MobyGamesHandler(MetadataHandler):
def __init__(self) -> None:
self.platform_url = "https://api.mobygames.com/v1/platforms"
self.games_url = "https://api.mobygames.com/v1/games"
def _request(self, url: str, timeout: int = 120) -> dict:
authorized_url = yarl.URL(url).update_query(api_key=MOBYGAMES_API_KEY)
try:
res = requests.get(authorized_url, timeout=timeout)
res.raise_for_status()
return res.json()
except requests.exceptions.ConnectionError as exc:
log.critical("Connection error: can't connect to Mobygames", exc_info=True)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Can't connect to Mobygames, check your internet connection",
) from exc
except HTTPError as err:
if err.response.status_code == 401:
# Sometimes Mobygames returns 401 even with a valid API key
return {}
elif err.response.status_code == 429:
# Retry after 2 seconds if rate limit hit
time.sleep(2)
else:
# Log the error and return an empty dict if the request fails with a different code
log.error(err)
return {}
except Timeout:
# Retry the request once if it times out
pass
try:
res = requests.get(url, timeout=timeout)
res.raise_for_status()
except (HTTPError, Timeout) as err:
if err.response.status_code == 401:
# Sometimes Mobygames returns 401 even with a valid API key
return {}
# Log the error and return an empty dict if the request fails with a different code
log.error(err)
return {}
return res.json()
def _search_rom(self, search_term: str, platform_moby_id: int) -> dict | None:
if not platform_moby_id:
return None
search_term = uc(search_term)
url = yarl.URL(self.games_url).with_query(
platform=[platform_moby_id],
title=quote(search_term, safe="/ "),
)
roms = self._request(str(url)).get("games", [])
exact_matches = [
rom
for rom in roms
if (
rom["title"].lower() == search_term.lower()
or (
self._normalize_exact_match(rom["title"])
== self._normalize_exact_match(search_term)
)
)
]
return pydash.get(exact_matches or roms, "[0]", None)
def get_platform(self, slug: str) -> MobyGamesPlatform:
platform = SLUG_TO_MOBY_ID.get(slug, None)
if not platform:
return MobyGamesPlatform(moby_id=None, slug=slug)
return MobyGamesPlatform(
moby_id=platform["id"],
slug=slug,
name=platform["name"],
)
async def get_rom(self, file_name: str, platform_moby_id: int) -> MobyGamesRom:
from handler.filesystem import fs_rom_handler
if not MOBY_API_ENABLED:
return MobyGamesRom(moby_id=None)
if not platform_moby_id:
return MobyGamesRom(moby_id=None)
search_term = fs_rom_handler.get_file_name_with_no_tags(file_name)
fallback_rom = MobyGamesRom(moby_id=None)
# Support for PS2 OPL filename format
match = re.match(PS2_OPL_REGEX, file_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 = re.search(SONY_SERIAL_REGEX, file_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 = re.search(SWITCH_TITLEDB_REGEX, file_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 = re.search(SWITCH_PRODUCT_ID_REGEX, file_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)
search_term = self.normalize_search_term(search_term)
res = self._search_rom(search_term, platform_moby_id)
# Split the search term since mobygames search doesn't support special caracters
if not res and ":" in search_term:
for term in search_term.split(":")[::-1]:
res = self._search_rom(term, platform_moby_id)
if res:
break
# Some MAME games have two titles split by a slash
if not res and "/" in search_term:
for term in search_term.split("/"):
res = self._search_rom(term.strip(), platform_moby_id)
if res:
break
if not res:
return fallback_rom
rom = {
"moby_id": res["game_id"],
"name": res["title"],
"slug": res["moby_url"].split("/")[-1],
"summary": res.get("description", ""),
"url_cover": pydash.get(res, "sample_cover.image", ""),
"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})
def get_rom_by_id(self, moby_id: int) -> MobyGamesRom:
if not MOBY_API_ENABLED:
return MobyGamesRom(moby_id=moby_id)
url = yarl.URL(self.games_url).with_query(id=moby_id)
roms = self._request(str(url)).get("games", [])
res = pydash.get(roms, "[0]", None)
if not res:
return MobyGamesRom(moby_id=moby_id)
rom = {
"moby_id": res["game_id"],
"name": res["title"],
"slug": res["moby_url"].split("/")[-1],
"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})
def get_matched_roms_by_id(self, moby_id: int) -> list[MobyGamesRom]:
if not MOBY_API_ENABLED:
return []
return [self.get_rom_by_id(moby_id)]
def get_matched_roms_by_name(
self, search_term: str, platform_moby_id: int
) -> list[MobyGamesRom]:
if not MOBY_API_ENABLED:
return []
if not platform_moby_id:
return []
search_term = uc(search_term)
url = yarl.URL(self.games_url).with_query(
platform=[platform_moby_id], title=quote(search_term, safe="/ ")
)
matched_roms = self._request(str(url)).get("games", [])
return [
MobyGamesRom(
{
k: v
for k, v in {
"moby_id": rom["game_id"],
"name": rom["title"],
"slug": rom["moby_url"].split("/")[-1],
"summary": rom.get("description", ""),
"url_cover": pydash.get(rom, "sample_cover.image", ""),
"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
]
SLUG_TO_MOBY_ID: Final = {
"1292-advanced-programmable-video-system": {
"id": 253,
"name": "1292 Advanced Programmable Video System",
},
"3do": {"id": 35, "name": "3DO"},
"abc-80": {"id": 318, "name": "ABC 80"},
"apf": {"id": 213, "name": "APF MP1000/Imagination Machine"},
"acorn-32-bit": {"id": 117, "name": "Acorn 32-bit"},
"acorn-archimedes": {"id": 117, "name": "Acorn Archimedes"}, # IGDB
"adventure-vision": {"id": 210, "name": "Adventure Vision"},
"airconsole": {"id": 305, "name": "AirConsole"},
"alice-3290": {"id": 194, "name": "Alice 32/90"},
"altair-680": {"id": 265, "name": "Altair 680"},
"altair-8800": {"id": 222, "name": "Altair 8800"},
"amazon-alexa": {"id": 237, "name": "Amazon Alexa"},
"amiga": {"id": 19, "name": "Amiga"},
"amiga-cd32": {"id": 56, "name": "Amiga CD32"},
"cpc": {"id": 60, "name": "Amstrad CPC"},
"acpc": {"id": 60, "name": "Amstrad CPC"}, # IGDB
"amstrad-pcw": {"id": 136, "name": "Amstrad PCW"},
"android": {"id": 91, "name": "Android"},
"antstream": {"id": 286, "name": "Antstream"},
"apple-i": {"id": 245, "name": "Apple I"},
"apple2": {"id": 31, "name": "Apple II"},
"appleii": {"id": 31, "name": "Apple II"}, # IGDB
"apple2gs": {"id": 51, "name": "Apple IIGD"},
"apple-iigs": {"id": 51, "name": "Apple IIGD"}, # IGDB
"arcade": {"id": 143, "name": "Arcade"},
"arcadia-2001": {"id": 162, "name": "Arcadia 2001"},
"arduboy": {"id": 215, "name": "Arduboy"},
"astral-2000": {"id": 241, "name": "Astral 2000"},
"atari-2600": {"id": 28, "name": "Atari 2600"},
"atari2600": {"id": 28, "name": "Atari 2600"}, # IGDB
"atari-5200": {"id": 33, "name": "Atari 5200"},
"atari5200": {"id": 33, "name": "Atari 5200"}, # IGDB
"atari-7800": {"id": 34, "name": "Atari 7800"},
"atari7800": {"id": 34, "name": "Atari 7800"}, # IGDB
"atari-8-bit": {"id": 39, "name": "Atari 8-bit"},
"atari8bit": {"id": 39, "name": "Atari 8-bit"}, # IGDB
"atari-st": {"id": 24, "name": "Atari ST"},
"atari-vcs": {"id": 319, "name": "Atari VCS"},
"atom": {"id": 129, "name": "Atom"},
"bbc-micro": {"id": 92, "name": "BBC Micro"},
"bbcmicro": {"id": 92, "name": "BBC Micro"}, # IGDB
"brew": {"id": 63, "name": "BREW"},
"bally-astrocade": {"id": 160, "name": "Bally Astrocade"},
"astrocade": {"id": 160, "name": "Bally Astrocade"}, # IGDB
"beos": {"id": 165, "name": "BeOS"},
"blackberry": {"id": 90, "name": "BlackBerry"},
"blacknut": {"id": 290, "name": "Blacknut"},
"blu-ray-disc-player": {"id": 168, "name": "Blu-ray Player"},
"blu-ray-player": {"id": 169, "name": "Blu-ray Player"}, # IGDB
"browser": {"id": 84, "name": "Browser"},
"bubble": {"id": 231, "name": "Bubble"},
"cd-i": {"id": 73, "name": "CD-i"},
"philips-cd-i": {"id": 73, "name": "CD-i"}, # IGDB
"cdtv": {"id": 83, "name": "CDTV"},
"commodore-cdtv": {"id": 83, "name": "CDTV"}, # IGDB
"fred-cosmac": {"id": 216, "name": "COSMAC"},
"camputers-lynx": {"id": 154, "name": "Camputers Lynx"},
"cpm": {"id": 261, "name": "CP/M"},
"casio-loopy": {"id": 124, "name": "Casio Loopy"},
"casio-pv-1000": {"id": 125, "name": "Casio PV-1000"},
"casio-programmable-calculator": {
"id": 306,
"name": "Casio Programmable Calculator",
},
"champion-2711": {"id": 298, "name": "Champion 2711"},
"channel-f": {"id": 76, "name": "Channel F"},
"fairchild-channel-f": {"id": 76, "name": "Channel F"}, # IGDB
"clickstart": {"id": 188, "name": "ClickStart"},
"colecoadam": {"id": 156, "name": "Coleco Adam"},
"colecovision": {"id": 29, "name": "ColecoVision"},
"colour-genie": {"id": 197, "name": "Colour Genie"},
"c128": {"id": 61, "name": "Commodore 128"},
"commodore-16-plus4": {"id": 115, "name": "Commodore 16, Plus/4"},
"c-plus-4": {"id": 115, "name": "Commodore Plus/4"}, # IGDB
"c16": {"id": 115, "name": "Commodore 16"}, # IGDB
"c64": {"id": 27, "name": "Commodore 64"},
"pet": {"id": 77, "name": "Commodore PET/CBM"},
"cpet": {"id": 77, "name": "Commodore PET/CBM"}, # IGDB
"compal-80": {"id": 277, "name": "Compal 80"},
"compucolor-i": {"id": 243, "name": "Compucolor I"},
"compucolor-ii": {"id": 198, "name": "Compucolor II"},
"compucorp-programmable-calculator": {
"id": 238,
"name": "Compucorp Programmable Calculator",
},
"creativision": {"id": 212, "name": "CreatiVision"},
"cybervision": {"id": 301, "name": "Cybervision"},
"dos": {"id": 2, "name": "DOS"},
"dvd-player": {"id": 166, "name": "DVD Player"},
"danger-os": {"id": 285, "name": "Danger OS"},
"dedicated-console": {"id": 204, "name": "Dedicated console"},
"dedicated-handheld": {"id": 205, "name": "Dedicated handheld"},
"didj": {"id": 184, "name": "Didj"},
"doja": {"id": 72, "name": "DoJa"},
"dragon-3264": {"id": 79, "name": "Dragon 32/64"},
"dragon-32-slash-64": {"id": 79, "name": "Dragon 32/64"}, # IGDB
"dreamcast": {"id": 8, "name": "Dreamcast"},
"dc": {"id": 8, "name": "Dreamcast"}, # IGDB
"ecd-micromind": {"id": 269, "name": "ECD Micromind"},
"electron": {"id": 93, "name": "Electron"},
"acorn-electron": {"id": 93, "name": "Electron"}, # IGDB
"enterprise": {"id": 161, "name": "Enterprise"},
"epoch-cassette-vision": {"id": 137, "name": "Epoch Cassette Vision"},
"epoch-game-pocket-computer": {"id": 139, "name": "Epoch Game Pocket Computer"},
"epoch-super-cassette-vision": {"id": 138, "name": "Epoch Super Cassette Vision"},
"evercade": {"id": 284, "name": "Evercade"},
"exen": {"id": 70, "name": "ExEn"},
"exelvision": {"id": 195, "name": "Exelvision"},
"exidy-sorcerer": {"id": 176, "name": "Exidy Sorcerer"},
"fmtowns": {"id": 102, "name": "FM Towns"},
"fm-towns": {"id": 102, "name": "FM Towns"}, # IGDB
"fm-7": {"id": 126, "name": "FM-7"},
"mobile-custom": {"id": 315, "name": "Feature phone"},
"fire-os": {"id": 159, "name": "Fire OS"},
"amazon-fire-tv": {"id": 159, "name": "Fire TV"},
"freebox": {"id": 268, "name": "Freebox"},
"g-cluster": {"id": 302, "name": "G-cluster"},
"gimini": {"id": 251, "name": "GIMINI"},
"gnex": {"id": 258, "name": "GNEX"},
"gp2x": {"id": 122, "name": "GP2X"},
"gp2x-wiz": {"id": 123, "name": "GP2X Wiz"},
"gp32": {"id": 108, "name": "GP32"},
"gvm": {"id": 257, "name": "GVM"},
"galaksija": {"id": 236, "name": "Galaksija"},
"gameboy": {"id": 10, "name": "Game Boy"},
"gb": {"id": 10, "name": "Game Boy"}, # IGDB
"gameboy-advance": {"id": 12, "name": "Game Boy Advance"},
"gba": {"id": 12, "name": "Game Boy Advance"}, # IGDB
"gameboy-color": {"id": 11, "name": "Game Boy Color"},
"gbc": {"id": 11, "name": "Game Boy Color"}, # IGDB
"game-gear": {"id": 25, "name": "Game Gear"},
"gamegear": {"id": 25, "name": "Game Gear"}, # IGDB
"game-wave": {"id": 104, "name": "Game Wave"},
"game-com": {"id": 50, "name": "Game.Com"},
"game-dot-com": {"id": 50, "name": "Game.Com"}, # IGDB
"gamecube": {"id": 14, "name": "GameCube"},
"ngc": {"id": 14, "name": "GameCube"}, # IGDB
"gamestick": {"id": 155, "name": "GameStick"},
"genesis": {"id": 16, "name": "Genesis/Mega Drive"},
"genesis-slash-megadrive": {"id": 16, "name": "Genesis/Mega Drive"},
"gizmondo": {"id": 55, "name": "Gizmondo"},
"gloud": {"id": 292, "name": "Gloud"},
"glulx": {"id": 172, "name": "Glulx"},
"hd-dvd-player": {"id": 167, "name": "HD DVD Player"},
"hp-9800": {"id": 219, "name": "HP 9800"},
"hp-programmable-calculator": {"id": 234, "name": "HP Programmable Calculator"},
"heathzenith": {"id": 262, "name": "Heath/Zenith H8/H89"},
"heathkit-h11": {"id": 248, "name": "Heathkit H11"},
"hitachi-s1": {"id": 274, "name": "Hitachi S1"},
"hugo": {"id": 170, "name": "Hugo"},
"hyperscan": {"id": 192, "name": "HyperScan"},
"ibm-5100": {"id": 250, "name": "IBM 5100"},
"ideal-computer": {"id": 252, "name": "Ideal-Computer"},
"intel-8008": {"id": 224, "name": "Intel 8008"},
"intel-8080": {"id": 225, "name": "Intel 8080"},
"intel-8086": {"id": 317, "name": "Intel 8086 / 8088"},
"intellivision": {"id": 30, "name": "Intellivision"},
"interact-model-one": {"id": 295, "name": "Interact Model One"},
"interton-video-2000": {"id": 221, "name": "Interton Video 2000"},
"j2me": {"id": 64, "name": "J2ME"},
"jaguar": {"id": 17, "name": "Jaguar"},
"jolt": {"id": 247, "name": "Jolt"},
"jupiter-ace": {"id": 153, "name": "Jupiter Ace"},
"kim-1": {"id": 226, "name": "KIM-1"},
"kaios": {"id": 313, "name": "KaiOS"},
"kindle": {"id": 145, "name": "Kindle Classic"},
"laser200": {"id": 264, "name": "Laser 200"},
"laseractive": {"id": 163, "name": "LaserActive"},
"leapfrog-explorer": {"id": 185, "name": "LeapFrog Explorer"},
"leapster-explorer-slash-leadpad-explorer": {
"id": 186,
"name": "Leapster Explorer/LeapPad Explorer",
},
"leaptv": {"id": 186, "name": "LeapTV"},
"leapster": {"id": 183, "name": "Leapster"},
"linux": {"id": 1, "name": "Linux"},
"luna": {"id": 297, "name": "Luna"},
"lynx": {"id": 18, "name": "Lynx"},
"mos-technology-6502": {"id": 240, "name": "MOS Technology 6502"},
"mre": {"id": 229, "name": "MRE"},
"msx": {"id": 57, "name": "MSX"},
"macintosh": {"id": 74, "name": "Macintosh"},
"mac": {"id": 74, "name": "Macintosh"}, # IGDB
"maemo": {"id": 157, "name": "Maemo"},
"mainframe": {"id": 208, "name": "Mainframe"},
"matsushitapanasonic-jr": {"id": 307, "name": "Matsushita/Panasonic JR"},
"mattel-aquarius": {"id": 135, "name": "Mattel Aquarius"},
"meego": {"id": 158, "name": "MeeGo"},
"memotech-mtx": {"id": 148, "name": "Memotech MTX"},
"meritum": {"id": 311, "name": "Meritum"},
"microbee": {"id": 200, "name": "Microbee"},
"microtan-65": {"id": 232, "name": "Microtan 65"},
"microvision": {"id": 97, "name": "Microvision"},
"microvision--1": {"id": 97, "name": "Microvision"}, # IGDB
"mophun": {"id": 71, "name": "Mophun"},
"motorola-6800": {"id": 235, "name": "Motorola 6800"},
"motorola-68k": {"id": 275, "name": "Motorola 68k"},
"ngage": {"id": 32, "name": "N-Gage"},
"ngage2": {"id": 89, "name": "N-Gage (service)"},
"nes": {"id": 22, "name": "NES"},
"famicom": {"id": 22, "name": "NES"},
"nascom": {"id": 175, "name": "Nascom"},
"neo-geo": {"id": 36, "name": "Neo Geo"},
"neogeoaes": {"id": 36, "name": "Neo Geo"}, # IGDB
"neogeomvs": {"id": 36, "name": "Neo Geo"}, # IGDB
"neo-geo-cd": {"id": 54, "name": "Neo Geo CD"},
"neo-geo-pocket": {"id": 52, "name": "Neo Geo Pocket"},
"neo-geo-pocket-color": {"id": 53, "name": "Neo Geo Pocket Color"},
"neo-geo-x": {"id": 279, "name": "Neo Geo X"},
"new-nintendo-3ds": {"id": 174, "name": "New Nintendo 3DS"},
"newbrain": {"id": 177, "name": "NewBrain"},
"newton": {"id": 207, "name": "Newton"},
"3ds": {"id": 101, "name": "Nintendo 3DS"},
"n64": {"id": 9, "name": "Nintendo 64"},
"nintendo-ds": {"id": 44, "name": "Nintendo DS"},
"nds": {"id": 44, "name": "Nintendo DS"}, # IGDB
"nintendo-dsi": {"id": 87, "name": "Nintendo DSi"},
"switch": {"id": 203, "name": "Nintendo Switch"},
"northstar": {"id": 266, "name": "North Star"},
"noval-760": {"id": 244, "name": "Noval 760"},
"nuon": {"id": 116, "name": "Nuon"},
"ooparts": {"id": 300, "name": "OOParts"},
"os2": {"id": 146, "name": "OS/2"},
"oculus-go": {"id": 218, "name": "Oculus Go"},
"odyssey": {"id": 75, "name": "Odyssey"},
"odyssey--1": {"id": 75, "name": "Odyssey"}, # IGDB
"odyssey-2": {"id": 78, "name": "Odyssey 2"},
"odyssey-2-slash-videopac-g7000": {"id": 78, "name": "Odyssey 2/Videopac G7000"},
"ohio-scientific": {"id": 178, "name": "Ohio Scientific"},
"onlive": {"id": 282, "name": "OnLive"},
"onlive-game-system": {"id": 282, "name": "OnLive Game System"}, # IGDB
"orao": {"id": 270, "name": "Orao"},
"oric": {"id": 111, "name": "Oric"},
"ouya": {"id": 144, "name": "Ouya"},
"pc-booter": {"id": 4, "name": "PC Booter"},
"pc-6001": {"id": 149, "name": "PC-6001"},
"pc-8000": {"id": 201, "name": "PC-8000"},
"pc88": {"id": 94, "name": "PC-88"},
"pc-8800-series": {"id": 94, "name": "PC-8800 Series"}, # IGDB
"pc98": {"id": 95, "name": "PC-98"},
"pc-9800-series": {"id": 95, "name": "PC-9800 Series"}, # IGDB
"pc-fx": {"id": 59, "name": "PC-FX"},
"pico": {"id": 316, "name": "PICO"},
"ps-vita": {"id": 105, "name": "PS Vita"},
"psvita": {"id": 105, "name": "PS Vita"}, # IGDB
"psp": {"id": 46, "name": "PSP"},
"palmos": {"id": 65, "name": "Palm OS"},
"palm-os": {"id": 65, "name": "Palm OS"}, # IGDB
"pandora": {"id": 308, "name": "Pandora"},
"pebble": {"id": 304, "name": "Pebble"},
"philips-vg-5000": {"id": 133, "name": "Philips VG 5000"},
"photocd": {"id": 272, "name": "Photo CD"},
"pippin": {"id": 112, "name": "Pippin"},
"playstation": {"id": 6, "name": "PlayStation"},
"ps": {"id": 6, "name": "PlayStation"}, # IGDB
"ps2": {"id": 7, "name": "PlayStation 2"},
"ps3": {"id": 81, "name": "PlayStation 3"},
"playstation-4": {"id": 141, "name": "PlayStation 4"},
"ps4--1": {"id": 141, "name": "PlayStation 4"}, # IGDB
"playstation-5": {"id": 288, "name": "PlayStation 5"},
"ps5": {"id": 288, "name": "PlayStation 5"}, # IGDB
"playstation-now": {"id": 294, "name": "PlayStation Now"},
"playdate": {"id": 303, "name": "Playdate"},
"playdia": {"id": 107, "name": "Playdia"},
"plex-arcade": {"id": 291, "name": "Plex Arcade"},
"pokitto": {"id": 230, "name": "Pokitto"},
"pokemon-mini": {"id": 152, "name": "Pokémon Mini"},
"poly-88": {"id": 249, "name": "Poly-88"},
"oculus-quest": {"id": 271, "name": "Quest"},
"rca-studio-ii": {"id": 113, "name": "RCA Studio II"},
"research-machines-380z": {"id": 309, "name": "Research Machines 380Z"},
"roku": {"id": 196, "name": "Roku"},
"sam-coupe": {"id": 120, "name": "SAM Coupé"},
"scmp": {"id": 255, "name": "SC/MP"},
"sd-200270290": {"id": 267, "name": "SD-200/270/290"},
"sega-32x": {"id": 21, "name": "SEGA 32X"},
"sega32": {"id": 21, "name": "SEGA 32X"}, # IGDB
"sega-cd": {"id": 20, "name": "SEGA CD"},
"segacd": {"id": 20, "name": "SEGA CD"}, # IGDB
"sega-master-system": {"id": 26, "name": "SEGA Master System"},
"sega-pico": {"id": 103, "name": "SEGA Pico"},
"sega-saturn": {"id": 23, "name": "SEGA Saturn"},
"saturn": {"id": 23, "name": "SEGA Saturn"}, # IGDB
"sg-1000": {"id": 114, "name": "SG-1000"},
"sk-vm": {"id": 259, "name": "SK-VM"},
"smc-777": {"id": 273, "name": "SMC-777"},
"snes": {"id": 15, "name": "SNES"},
"sfam": {"id": 15, "name": "SNES"},
"sri-5001000": {"id": 242, "name": "SRI-500/1000"},
"swtpc-6800": {"id": 228, "name": "SWTPC 6800"},
"sharp-mz-80b20002500": {"id": 182, "name": "Sharp MZ-80B/2000/2500"},
"sharp-mz-80k7008001500": {"id": 181, "name": "Sharp MZ-80K/700/800/1500"},
"sharp-x1": {"id": 121, "name": "Sharp X1"},
"x1": {"id": 121, "name": "Sharp X1"}, # IGDB
"sharp-x68000": {"id": 106, "name": "Sharp X68000"},
"sharp-zaurus": {"id": 202, "name": "Sharp Zaurus"},
"signetics-2650": {"id": 278, "name": "Signetics 2650"},
"sinclair-ql": {"id": 131, "name": "Sinclair QL"},
"socrates": {"id": 190, "name": "Socrates"},
"sol-20": {"id": 199, "name": "Sol-20"},
"sord-m5": {"id": 134, "name": "Sord M5"},
"spectravideo": {"id": 85, "name": "Spectravideo"},
"stadia": {"id": 281, "name": "Stadia"},
"super-acan": {"id": 110, "name": "Super A'can"},
"super-vision-8000": {"id": 296, "name": "Super Vision 8000"},
"supergrafx": {"id": 127, "name": "SuperGrafx"},
"supervision": {"id": 109, "name": "Supervision"},
"sure-shot-hd": {"id": 287, "name": "Sure Shot HD"},
"symbian": {"id": 67, "name": "Symbian"},
"tads": {"id": 171, "name": "TADS"},
"ti-programmable-calculator": {"id": 239, "name": "TI Programmable Calculator"},
"ti-994a": {"id": 47, "name": "TI-99/4A"},
"ti-99": {"id": 47, "name": "TI-99/4A"}, # IGDB
"tim": {"id": 246, "name": "TIM"},
"trs-80": {"id": 58, "name": "TRS-80"},
"trs-80-coco": {"id": 62, "name": "TRS-80 Color Computer"},
"trs-80-color-computer": {"id": 62, "name": "TRS-80 Color Computer"}, # IGDB
"trs-80-mc-10": {"id": 193, "name": "TRS-80 MC-10"},
"trs-80-model-100": {"id": 312, "name": "TRS-80 Model 100"},
"taito-x-55": {"id": 283, "name": "Taito X-55"},
"tatung-einstein": {"id": 150, "name": "Tatung Einstein"},
"tektronix-4050": {"id": 223, "name": "Tektronix 4050"},
"tele-spiel": {"id": 220, "name": "Tele-Spiel ES-2201"},
"telstar-arcade": {"id": 233, "name": "Telstar Arcade"},
"terminal": {"id": 209, "name": "Terminal"},
"thomson-mo": {"id": 147, "name": "Thomson MO"},
"thomson-mo5": {"id": 147, "name": "Thomson MO5"},
"thomson-to": {"id": 130, "name": "Thomson TO"},
"tiki-100": {"id": 263, "name": "Tiki 100"},
"timex-sinclair-2068": {"id": 173, "name": "Timex Sinclair 2068"},
"tizen": {"id": 206, "name": "Tizen"},
"tomahawk-f1": {"id": 256, "name": "Tomahawk F1"},
"tomy-tutor": {"id": 151, "name": "Tomy Tutor"},
"triton": {"id": 310, "name": "Triton"},
"turbografx-cd": {"id": 45, "name": "TurboGrafx CD"},
"turbografx-16-slash-pc-engine-cd": {"id": 45, "name": "TurboGrafx CD"},
"turbo-grafx": {"id": 40, "name": "TurboGrafx-16"},
"turbografx16--1": {"id": 40, "name": "TurboGrafx-16"}, # IGDB
"vflash": {"id": 189, "name": "V.Flash"},
"vsmile": {"id": 42, "name": "V.Smile"},
"vic-20": {"id": 43, "name": "VIC-20"},
"vis": {"id": 164, "name": "VIS"},
"vectrex": {"id": 37, "name": "Vectrex"},
"versatile": {"id": 299, "name": "Versatile"},
"videobrain": {"id": 214, "name": "VideoBrain"},
"videopac-g7400": {"id": 128, "name": "Videopac+ G7400"},
"virtual-boy": {"id": 38, "name": "Virtual Boy"},
"virtualboy": {"id": 38, "name": "Virtual Boy"},
"wipi": {"id": 260, "name": "WIPI"},
"wang2200": {"id": 217, "name": "Wang 2200"},
"wii": {"id": 82, "name": "Wii"},
"wii-u": {"id": 132, "name": "Wii U"},
"wiiu": {"id": 132, "name": "Wii U"},
"windows": {"id": 3, "name": "Windows"},
"win": {"id": 3, "name": "Windows"}, # IGDB
"win3x": {"id": 5, "name": "Windows 3.x"},
"windows-apps": {"id": 140, "name": "Windows Apps"},
"windowsmobile": {"id": 66, "name": "Windows Mobile"},
"windows-mobile": {"id": 66, "name": "Windows Mobile"}, # IGDB
"windows-phone": {"id": 98, "name": "Windows Phone"},
"winphone": {"id": 98, "name": "Windows Phone"}, # IGDB
"wonderswan": {"id": 48, "name": "WonderSwan"},
"wonderswan-color": {"id": 49, "name": "WonderSwan Color"},
"xavixport": {"id": 191, "name": "XaviXPORT"},
"xbox": {"id": 13, "name": "Xbox"},
"xbox360": {"id": 69, "name": "Xbox 360"},
"xboxcloudgaming": {"id": 293, "name": "Xbox Cloud Gaming"},
"xbox-one": {"id": 142, "name": "Xbox One"},
"xboxone": {"id": 142, "name": "Xbox One"},
"xbox-series": {"id": 289, "name": "Xbox Series"},
"series-x": {"id": 289, "name": "Xbox Series X"}, # IGDB
"xerox-alto": {"id": 254, "name": "Xerox Alto"},
"z-machine": {"id": 169, "name": "Z-machine"},
"zx-spectrum": {"id": 41, "name": "ZX Spectrum"},
"zx-spectrum-next": {"id": 280, "name": "ZX Spectrum Next"},
"zx80": {"id": 118, "name": "ZX80"},
"zx81": {"id": 119, "name": "ZX81"},
"sinclair-zx81": {"id": 119, "name": "ZX81"}, # IGDB
"zeebo": {"id": 88, "name": "Zeebo"},
"z80": {"id": 227, "name": "Zilog Z80"},
"zilog-z8000": {"id": 276, "name": "Zilog Z8000"},
"zodiac": {"id": 68, "name": "Zodiac"},
"zune": {"id": 211, "name": "Zune"},
"bada": {"id": 99, "name": "bada"},
"digiblast": {"id": 187, "name": "digiBlast"},
"ipad": {"id": 96, "name": "iPad"},
"iphone": {"id": 86, "name": "iPhone"},
"ios": {"id": 86, "name": "iOS"},
"ipod-classic": {"id": 80, "name": "iPod Classic"},
"iircade": {"id": 314, "name": "iiRcade"},
"tvos": {"id": 179, "name": "tvOS"},
"watchos": {"id": 180, "name": "watchOS"},
"webos": {"id": 100, "name": "webOS"},
}
# Reverse lookup
MOBY_ID_TO_SLUG = {v["id"]: k for k, v in SLUG_TO_MOBY_ID.items()}