mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 00:27:41 +01:00
Add config to store media on disk
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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": {}})
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -45,6 +45,11 @@ scan:
|
||||
language:
|
||||
- jp
|
||||
- es
|
||||
media:
|
||||
- box2d
|
||||
- box3d
|
||||
- physical
|
||||
- miximage
|
||||
|
||||
emulatorjs:
|
||||
debug: true
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<string> | null);
|
||||
|
||||
39
frontend/src/__generated__/models/RomSSMetadata.ts
generated
39
frontend/src/__generated__/models/RomSSMetadata.ts
generated
@@ -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<string>;
|
||||
|
||||
@@ -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<BoxartStyleOption>(
|
||||
"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) => {
|
||||
|
||||
@@ -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<BoxartStyleOption>(
|
||||
"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")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import { computed, onMounted, useTemplateRef, watch } from "vue";
|
||||
import type { BoxartStyleOption } from "@/components/Settings/UserInterface/Interface.vue";
|
||||
import Skeleton from "@/components/common/Game/Card/Skeleton.vue";
|
||||
import {
|
||||
continuePlayingElementRegistry,
|
||||
@@ -8,7 +9,8 @@ import {
|
||||
} from "@/console/composables/useElementRegistry";
|
||||
import storeCollections from "@/stores/collections";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
import storeRoms, { type SimpleRom } from "@/stores/roms";
|
||||
import { type SimpleRom } from "@/stores/roms";
|
||||
import { FRONTEND_RESOURCES_PATH } from "@/utils";
|
||||
import {
|
||||
EXTENSION_REGEX,
|
||||
getMissingCoverImage,
|
||||
@@ -25,11 +27,11 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const heartbeatStore = storeHeartbeat();
|
||||
const romsStore = storeRoms();
|
||||
|
||||
const boxartStyle = useLocalStorage<
|
||||
"box2d" | "box3d" | "physical" | "miximage" | "fanart"
|
||||
>("settings.boxartStyle", "box2d");
|
||||
const boxartStyle = useLocalStorage<BoxartStyleOption>(
|
||||
"settings.boxartStyle",
|
||||
"cover",
|
||||
);
|
||||
|
||||
const isWebpEnabled = computed(
|
||||
() => heartbeatStore.value.TASKS?.ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP,
|
||||
@@ -37,14 +39,15 @@ const isWebpEnabled = computed(
|
||||
|
||||
// User selected alternative cover image
|
||||
const boxartStyleCover = computed(() => {
|
||||
if (boxartStyle.value === "box2d") return null;
|
||||
if (boxartStyle.value === "cover") return null;
|
||||
const ssMedia = props.rom.ss_metadata?.[boxartStyle.value];
|
||||
const gamelistMedia = props.rom.gamelist_metadata?.[boxartStyle.value];
|
||||
return ssMedia || gamelistMedia;
|
||||
});
|
||||
|
||||
const largeCover = computed(() => {
|
||||
if (boxartStyleCover.value) return boxartStyleCover.value;
|
||||
if (boxartStyleCover.value)
|
||||
return `${FRONTEND_RESOURCES_PATH}/${boxartStyleCover.value}`;
|
||||
const pathCoverLarge = isWebpEnabled.value
|
||||
? props.rom.path_cover_large?.replace(EXTENSION_REGEX, ".webp")
|
||||
: props.rom.path_cover_large;
|
||||
@@ -52,7 +55,8 @@ const largeCover = computed(() => {
|
||||
});
|
||||
|
||||
const smallCover = computed(() => {
|
||||
if (boxartStyleCover.value) return boxartStyleCover.value;
|
||||
if (boxartStyleCover.value)
|
||||
return `${FRONTEND_RESOURCES_PATH}/${boxartStyleCover.value}`;
|
||||
const pathCoverSmall = isWebpEnabled.value
|
||||
? props.rom.path_cover_small?.replace(EXTENSION_REGEX, ".webp")
|
||||
: props.rom.path_cover_small;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"backlogged": "Rückstand",
|
||||
"boxart-box2d": "2D-Box",
|
||||
"boxart-cover": "2D-Box",
|
||||
"boxart-box3d": "3D-Box",
|
||||
"boxart-desc": "Wählen Sie den Boxart-Stil für Spielkarten",
|
||||
"boxart-fanart": "Fanart",
|
||||
"boxart-miximage": "Mix-Bild",
|
||||
"boxart-physical": "Physisch",
|
||||
"boxart-style": "Boxart-Stil",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"backlogged": "Backlogged",
|
||||
"boxart-box2d": "2D Box",
|
||||
"boxart-cover": "2D Box",
|
||||
"boxart-box3d": "3D Box",
|
||||
"boxart-desc": "Choose the boxart style for game cards",
|
||||
"boxart-fanart": "Fanart",
|
||||
"boxart-miximage": "Mix Image",
|
||||
"boxart-physical": "Physical",
|
||||
"boxart-style": "Boxart style",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"backlogged": "Backlogged",
|
||||
"boxart-box2d": "2D Box",
|
||||
"boxart-cover": "2D Box",
|
||||
"boxart-box3d": "3D Box",
|
||||
"boxart-desc": "Choose the boxart style for game cards",
|
||||
"boxart-fanart": "Fanart",
|
||||
"boxart-miximage": "Mix Image",
|
||||
"boxart-physical": "Physical",
|
||||
"boxart-style": "Boxart style",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"backlogged": "Pendiente",
|
||||
"boxart-box2d": "Caja 2D",
|
||||
"boxart-cover": "Caja 2D",
|
||||
"boxart-box3d": "Caja 3D",
|
||||
"boxart-desc": "Elige el estilo de carátula para las tarjetas de juegos",
|
||||
"boxart-fanart": "Fanart",
|
||||
"boxart-miximage": "Imagen mixta",
|
||||
"boxart-physical": "Físico",
|
||||
"boxart-style": "Estilo de carátula",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"backlogged": "En attente",
|
||||
"boxart-box2d": "Boîte 2D",
|
||||
"boxart-cover": "Boîte 2D",
|
||||
"boxart-box3d": "Boîte 3D",
|
||||
"boxart-desc": "Choisissez le style de jaquette pour les cartes de jeux",
|
||||
"boxart-fanart": "Fanart",
|
||||
"boxart-miximage": "Image mixte",
|
||||
"boxart-physical": "Physique",
|
||||
"boxart-style": "Style de jaquette",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"backlogged": "In sospeso",
|
||||
"boxart-box2d": "Scatola 2D",
|
||||
"boxart-cover": "Scatola 2D",
|
||||
"boxart-box3d": "Scatola 3D",
|
||||
"boxart-desc": "Scegli lo stile della copertina per le carte dei giochi",
|
||||
"boxart-fanart": "Fanart",
|
||||
"boxart-miximage": "Immagine mista",
|
||||
"boxart-physical": "Fisico",
|
||||
"boxart-style": "Stile copertina",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"backlogged": "バックログ",
|
||||
"boxart-box2d": "2Dボックス",
|
||||
"boxart-cover": "2Dボックス",
|
||||
"boxart-box3d": "3Dボックス",
|
||||
"boxart-desc": "ゲームカードのボックスアートスタイルを選択",
|
||||
"boxart-fanart": "ファンアート",
|
||||
"boxart-miximage": "ミックス画像",
|
||||
"boxart-physical": "物理",
|
||||
"boxart-style": "ボックスアートスタイル",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"backlogged": "백로그",
|
||||
"boxart-box2d": "2D 박스",
|
||||
"boxart-cover": "2D 박스",
|
||||
"boxart-box3d": "3D 박스",
|
||||
"boxart-desc": "게임 카드의 박스아트 스타일을 선택하세요",
|
||||
"boxart-fanart": "팬아트",
|
||||
"boxart-miximage": "믹스 이미지",
|
||||
"boxart-physical": "물리적",
|
||||
"boxart-style": "박스아트 스타일",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"backlogged": "Zaległości",
|
||||
"boxart-box2d": "Pudełko 2D",
|
||||
"boxart-cover": "Pudełko 2D",
|
||||
"boxart-box3d": "Pudełko 3D",
|
||||
"boxart-desc": "Wybierz styl okładki dla kart gier",
|
||||
"boxart-fanart": "Fanart",
|
||||
"boxart-miximage": "Obraz mieszany",
|
||||
"boxart-physical": "Fizyczny",
|
||||
"boxart-style": "Styl okładki",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"backlogged": "Pendente",
|
||||
"boxart-box2d": "Caixa 2D",
|
||||
"boxart-cover": "Caixa 2D",
|
||||
"boxart-box3d": "Caixa 3D",
|
||||
"boxart-desc": "Escolha o estilo da capa para os cartões de jogos",
|
||||
"boxart-fanart": "Fanart",
|
||||
"boxart-miximage": "Imagem mista",
|
||||
"boxart-physical": "Físico",
|
||||
"boxart-style": "Estilo da capa",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"backlogged": "În așteptare",
|
||||
"boxart-box2d": "Cutie 2D",
|
||||
"boxart-cover": "Cutie 2D",
|
||||
"boxart-box3d": "Cutie 3D",
|
||||
"boxart-desc": "Alege stilul copertii pentru cardurile de jocuri",
|
||||
"boxart-fanart": "Fanart",
|
||||
"boxart-miximage": "Imagine mixtă",
|
||||
"boxart-physical": "Fizic",
|
||||
"boxart-style": "Stilul copertii",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"backlogged": "В очереди",
|
||||
"boxart-box2d": "2D коробка",
|
||||
"boxart-cover": "2D коробка",
|
||||
"boxart-box3d": "3D коробка",
|
||||
"boxart-desc": "Выберите стиль обложки для карточек игр",
|
||||
"boxart-fanart": "Фанарт",
|
||||
"boxart-miximage": "Смешанное изображение",
|
||||
"boxart-physical": "Физический",
|
||||
"boxart-style": "Стиль обложки",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"backlogged": "待办",
|
||||
"boxart-box2d": "2D盒子",
|
||||
"boxart-cover": "2D盒子",
|
||||
"boxart-box3d": "3D盒子",
|
||||
"boxart-desc": "选择游戏卡片的封面样式",
|
||||
"boxart-fanart": "粉丝艺术",
|
||||
"boxart-miximage": "混合图像",
|
||||
"boxart-physical": "物理",
|
||||
"boxart-style": "封面样式",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"backlogged": "待辦",
|
||||
"boxart-box2d": "2D盒子",
|
||||
"boxart-cover": "2D盒子",
|
||||
"boxart-box3d": "3D盒子",
|
||||
"boxart-desc": "選擇遊戲卡片的封面樣式",
|
||||
"boxart-fanart": "粉絲藝術",
|
||||
"boxart-miximage": "混合圖像",
|
||||
"boxart-physical": "物理",
|
||||
"boxart-style": "封面樣式",
|
||||
|
||||
Reference in New Issue
Block a user