From 463bb27ea97abc329f8990730fb9bb6b48ee2845 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Fri, 24 Oct 2025 16:04:54 -0400 Subject: [PATCH] Add config to store media on disk --- backend/config/config_manager.py | 22 ++ backend/endpoints/rom.py | 2 +- backend/endpoints/search.py | 6 +- backend/endpoints/sockets/scan.py | 19 ++ .../handler/filesystem/resources_handler.py | 54 ++++ backend/handler/metadata/base_handler.py | 14 + backend/handler/metadata/gamelist_handler.py | 160 ++++++---- backend/handler/metadata/ss_handler.py | 288 +++++++++++------- backend/handler/scan_handler.py | 8 +- .../tests/config/fixtures/config/config.yml | 5 + examples/config.example.yml | 23 +- .../__generated__/models/RomFileCategory.ts | 2 +- .../models/RomGamelistMetadata.ts | 30 +- .../src/__generated__/models/RomSSMetadata.ts | 39 +-- .../Settings/UserInterface/Interface.vue | 21 +- .../src/components/common/Game/Card/Base.vue | 18 +- frontend/src/console/components/GameCard.vue | 20 +- frontend/src/locales/de_DE/settings.json | 3 +- frontend/src/locales/en_GB/settings.json | 3 +- frontend/src/locales/en_US/settings.json | 3 +- frontend/src/locales/es_ES/settings.json | 3 +- frontend/src/locales/fr_FR/settings.json | 3 +- frontend/src/locales/it_IT/settings.json | 3 +- frontend/src/locales/ja_JP/settings.json | 3 +- frontend/src/locales/ko_KR/settings.json | 3 +- frontend/src/locales/pl_PL/settings.json | 3 +- frontend/src/locales/pt_BR/settings.json | 3 +- frontend/src/locales/ro_RO/settings.json | 3 +- frontend/src/locales/ru_RU/settings.json | 3 +- frontend/src/locales/zh_CN/settings.json | 3 +- frontend/src/locales/zh_TW/settings.json | 3 +- 31 files changed, 521 insertions(+), 252 deletions(-) diff --git a/backend/config/config_manager.py b/backend/config/config_manager.py index 559dad5af..abea867b0 100644 --- a/backend/config/config_manager.py +++ b/backend/config/config_manager.py @@ -20,6 +20,7 @@ from config import ( ROMM_DB_DRIVER, ) from exceptions.config_exceptions import ConfigNotWritableException +from handler.metadata.base_handler import MetadataMediaType from logger.formatter import BLUE from logger.formatter import highlight as hl from logger.logger import log @@ -66,6 +67,7 @@ class Config: SCAN_ARTWORK_PRIORITY: list[str] SCAN_REGION_PRIORITY: list[str] SCAN_LANGUAGE_PRIORITY: list[str] + SCAN_MEDIA: list[str] def __init__(self, **entries): self.__dict__.update(entries) @@ -242,6 +244,15 @@ class ConfigManager: "scan.priority.language", ["en", "fr"], ), + SCAN_MEDIA=pydash.get( + self._raw_config, + "scan.media", + [ + "box2d", + "screenshot", + "manual", + ], + ), ) def _get_ejs_controls(self) -> dict[str, EjsControls]: @@ -420,6 +431,17 @@ class ConfigManager: log.critical("Invalid config.yml: scan.priority.language must be a list") sys.exit(3) + if not isinstance(self.config.SCAN_MEDIA, list): + log.critical("Invalid config.yml: scan.media must be a list") + sys.exit(3) + + for media in self.config.SCAN_MEDIA: + if media not in MetadataMediaType: + log.critical( + f"Invalid config.yml: scan.media.{media} is not a valid media type" + ) + sys.exit(3) + def get_config(self) -> Config: try: with open(self.config_file, "r") as config_file: diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index 4c0c9d672..3f0215f33 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -826,7 +826,7 @@ async def update_rom( cleaned_data.update({"moby_id": None, "moby_metadata": {}}) if cleaned_data["ss_id"] and int(cleaned_data["ss_id"]) != rom.ss_id: - ss_rom = await meta_ss_handler.get_rom_by_id(cleaned_data["ss_id"]) + ss_rom = await meta_ss_handler.get_rom_by_id(rom, cleaned_data["ss_id"]) cleaned_data.update(ss_rom) elif rom.ss_id and not cleaned_data["ss_id"]: cleaned_data.update({"ss_id": None, "ss_metadata": {}}) diff --git a/backend/endpoints/search.py b/backend/endpoints/search.py index 0d958f51f..3a5509dc7 100644 --- a/backend/endpoints/search.py +++ b/backend/endpoints/search.py @@ -97,7 +97,7 @@ async def search_rom( igdb_rom, moby_rom, ss_rom = await asyncio.gather( meta_igdb_handler.get_matched_rom_by_id(int(search_term)), meta_moby_handler.get_matched_rom_by_id(int(search_term)), - meta_ss_handler.get_matched_rom_by_id(int(search_term)), + meta_ss_handler.get_matched_rom_by_id(rom, int(search_term)), ) except ValueError as exc: log.error(f"Search error: invalid ID '{search_term}'") @@ -123,7 +123,9 @@ async def search_rom( meta_moby_handler.get_matched_roms_by_name( search_term, rom.platform.moby_id ), - meta_ss_handler.get_matched_roms_by_name(search_term, rom.platform.ss_id), + meta_ss_handler.get_matched_roms_by_name( + rom, search_term, rom.platform.ss_id + ), meta_flashpoint_handler.get_matched_roms_by_name( search_term, rom.platform.slug ), diff --git a/backend/endpoints/sockets/scan.py b/backend/endpoints/sockets/scan.py index fd8d95cc2..767cea511 100644 --- a/backend/endpoints/sockets/scan.py +++ b/backend/endpoints/sockets/scan.py @@ -27,6 +27,7 @@ from handler.filesystem import ( fs_rom_handler, ) from handler.filesystem.roms_handler import FSRom +from handler.metadata.ss_handler import get_preferred_media_types from handler.redis_handler import high_prio_queue, redis_client from handler.scan_handler import ( ScanType, @@ -337,6 +338,24 @@ async def _identify_rom( if badge_url and badge_path: await fs_resource_handler.store_ra_badge(badge_url, badge_path) + if _added_rom.ss_metadata: + preferred_media_types = get_preferred_media_types() + for media_type in preferred_media_types: + if _added_rom.ss_metadata.get(f"{media_type.value}_path"): + await fs_resource_handler.store_media_file( + _added_rom.ss_metadata[f"{media_type.value}_url"], + _added_rom.ss_metadata[f"{media_type.value}_path"], + ) + + if _added_rom.gamelist_metadata: + preferred_media_types = get_preferred_media_types() + for media_type in preferred_media_types: + if _added_rom.gamelist_metadata.get(f"{media_type.value}_path"): + await fs_resource_handler.store_media_file( + _added_rom.gamelist_metadata[f"{media_type.value}_url"], + _added_rom.gamelist_metadata[f"{media_type.value}_path"], + ) + path_cover_s, path_cover_l = await fs_resource_handler.get_cover( entity=_added_rom, overwrite=should_update_props, diff --git a/backend/handler/filesystem/resources_handler.py b/backend/handler/filesystem/resources_handler.py index b45ef5d4e..3afface3b 100644 --- a/backend/handler/filesystem/resources_handler.py +++ b/backend/handler/filesystem/resources_handler.py @@ -9,6 +9,7 @@ from fastapi import status from PIL import Image, ImageFile, UnidentifiedImageError from config import ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP, RESOURCES_BASE_PATH +from handler.metadata.base_handler import MetadataMediaType from logger.logger import log from models.collection import Collection from models.rom import Rom @@ -26,6 +27,7 @@ class FSResourcesHandler(FSHandler): def get_platform_resources_path(self, platform_id: int) -> str: return os.path.join("roms", str(platform_id)) + # Cover art def cover_exists(self, entity: Rom | Collection, size: CoverSize) -> bool: """Check if rom cover exists in filesystem @@ -227,6 +229,7 @@ class FSResourcesHandler(FSHandler): path_cover_s.relative_to(self.base_path) ) + # Screenshots async def _store_screenshot(self, rom: Rom, url_screenhot: str, idx: int): """Store roms resources in filesystem @@ -322,6 +325,7 @@ class FSResourcesHandler(FSHandler): return path_screenshots + # Manuals def manual_exists(self, rom: Rom) -> bool: """Check if rom manual exists in filesystem @@ -415,6 +419,7 @@ class FSResourcesHandler(FSHandler): async def remove_manual(self, rom: Rom): await self.remove_directory(f"{rom.fs_resources_path}/manual") + # Retroachievements async def store_ra_badge(self, url: str, path: str) -> None: httpx_client = ctx_httpx_client.get() directory, filename = os.path.split(path) @@ -447,3 +452,52 @@ class FSResourcesHandler(FSHandler): async def create_ra_resources_path(self, platform_id: int, rom_id: int) -> None: await self.make_directory(self.get_ra_resources_path(platform_id, rom_id)) + + # Mixed media + def get_media_resources_path( + self, + platform_id: int, + rom_id: int, + media_type: MetadataMediaType, + ) -> str: + return os.path.join("roms", str(platform_id), str(rom_id), media_type.value) + + async def create_media_resources_path( + self, + platform_id: int, + rom_id: int, + media_type: MetadataMediaType, + ) -> None: + await self.make_directory( + self.get_media_resources_path(platform_id, rom_id, media_type) + ) + + async def store_media_file(self, url: str, path: str) -> None: + httpx_client = ctx_httpx_client.get() + directory, filename = os.path.split(path) + + if await self.file_exists(path): + log.debug(f"Media file {path} already exists, skipping download") + return + + try: + async with httpx_client.stream("GET", url, timeout=120) as response: + if response.status_code == status.HTTP_200_OK: + async with await self.write_file_streamed( + path=directory, filename=filename + ) as f: + async for chunk in response.aiter_raw(): + await f.write(chunk) + except httpx.TransportError as exc: + log.error(f"Unable to fetch media file at {url}: {str(exc)}") + return None + + async def remove_media_resources_path( + self, + platform_id: int, + rom_id: int, + media_type: MetadataMediaType, + ) -> None: + await self.remove_directory( + self.get_media_resources_path(platform_id, rom_id, media_type) + ) diff --git a/backend/handler/metadata/base_handler.py b/backend/handler/metadata/base_handler.py index 023cf84c6..4d910c6af 100644 --- a/backend/handler/metadata/base_handler.py +++ b/backend/handler/metadata/base_handler.py @@ -54,6 +54,20 @@ class BaseRom(TypedDict): url_manual: NotRequired[str] +class MetadataMediaType(enum.StrEnum): + BEZEL = "bezel" + BOX2D = "box2d" + BOX3D = "box3d" + MIXIMAGE = "miximage" + PHYSICAL = "physical" + SCREENSHOT = "screenshot" + TITLE_SCREEN = "title_screen" + MARQUEE = "marquee" + FANART = "fanart" + VIDEO = "video" + MANUAL = "manual" + + # This caches results to avoid repeated normalization of the same search term @lru_cache(maxsize=1024) def _normalize_search_term( diff --git a/backend/handler/metadata/gamelist_handler.py b/backend/handler/metadata/gamelist_handler.py index 8e96c51dd..311cd23be 100644 --- a/backend/handler/metadata/gamelist_handler.py +++ b/backend/handler/metadata/gamelist_handler.py @@ -7,29 +7,43 @@ from xml.etree.ElementTree import Element # trunk-ignore(bandit/B405) import pydash from defusedxml import ElementTree as ET -from handler.filesystem import fs_platform_handler +from config.config_manager import config_manager as cm +from handler.filesystem import fs_platform_handler, fs_resource_handler from logger.logger import log from models.platform import Platform +from models.rom import Rom -from .base_handler import BaseRom, MetadataHandler +from .base_handler import BaseRom, MetadataHandler, MetadataMediaType # https://github.com/Aloshi/EmulationStation/blob/master/GAMELISTS.md#reference +def get_preferred_media_types() -> list[MetadataMediaType]: + """Get preferred media types from config""" + config = cm.get_config() + return [MetadataMediaType(media) for media in config.SCAN_MEDIA] + + class GamelistMetadataMedia(TypedDict): - box2d: str | None - box2d_back: str | None - box3d: str | None - fanart: str | None - image: str | None - manual: str | None - marquee: str | None - miximage: str | None - physical: str | None - screenshot: str | None - thumbnail: str | None - title_screen: str | None - video: str | None + box2d_url: str | None + box2d_back_url: str | None + box3d_url: str | None + fanart_url: str | None + image_url: str | None + manual_url: str | None + marquee_url: str | None + miximage_url: str | None + physical_url: str | None + screenshot_url: str | None + thumbnail_url: str | None + title_screen_url: str | None + video_url: str | None + + # Resources stored in filesystem + box3d_path: str | None + miximage_path: str | None + physical_path: str | None + video_path: str | None class GamelistMetadata(GamelistMetadataMedia): @@ -49,7 +63,9 @@ class GamelistRom(BaseRom): gamelist_metadata: NotRequired[GamelistMetadata] -def extract_media_from_gamelist_rom(game: Element) -> GamelistMetadataMedia: +def extract_media_from_gamelist_rom(rom: Rom, game: Element) -> GamelistMetadataMedia: + preferred_media_types = get_preferred_media_types() + image_elem = game.find("image") video_elem = game.find("video") box3d_elem = game.find("box3d") @@ -65,75 +81,103 @@ def extract_media_from_gamelist_rom(game: Element) -> GamelistMetadataMedia: thumbnail_elem = game.find("thumbnail") return GamelistMetadataMedia( - image=( + image_url=( image_elem.text.replace("./", "") if image_elem is not None and image_elem.text else None ), - video=( - video_elem.text.replace("./", "") - if video_elem is not None and video_elem.text - else None - ), - box2d=( + box2d_url=( box2d_elem.text.replace("./", "") if box2d_elem is not None and box2d_elem.text else None ), - box2d_back=( + box2d_back_url=( box2d_back_elem.text.replace("./", "") if box2d_back_elem is not None and box2d_back_elem.text else None ), - box3d=( + box3d_url=( box3d_elem.text.replace("./", "") if box3d_elem is not None and box3d_elem.text else None ), - fanart=( + box3d_path=( + f"{fs_resource_handler.get_media_resources_path(rom.platform_id, rom.id, MetadataMediaType.BOX3D)}/box3d.png" + if box3d_elem is not None + and box3d_elem.text + and MetadataMediaType.BOX3D in preferred_media_types + else None + ), + fanart_url=( fanart_elem.text.replace("./", "") if fanart_elem is not None and fanart_elem.text else None ), - manual=( + manual_url=( manual_elem.text.replace("./", "") if manual_elem is not None and manual_elem.text else None ), - marquee=( + marquee_url=( marquee_elem.text.replace("./", "") if marquee_elem is not None and marquee_elem.text else None ), - miximage=( + miximage_url=( miximage_elem.text.replace("./", "") if miximage_elem is not None and miximage_elem.text else None ), - physical=( + miximage_path=( + f"{fs_resource_handler.get_media_resources_path(rom.platform_id, rom.id, MetadataMediaType.MIXIMAGE)}/miximage.png" + if miximage_elem is not None + and miximage_elem.text + and MetadataMediaType.MIXIMAGE in preferred_media_types + else None + ), + physical_url=( physical_elem.text.replace("./", "") if physical_elem is not None and physical_elem.text else None ), - screenshot=( + physical_path=( + f"{fs_resource_handler.get_media_resources_path(rom.platform_id, rom.id, MetadataMediaType.PHYSICAL)}/physical.png" + if physical_elem is not None + and physical_elem.text + and MetadataMediaType.PHYSICAL in preferred_media_types + else None + ), + screenshot_url=( screenshot_elem.text.replace("./", "") if screenshot_elem is not None and screenshot_elem.text else None ), - title_screen=( + title_screen_url=( title_screen_elem.text.replace("./", "") if title_screen_elem is not None and title_screen_elem.text else None ), - thumbnail=( + thumbnail_url=( thumbnail_elem.text.replace("./", "") if thumbnail_elem is not None and thumbnail_elem.text else None ), + video_url=( + video_elem.text.replace("./", "") + if video_elem is not None and video_elem.text + else None + ), + video_path=( + f"{fs_resource_handler.get_media_resources_path(rom.platform_id, rom.id, MetadataMediaType.VIDEO)}/video.mp4" + if video_elem is not None + and video_elem.text + and MetadataMediaType.VIDEO in preferred_media_types + else None + ), ) -def extract_metadata_from_gamelist_rom(game: Element) -> GamelistMetadata: +def extract_metadata_from_gamelist_rom(rom: Rom, game: Element) -> GamelistMetadata: rating_elem = game.find("rating") releasedate_elem = game.find("releasedate") developer_elem = game.find("developer") @@ -178,7 +222,7 @@ def extract_metadata_from_gamelist_rom(game: Element) -> GamelistMetadata: genres=pydash.compact([genre]), player_count=players, md5_hash=md5, - **extract_media_from_gamelist_rom(game), + **extract_media_from_gamelist_rom(rom, game), ) @@ -204,9 +248,10 @@ class GamelistHandler(MetadataHandler): return None def _parse_gamelist_xml( - self, gamelist_path: Path, platform: Platform + self, gamelist_path: Path, platform: Platform, rom: Rom ) -> dict[str, GamelistRom]: """Parse a gamelist.xml file and return ROM data indexed by filename""" + preferred_media_types = get_preferred_media_types() roms_data: dict[str, GamelistRom] = {} try: @@ -250,7 +295,7 @@ class GamelistHandler(MetadataHandler): ) # Build ROM data - rom_metadata = extract_metadata_from_gamelist_rom(game) + rom_metadata = extract_metadata_from_gamelist_rom(rom, game) rom_data = GamelistRom( gamelist_id=str(uuid.uuid4()), name=name, @@ -265,7 +310,7 @@ class GamelistHandler(MetadataHandler): ) # Choose which cover style to use - cover_path = rom_metadata["box2d"] + cover_path = rom_metadata["box2d_url"] if cover_path: cover_path_path = fs_platform_handler.validate_path( f"{platform_dir}/{cover_path}" @@ -273,38 +318,43 @@ class GamelistHandler(MetadataHandler): rom_data["url_cover"] = f"file://{str(cover_path_path)}" # Grab the manual - if rom_metadata["manual"]: + if ( + rom_metadata["manual_url"] + and MetadataMediaType.MANUAL in preferred_media_types + ): manual_path = fs_platform_handler.validate_path( - f"{platform_dir}/{rom_metadata['manual']}" + f"{platform_dir}/{rom_metadata['manual_url']}" ) rom_data["url_manual"] = f"file://{str(manual_path)}" # Build list of screenshot URLs url_screenshots = [] - if rom_metadata["screenshot"]: + if ( + rom_metadata["screenshot_url"] + and MetadataMediaType.SCREENSHOT in preferred_media_types + ): screenshot_path = fs_platform_handler.validate_path( - f"{platform_dir}/{rom_metadata['screenshot']}" + f"{platform_dir}/{rom_metadata['screenshot_url']}" ) url_screenshots.append(f"file://{str(screenshot_path)}") - if rom_metadata["title_screen"]: + if ( + rom_metadata["title_screen_url"] + and MetadataMediaType.TITLE_SCREEN in preferred_media_types + ): title_screen_path = fs_platform_handler.validate_path( - f"{platform_dir}/{rom_metadata['title_screen']}" + f"{platform_dir}/{rom_metadata['title_screen_url']}" ) url_screenshots.append(f"file://{str(title_screen_path)}") - if rom_metadata["miximage"]: + if ( + rom_metadata["miximage_url"] + and MetadataMediaType.MIXIMAGE in preferred_media_types + ): miximage_path = fs_platform_handler.validate_path( - f"{platform_dir}/{rom_metadata['miximage']}" + f"{platform_dir}/{rom_metadata['miximage_url']}" ) url_screenshots.append(f"file://{str(miximage_path)}") rom_data["url_screenshots"] = url_screenshots - # TODO: Add support for importing videos - # if rom_metadata["video"]: - # video_path = fs_platform_handler.validate_path( - # f"{platform_dir}/{rom_metadata['video']}" - # ) - # rom_data["url_video"] = f"file://{str(video_path)}") - # Store by filename for matching roms_data[rom_filename] = rom_data except ET.ParseError as e: @@ -314,7 +364,7 @@ class GamelistHandler(MetadataHandler): return roms_data - async def get_rom(self, fs_name: str, platform: Platform) -> GamelistRom: + async def get_rom(self, fs_name: str, platform: Platform, rom: Rom) -> GamelistRom: """Get ROM metadata from gamelist.xml files""" if not self.is_enabled(): return GamelistRom(gamelist_id=None) @@ -325,7 +375,7 @@ class GamelistHandler(MetadataHandler): return GamelistRom(gamelist_id=None) # Parse the gamelist file - all_roms_data = self._parse_gamelist_xml(gamelist_file_path, platform) + all_roms_data = self._parse_gamelist_xml(gamelist_file_path, platform, rom) # Try to find exact match first if fs_name in all_roms_data: diff --git a/backend/handler/metadata/ss_handler.py b/backend/handler/metadata/ss_handler.py index b6685b408..8f5c6da82 100644 --- a/backend/handler/metadata/ss_handler.py +++ b/backend/handler/metadata/ss_handler.py @@ -11,7 +11,9 @@ from adapters.services.screenscraper import ScreenScraperService from adapters.services.screenscraper_types import SSGame, SSGameDate from config import SCREENSCRAPER_PASSWORD, SCREENSCRAPER_USER from config.config_manager import config_manager as cm +from handler.filesystem import fs_resource_handler from logger.logger import log +from models.rom import Rom from .base_handler import ( PS2_OPL_REGEX, @@ -20,6 +22,7 @@ from .base_handler import ( SWITCH_TITLEDB_REGEX, BaseRom, MetadataHandler, + MetadataMediaType, ) from .base_handler import UniversalPlatformSlug as UPS @@ -41,6 +44,12 @@ def get_preferred_languages() -> list[str]: return list(dict.fromkeys(config.SCAN_LANGUAGE_PRIORITY + ["en", "fr"])) +def get_preferred_media_types() -> list[MetadataMediaType]: + """Get preferred media types from config""" + config = cm.get_config() + return [MetadataMediaType(media) for media in config.SCAN_MEDIA] + + PS1_SS_ID: Final = 57 PS2_SS_ID: Final = 58 PSP_SS_ID: Final = 61 @@ -133,23 +142,30 @@ class SSAgeRating(TypedDict): class SSMetadataMedia(TypedDict): - bezel: str | None # bezel-16-9 - box2d: str | None # box-2D - box2d_side: str | None # box-2D-side - box2d_back: str | None # box-2D-back - box3d: str | None # box-3D - fanart: str | None # fanart - fullbox: str | None # box-texture - logo: str | None # wheel-hd - manual: str | None # manual - marquee: str | None # screenmarquee - miximage: str | None # mixrbv1 | mixrbv2 - physical: str | None # support-2D - screenshot: str | None # ss - steamgrid: str | None # steamgrid - title_screen: str | None # sstitle - video: str | None # video - video_normalized: str | None # video-normalized + bezel_url: str | None # bezel-16-9 + box2d_url: str | None # box-2D + box2d_side_url: str | None # box-2D-side + box2d_back_url: str | None # box-2D-back + box3d_url: str | None # box-3D + fanart_url: str | None # fanart + fullbox_url: str | None # box-texture + logo_url: str | None # wheel-hd + manual_url: str | None # manual + marquee_url: str | None # screenmarquee + miximage_url: str | None # mixrbv1 | mixrbv2 + physical_url: str | None # support-2D + screenshot_url: str | None # ss + steamgrid_url: str | None # steamgrid + title_screen_url: str | None # sstitle + video_url: str | None # video + video_normalized_url: str | None # video-normalized + + # Resources stored in filesystem + bezel_path: str | None + box3d_path: str | None + miximage_path: str | None + physical_path: str | None + video_path: str | None class SSMetadata(SSMetadataMedia): @@ -167,25 +183,32 @@ class SSRom(BaseRom): ss_metadata: NotRequired[SSMetadata] -def extract_media_from_ss_rom(game: SSGame) -> SSMetadataMedia: +def extract_media_from_ss_game(rom: Rom, game: SSGame) -> SSMetadataMedia: + preferred_media_types = get_preferred_media_types() + ss_media = SSMetadataMedia( - bezel=None, - box2d=None, - box2d_back=None, - box2d_side=None, - box3d=None, - fanart=None, - fullbox=None, - logo=None, - manual=None, - marquee=None, - miximage=None, - physical=None, - screenshot=None, - steamgrid=None, - title_screen=None, - video=None, - video_normalized=None, + bezel_url=None, + box2d_url=None, + box2d_back_url=None, + box2d_side_url=None, + box3d_url=None, + fanart_url=None, + fullbox_url=None, + logo_url=None, + manual_url=None, + marquee_url=None, + miximage_url=None, + physical_url=None, + screenshot_url=None, + steamgrid_url=None, + title_screen_url=None, + video_url=None, + video_normalized_url=None, + bezel_path=None, + box3d_path=None, + miximage_path=None, + physical_path=None, + video_path=None, ) for region in get_preferred_regions(): @@ -193,53 +216,78 @@ def extract_media_from_ss_rom(game: SSGame) -> SSMetadataMedia: if not media.get("region") == region or media.get("parent") != "jeu": continue - if media.get("type") == "box-2D-back" and not ss_media["box2d_back"]: - ss_media["box2d_back"] = media["url"] - elif media.get("type") == "bezel-16-9" and not ss_media["bezel"]: - ss_media["bezel"] = media["url"] - elif media.get("type") == "box-2D" and not ss_media["box2d"]: - ss_media["box2d"] = media["url"] - elif media.get("type") == "fanart" and not ss_media["fanart"]: - ss_media["fanart"] = media["url"] - elif media.get("type") == "box-texture" and not ss_media["fullbox"]: - ss_media["fullbox"] = media["url"] - elif media.get("type") == "wheel-hd" and not ss_media["logo"]: - ss_media["logo"] = media["url"] - elif media.get("type") == "manual" and not ss_media["manual"]: - ss_media["manual"] = media["url"] - elif media.get("type") == "screenmarquee" and not ss_media["marquee"]: - ss_media["marquee"] = media["url"] + if media.get("type") == "box-2D-back" and not ss_media["box2d_back_url"]: + ss_media["box2d_back_url"] = media["url"] + elif media.get("type") == "bezel-16-9" and not ss_media["bezel_url"]: + ss_media["bezel_url"] = media["url"] + if MetadataMediaType.BEZEL in preferred_media_types: + ss_media["bezel_path"] = ( + f"{fs_resource_handler.get_media_resources_path(rom.platform_id, rom.id, MetadataMediaType.BEZEL)}/bezel.png" + ) + elif media.get("type") == "box-2D" and not ss_media["box2d_url"]: + ss_media["box2d_url"] = media["url"] + elif media.get("type") == "fanart" and not ss_media["fanart_url"]: + ss_media["fanart_url"] = media["url"] + elif media.get("type") == "box-texture" and not ss_media["fullbox_url"]: + ss_media["fullbox_url"] = media["url"] + elif media.get("type") == "wheel-hd" and not ss_media["logo_url"]: + ss_media["logo_url"] = media["url"] + elif media.get("type") == "manual" and not ss_media["manual_url"]: + ss_media["manual_url"] = media["url"] + elif media.get("type") == "screenmarquee" and not ss_media["marquee_url"]: + ss_media["marquee_url"] = media["url"] elif ( media.get("type") == "miximage1" or media.get("type") == "miximage2" or media.get("type") == "mixrbv1" or media.get("type") == "mixrbv2" - ) and not ss_media["miximage"]: - ss_media["miximage"] = media["url"] - elif media.get("type") == "support-2D" and not ss_media["physical"]: - ss_media["physical"] = media["url"] - elif media.get("type") == "ss" and not ss_media["screenshot"]: - ss_media["screenshot"] = media["url"] - elif media.get("type") == "box-2D-side" and not ss_media["box2d_side"]: - ss_media["box2d_side"] = media["url"] - elif media.get("type") == "steamgrid" and not ss_media["steamgrid"]: - ss_media["steamgrid"] = media["url"] - elif media.get("type") == "box-3D" and not ss_media["box3d"]: - ss_media["box3d"] = media["url"] - elif media.get("type") == "sstitle" and not ss_media["title_screen"]: - ss_media["title_screen"] = media["url"] - elif media.get("type") == "video" and not ss_media["video"]: - ss_media["video"] = media["url"] + ) and not ss_media["miximage_url"]: + ss_media["miximage_url"] = media["url"] + if MetadataMediaType.MIXIMAGE in preferred_media_types: + ss_media["miximage_path"] = ( + f"{fs_resource_handler.get_media_resources_path(rom.platform_id, rom.id, MetadataMediaType.MIXIMAGE)}/miximage.png" + ) + elif media.get("type") == "support-2D" and not ss_media["physical_url"]: + ss_media["physical_url"] = media["url"] + if MetadataMediaType.PHYSICAL in preferred_media_types: + ss_media["physical_path"] = ( + f"{fs_resource_handler.get_media_resources_path(rom.platform_id, rom.id, MetadataMediaType.PHYSICAL)}/physical.png" + ) + elif ( + media.get("type") == "ss" + and not ss_media["screenshot_url"] + and MetadataMediaType.SCREENSHOT in preferred_media_types + ): + ss_media["screenshot_url"] = media["url"] + ss_media["screenshot_url"] = media["url"] + elif media.get("type") == "box-2D-side" and not ss_media["box2d_side_url"]: + ss_media["box2d_side_url"] = media["url"] + elif media.get("type") == "steamgrid" and not ss_media["steamgrid_url"]: + ss_media["steamgrid_url"] = media["url"] + elif media.get("type") == "box-3D" and not ss_media["box3d_url"]: + ss_media["box3d_url"] = media["url"] + if MetadataMediaType.BOX3D in preferred_media_types: + ss_media["box3d_path"] = ( + f"{fs_resource_handler.get_media_resources_path(rom.platform_id, rom.id, MetadataMediaType.BOX3D)}/box3d.png" + ) + elif media.get("type") == "sstitle" and not ss_media["title_screen_url"]: + ss_media["title_screen_url"] = media["url"] + elif media.get("type") == "video" and not ss_media["video_url"]: + ss_media["video_url"] = media["url"] + if MetadataMediaType.VIDEO in preferred_media_types: + ss_media["video_path"] = ( + f"{fs_resource_handler.get_media_resources_path(rom.platform_id, rom.id, MetadataMediaType.VIDEO)}/video.mp4" + ) elif ( media.get("type") == "video-normalized" - and not ss_media["video_normalized"] + and not ss_media["video_normalized_url"] ): - ss_media["video_normalized"] = media["url"] + ss_media["video_normalized_url"] = media["url"] return ss_media -def extract_metadata_from_ss_rom(rom: SSGame) -> SSMetadata: +def extract_metadata_from_ss_rom(rom: Rom, game: SSGame) -> SSMetadata: preferred_languages = get_preferred_languages() def _normalize_score(score: str) -> str: @@ -262,19 +310,19 @@ def extract_metadata_from_ss_rom(rom: SSGame) -> SSMetadata: except ValueError: return None - def _get_genres(rom: SSGame) -> list[str]: + def _get_genres(game: SSGame) -> list[str]: return [ genre_name["text"] - for genre in rom.get("genres", []) + for genre in game.get("genres", []) for genre_name in genre.get("noms", []) if genre_name.get("langue") == "en" ] - def _get_franchises(rom: SSGame) -> list[str]: + def _get_franchises(game: SSGame) -> list[str]: for lang in preferred_languages: franchises = [ franchise_name["text"] - for franchise in rom.get("familles", []) + for franchise in game.get("familles", []) for franchise_name in franchise.get("noms", []) if franchise_name.get("langue") == lang ] @@ -282,11 +330,11 @@ def extract_metadata_from_ss_rom(rom: SSGame) -> SSMetadata: return franchises return [] - def _get_game_modes(rom: SSGame) -> list[str]: + def _get_game_modes(game: SSGame) -> list[str]: for lang in preferred_languages: modes = [ mode_name["text"] - for mode in rom.get("modes", []) + for mode in game.get("modes", []) for mode_name in mode.get("noms", []) if mode_name.get("langue") == lang ] @@ -296,25 +344,26 @@ def extract_metadata_from_ss_rom(rom: SSGame) -> SSMetadata: return SSMetadata( { - "ss_score": _normalize_score(rom.get("note", {}).get("text", "")), - "alternative_names": [name["text"] for name in rom.get("noms", [])], + "ss_score": _normalize_score(game.get("note", {}).get("text", "")), + "alternative_names": [name["text"] for name in game.get("noms", [])], "companies": pydash.compact( [ - rom.get("editeur", {}).get("text"), - rom.get("developpeur", {}).get("text"), + game.get("editeur", {}).get("text"), + game.get("developpeur", {}).get("text"), ] ), - "genres": _get_genres(rom), - "first_release_date": _get_lowest_date(rom.get("dates", [])), - "franchises": _get_franchises(rom), - "game_modes": _get_game_modes(rom), - **extract_media_from_ss_rom(rom), + "genres": _get_genres(game), + "first_release_date": _get_lowest_date(game.get("dates", [])), + "franchises": _get_franchises(game), + "game_modes": _get_game_modes(game), + **extract_media_from_ss_game(rom, game), } ) -def build_ss_rom(game: SSGame) -> SSRom: - ss_metadata = extract_metadata_from_ss_rom(game) +def build_ss_game(rom: Rom, game: SSGame) -> SSRom: + ss_metadata = extract_metadata_from_ss_rom(rom, game) + preferred_media_types = get_preferred_media_types() res_name = "" for region in get_preferred_regions(): @@ -342,18 +391,39 @@ def build_ss_rom(game: SSGame) -> SSRom: if res_summary: break - url_cover = ss_metadata["box2d"] - url_manual = ss_metadata["manual"] + url_cover = ss_metadata["box2d_url"] + url_manual = ( + ss_metadata["manual_url"] + if MetadataMediaType.MANUAL in preferred_media_types + else None + ) url_screenshots = pydash.compact( [ - ss_metadata["screenshot"], - ss_metadata["title_screen"], - ss_metadata["miximage"], + ( + ss_metadata["screenshot_url"] + if MetadataMediaType.SCREENSHOT in preferred_media_types + else None + ), + ( + ss_metadata["title_screen_url"] + if MetadataMediaType.TITLE_SCREEN in preferred_media_types + else None + ), + ( + ss_metadata["marquee_url"] + if MetadataMediaType.MARQUEE in preferred_media_types + else None + ), + ( + ss_metadata["fanart_url"] + if MetadataMediaType.FANART in preferred_media_types + else None + ), ] ) ss_id = int(game["id"]) if game.get("id") is not None else None - rom: SSRom = { + game_rom: SSRom = { "ss_id": ss_id, "name": res_name.replace(" : ", ": "), # Normalize colons "summary": res_summary, @@ -363,7 +433,7 @@ def build_ss_rom(game: SSGame) -> SSRom: "ss_metadata": ss_metadata, } - return SSRom({k: v for k, v in rom.items() if v}) # type: ignore[misc] + return SSRom({k: v for k, v in game_rom.items() if v}) # type: ignore[misc] class SSHandler(MetadataHandler): @@ -438,7 +508,7 @@ class SSHandler(MetadataHandler): name=platform["name"], ) - async def get_rom(self, file_name: str, platform_ss_id: int) -> SSRom: + async def get_rom(self, rom: Rom, file_name: str, platform_ss_id: int) -> SSRom: from handler.filesystem import fs_rom_handler if not self.is_enabled(): @@ -451,7 +521,7 @@ class SSHandler(MetadataHandler): ss_id_from_tag = self.extract_ss_id_from_filename(file_name) if ss_id_from_tag: log.debug(f"Found ScreenScraper ID tag in filename: {ss_id_from_tag}") - rom_by_id = await self.get_rom_by_id(ss_id_from_tag) + rom_by_id = await self.get_rom_by_id(rom, ss_id_from_tag) if rom_by_id["ss_id"]: log.debug( f"Successfully matched ROM by ScreenScraper ID tag: {file_name} -> {ss_id_from_tag}" @@ -541,9 +611,9 @@ class SSHandler(MetadataHandler): if not res or not res.get("id"): return fallback_rom - return build_ss_rom(res) + return build_ss_game(rom, res) - async def get_rom_by_id(self, ss_id: int) -> SSRom: + async def get_rom_by_id(self, rom: Rom, ss_id: int) -> SSRom: if not self.is_enabled(): return SSRom(ss_id=None) @@ -551,17 +621,17 @@ class SSHandler(MetadataHandler): if not res: return SSRom(ss_id=None) - return build_ss_rom(res) + return build_ss_game(rom, res) - async def get_matched_rom_by_id(self, ss_id: int) -> SSRom | None: + async def get_matched_rom_by_id(self, rom: Rom, ss_id: int) -> SSRom | None: if not self.is_enabled(): return None - rom = await self.get_rom_by_id(ss_id) - return rom if rom.get("ss_id", "") else None + game_rom = await self.get_rom_by_id(rom, ss_id) + return game_rom if game_rom.get("ss_id", "") else None async def get_matched_roms_by_name( - self, search_term: str, platform_ss_id: int | None + self, rom: Rom, search_term: str, platform_ss_id: int | None ) -> list[SSRom]: if not self.is_enabled(): return [] @@ -569,18 +639,18 @@ class SSHandler(MetadataHandler): if not platform_ss_id: return [] - matched_roms = await self.ss_service.search_games( + matched_games = await self.ss_service.search_games( term=quote(uc(search_term), safe="/ "), system_id=platform_ss_id, ) - def _is_ss_region(rom: SSGame) -> bool: - return any(name.get("region") == "ss" for name in rom.get("noms", [])) + def _is_ss_region(game: SSGame) -> bool: + return any(name.get("region") == "ss" for name in game.get("noms", [])) return [ - build_ss_rom(rom) - for rom in matched_roms - if _is_ss_region(rom) and rom.get("id") + build_ss_game(rom, game) + for game in matched_games + if _is_ss_region(game) and game.get("id") ] diff --git a/backend/handler/scan_handler.py b/backend/handler/scan_handler.py index 5283f1e91..91e4befd6 100644 --- a/backend/handler/scan_handler.py +++ b/backend/handler/scan_handler.py @@ -470,7 +470,9 @@ async def scan_rom( or (scan_type == ScanType.UPDATE and rom.gamelist_id) or (scan_type == ScanType.UNMATCHED and not rom.gamelist_id) ): - return await meta_gamelist_handler.get_rom(rom_attrs["fs_name"], platform) + return await meta_gamelist_handler.get_rom( + rom_attrs["fs_name"], platform, rom + ) return GamelistRom(gamelist_id=None) @@ -553,10 +555,10 @@ async def scan_rom( ) ): if scan_type == ScanType.UPDATE and rom.ss_id: - return await meta_ss_handler.get_rom_by_id(rom.ss_id) + return await meta_ss_handler.get_rom_by_id(rom, rom.ss_id) else: return await meta_ss_handler.get_rom( - rom_attrs["fs_name"], platform_ss_id=platform.ss_id + rom, rom_attrs["fs_name"], platform_ss_id=platform.ss_id ) return SSRom(ss_id=None) diff --git a/backend/tests/config/fixtures/config/config.yml b/backend/tests/config/fixtures/config/config.yml index c2cea669e..7026b6822 100644 --- a/backend/tests/config/fixtures/config/config.yml +++ b/backend/tests/config/fixtures/config/config.yml @@ -45,6 +45,11 @@ scan: language: - jp - es + media: + - box2d + - box3d + - physical + - miximage emulatorjs: debug: true diff --git a/examples/config.example.yml b/examples/config.example.yml index 272dd4f52..16138898e 100644 --- a/examples/config.example.yml +++ b/examples/config.example.yml @@ -47,10 +47,10 @@ system: # The folder name where your roms are located filesystem: {} # { roms_folder: 'roms' } For example if your folder structure is /home/user/library/roms_folder -# Metadata priority during scans -# Below are the default priority values used # scan: +# # Metadata priority during scans # priority: +# # Below are the default priority values used # metadata: # Top-level metadata source priority # - "igdb" # IGDB (highest priority) # - "moby" # MobyGames @@ -80,6 +80,25 @@ filesystem: {} # { roms_folder: 'roms' } For example if your folder structure is # language: # Cover art and game title (only used by Screenscraper) # - "en" # - "fr" +# # Media assets to download +# # Only used by Screenscraper and ES-DE gamelist.xml +# media: +# # Used as alternative cover art +# - box2d # Normal cover art (enabled by default) +# - box3d # 3D box art +# - miximage # Mixed image of multiple media +# - physical # Disc, cartridge, etc. +# # Added to the screenshots carousel +# - screenshot # Screenshot (enabled by default) +# - title_screen # Title screen +# - marquee # Transparent logo +# - fanart # User uploaded artwork +# # Bezel displayed around the emulatorjs window +# - bezel +# # Manual in PDF format +# - manual (enabled by default) +# # Gameplay video +# - video # Video (warning: large file size) # EmulatorJS per-core options # emulatorjs: diff --git a/frontend/src/__generated__/models/RomFileCategory.ts b/frontend/src/__generated__/models/RomFileCategory.ts index 9df2ae05a..d132f180d 100644 --- a/frontend/src/__generated__/models/RomFileCategory.ts +++ b/frontend/src/__generated__/models/RomFileCategory.ts @@ -2,4 +2,4 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -export type RomFileCategory = 'dlc' | 'hack' | 'manual' | 'patch' | 'update' | 'mod' | 'demo' | 'translation' | 'prototype'; +export type RomFileCategory = 'game' | 'dlc' | 'hack' | 'manual' | 'patch' | 'update' | 'mod' | 'demo' | 'translation' | 'prototype'; diff --git a/frontend/src/__generated__/models/RomGamelistMetadata.ts b/frontend/src/__generated__/models/RomGamelistMetadata.ts index 9c567a1d6..6a4be2d60 100644 --- a/frontend/src/__generated__/models/RomGamelistMetadata.ts +++ b/frontend/src/__generated__/models/RomGamelistMetadata.ts @@ -3,19 +3,23 @@ /* tslint:disable */ /* eslint-disable */ export type RomGamelistMetadata = { - box2d?: (string | null); - box2d_back?: (string | null); - box3d?: (string | null); - fanart?: (string | null); - image?: (string | null); - manual?: (string | null); - marquee?: (string | null); - miximage?: (string | null); - physical?: (string | null); - screenshot?: (string | null); - thumbnail?: (string | null); - title_screen?: (string | null); - video?: (string | null); + box2d_url?: (string | null); + box2d_back_url?: (string | null); + box3d_url?: (string | null); + fanart_url?: (string | null); + image_url?: (string | null); + manual_url?: (string | null); + marquee_url?: (string | null); + miximage_url?: (string | null); + physical_url?: (string | null); + screenshot_url?: (string | null); + thumbnail_url?: (string | null); + title_screen_url?: (string | null); + video_url?: (string | null); + box3d_path?: (string | null); + miximage_path?: (string | null); + physical_path?: (string | null); + video_path?: (string | null); rating?: (number | null); first_release_date?: (string | null); companies?: (Array | null); diff --git a/frontend/src/__generated__/models/RomSSMetadata.ts b/frontend/src/__generated__/models/RomSSMetadata.ts index 0e3b2a11c..069b3aa70 100644 --- a/frontend/src/__generated__/models/RomSSMetadata.ts +++ b/frontend/src/__generated__/models/RomSSMetadata.ts @@ -3,23 +3,28 @@ /* tslint:disable */ /* eslint-disable */ export type RomSSMetadata = { - bezel?: (string | null); - box2d?: (string | null); - box2d_side?: (string | null); - box2d_back?: (string | null); - box3d?: (string | null); - fanart?: (string | null); - fullbox?: (string | null); - logo?: (string | null); - manual?: (string | null); - marquee?: (string | null); - miximage?: (string | null); - physical?: (string | null); - screenshot?: (string | null); - steamgrid?: (string | null); - title_screen?: (string | null); - video?: (string | null); - video_normalized?: (string | null); + bezel_url?: (string | null); + box2d_url?: (string | null); + box2d_side_url?: (string | null); + box2d_back_url?: (string | null); + box3d_url?: (string | null); + fanart_url?: (string | null); + fullbox_url?: (string | null); + logo_url?: (string | null); + manual_url?: (string | null); + marquee_url?: (string | null); + miximage_url?: (string | null); + physical_url?: (string | null); + screenshot_url?: (string | null); + steamgrid_url?: (string | null); + title_screen_url?: (string | null); + video_url?: (string | null); + video_normalized_url?: (string | null); + bezel_path?: (string | null); + box3d_path?: (string | null); + miximage_path?: (string | null); + physical_path?: (string | null); + video_path?: (string | null); ss_score?: string; first_release_date?: (number | null); alternative_names?: Array; diff --git a/frontend/src/components/Settings/UserInterface/Interface.vue b/frontend/src/components/Settings/UserInterface/Interface.vue index 8fb9a214b..48601e522 100644 --- a/frontend/src/components/Settings/UserInterface/Interface.vue +++ b/frontend/src/components/Settings/UserInterface/Interface.vue @@ -52,7 +52,15 @@ const enableExperimentalCacheRef = useLocalStorage( ); // Boxart -const boxartStyleRef = useLocalStorage("settings.boxartStyle", "box2d"); +export type BoxartStyleOption = + | "cover" + | "box3d_path" + | "physical_path" + | "miximage_path"; +const boxartStyleRef = useLocalStorage( + "settings.boxartStyle", + "cover", +); const homeOptions = computed(() => [ { @@ -185,11 +193,10 @@ const galleryOptions = computed(() => [ ]); const boxartStyleOptions = computed(() => [ - { title: t("settings.boxart-box2d"), value: "box2d" }, - { title: t("settings.boxart-box3d"), value: "box3d" }, - { title: t("settings.boxart-physical"), value: "physical" }, - { title: t("settings.boxart-miximage"), value: "miximage" }, - { title: t("settings.boxart-fanart"), value: "fanart" }, + { title: t("settings.boxart-cover"), value: "cover" }, + { title: t("settings.boxart-box3d"), value: "box3d_path" }, + { title: t("settings.boxart-physical"), value: "physical_path" }, + { title: t("settings.boxart-miximage"), value: "miximage_path" }, ]); const setPlatformDrawerGroupBy = (value: string) => { @@ -211,7 +218,7 @@ const setVirtualCollectionType = async (value: string) => { virtualCollectionTypeRef.value = value; collectionsStore.fetchVirtualCollections(value); }; -const setBoxartStyle = (value: string) => { +const setBoxartStyle = (value: BoxartStyleOption) => { boxartStyleRef.value = value; }; const toggleShowStats = (value: boolean) => { diff --git a/frontend/src/components/common/Game/Card/Base.vue b/frontend/src/components/common/Game/Card/Base.vue index 41a112805..3aa915acc 100644 --- a/frontend/src/components/common/Game/Card/Base.vue +++ b/frontend/src/components/common/Game/Card/Base.vue @@ -12,6 +12,7 @@ import { } from "vue"; import { useDisplay } from "vuetify"; import type { SearchRomSchema } from "@/__generated__"; +import type { BoxartStyleOption } from "@/components/Settings/UserInterface/Interface.vue"; import ActionBar from "@/components/common/Game/Card/ActionBar.vue"; import Flags from "@/components/common/Game/Card/Flags.vue"; import Skeleton from "@/components/common/Game/Card/Skeleton.vue"; @@ -26,6 +27,7 @@ import storePlatforms from "@/stores/platforms"; import storeRoms from "@/stores/roms"; import { type SimpleRom } from "@/stores/roms"; import type { Events } from "@/types/emitter"; +import { FRONTEND_RESOURCES_PATH } from "@/utils"; import { getMissingCoverImage, getUnmatchedCoverImage, @@ -124,9 +126,10 @@ const activeMenu = ref(false); const showActionBarAlways = useLocalStorage("settings.showActionBar", false); const showGameTitleAlways = useLocalStorage("settings.showGameTitle", false); const showSiblings = useLocalStorage("settings.showSiblings", true); -const boxartStyle = useLocalStorage< - "box2d" | "box3d" | "physical" | "miximage" | "fanart" ->("settings.boxartStyle", "box2d"); +const boxartStyle = useLocalStorage( + "settings.boxartStyle", + "cover", +); const hasNotes = computed(() => { if (!romsStore.isSimpleRom(props.rom)) return false; @@ -154,7 +157,7 @@ const boxartStyleCover = computed(() => { if ( props.coverSrc || !romsStore.isSimpleRom(props.rom) || - boxartStyle.value === "box2d" + boxartStyle.value === "cover" ) return null; const ssMedia = props.rom.ss_metadata?.[boxartStyle.value]; @@ -164,7 +167,9 @@ const boxartStyleCover = computed(() => { const largeCover = computed(() => { if (props.coverSrc) return props.coverSrc; - if (boxartStyleCover.value) return boxartStyleCover.value; + debugger; + if (boxartStyleCover.value) + return `${FRONTEND_RESOURCES_PATH}/${boxartStyleCover.value}`; if (!romsStore.isSimpleRom(props.rom)) { return ( props.rom.igdb_url_cover || @@ -182,7 +187,8 @@ const largeCover = computed(() => { const smallCover = computed(() => { if (props.coverSrc) return props.coverSrc; - if (boxartStyleCover.value) return boxartStyleCover.value; + if (boxartStyleCover.value) + return `${FRONTEND_RESOURCES_PATH}/${boxartStyleCover.value}`; if (!romsStore.isSimpleRom(props.rom)) return ""; const pathCoverSmall = isWebpEnabled.value ? props.rom.path_cover_small?.replace(EXTENSION_REGEX, ".webp") diff --git a/frontend/src/console/components/GameCard.vue b/frontend/src/console/components/GameCard.vue index c0ee1a575..dd6b05fdb 100644 --- a/frontend/src/console/components/GameCard.vue +++ b/frontend/src/console/components/GameCard.vue @@ -1,6 +1,7 @@