diff --git a/backend/alembic/versions/0070_ss_age_ratings.py b/backend/alembic/versions/0070_ss_age_ratings.py index 6fb0653a9..af4634565 100644 --- a/backend/alembic/versions/0070_ss_age_ratings.py +++ b/backend/alembic/versions/0070_ss_age_ratings.py @@ -1,8 +1,8 @@ -"""Add ScreenScraper age ratings to roms_metadata view +"""Fix roms_metadata view: empty JSON arrays should not win COALESCE over populated ones -Revision ID: 0070_ss_age_ratings -Revises: 0069_sibling_roms_fs_name -Create Date: 2026-03-08 21:00:00.000000 +Revision ID: 0074_fix_empty_json_arrays +Revises: 0073_sibling_roms_metadata_only +Create Date: 2026-03-31 00:00:00.000000 """ @@ -12,8 +12,8 @@ from alembic import op from utils.database import is_postgresql # revision identifiers, used by Alembic. -revision = "0070_ss_age_ratings" -down_revision = "0069_sibling_roms_fs_name" +revision = "0074_fix_empty_json_arrays" +down_revision = "0073_sibling_roms_metadata_only" branch_labels = None depends_on = None @@ -28,23 +28,23 @@ def upgrade(): NOW() AS created_at, NOW() AS updated_at, COALESCE( - (r.manual_metadata -> 'genres'), - (r.igdb_metadata -> 'genres'), - (r.moby_metadata -> 'genres'), - (r.ss_metadata -> 'genres'), - (r.launchbox_metadata -> 'genres'), - (r.ra_metadata -> 'genres'), - (r.flashpoint_metadata -> 'genres'), - (r.gamelist_metadata -> 'genres'), + NULLIF(r.manual_metadata -> 'genres', '[]'::jsonb), + NULLIF(r.igdb_metadata -> 'genres', '[]'::jsonb), + NULLIF(r.moby_metadata -> 'genres', '[]'::jsonb), + NULLIF(r.ss_metadata -> 'genres', '[]'::jsonb), + NULLIF(r.launchbox_metadata -> 'genres', '[]'::jsonb), + NULLIF(r.ra_metadata -> 'genres', '[]'::jsonb), + NULLIF(r.flashpoint_metadata -> 'genres', '[]'::jsonb), + NULLIF(r.gamelist_metadata -> 'genres', '[]'::jsonb), '[]'::jsonb ) AS genres, COALESCE( - (r.manual_metadata -> 'franchises'), - (r.igdb_metadata -> 'franchises'), - (r.ss_metadata -> 'franchises'), - (r.flashpoint_metadata -> 'franchises'), - (r.gamelist_metadata -> 'franchises'), + NULLIF(r.manual_metadata -> 'franchises', '[]'::jsonb), + NULLIF(r.igdb_metadata -> 'franchises', '[]'::jsonb), + NULLIF(r.ss_metadata -> 'franchises', '[]'::jsonb), + NULLIF(r.flashpoint_metadata -> 'franchises', '[]'::jsonb), + NULLIF(r.gamelist_metadata -> 'franchises', '[]'::jsonb), '[]'::jsonb ) AS franchises, @@ -54,21 +54,21 @@ def upgrade(): ) AS collections, COALESCE( - (r.manual_metadata -> 'companies'), - (r.igdb_metadata -> 'companies'), - (r.ss_metadata -> 'companies'), - (r.ra_metadata -> 'companies'), - (r.launchbox_metadata -> 'companies'), - (r.flashpoint_metadata -> 'companies'), - (r.gamelist_metadata -> 'companies'), + NULLIF(r.manual_metadata -> 'companies', '[]'::jsonb), + NULLIF(r.igdb_metadata -> 'companies', '[]'::jsonb), + NULLIF(r.ss_metadata -> 'companies', '[]'::jsonb), + NULLIF(r.ra_metadata -> 'companies', '[]'::jsonb), + NULLIF(r.launchbox_metadata -> 'companies', '[]'::jsonb), + NULLIF(r.flashpoint_metadata -> 'companies', '[]'::jsonb), + NULLIF(r.gamelist_metadata -> 'companies', '[]'::jsonb), '[]'::jsonb ) AS companies, COALESCE( - (r.manual_metadata -> 'game_modes'), - (r.igdb_metadata -> 'game_modes'), - (r.ss_metadata -> 'game_modes'), - (r.flashpoint_metadata -> 'game_modes'), + NULLIF(r.manual_metadata -> 'game_modes', '[]'::jsonb), + NULLIF(r.igdb_metadata -> 'game_modes', '[]'::jsonb), + NULLIF(r.ss_metadata -> 'game_modes', '[]'::jsonb), + NULLIF(r.flashpoint_metadata -> 'game_modes', '[]'::jsonb), '[]'::jsonb ) AS game_modes, @@ -220,23 +220,23 @@ def upgrade(): NOW() AS created_at, NOW() AS updated_at, COALESCE( - JSON_EXTRACT(r.manual_metadata, '$.genres'), - JSON_EXTRACT(r.igdb_metadata, '$.genres'), - JSON_EXTRACT(r.moby_metadata, '$.genres'), - JSON_EXTRACT(r.ss_metadata, '$.genres'), - JSON_EXTRACT(r.launchbox_metadata, '$.genres'), - JSON_EXTRACT(r.ra_metadata, '$.genres'), - JSON_EXTRACT(r.flashpoint_metadata, '$.genres'), - JSON_EXTRACT(r.gamelist_metadata, '$.genres'), + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.manual_metadata, '$.genres')) > 0 THEN JSON_EXTRACT(r.manual_metadata, '$.genres') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.igdb_metadata, '$.genres')) > 0 THEN JSON_EXTRACT(r.igdb_metadata, '$.genres') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.moby_metadata, '$.genres')) > 0 THEN JSON_EXTRACT(r.moby_metadata, '$.genres') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.ss_metadata, '$.genres')) > 0 THEN JSON_EXTRACT(r.ss_metadata, '$.genres') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.launchbox_metadata, '$.genres')) > 0 THEN JSON_EXTRACT(r.launchbox_metadata, '$.genres') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.ra_metadata, '$.genres')) > 0 THEN JSON_EXTRACT(r.ra_metadata, '$.genres') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.flashpoint_metadata, '$.genres')) > 0 THEN JSON_EXTRACT(r.flashpoint_metadata, '$.genres') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.gamelist_metadata, '$.genres')) > 0 THEN JSON_EXTRACT(r.gamelist_metadata, '$.genres') ELSE NULL END, JSON_ARRAY() ) AS genres, COALESCE( - JSON_EXTRACT(r.manual_metadata, '$.franchises'), - JSON_EXTRACT(r.igdb_metadata, '$.franchises'), - JSON_EXTRACT(r.ss_metadata, '$.franchises'), - JSON_EXTRACT(r.flashpoint_metadata, '$.franchises'), - JSON_EXTRACT(r.gamelist_metadata, '$.franchises'), + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.manual_metadata, '$.franchises')) > 0 THEN JSON_EXTRACT(r.manual_metadata, '$.franchises') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.igdb_metadata, '$.franchises')) > 0 THEN JSON_EXTRACT(r.igdb_metadata, '$.franchises') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.ss_metadata, '$.franchises')) > 0 THEN JSON_EXTRACT(r.ss_metadata, '$.franchises') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.flashpoint_metadata, '$.franchises')) > 0 THEN JSON_EXTRACT(r.flashpoint_metadata, '$.franchises') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.gamelist_metadata, '$.franchises')) > 0 THEN JSON_EXTRACT(r.gamelist_metadata, '$.franchises') ELSE NULL END, JSON_ARRAY() ) AS franchises, @@ -246,21 +246,21 @@ def upgrade(): ) AS collections, COALESCE( - JSON_EXTRACT(r.manual_metadata, '$.companies'), - JSON_EXTRACT(r.igdb_metadata, '$.companies'), - JSON_EXTRACT(r.ss_metadata, '$.companies'), - JSON_EXTRACT(r.ra_metadata, '$.companies'), - JSON_EXTRACT(r.launchbox_metadata, '$.companies'), - JSON_EXTRACT(r.flashpoint_metadata, '$.companies'), - JSON_EXTRACT(r.gamelist_metadata, '$.companies'), + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.manual_metadata, '$.companies')) > 0 THEN JSON_EXTRACT(r.manual_metadata, '$.companies') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.igdb_metadata, '$.companies')) > 0 THEN JSON_EXTRACT(r.igdb_metadata, '$.companies') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.ss_metadata, '$.companies')) > 0 THEN JSON_EXTRACT(r.ss_metadata, '$.companies') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.ra_metadata, '$.companies')) > 0 THEN JSON_EXTRACT(r.ra_metadata, '$.companies') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.launchbox_metadata, '$.companies')) > 0 THEN JSON_EXTRACT(r.launchbox_metadata, '$.companies') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.flashpoint_metadata, '$.companies')) > 0 THEN JSON_EXTRACT(r.flashpoint_metadata, '$.companies') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.gamelist_metadata, '$.companies')) > 0 THEN JSON_EXTRACT(r.gamelist_metadata, '$.companies') ELSE NULL END, JSON_ARRAY() ) AS companies, COALESCE( - JSON_EXTRACT(r.manual_metadata, '$.game_modes'), - JSON_EXTRACT(r.igdb_metadata, '$.game_modes'), - JSON_EXTRACT(r.ss_metadata, '$.game_modes'), - JSON_EXTRACT(r.flashpoint_metadata, '$.game_modes'), + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.manual_metadata, '$.game_modes')) > 0 THEN JSON_EXTRACT(r.manual_metadata, '$.game_modes') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.igdb_metadata, '$.game_modes')) > 0 THEN JSON_EXTRACT(r.igdb_metadata, '$.game_modes') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.ss_metadata, '$.game_modes')) > 0 THEN JSON_EXTRACT(r.ss_metadata, '$.game_modes') ELSE NULL END, + CASE WHEN JSON_LENGTH(JSON_EXTRACT(r.flashpoint_metadata, '$.game_modes')) > 0 THEN JSON_EXTRACT(r.flashpoint_metadata, '$.game_modes') ELSE NULL END, JSON_ARRAY() ) AS game_modes, @@ -270,7 +270,11 @@ def upgrade(): WHEN JSON_CONTAINS_PATH(r.igdb_metadata, 'one', '$.age_ratings') AND JSON_LENGTH(JSON_EXTRACT(r.igdb_metadata, '$.age_ratings')) > 0 THEN - JSON_EXTRACT(r.igdb_metadata, '$.age_ratings[*].rating') + IF( + JSON_TYPE(JSON_EXTRACT(r.igdb_metadata, '$.age_ratings[*].rating')) = 'ARRAY', + JSON_EXTRACT(r.igdb_metadata, '$.age_ratings[*].rating'), + JSON_ARRAY(JSON_UNQUOTE(JSON_EXTRACT(r.igdb_metadata, '$.age_ratings[*].rating'))) + ) ELSE NULL END, @@ -278,7 +282,11 @@ def upgrade(): WHEN JSON_CONTAINS_PATH(r.ss_metadata, 'one', '$.age_ratings') AND JSON_LENGTH(JSON_EXTRACT(r.ss_metadata, '$.age_ratings')) > 0 THEN - JSON_EXTRACT(r.ss_metadata, '$.age_ratings[*].rating') + IF( + JSON_TYPE(JSON_EXTRACT(r.ss_metadata, '$.age_ratings[*].rating')) = 'ARRAY', + JSON_EXTRACT(r.ss_metadata, '$.age_ratings[*].rating'), + JSON_ARRAY(JSON_UNQUOTE(JSON_EXTRACT(r.ss_metadata, '$.age_ratings[*].rating'))) + ) ELSE NULL END, diff --git a/backend/alembic/versions/0073_fix_empty_json_arrays.py b/backend/alembic/versions/0074_fix_empty_json_arrays.py similarity index 96% rename from backend/alembic/versions/0073_fix_empty_json_arrays.py rename to backend/alembic/versions/0074_fix_empty_json_arrays.py index c56b9f13a..af4634565 100644 --- a/backend/alembic/versions/0073_fix_empty_json_arrays.py +++ b/backend/alembic/versions/0074_fix_empty_json_arrays.py @@ -1,7 +1,7 @@ """Fix roms_metadata view: empty JSON arrays should not win COALESCE over populated ones -Revision ID: 0073_fix_empty_json_arrays -Revises: 0072_client_tokens +Revision ID: 0074_fix_empty_json_arrays +Revises: 0073_sibling_roms_metadata_only Create Date: 2026-03-31 00:00:00.000000 """ @@ -12,8 +12,8 @@ from alembic import op from utils.database import is_postgresql # revision identifiers, used by Alembic. -revision = "0073_fix_empty_json_arrays" -down_revision = "0072_client_tokens" +revision = "0074_fix_empty_json_arrays" +down_revision = "0073_sibling_roms_metadata_only" branch_labels = None depends_on = None @@ -270,7 +270,11 @@ def upgrade(): WHEN JSON_CONTAINS_PATH(r.igdb_metadata, 'one', '$.age_ratings') AND JSON_LENGTH(JSON_EXTRACT(r.igdb_metadata, '$.age_ratings')) > 0 THEN - JSON_EXTRACT(r.igdb_metadata, '$.age_ratings[*].rating') + IF( + JSON_TYPE(JSON_EXTRACT(r.igdb_metadata, '$.age_ratings[*].rating')) = 'ARRAY', + JSON_EXTRACT(r.igdb_metadata, '$.age_ratings[*].rating'), + JSON_ARRAY(JSON_UNQUOTE(JSON_EXTRACT(r.igdb_metadata, '$.age_ratings[*].rating'))) + ) ELSE NULL END, @@ -278,7 +282,11 @@ def upgrade(): WHEN JSON_CONTAINS_PATH(r.ss_metadata, 'one', '$.age_ratings') AND JSON_LENGTH(JSON_EXTRACT(r.ss_metadata, '$.age_ratings')) > 0 THEN - JSON_EXTRACT(r.ss_metadata, '$.age_ratings[*].rating') + IF( + JSON_TYPE(JSON_EXTRACT(r.ss_metadata, '$.age_ratings[*].rating')) = 'ARRAY', + JSON_EXTRACT(r.ss_metadata, '$.age_ratings[*].rating'), + JSON_ARRAY(JSON_UNQUOTE(JSON_EXTRACT(r.ss_metadata, '$.age_ratings[*].rating'))) + ) ELSE NULL END, diff --git a/backend/config/config_manager.py b/backend/config/config_manager.py index 9edada19e..b6e5aab38 100644 --- a/backend/config/config_manager.py +++ b/backend/config/config_manager.py @@ -127,8 +127,8 @@ class Config: SCAN_REGION_PRIORITY: list[str] SCAN_LANGUAGE_PRIORITY: list[str] SCAN_MEDIA: list[str] - GAMELIST_THUMBNAIL_MEDIA: str - GAMELIST_IMAGE_MEDIA: str + GAMELIST_MEDIA_THUMBNAIL: str + GAMELIST_MEDIA_IMAGE: str def __init__(self, **entries): self.__dict__.update(entries) @@ -298,7 +298,7 @@ class ConfigManager: self._raw_config, "filesystem.firmware_folder", "bios" ), GAMELIST_AUTO_EXPORT_ON_SCAN=pydash.get( - self._raw_config, "scan.export_gamelist", False + self._raw_config, "scan.gamelist.export", False ), SKIP_HASH_CALCULATION=pydash.get( self._raw_config, "filesystem.skip_hash_calculation", False @@ -372,14 +372,14 @@ class ConfigManager: "manual", ], ), - GAMELIST_THUMBNAIL_MEDIA=pydash.get( + GAMELIST_MEDIA_THUMBNAIL=pydash.get( self._raw_config, - "export.gamelist.media.thumbnail", + "scan.gamelist.media.thumbnail", "cover", ), - GAMELIST_IMAGE_MEDIA=pydash.get( + GAMELIST_MEDIA_IMAGE=pydash.get( self._raw_config, - "export.gamelist.media.image", + "scan.gamelist.media.image", "screenshot", ), ) @@ -452,7 +452,7 @@ class ConfigManager: ) sys.exit(3) if not isinstance(self.config.GAMELIST_AUTO_EXPORT_ON_SCAN, bool): - log.critical("Invalid config.yml: scan.export_gamelist must be a boolean") + log.critical("Invalid config.yml: scan.gamelist.export must be a boolean") sys.exit(3) if not isinstance(self.config.PLATFORMS_BINDING, dict): @@ -660,15 +660,13 @@ class ConfigManager: "language": self.config.SCAN_LANGUAGE_PRIORITY, }, "media": self.config.SCAN_MEDIA, - "export_gamelist": self.config.GAMELIST_AUTO_EXPORT_ON_SCAN, - }, - "export": { "gamelist": { + "export": self.config.GAMELIST_AUTO_EXPORT_ON_SCAN, "media": { - "thumbnail": self.config.GAMELIST_THUMBNAIL_MEDIA, - "image": self.config.GAMELIST_IMAGE_MEDIA, - } - } + "thumbnail": self.config.GAMELIST_MEDIA_THUMBNAIL, + "image": self.config.GAMELIST_MEDIA_IMAGE, + }, + }, }, } diff --git a/backend/endpoints/configs.py b/backend/endpoints/configs.py index 6314060ef..8da456f00 100644 --- a/backend/endpoints/configs.py +++ b/backend/endpoints/configs.py @@ -70,8 +70,8 @@ def get_config(request: Request) -> ConfigResponse: SCAN_REGION_PRIORITY=cfg.SCAN_REGION_PRIORITY, SCAN_LANGUAGE_PRIORITY=cfg.SCAN_LANGUAGE_PRIORITY, SCAN_MEDIA=cfg.SCAN_MEDIA, - GAMELIST_THUMBNAIL_MEDIA=cfg.GAMELIST_THUMBNAIL_MEDIA, - GAMELIST_IMAGE_MEDIA=cfg.GAMELIST_IMAGE_MEDIA, + GAMELIST_MEDIA_THUMBNAIL=cfg.GAMELIST_MEDIA_THUMBNAIL, + GAMELIST_MEDIA_IMAGE=cfg.GAMELIST_MEDIA_IMAGE, ) diff --git a/backend/endpoints/responses/config.py b/backend/endpoints/responses/config.py index 7caa92702..6007be53a 100644 --- a/backend/endpoints/responses/config.py +++ b/backend/endpoints/responses/config.py @@ -31,5 +31,5 @@ class ConfigResponse(TypedDict): SCAN_REGION_PRIORITY: list[str] SCAN_LANGUAGE_PRIORITY: list[str] SCAN_MEDIA: list[str] - GAMELIST_THUMBNAIL_MEDIA: str - GAMELIST_IMAGE_MEDIA: str + GAMELIST_MEDIA_THUMBNAIL: str + GAMELIST_MEDIA_IMAGE: str diff --git a/backend/tests/config/fixtures/config/config.yml b/backend/tests/config/fixtures/config/config.yml index 94781ffb1..1e7c22a0d 100644 --- a/backend/tests/config/fixtures/config/config.yml +++ b/backend/tests/config/fixtures/config/config.yml @@ -51,6 +51,11 @@ scan: - box3d - physical - miximage + gamelist: + export: true + media: + thumbnail: "box3d" + image: "title_screen" emulatorjs: debug: true @@ -75,9 +80,3 @@ emulatorjs: 0: value: x value2: BUTTON_2 - -export: - gamelist: - media: - thumbnail: "box3d" - image: "title_screen" diff --git a/backend/tests/config/test_config_loader.py b/backend/tests/config/test_config_loader.py index fb463bb29..0f29aa6da 100644 --- a/backend/tests/config/test_config_loader.py +++ b/backend/tests/config/test_config_loader.py @@ -77,8 +77,8 @@ def test_config_loader(): assert loader.config.SCAN_ARTWORK_PRIORITY == ["igdb", "ss"] assert loader.config.SCAN_REGION_PRIORITY == ["jp", "eu", "wor"] assert loader.config.SCAN_LANGUAGE_PRIORITY == ["jp", "es"] - assert loader.config.GAMELIST_THUMBNAIL_MEDIA == "box3d" - assert loader.config.GAMELIST_IMAGE_MEDIA == "title_screen" + assert loader.config.GAMELIST_MEDIA_THUMBNAIL == "box3d" + assert loader.config.GAMELIST_MEDIA_IMAGE == "title_screen" def test_empty_config_loader(): @@ -111,5 +111,5 @@ def test_empty_config_loader(): assert loader.config.EJS_NETPLAY_ICE_SERVERS == [] assert loader.config.EJS_SETTINGS == {} assert loader.config.EJS_CONTROLS == {} - assert loader.config.GAMELIST_THUMBNAIL_MEDIA == "cover" - assert loader.config.GAMELIST_IMAGE_MEDIA == "screenshot" + assert loader.config.GAMELIST_MEDIA_THUMBNAIL == "cover" + assert loader.config.GAMELIST_MEDIA_IMAGE == "screenshot" diff --git a/backend/tests/endpoints/test_config.py b/backend/tests/endpoints/test_config.py index a84635983..7d24684cc 100644 --- a/backend/tests/endpoints/test_config.py +++ b/backend/tests/endpoints/test_config.py @@ -36,8 +36,8 @@ def test_config(client): assert config.get("EXCLUDED_MULTI_PARTS_FILES") == sorted(DEFAULT_EXCLUDED_FILES) assert config.get("PLATFORMS_BINDING") == {} assert not config.get("SKIP_HASH_CALCULATION") - assert config.get("GAMELIST_THUMBNAIL_MEDIA") == "cover" - assert config.get("GAMELIST_IMAGE_MEDIA") == "screenshot" + assert config.get("GAMELIST_MEDIA_THUMBNAIL") == "cover" + assert config.get("GAMELIST_MEDIA_IMAGE") == "screenshot" def test_add_platform_binding_payload_shape(client, access_token: str): diff --git a/backend/utils/gamelist_exporter.py b/backend/utils/gamelist_exporter.py index c4e949b51..3fdb2fb7b 100644 --- a/backend/utils/gamelist_exporter.py +++ b/backend/utils/gamelist_exporter.py @@ -16,6 +16,13 @@ from logger.logger import log from models.rom import Rom +def get_media_options_for_export() -> tuple[str, str]: + """Get media options for export from config""" + config = cm.get_config() + + return config.GAMELIST_MEDIA_IMAGE, config.GAMELIST_MEDIA_THUMBNAIL + + class GamelistExporter: """Export RomM collections to ES-DE gamelist.xml format""" @@ -26,7 +33,9 @@ class GamelistExporter: """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: Request | None) -> Element: + def _create_game_element( + self, rom: Rom, request: Request | None, media_image: str, media_thumbnail: str + ) -> Element: """Create a element for a ROM""" game = Element("game") @@ -52,24 +61,34 @@ class GamelistExporter: SubElement(game, "desc").text = rom.summary # Media files - config = cm.get_config() thumbnail_path: str | None = None - thumbnail_option = config.GAMELIST_THUMBNAIL_MEDIA - if thumbnail_option == "box3d": - if rom.ss_metadata and rom.ss_metadata.get("box3d_path"): - thumbnail_path = f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata['box3d_path']}" - elif rom.gamelist_metadata and rom.gamelist_metadata.get("box3d"): - thumbnail_path = rom.gamelist_metadata["box3d"] - elif thumbnail_option == "miximage": - if rom.ss_metadata and rom.ss_metadata.get("miximage_path"): - thumbnail_path = f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata['miximage_path']}" - elif rom.gamelist_metadata and rom.gamelist_metadata.get("miximage_path"): - thumbnail_path = f"{FRONTEND_RESOURCES_PATH}/{rom.gamelist_metadata['miximage_path']}" - elif thumbnail_option == "physical": - if rom.ss_metadata and rom.ss_metadata.get("physical_path"): - thumbnail_path = f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata['physical_path']}" - elif rom.gamelist_metadata and rom.gamelist_metadata.get("physical_path"): - thumbnail_path = f"{FRONTEND_RESOURCES_PATH}/{rom.gamelist_metadata['physical_path']}" + match media_thumbnail: + case "box3d": + if rom.ss_metadata and rom.ss_metadata.get("box3d_path"): + thumbnail_path = ( + f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata['box3d_path']}" + ) + elif rom.gamelist_metadata and rom.gamelist_metadata.get("box3d"): + thumbnail_path = rom.gamelist_metadata["box3d"] + case "miximage": + if rom.ss_metadata and rom.ss_metadata.get("miximage_path"): + thumbnail_path = ( + f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata['miximage_path']}" + ) + elif rom.gamelist_metadata and rom.gamelist_metadata.get( + "miximage_path" + ): + thumbnail_path = f"{FRONTEND_RESOURCES_PATH}/{rom.gamelist_metadata['miximage_path']}" + case "physical": + if rom.ss_metadata and rom.ss_metadata.get("physical_path"): + thumbnail_path = ( + f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata['physical_path']}" + ) + elif rom.gamelist_metadata and rom.gamelist_metadata.get( + "physical_path" + ): + thumbnail_path = f"{FRONTEND_RESOURCES_PATH}/{rom.gamelist_metadata['physical_path']}" + # "cover" and "box2d" both map to path_cover_l (box2d IS the front cover) if thumbnail_path is None and rom.path_cover_l: thumbnail_path = f"{FRONTEND_RESOURCES_PATH}/{rom.path_cover_l}" @@ -89,20 +108,27 @@ class GamelistExporter: ) image_path: str | None = None - image_option = config.GAMELIST_IMAGE_MEDIA - if image_option == "title_screen": - if rom.ss_metadata and rom.ss_metadata.get("title_screen_path"): - image_path = f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata['title_screen_path']}" - elif rom.gamelist_metadata and rom.gamelist_metadata.get("title_screen_path"): - image_path = f"{FRONTEND_RESOURCES_PATH}/{rom.gamelist_metadata['title_screen_path']}" - elif image_option == "miximage": - if rom.ss_metadata and rom.ss_metadata.get("miximage_path"): - image_path = f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata['miximage_path']}" - elif rom.gamelist_metadata and rom.gamelist_metadata.get("miximage_path"): - image_path = f"{FRONTEND_RESOURCES_PATH}/{rom.gamelist_metadata['miximage_path']}" - elif image_option == "cover": - if rom.path_cover_l: - image_path = f"{FRONTEND_RESOURCES_PATH}/{rom.path_cover_l}" + match media_image: + case "title_screen": + if rom.ss_metadata and rom.ss_metadata.get("title_screen_path"): + image_path = f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata['title_screen_path']}" + elif rom.gamelist_metadata and rom.gamelist_metadata.get( + "title_screen_path" + ): + image_path = f"{FRONTEND_RESOURCES_PATH}/{rom.gamelist_metadata['title_screen_path']}" + case "miximage": + if rom.ss_metadata and rom.ss_metadata.get("miximage_path"): + image_path = ( + f"{FRONTEND_RESOURCES_PATH}/{rom.ss_metadata['miximage_path']}" + ) + elif rom.gamelist_metadata and rom.gamelist_metadata.get( + "miximage_path" + ): + image_path = f"{FRONTEND_RESOURCES_PATH}/{rom.gamelist_metadata['miximage_path']}" + case "cover": + if rom.path_cover_l: + image_path = f"{FRONTEND_RESOURCES_PATH}/{rom.path_cover_l}" + # "screenshot" (default) and anything else falls through to path_screenshots if image_path is None and rom.path_screenshots: image_path = f"{FRONTEND_RESOURCES_PATH}/{rom.path_screenshots[0]}" @@ -232,9 +258,16 @@ class GamelistExporter: # Create root element root = Element("gameList") + media_image, media_thumbnail = get_media_options_for_export() + for rom in roms: if rom and not rom.missing_from_fs and rom.fs_name != "gamelist.xml": - game_element = self._create_game_element(rom, request=request) + game_element = self._create_game_element( + rom, + request=request, + media_image=media_image, + media_thumbnail=media_thumbnail, + ) root.append(game_element) # Convert to XML string diff --git a/examples/config.batocera-retrobat.yml b/examples/config.batocera-retrobat.yml index 429020078..96a05b699 100644 --- a/examples/config.batocera-retrobat.yml +++ b/examples/config.batocera-retrobat.yml @@ -183,8 +183,7 @@ system: zx81: zx81 zxspectrum: zxs versions: {} - -export: +scan: gamelist: media: # Use the 3D box art as the tag, preferred by Batocera/RetroBAT @@ -194,4 +193,4 @@ export: # Use the miximage as the tag, preferred by Batocera/RetroBAT # Falls back to first screenshot if not available # Other options: "screenshot" (default), "title_screen", "cover" - image: "miximage" \ No newline at end of file + image: miximage diff --git a/examples/config.example.yml b/examples/config.example.yml index 2c67f4040..103b8ea49 100644 --- a/examples/config.example.yml +++ b/examples/config.example.yml @@ -134,23 +134,12 @@ # - logo # Transparent logo # # Other media assets (might be used in the future) # - marquee # Custom marquee - -# export: # gamelist: +# export: false # Whether to export gamelist.xml for ES-DE/Batocera/RetroBAT # media: -# # Media asset to use as the tag in the exported gamelist.xml -# # cover - front cover art / box2d (default) -# # box2d - same as cover -# # box3d - 3D box art (falls back to cover if not available) -# # miximage - mixed image (falls back to cover if not available) -# # physical - disc/cartridge image (falls back to cover if not available) -# thumbnail: cover -# # Media asset to use as the tag in the exported gamelist.xml -# # screenshot - first screenshot (default) -# # title_screen - title screen image (falls back to screenshot if not available) -# # miximage - mixed image (falls back to screenshot if not available) -# # cover - front cover art (falls back to screenshot if not available) -# image: screenshot +# # Select a media type from the list above (eg. box2d, screenshot, etc.) +# thumbnail: cover # Use as the tag in the exported gamelist.xml +# image: screenshot # Use as the tag in the exported gamelist.xml # EmulatorJS per-core options # emulatorjs: @@ -180,4 +169,4 @@ # value2: BUTTON_2 # Mapping for connected controller # 1: # Player 2 # 2: # Player 3 -# 3: # Player 4 \ No newline at end of file +# 3: # Player 4