Skip to content
Merged
1 change: 1 addition & 0 deletions backend/config/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class EjsControlsButton(TypedDict):
class MetadataMediaType(enum.StrEnum):
BEZEL = "bezel"
BOX2D = "box2d"
BOX2D_BACK = "box2d_back"
BOX3D = "box3d"
MIXIMAGE = "miximage"
PHYSICAL = "physical"
Expand Down
10 changes: 8 additions & 2 deletions backend/endpoints/gamelist.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ async def export_gamelist(
platform_ids: Annotated[
List[int], Query(description="List of platform IDs to export")
],
local_export: Annotated[
bool, Query(description="Use local paths instead of URLs")
] = False,
) -> Response:
"""Export platforms/ROMs to gamelist.xml format and write to platform directories"""
if not platform_ids:
Expand All @@ -32,12 +35,15 @@ async def export_gamelist(
)

try:
exporter = GamelistExporter()
exporter = GamelistExporter(local_export=local_export)
files_written = []

# Export each platform to its respective directory
for platform_id in platform_ids:
success = await exporter.export_platform_to_file(platform_id, request)
success = await exporter.export_platform_to_file(
platform_id,
request,
)
if success:
files_written.append(f"gamelist_{platform_id}.xml")
else:
Expand Down
20 changes: 19 additions & 1 deletion backend/handler/metadata/ss_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class SSMetadataMedia(TypedDict):
box3d_url: str | None # box-3D
fanart_url: str | None # fanart
fullbox_url: str | None # box-texture
logo_url: str | None # wheel-hd
logo_url: str | None # wheel-hd or wheel
manual_url: str | None # manual
marquee_url: str | None # screenmarquee
miximage_url: str | None # mixrbv1 | mixrbv2
Expand All @@ -168,7 +168,9 @@ class SSMetadataMedia(TypedDict):

# Resources stored in filesystem
bezel_path: str | None
box2d_back_path: str | None
box3d_path: str | None
fanart_path: str | None
miximage_path: str | None
physical_path: str | None
marquee_path: str | None
Expand Down Expand Up @@ -213,7 +215,9 @@ def extract_media_from_ss_game(rom: Rom, game: SSGame) -> SSMetadataMedia:
video_url=None,
video_normalized_url=None,
bezel_path=None,
box2d_back_path=None,
box3d_path=None,
fanart_path=None,
miximage_path=None,
physical_path=None,
marquee_path=None,
Expand All @@ -228,6 +232,10 @@ def extract_media_from_ss_game(rom: Rom, game: SSGame) -> SSMetadataMedia:

if media.get("type") == "box-2D-back" and not ss_media["box2d_back_url"]:
ss_media["box2d_back_url"] = media["url"]
if MetadataMediaType.BOX2D_BACK in preferred_media_types:
ss_media["box2d_back_path"] = (
f"{fs_resource_handler.get_media_resources_path(rom.platform_id, rom.id, MetadataMediaType.BOX2D_BACK)}/box2d_back.png"
)
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:
Expand All @@ -238,11 +246,21 @@ def extract_media_from_ss_game(rom: Rom, game: SSGame) -> SSMetadataMedia:
ss_media["box2d_url"] = media["url"]
elif media.get("type") == "fanart" and not ss_media["fanart_url"]:
ss_media["fanart_url"] = media["url"]
if MetadataMediaType.FANART in preferred_media_types:
ss_media["fanart_path"] = (
f"{fs_resource_handler.get_media_resources_path(rom.platform_id, rom.id, MetadataMediaType.FANART)}/fanart.png"
)
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"]

if MetadataMediaType.LOGO in preferred_media_types:
ss_media["logo_path"] = (
f"{fs_resource_handler.get_media_resources_path(rom.platform_id, rom.id, MetadataMediaType.LOGO)}/logo.png"
)
elif media.get("type") == "wheel" and not ss_media["logo_url"]:
ss_media["logo_url"] = media["url"]
if MetadataMediaType.LOGO in preferred_media_types:
ss_media["logo_path"] = (
f"{fs_resource_handler.get_media_resources_path(rom.platform_id, rom.id, MetadataMediaType.LOGO)}/logo.png"
Expand Down
68 changes: 44 additions & 24 deletions backend/utils/gamelist_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
tostring,
)

from fastapi import Request

from config import FRONTEND_RESOURCES_PATH, YOUTUBE_BASE_URL
from handler.database import db_platform_handler, db_rom_handler
from handler.filesystem import fs_platform_handler
Expand All @@ -16,34 +18,38 @@
class GamelistExporter:
"""Export RomM collections to ES-DE gamelist.xml format"""

def __init__(self, local_export: bool = False):
self.local_export = local_export

def _format_release_date(self, timestamp: int) -> str:
"""Format release date to YYYYMMDDTHHMMSS format"""
return datetime.fromtimestamp(timestamp / 1000).strftime("%Y%m%dT%H%M%S")

def _create_game_element(self, rom: Rom, request=None) -> Element:
def _create_game_element(self, rom: Rom, request: Request) -> Element:
"""Create a <game> element for a ROM"""
game = Element("game")

# Basic game info
if request:
if self.local_export:
SubElement(game, "path").text = f"./{rom.fs_name}"
else:
SubElement(game, "path").text = str(
request.url_for(
"get_rom_content",
id=rom.id,
file_name=rom.fs_name,
)
)
else:
SubElement(game, "path").text = f"./{rom.fs_name}"

SubElement(game, "name").text = rom.name or rom.fs_name

if rom.summary:
SubElement(game, "desc").text = rom.summary

# Media files
if rom.path_cover_large:
SubElement(game, "cover").text = (
f"{FRONTEND_RESOURCES_PATH}/{rom.path_cover_large}"
if rom.path_cover_l:
SubElement(game, "thumbnail").text = (
f"{FRONTEND_RESOURCES_PATH}/{rom.path_cover_l}"
)

if rom.youtube_video_id:
Expand Down Expand Up @@ -91,26 +97,40 @@ def _create_game_element(self, rom: Rom, request=None) -> Element:

# Provider specific metadata
if rom.ss_metadata:
if rom.ss_metadata.get("box3d"):
SubElement(game, "box3d").text = rom.ss_metadata["box3d"]
if rom.ss_metadata.get("box2d_back"):
SubElement(game, "backcover").text = rom.ss_metadata["box2d_back"]
if rom.ss_metadata.get("fanart"):
SubElement(game, "fanart").text = rom.ss_metadata["fanart"]
if rom.ss_metadata.get("marquee"):
SubElement(game, "marquee").text = rom.ss_metadata["marquee"]
if rom.ss_metadata.get("miximage"):
SubElement(game, "miximage").text = rom.ss_metadata["miximage"]
if rom.ss_metadata.get("physical"):
SubElement(game, "physicalmedia").text = rom.ss_metadata["physical"]
if rom.ss_metadata.get("box3d_path"):
SubElement(game, "box3d").text = (
f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata["box3d_path"]}"
)
if rom.ss_metadata.get("box2d_back_path"):
SubElement(game, "boxback").text = (
f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata["box2d_back_path"]}"
)
if rom.ss_metadata.get("fanart_path"):
SubElement(game, "fanart").text = (
f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata["fanart_path"]}"
)
if rom.ss_metadata.get("logo_path"):
SubElement(game, "marquee").text = (
f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata["logo_path"]}"
)
if rom.ss_metadata.get("miximage_path"):
SubElement(game, "miximage").text = (
f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata["miximage_path"]}"
)
if rom.ss_metadata.get("physical_path"):
SubElement(game, "physicalmedia").text = (
f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata["physical_path"]}"
)
if rom.ss_metadata.get("title_screen"):
SubElement(game, "title_screen").text = rom.ss_metadata["title_screen"]
SubElement(game, "title_screen").text = (
f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata["title_screen"]}"
)

if rom.gamelist_metadata:
if rom.gamelist_metadata.get("box3d"):
SubElement(game, "box3d").text = rom.gamelist_metadata["box3d"]
if rom.gamelist_metadata.get("box2d_back"):
SubElement(game, "backcover").text = rom.gamelist_metadata["box2d_back"]
SubElement(game, "boxback").text = rom.gamelist_metadata["box2d_back"]
if rom.gamelist_metadata.get("fanart"):
SubElement(game, "fanart").text = rom.gamelist_metadata["fanart"]
if rom.gamelist_metadata.get("marquee"):
Expand All @@ -135,7 +155,7 @@ def _create_game_element(self, rom: Rom, request=None) -> Element:

return game

def export_platform_to_xml(self, platform_id: int, request=None) -> str:
def export_platform_to_xml(self, platform_id: int, request: Request) -> str:
"""Export a platform's ROMs to gamelist.xml format

Args:
Expand All @@ -154,7 +174,7 @@ def export_platform_to_xml(self, platform_id: int, request=None) -> str:
root = Element("gameList")

for rom in roms:
if rom and not rom.missing_from_fs:
if rom and not rom.missing_from_fs and rom.fs_name != "gamelist.xml":
game_element = self._create_game_element(rom, request=request)
root.append(game_element)

Expand All @@ -168,7 +188,7 @@ def export_platform_to_xml(self, platform_id: int, request=None) -> str:
async def export_platform_to_file(
self,
platform_id: int,
request=None,
request: Request,
) -> bool:
"""Export platform ROMs to gamelist.xml file in the platform's directory

Expand Down
4 changes: 3 additions & 1 deletion examples/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ filesystem: {} # { roms_folder: 'roms' } For example if your folder structure is
# - manual # Manual (enabled by default)
# # Gameplay video
# - video # Video (warning: large file size)
# # Media used for batocera gamelist.xml export
# - box2d_back # Back cover art
# - logo # Transparent logo
# # Other media assets (might be used in the future)
# - marquee # Custom marquee
# - logo # Transparent logo

# EmulatorJS per-core options
# emulatorjs:
Expand Down
Loading