diff --git a/backend/alembic/versions/0015_mobygames_data.py b/backend/alembic/versions/0015_mobygames_data.py index bd48e8fe3..ffe5ac0d4 100644 --- a/backend/alembic/versions/0015_mobygames_data.py +++ b/backend/alembic/versions/0015_mobygames_data.py @@ -25,6 +25,9 @@ def upgrade() -> None: batch_op.add_column(sa.Column("moby_id", sa.Integer(), nullable=True)) batch_op.add_column(sa.Column("moby_metadata", mysql.JSON(), nullable=True)) + with op.batch_alter_table("roms", schema=None) as batch_op: + batch_op.execute("update roms set moby_metadata = '\\{\\}'") + # ### end Alembic commands ### diff --git a/backend/endpoints/responses/rom.py b/backend/endpoints/responses/rom.py index 79b18bb91..3b5b7ff40 100644 --- a/backend/endpoints/responses/rom.py +++ b/backend/endpoints/responses/rom.py @@ -1,27 +1,29 @@ import re -from typing import Optional +from typing import Optional, get_type_hints +from typing_extensions import NotRequired, TypedDict from endpoints.responses.assets import SaveSchema, ScreenshotSchema, StateSchema from fastapi import Request from fastapi.responses import StreamingResponse from handler import socket_handler -from handler.metadata_handler.igdb_handler import IGDBRelatedGame +from handler.metadata_handler.igdb_handler import IGDBMetadata +from handler.metadata_handler.moby_handler import MobyMetadata from pydantic import BaseModel, computed_field, Field from models.rom import Rom -from typing_extensions import TypedDict, NotRequired SORT_COMPARE_REGEX = r"^([Tt]he|[Aa]|[Aa]nd)\s" - -class RomMetadata(TypedDict): - expansions: NotRequired[list[IGDBRelatedGame]] - dlcs: NotRequired[list[IGDBRelatedGame]] - remasters: NotRequired[list[IGDBRelatedGame]] - remakes: NotRequired[list[IGDBRelatedGame]] - expanded_games: NotRequired[list[IGDBRelatedGame]] - ports: NotRequired[list[IGDBRelatedGame]] - similar_games: NotRequired[list[IGDBRelatedGame]] +RomIGDBMetadata = TypedDict( + "RomIGDBMetadata", + {k: NotRequired[v] for k, v in get_type_hints(IGDBMetadata).items()}, + total=False, +) +RomMobyMetadata = TypedDict( + "RomMobyMetadata", + {k: NotRequired[v] for k, v in get_type_hints(MobyMetadata).items()}, + total=False, +) class RomSchema(BaseModel): @@ -46,8 +48,6 @@ class RomSchema(BaseModel): summary: Optional[str] # Metadata fields - total_rating: Optional[str] - aggregated_rating: Optional[str] first_release_date: Optional[int] alternative_names: list[str] genres: list[str] @@ -55,7 +55,8 @@ class RomSchema(BaseModel): collections: list[str] companies: list[str] game_modes: list[str] - igdb_metadata: Optional[RomMetadata] + igdb_metadata: Optional[RomIGDBMetadata] + moby_metadata: Optional[RomMobyMetadata] path_cover_s: Optional[str] path_cover_l: Optional[str] diff --git a/backend/endpoints/search.py b/backend/endpoints/search.py index afbb840ca..b80676988 100644 --- a/backend/endpoints/search.py +++ b/backend/endpoints/search.py @@ -13,7 +13,6 @@ router = APIRouter() async def search_rom( request: Request, rom_id: str, - source: str, search_term: str = None, search_by: str = "name", search_extended: bool = False, diff --git a/backend/handler/db_handler/db_roms_handler.py b/backend/handler/db_handler/db_roms_handler.py index e45f1a73d..c1cc08711 100644 --- a/backend/handler/db_handler/db_roms_handler.py +++ b/backend/handler/db_handler/db_roms_handler.py @@ -6,7 +6,7 @@ from sqlalchemy.orm import Session class DBRomsHandler(DBHandler): - def _filter(data: Select[Rom], platform_id: int | None, search_term: str): + def _filter(self, data: Select[Rom], platform_id: int | None, search_term: str): if platform_id: data = data.filter_by(platform_id=platform_id) @@ -20,7 +20,7 @@ class DBRomsHandler(DBHandler): return data - def _order(data: Select[Rom], order_by: str, order_dir: str): + def _order(self, data: Select[Rom], order_by: str, order_dir: str): if order_by == "id": _column = Rom.id else: diff --git a/backend/handler/metadata_handler/__init__.py b/backend/handler/metadata_handler/__init__.py index 2fdf2a4dd..68f6478ab 100644 --- a/backend/handler/metadata_handler/__init__.py +++ b/backend/handler/metadata_handler/__init__.py @@ -40,7 +40,7 @@ class MetadataHandler: @staticmethod def _normalize_cover_url(url: str) -> str: return f"https:{url.replace('https:', '')}" - + async def _ps2_opl_format(self, match: re.Match[str], search_term: str) -> str: serial_code = match.group(1) @@ -52,7 +52,9 @@ class MetadataHandler: return search_term - async def _switch_titledb_format(self, match: re.Match[str], search_term: str) -> str: + async def _switch_titledb_format( + self, match: re.Match[str], search_term: str + ) -> str: titledb_index = {} title_id = match.group(1) @@ -74,7 +76,9 @@ class MetadataHandler: return search_term - async def _switch_productid_format(self, match: re.Match[str], search_term: str) -> str: + async def _switch_productid_format( + self, match: re.Match[str], search_term: str + ) -> str: product_id_index = {} product_id = match.group(1) diff --git a/backend/handler/metadata_handler/igdb_handler.py b/backend/handler/metadata_handler/igdb_handler.py index ca82b5cad..d26ef6a44 100644 --- a/backend/handler/metadata_handler/igdb_handler.py +++ b/backend/handler/metadata_handler/igdb_handler.py @@ -2,7 +2,8 @@ import functools import re import sys import time -from typing import Final, Optional, TypedDict, NotRequired +from typing import Final, Optional +from typing_extensions import NotRequired, TypedDict import pydash import requests @@ -29,7 +30,6 @@ ARCADE_IGDB_IDS: Final = [52, 79, 80] class IGDBPlatform(TypedDict): igdb_id: int - slug: str name: NotRequired[str] @@ -190,19 +190,22 @@ class IGDBHandler(MetadataHandler): return res.json() def _search_rom( - self, search_term: str, platform_idgb_id: int, category: int = 0 + self, search_term: str, platform_igdb_id: int, category: int = 0 ) -> dict | None: + if not platform_igdb_id: + return None + search_term = uc(search_term) category_filter: str = f"& category={category}" if category else "" roms = self._request( self.games_endpoint, - data=f'search "{search_term}"; fields {",".join(self.games_fields)}; where platforms=[{platform_idgb_id}] {category_filter};', + data=f'search "{search_term}"; fields {",".join(self.games_fields)}; where platforms=[{platform_igdb_id}] {category_filter};', ) if not roms: roms = self._request( self.search_endpoint, - data=f'fields {",".join(self.search_fields)}; where game.platforms=[{platform_idgb_id}] & (name ~ *"{search_term}"* | alternative_name ~ *"{search_term}"*);', + data=f'fields {",".join(self.search_fields)}; where game.platforms=[{platform_igdb_id}] & (name ~ *"{search_term}"* | alternative_name ~ *"{search_term}"*);', ) if roms: roms = self._request( @@ -250,36 +253,39 @@ class IGDBHandler(MetadataHandler): return IGDBPlatform(igdb_id=None, slug=slug) @check_twitch_token - async def get_rom(self, file_name: str, platform_idgb_id: int) -> IGDBRom: + async def get_rom(self, file_name: str, platform_igdb_id: int) -> IGDBRom: from handler import fs_rom_handler + if not platform_igdb_id: + return IGDBRom(igdb_id=None) + search_term = fs_rom_handler.get_file_name_with_no_tags(file_name) # Support for PS2 OPL filename format match = re.match(PS2_OPL_REGEX, file_name) - if platform_idgb_id == PS2_IGDB_ID and match: + if platform_igdb_id == PS2_IGDB_ID and match: search_term = await self._ps2_opl_format(match, search_term) # Support for switch titleID filename format match = re.search(SWITCH_TITLEDB_REGEX, file_name) - if platform_idgb_id == SWITCH_IGDB_ID and match: + if platform_igdb_id == SWITCH_IGDB_ID and match: search_term = await self._switch_titledb_format(match, search_term) # Support for switch productID filename format match = re.search(SWITCH_PRODUCT_ID_REGEX, file_name) - if platform_idgb_id == SWITCH_IGDB_ID and match: + if platform_igdb_id == SWITCH_IGDB_ID and match: search_term = await self._switch_productid_format(match, search_term) # Support for MAME arcade filename format - if platform_idgb_id in ARCADE_IGDB_IDS: + if platform_igdb_id in ARCADE_IGDB_IDS: search_term = await self._mame_format(search_term) - search_term = self._normalize_search_term(search_term) + search_term = self.normalize_search_term(search_term) rom = ( - self._search_rom(search_term, platform_idgb_id, MAIN_GAME_CATEGORY) - or self._search_rom(search_term, platform_idgb_id, EXPANDED_GAME_CATEGORY) - or self._search_rom(search_term, platform_idgb_id) + self._search_rom(search_term, platform_igdb_id, MAIN_GAME_CATEGORY) + or self._search_rom(search_term, platform_igdb_id, EXPANDED_GAME_CATEGORY) + or self._search_rom(search_term, platform_igdb_id) ) if not rom: @@ -332,22 +338,22 @@ class IGDBHandler(MetadataHandler): @check_twitch_token def get_matched_roms_by_name( - self, search_term: str, platform_idgb_id: int, search_extended: bool = False + self, search_term: str, platform_igdb_id: int, search_extended: bool = False ) -> list[IGDBRom]: - if not platform_idgb_id: + if not platform_igdb_id: return [] search_term = uc(search_term) matched_roms = self._request( self.games_endpoint, - data=f'search "{search_term}"; fields {",".join(self.games_fields)}; where platforms=[{platform_idgb_id}];', + data=f'search "{search_term}"; fields {",".join(self.games_fields)}; where platforms=[{platform_igdb_id}];', ) if not matched_roms or search_extended: log.info("Extended searching...") alternative_matched_roms = self._request( self.search_endpoint, - data=f'fields {",".join(self.search_fields)}; where game.platforms=[{platform_idgb_id}] & (name ~ *"{search_term}"* | alternative_name ~ *"{search_term}"*);', + data=f'fields {",".join(self.search_fields)}; where game.platforms=[{platform_igdb_id}] & (name ~ *"{search_term}"* | alternative_name ~ *"{search_term}"*);', ) if alternative_matched_roms: diff --git a/backend/handler/metadata_handler/moby_handler.py b/backend/handler/metadata_handler/moby_handler.py index 359b5133d..69ac63ae6 100644 --- a/backend/handler/metadata_handler/moby_handler.py +++ b/backend/handler/metadata_handler/moby_handler.py @@ -4,10 +4,12 @@ import yarl import re import time from config import MOBYGAMES_API_KEY -from typing import Final, TypedDict, NotRequired, Optional +from typing import Final, Optional +from typing_extensions import NotRequired, TypedDict from requests.exceptions import HTTPError, Timeout from logger.logger import log from unidecode import unidecode as uc +from urllib.parse import quote_plus from . import ( MetadataHandler, @@ -23,7 +25,6 @@ ARCADE_MOBY_IDS: Final = [143, 36] class MobyGamesPlatform(TypedDict): moby_id: int - slug: str name: NotRequired[str] @@ -53,7 +54,6 @@ def extract_metadata_from_moby_rom(rom: dict) -> MobyMetadata: "platforms": [ { "moby_id": p["platform_id"], - "slug": MOBY_ID_TO_SLUG[p["platform_id"]], "name": p["platform_name"], } for p in rom.get("platforms", []) @@ -95,8 +95,13 @@ class MobyGamesHandler(MetadataHandler): 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 or 0], title=search_term + platform=[platform_moby_id or 0], + title=quote_plus(search_term), ) roms = self._request(str(url)).get("games", []) @@ -121,6 +126,9 @@ class MobyGamesHandler(MetadataHandler): async def get_rom(self, file_name: str, platform_moby_id: int) -> MobyGamesRom: from handler import fs_rom_handler + if not platform_moby_id: + return MobyGamesRom(moby_id=None) + search_term = fs_rom_handler.get_file_name_with_no_tags(file_name) # Support for PS2 OPL filename format @@ -143,7 +151,7 @@ class MobyGamesHandler(MetadataHandler): search_term = await self._mame_format(search_term) search_term = self.normalize_search_term(search_term) - res = self._search_rom(uc(search_term), platform_moby_id) + res = self._search_rom(search_term, platform_moby_id) if not res: return MobyGamesRom(moby_id=None) @@ -155,6 +163,7 @@ class MobyGamesHandler(MetadataHandler): "summary": res.get("description", ""), "url_cover": res.get("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}) @@ -174,6 +183,7 @@ class MobyGamesHandler(MetadataHandler): "summary": res.get("description", None), "url_cover": res.get("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}) @@ -187,8 +197,9 @@ class MobyGamesHandler(MetadataHandler): if not platform_moby_id: return [] + search_term = uc(search_term) url = yarl.URL(self.games_url).with_query( - platform=[platform_moby_id or 0], title=search_term + platform=[platform_moby_id or 0], title=quote_plus(search_term) ) matched_roms = self._request(str(url))["games"] @@ -205,6 +216,7 @@ class MobyGamesHandler(MetadataHandler): "url_screenshots": [ s["image"] for s in rom.get("sample_screenshots", []) ], + "moby_metadata": extract_metadata_from_moby_rom(rom), }.items() if v } diff --git a/backend/models/rom.py b/backend/models/rom.py index 6fd6d4bd5..0aacf6a3b 100644 --- a/backend/models/rom.py +++ b/backend/models/rom.py @@ -118,18 +118,6 @@ class Rom(BaseModel): ).all() # Metadata fields - @property - def total_rating(self) -> str: - return ( - self.igdb_metadata.get("total_rating", None) - or self.moby_metadata.get("moby_score", None) - or "" - ) - - @property - def aggregated_rating(self) -> str: - return self.igdb_metadata.get("aggregated_rating", "") - @property def alternative_names(self) -> list[str]: return ( diff --git a/frontend/src/__generated__/index.ts b/frontend/src/__generated__/index.ts index 4905154c1..f90a6c077 100644 --- a/frontend/src/__generated__/index.ts +++ b/frontend/src/__generated__/index.ts @@ -15,11 +15,14 @@ export type { ConfigResponse } from './models/ConfigResponse'; export type { CursorPage_RomSchema_ } from './models/CursorPage_RomSchema_'; export type { HeartbeatResponse } from './models/HeartbeatResponse'; export type { HTTPValidationError } from './models/HTTPValidationError'; +export type { IGDBPlatform } from './models/IGDBPlatform'; export type { IGDBRelatedGame } from './models/IGDBRelatedGame'; export type { MessageResponse } from './models/MessageResponse'; +export type { MobyGamesPlatform } from './models/MobyGamesPlatform'; export type { PlatformSchema } from './models/PlatformSchema'; export type { Role } from './models/Role'; -export type { RomMetadata } from './models/RomMetadata'; +export type { RomIGDBMetadata } from './models/RomIGDBMetadata'; +export type { RomMobyMetadata } from './models/RomMobyMetadata'; export type { RomSchema } from './models/RomSchema'; export type { SaveSchema } from './models/SaveSchema'; export type { SchedulerDict } from './models/SchedulerDict'; diff --git a/frontend/src/__generated__/models/IGDBPlatform.ts b/frontend/src/__generated__/models/IGDBPlatform.ts new file mode 100644 index 000000000..ffcb57898 --- /dev/null +++ b/frontend/src/__generated__/models/IGDBPlatform.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type IGDBPlatform = { + igdb_id: number; + name?: string; +}; + diff --git a/frontend/src/__generated__/models/MobyGamesPlatform.ts b/frontend/src/__generated__/models/MobyGamesPlatform.ts new file mode 100644 index 000000000..3a9e84b99 --- /dev/null +++ b/frontend/src/__generated__/models/MobyGamesPlatform.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type MobyGamesPlatform = { + moby_id: number; + name?: string; +}; + diff --git a/frontend/src/__generated__/models/RomMetadata.ts b/frontend/src/__generated__/models/RomIGDBMetadata.ts similarity index 53% rename from frontend/src/__generated__/models/RomMetadata.ts rename to frontend/src/__generated__/models/RomIGDBMetadata.ts index 7f3bb23a0..09321f978 100644 --- a/frontend/src/__generated__/models/RomMetadata.ts +++ b/frontend/src/__generated__/models/RomIGDBMetadata.ts @@ -3,9 +3,20 @@ /* tslint:disable */ /* eslint-disable */ +import type { IGDBPlatform } from './IGDBPlatform'; import type { IGDBRelatedGame } from './IGDBRelatedGame'; -export type RomMetadata = { +export type RomIGDBMetadata = { + total_rating?: string; + aggregated_rating?: string; + first_release_date?: (number | null); + genres?: Array; + franchises?: Array; + alternative_names?: Array; + collections?: Array; + companies?: Array; + game_modes?: Array; + platforms?: Array; expansions?: Array; dlcs?: Array; remasters?: Array; diff --git a/frontend/src/__generated__/models/RomMobyMetadata.ts b/frontend/src/__generated__/models/RomMobyMetadata.ts new file mode 100644 index 000000000..9f868b6cf --- /dev/null +++ b/frontend/src/__generated__/models/RomMobyMetadata.ts @@ -0,0 +1,14 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { MobyGamesPlatform } from './MobyGamesPlatform'; + +export type RomMobyMetadata = { + moby_score?: string; + genres?: Array; + alternate_titles?: Array; + platforms?: Array; +}; + diff --git a/frontend/src/__generated__/models/RomSchema.ts b/frontend/src/__generated__/models/RomSchema.ts index 2010ca496..b41e6f297 100644 --- a/frontend/src/__generated__/models/RomSchema.ts +++ b/frontend/src/__generated__/models/RomSchema.ts @@ -3,7 +3,8 @@ /* tslint:disable */ /* eslint-disable */ -import type { RomMetadata } from './RomMetadata'; +import type { RomIGDBMetadata } from './RomIGDBMetadata'; +import type { RomMobyMetadata } from './RomMobyMetadata'; import type { SaveSchema } from './SaveSchema'; import type { ScreenshotSchema } from './ScreenshotSchema'; import type { StateSchema } from './StateSchema'; @@ -25,8 +26,6 @@ export type RomSchema = { name: (string | null); slug: (string | null); summary: (string | null); - total_rating: (string | null); - aggregated_rating: (string | null); first_release_date: (number | null); alternative_names: Array; genres: Array; @@ -34,7 +33,8 @@ export type RomSchema = { collections: Array; companies: Array; game_modes: Array; - igdb_metadata: (RomMetadata | null); + igdb_metadata: (RomIGDBMetadata | null); + moby_metadata: (RomMobyMetadata | null); path_cover_s: (string | null); path_cover_l: (string | null); has_cover: boolean; diff --git a/frontend/src/components/Details/SourceTable.vue b/frontend/src/components/Details/SourceTable.vue index b7e535992..b1702b5b9 100644 --- a/frontend/src/components/Details/SourceTable.vue +++ b/frontend/src/components/Details/SourceTable.vue @@ -49,7 +49,7 @@ defineProps<{ rom: Rom }>(); - {{ rom.total_rating }} + {{ rom.igdb_metadata?.total_rating }} @@ -80,7 +80,7 @@ defineProps<{ rom: Rom }>(); - {{ rom.total_rating }} + {{ rom.moby_metadata?.moby_score }} diff --git a/frontend/src/components/Details/Title.vue b/frontend/src/components/Details/Title.vue index 5329510e7..0769a5e13 100644 --- a/frontend/src/components/Details/Title.vue +++ b/frontend/src/components/Details/Title.vue @@ -90,7 +90,7 @@ const { smAndDown } = useDisplay(); ID: {{ rom.igdb_id }} - Rating: {{ rom.total_rating }} + Rating: {{ rom.igdb_metadata?.total_rating }} @@ -112,7 +112,7 @@ const { smAndDown } = useDisplay(); ID: {{ rom.moby_id }} - Rating: {{ rom.total_rating }} + Rating: {{ rom.moby_metadata?.moby_score }}