diff --git a/backend/config/config_manager.py b/backend/config/config_manager.py index 9d6400d85..b8aa92687 100644 --- a/backend/config/config_manager.py +++ b/backend/config/config_manager.py @@ -75,6 +75,7 @@ class MetadataMediaType(enum.StrEnum): BOX2D_BACK = "box2d_back" BOX3D = "box3d" MIXIMAGE = "miximage" + MIXIMAGE_V2 = "miximage_v2" PHYSICAL = "physical" SCREENSHOT = "screenshot" TITLE_SCREEN = "title_screen" @@ -665,6 +666,7 @@ class ConfigManager: MetadataMediaType.BOX2D, MetadataMediaType.BOX3D, MetadataMediaType.MIXIMAGE, + MetadataMediaType.MIXIMAGE_V2, MetadataMediaType.PHYSICAL, } if not isinstance(self.config.GAMELIST_MEDIA_THUMBNAIL, str): @@ -683,6 +685,7 @@ class ConfigManager: valid_image_options = { MetadataMediaType.TITLE_SCREEN, MetadataMediaType.MIXIMAGE, + MetadataMediaType.MIXIMAGE_V2, MetadataMediaType.BOX2D, MetadataMediaType.SCREENSHOT, } diff --git a/backend/handler/metadata/ss_handler.py b/backend/handler/metadata/ss_handler.py index 380ab06af..b5418384a 100644 --- a/backend/handler/metadata/ss_handler.py +++ b/backend/handler/metadata/ss_handler.py @@ -171,7 +171,8 @@ class SSMetadataMedia(TypedDict): logo_url: str | None # wheel-hd or wheel manual_url: str | None # manual marquee_url: str | None # screenmarquee - miximage_url: str | None # mixrbv1 | mixrbv2 + miximage_url: str | None # miximage1 | miximage2 | mixrbv1 + miximage_v2_url: str | None # mixrbv2 physical_url: str | None # support-2D screenshot_url: str | None # ss steamgrid_url: str | None # steamgrid @@ -185,6 +186,7 @@ class SSMetadataMedia(TypedDict): box3d_path: str | None fanart_path: str | None miximage_path: str | None + miximage_v2_path: str | None physical_path: str | None marquee_path: str | None logo_path: str | None @@ -233,6 +235,7 @@ def extract_media_from_ss_game(rom: Rom, game: SSGame) -> SSMetadataMedia: manual_url=None, marquee_url=None, miximage_url=None, + miximage_v2_url=None, physical_url=None, screenshot_url=None, steamgrid_url=None, @@ -244,6 +247,7 @@ def extract_media_from_ss_game(rom: Rom, game: SSGame) -> SSMetadataMedia: box3d_path=None, fanart_path=None, miximage_path=None, + miximage_v2_path=None, physical_path=None, marquee_path=None, logo_path=None, @@ -322,7 +326,6 @@ def extract_media_from_ss_game(rom: Rom, game: SSGame) -> SSMetadataMedia: media.get("type") == "miximage1" or media.get("type") == "miximage2" or media.get("type") == "mixrbv1" - or media.get("type") == "mixrbv2" ) and not ss_media["miximage_url"]: ss_media["miximage_url"] = strip_sensitive_query_params( media["url"], SENSITIVE_KEYS @@ -331,6 +334,17 @@ def extract_media_from_ss_game(rom: Rom, game: SSGame) -> SSMetadataMedia: 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") == "mixrbv2" + and not ss_media["miximage_v2_url"] + ): + ss_media["miximage_v2_url"] = strip_sensitive_query_params( + media["url"], SENSITIVE_KEYS + ) + if MetadataMediaType.MIXIMAGE_V2 in preferred_media_types: + ss_media["miximage_v2_path"] = ( + f"{fs_resource_handler.get_media_resources_path(rom.platform_id, rom.id, MetadataMediaType.MIXIMAGE_V2)}/miximage_v2.png" + ) elif media.get("type") == "support-2D" and not ss_media["physical_url"]: ss_media["physical_url"] = strip_sensitive_query_params( media["url"], SENSITIVE_KEYS diff --git a/backend/tests/handler/metadata/test_ss_handler.py b/backend/tests/handler/metadata/test_ss_handler.py index 3ccb1b659..e1002e8ed 100644 --- a/backend/tests/handler/metadata/test_ss_handler.py +++ b/backend/tests/handler/metadata/test_ss_handler.py @@ -19,7 +19,10 @@ from handler.metadata.ss_handler import ( ) -def _make_config(region_priority: list[str] | None = None) -> Config: +def _make_config( + region_priority: list[str] | None = None, + scan_media: list[str] | None = None, +) -> Config: """Build a minimal Config object for testing.""" return Config( EXCLUDED_PLATFORMS=[], @@ -34,7 +37,7 @@ def _make_config(region_priority: list[str] | None = None) -> Config: FIRMWARE_FOLDER_NAME="bios", SCAN_REGION_PRIORITY=region_priority or [], SCAN_LANGUAGE_PRIORITY=["en"], - SCAN_MEDIA=["box2d", "box3d", "screenshot"], + SCAN_MEDIA=scan_media if scan_media is not None else ["box2d", "box3d", "screenshot"], GAMELIST_MEDIA_THUMBNAIL=MetadataMediaType.BOX2D, GAMELIST_MEDIA_IMAGE=MetadataMediaType.SCREENSHOT, ) @@ -203,6 +206,138 @@ class TestExtractMediaFromSsGame: assert result["box2d_url"] is not None assert "box-2D(us)" in result["box2d_url"] + def _make_game_with_both_miximage_versions(self) -> SSGame: + """A game that has both mixrbv1 and mixrbv2 (v1 listed first, matching SS API order).""" + return cast( + SSGame, + { + "medias": [ + { + "type": "mixrbv1", + "parent": "jeu", + "region": "us", + "url": "https://screenscraper.example.com/mixrbv1", + "crc": "aabbccdd", + "md5": "deadbeef", + "sha1": "cafebabe", + "size": "12345", + "format": "png", + }, + { + "type": "mixrbv2", + "parent": "jeu", + "region": "us", + "url": "https://screenscraper.example.com/mixrbv2", + "crc": "11223344", + "md5": "feedface", + "sha1": "baadf00d", + "size": "67890", + "format": "png", + }, + ] + }, + ) + + def test_miximage_maps_to_mixrbv1(self): + """When 'miximage' is in SCAN_MEDIA, only mixrbv1 is downloaded.""" + config = _make_config(scan_media=["miximage"]) + rom = self._make_rom() + game = self._make_game_with_both_miximage_versions() + + with ( + patch("handler.metadata.ss_handler.cm.get_config", return_value=config), + patch( + "handler.metadata.ss_handler.fs_resource_handler.get_media_resources_path", + return_value="roms/1/100/miximage", + ), + ): + result = extract_media_from_ss_game(rom, game) + + assert result["miximage_url"] is not None + assert "mixrbv1" in result["miximage_url"] + assert result["miximage_path"] is not None + assert result["miximage_v2_url"] is not None + assert "mixrbv2" in result["miximage_v2_url"] + assert result["miximage_v2_path"] is None + + def test_miximage_v2_maps_to_mixrbv2(self): + """When 'miximage_v2' is in SCAN_MEDIA, only mixrbv2 is downloaded.""" + config = _make_config(scan_media=["miximage_v2"]) + rom = self._make_rom() + game = self._make_game_with_both_miximage_versions() + + with ( + patch("handler.metadata.ss_handler.cm.get_config", return_value=config), + patch( + "handler.metadata.ss_handler.fs_resource_handler.get_media_resources_path", + return_value="roms/1/100/miximage_v2", + ), + ): + result = extract_media_from_ss_game(rom, game) + + assert result["miximage_v2_url"] is not None + assert "mixrbv2" in result["miximage_v2_url"] + assert result["miximage_v2_path"] is not None + assert result["miximage_url"] is not None + assert "mixrbv1" in result["miximage_url"] + assert result["miximage_path"] is None + + def test_miximage_v2_not_downloaded_when_only_miximage_in_config(self): + """When only 'miximage' is in SCAN_MEDIA, miximage_v2_path is not set.""" + config = _make_config(scan_media=["miximage"]) + rom = self._make_rom() + game = self._make_game_with_both_miximage_versions() + + with ( + patch("handler.metadata.ss_handler.cm.get_config", return_value=config), + patch( + "handler.metadata.ss_handler.fs_resource_handler.get_media_resources_path", + return_value="roms/1/100/miximage", + ), + ): + result = extract_media_from_ss_game(rom, game) + + assert result["miximage_v2_path"] is None + + def test_miximage_v1_not_downloaded_when_only_miximage_v2_in_config(self): + """When only 'miximage_v2' is in SCAN_MEDIA, miximage_path is not set.""" + config = _make_config(scan_media=["miximage_v2"]) + rom = self._make_rom() + game = self._make_game_with_both_miximage_versions() + + with ( + patch("handler.metadata.ss_handler.cm.get_config", return_value=config), + patch( + "handler.metadata.ss_handler.fs_resource_handler.get_media_resources_path", + return_value="roms/1/100/miximage_v2", + ), + ): + result = extract_media_from_ss_game(rom, game) + + assert result["miximage_path"] is None + + def test_both_miximage_versions_downloaded_when_both_in_config(self): + """When both 'miximage' and 'miximage_v2' are in SCAN_MEDIA, both are downloaded.""" + config = _make_config(scan_media=["miximage", "miximage_v2"]) + rom = self._make_rom() + game = self._make_game_with_both_miximage_versions() + + with ( + patch("handler.metadata.ss_handler.cm.get_config", return_value=config), + patch( + "handler.metadata.ss_handler.fs_resource_handler.get_media_resources_path", + side_effect=lambda pid, rid, mt: f"roms/{pid}/{rid}/{mt.value}", + ), + ): + result = extract_media_from_ss_game(rom, game) + + assert result["miximage_url"] is not None + assert "mixrbv1" in result["miximage_url"] + assert result["miximage_path"] is not None + assert result["miximage_v2_url"] is not None + assert "mixrbv2" in result["miximage_v2_url"] + assert result["miximage_v2_path"] is not None + class TestExtractMetadataFromSsRom: def _make_rom(self, regions: list[str] | None = None) -> MagicMock: diff --git a/backend/utils/gamelist_exporter.py b/backend/utils/gamelist_exporter.py index 7a688b5da..1607c02c9 100644 --- a/backend/utils/gamelist_exporter.py +++ b/backend/utils/gamelist_exporter.py @@ -26,6 +26,7 @@ ASSET_DIRS: dict[str, str] = { "fanart": "fanart", "marquee": "marquees", "miximage": "miximages", + "miximage_v2": "miximages", "physical": "physical", "screenshot": "screenshots", "title_screen": "titlescreens", @@ -89,6 +90,7 @@ class GamelistExporter: "fanart": [ss.get("fanart_path", ""), gl.get("fanart_path", "")], "marquee": [ss.get("logo_path", ""), gl.get("marquee_path", "")], "miximage": [ss.get("miximage_path", ""), gl.get("miximage_path", "")], + "miximage_v2": [ss.get("miximage_v2_path", "")], "physical": [ss.get("physical_path", ""), gl.get("physical_path", "")], "title_screen": [ ss.get("title_screen_path", ""), @@ -262,6 +264,7 @@ class GamelistExporter: "fanart": "fanart", "marquee": "marquee", "miximage": "miximage", + "miximage_v2": "miximage", "physical": "physicalmedia", "title_screen": "title_screen", "bezel": "bezel", diff --git a/examples/config.example.yml b/examples/config.example.yml index cc4b636da..48a896813 100644 --- a/examples/config.example.yml +++ b/examples/config.example.yml @@ -117,7 +117,8 @@ # # Used as alternative cover art # - box2d # Normal cover art (always enabled) # - box3d # 3D box art -# - miximage # Mixed image of multiple media +# - miximage # Mixed image of multiple media (v1 / mixrbv1) +# - miximage_v2 # Mixed image of multiple media (v2 / mixrbv2) # - physical # Disc, cartridge, etc. # # Added to the screenshots carousel # - screenshot # Screenshot (enabled by default)