From d22e79938c8c12d18fd6da6300389a59ce8682eb Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Sun, 6 Jul 2025 18:01:04 -0400 Subject: [PATCH 1/4] [MUOS-20] Allow donwloading assets and screenshots --- RomM/api.py | 87 ++++++++++++++++++++++++++++++++- RomM/env.template | 4 ++ RomM/filesystem.py | 43 +++++++++++++++-- RomM/imageutils.py | 118 +++++++++++++++++++++++++++++++++++++++++++++ RomM/models.py | 11 +++++ 5 files changed, 258 insertions(+), 5 deletions(-) create mode 100644 RomM/imageutils.py diff --git a/RomM/api.py b/RomM/api.py index b56baef..2801ea9 100644 --- a/RomM/api.py +++ b/RomM/api.py @@ -1,4 +1,5 @@ import base64 +import datetime import json import math import os @@ -11,6 +12,7 @@ import platform_maps from filesystem import Filesystem +from imageutils import ImageUtils from models import Collection, Platform, Rom from PIL import Image from status import Status, View @@ -28,6 +30,7 @@ class API: def __init__(self): self.status = Status() self.file_system = Filesystem() + self.image_utils = ImageUtils() self.host = os.getenv("HOST", "") self.username = os.getenv("USERNAME", "") @@ -37,6 +40,11 @@ def __init__(self): self._include_collections = set(self._getenv_list("INCLUDE_COLLECTIONS")) self._exclude_collections = set(self._getenv_list("EXCLUDE_COLLECTIONS")) self._collection_type = os.getenv("COLLECTION_TYPE", "collection") + self._download_assets = os.getenv("DOWNLOAD_ASSETS", "false") in ("true", "1") + self._fullscreen_assets = os.getenv("FULLSCREEN_ASSETS", "false") in ( + "true", + "1", + ) if self.username and self.password: credentials = f"{self.username}:{self.password}" @@ -234,6 +242,7 @@ def fetch_platforms(self) -> None: self.status.valid_host = False self.status.valid_credentials = False return + platforms = json.loads(response.read().decode("utf-8")) _platforms: list[Platform] = [] @@ -251,7 +260,7 @@ def fetch_platforms(self) -> None: for platform in platforms: if platform["rom_count"] > 0: - platform_slug = platform["slug"].lower() + platform_slug: str = platform["slug"].lower() if ( platform_maps._env_maps and platform_slug in platform_maps._env_platforms @@ -458,7 +467,7 @@ def fetch_roms(self) -> None: _roms = [] for rom in roms: - platform_slug = rom["platform_slug"].lower() + platform_slug: str = rom["platform_slug"].lower() if ( platform_maps._env_maps and platform_slug in platform_maps._env_platforms @@ -476,13 +485,17 @@ def fetch_roms(self) -> None: ) if mapped_folder.lower() not in roms_subfolders: continue + if view == View.PLATFORMS and platform_slug != selected_platform_slug: continue + _roms.append( Rom( id=rom["id"], name=rom["name"], + summary=rom["summary"], fs_name=rom["fs_name"], + platform_id=rom["platform_id"], platform_slug=rom["platform_slug"], fs_extension=rom["fs_extension"], fs_size=self._human_readable_size(rom["fs_size_bytes"]), @@ -492,6 +505,15 @@ def fetch_roms(self) -> None: regions=rom["regions"], revision=rom["revision"], tags=rom["tags"], + path_cover_small=rom.get("path_cover_small", ""), + path_cover_large=rom.get("path_cover_large", ""), + merged_screenshots=rom["merged_screenshots"], + first_release_date=rom["first_release_date"], + average_rating=rom["average_rating"], + genres=rom["genres"], + franchises=rom["franchises"], + companies=rom["companies"], + age_ratings=rom["age_ratings"], ) ) @@ -532,6 +554,7 @@ def download_rom(self) -> None: except ValueError: self._reset_download_status() return + try: if request.type not in ("http", "https"): self._reset_download_status() @@ -608,5 +631,65 @@ def download_rom(self) -> None: except URLError: self._reset_download_status(valid_host=True) return + + filename = self._sanitize_filename(rom.fs_name).split(".")[0] + if rom.summary: + text_path = os.path.join( + self.file_system.get_catalogue_platform_path(rom.platform_slug), + "text", + f"{filename}.txt", + ) + os.makedirs(os.path.dirname(text_path), exist_ok=True) + with open(text_path, "w") as f: + f.write(rom.summary) + f.write("\n\n") + + if rom.first_release_date: + dt = datetime.datetime.fromtimestamp( + rom.first_release_date / 1000 + ) + formatted_date = dt.strftime("%Y-%m-%d") + f.write(f"First release date: {formatted_date}\n") + + if rom.average_rating: + f.write(f"Average rating: {rom.average_rating}\n") + + if rom.genres: + f.write(f"Genres: {', '.join(rom.genres)}\n") + + if rom.franchises: + f.write(f"Franchises: {', '.join(rom.franchises)}\n") + + if rom.companies: + f.write(f"Companies: {', '.join(rom.companies)}\n") + + # Don't download covers and previews if the user disabled the option + if not self._download_assets: + continue + + box_path = os.path.join( + self.file_system.get_catalogue_platform_path(rom.platform_slug), + "box", + f"{filename}.png", + ) + preview_path = os.path.join( + self.file_system.get_catalogue_platform_path(rom.platform_slug), + "preview", + f"{filename}.png", + ) + + # Download cover and preview images + os.makedirs(os.path.dirname(box_path), exist_ok=True) + os.makedirs(os.path.dirname(preview_path), exist_ok=True) + + self.image_utils.process_assets( + fullscreen=self._fullscreen_assets, + cover_url=f"{self.host}{rom.path_cover_small}", + screenshot_url=f"{self.host}{rom.merged_screenshots[0]}", + box_path=box_path, + preview_path=preview_path, + headers=self.headers, + ) + # End of download self._reset_download_status(valid_host=True, valid_credentials=True) diff --git a/RomM/env.template b/RomM/env.template index 96d0b7b..7e39b18 100644 --- a/RomM/env.template +++ b/RomM/env.template @@ -16,6 +16,10 @@ COLLECTION_TYPE=collection # Do not display collections with these names (comma separated) # EXCLUDE_COLLECTIONS="" +# Download cover images and screenshots +DOWNLOAD_ASSETS=1 +FULLSCREEN_ASSETS=1 + # Map RomM slugs to filesystem directories # For example, if your PlayStation directory is called "psx": # CUSTOM_MAPS='{"ps": "psx"}' diff --git a/RomM/filesystem.py b/RomM/filesystem.py index bc8828b..d33367c 100644 --- a/RomM/filesystem.py +++ b/RomM/filesystem.py @@ -16,7 +16,9 @@ class Filesystem: # Storage paths for ROMs _sd1_roms_storage_path: str - _sd2_roms_storage_path: str | None + _sd2_roms_storage_path: str | None = None + _sd1_catalogue_path: str | None = None + _sd2_catalogue_path: str | None = None # Resources path: Use current working directory + "resources" resources_path = os.path.join(os.getcwd(), "resources") @@ -35,15 +37,15 @@ def __init__(self) -> None: if self.is_muos: self._sd1_roms_storage_path = "/mnt/mmc/ROMS" self._sd2_roms_storage_path = "/mnt/sdcard/ROMS" + self._sd1_catalogue_path = "/mnt/mmc/MUOS/info/catalogue" + self._sd2_catalogue_path = "/mnt/sdcard/MUOS/info/catalogue" elif self.is_spruceos: self._sd1_roms_storage_path = "/mnt/SDCARD/Roms" - self._sd2_roms_storage_path = None else: # Go up two levels from the script's directory (e.g., from roms/ports/romm to roms/) base_path = os.path.abspath(os.path.join(os.getcwd(), "..", "..")) # Default to the ROMs directory, overridable via environment variable self._sd1_roms_storage_path = os.environ.get("ROMS_STORAGE_PATH", base_path) - self._sd2_roms_storage_path = None # Ensure the ROMs storage path exists if self._sd2_roms_storage_path and not os.path.exists( @@ -70,6 +72,14 @@ def _get_sd2_roms_storage_path(self) -> Optional[str]: """Return the secondary ROMs storage path if available.""" return self._sd2_roms_storage_path + def _get_sd1_catalogue_path(self) -> Optional[str]: + """Return the catalogue path for SD1.""" + return self._sd1_catalogue_path + + def _get_sd2_catalogue_path(self) -> Optional[str]: + """Return the catalogue path for SD2.""" + return self._sd2_catalogue_path + def _get_platform_storage_dir_from_mapping(self, platform: str) -> str: """ Return the platform-specific storage path, @@ -111,6 +121,20 @@ def _get_sd2_platforms_storage_path(self, platform: str) -> Optional[str]: return os.path.join(self._sd2_roms_storage_path, platforms_dir) return None + def get_sd1_catalogue_platform_path(self, platform: str) -> str: + if not self._sd1_catalogue_path: + raise ValueError("SD1 catalogue path is not set.") + + platforms_dir = self._get_platform_storage_dir_from_mapping(platform) + return os.path.join(self._sd1_catalogue_path, platforms_dir) + + def get_sd2_catalogue_platform_path(self, platform: str) -> str: + if not self._sd2_catalogue_path: + raise ValueError("SD2 catalogue path is not set.") + + platforms_dir = self._get_platform_storage_dir_from_mapping(platform) + return os.path.join(self._sd2_catalogue_path, platforms_dir) + ### # PUBLIC METHODS ### @@ -138,6 +162,19 @@ def get_platforms_storage_path(self, platform: str) -> str: return self._get_sd1_platforms_storage_path(platform) + def get_catalogue_path(self, platform: str) -> str | None: + """Return the catalogue path for a specific platform.""" + if self._current_sd == 2: + return self._get_sd2_catalogue_path() + + return self._get_sd1_catalogue_path() + + def get_catalogue_platform_path(self, platform: str) -> str: + if self._current_sd == 2: + return self.get_sd2_catalogue_platform_path(platform) + + return self.get_sd1_catalogue_platform_path(platform) + def is_rom_in_device(self, rom: Rom) -> bool: """Check if a ROM exists in the storage path.""" rom_path = os.path.join( diff --git a/RomM/imageutils.py b/RomM/imageutils.py new file mode 100644 index 0000000..8d5bece --- /dev/null +++ b/RomM/imageutils.py @@ -0,0 +1,118 @@ +from typing import Optional + +from PIL import Image, ImageDraw + + +class ImageUtils: + _instance: Optional["ImageUtils"] = None + _initialized: bool = False + + screen_width = 640 + screen_height = 480 + + def __new__(cls): + if cls._instance is None: + cls._instance = super(ImageUtils, cls).__new__(cls) + return cls._instance + + def __init__(self) -> None: + if self._initialized: + return + + self.fade_mask = self.generate_fade_mask() + self._initialized = True + + def generate_fade_mask(self) -> Image.Image: + fade_mask = Image.new("L", (self.screen_width, self.screen_height), 0) + draw = ImageDraw.Draw(fade_mask) + x_crit = self.screen_width / 3.0 + + for x in range(self.screen_width): + if x < x_crit: + t = x / x_crit + alpha = int((t**2) * (255 / 3)) # a x = x_crit, alpha = 255/3 ≈ 85 + else: + t = (x - x_crit) / (self.screen_width - x_crit) + alpha = int(85 + t * (255 - 85)) + draw.line([(x, 0), (x, self.screen_height)], fill=alpha) + + return fade_mask + + def add_rounded_corners(self, image, radius): + rounded_mask = Image.new("L", image.size, 0) + draw = ImageDraw.Draw(rounded_mask) + draw.rounded_rectangle( + (0, 0, image.size[0], image.size[1]), radius=radius, fill=255 + ) + image.putalpha(rounded_mask) + return image + + def load_image_from_url(self, url: str, headers) -> Image.Image | None: + from io import BytesIO + from urllib.request import Request, urlopen + + try: + req = Request(url, headers=headers) + with urlopen(req, timeout=60) as response: # trunk-ignore(bandit/B310) + data = response.read() + return Image.open(BytesIO(data)).convert("RGBA") + except Exception as e: + print(f"Error loading image from URL {url}: {e}") + return None + + def process_assets( + self, + fullscreen: bool, + cover_url: str, + screenshot_url: str, + box_path: str, + preview_path: str, + headers, + ) -> None: + if not cover_url and not screenshot_url: + return + + final_width, final_height = self.screen_width, self.screen_height + background = None + preview = ( + self.load_image_from_url(screenshot_url, headers) + if screenshot_url + else None + ) + + if preview: + preview = preview.resize((final_width, final_height)) + preview.save(preview_path) + + if fullscreen: + if preview: + background = preview + else: + background = Image.new( + "RGBA", (final_width, final_height), (0, 0, 0, 0) + ) + background.putalpha(self.fade_mask) + + foreground = self.load_image_from_url(cover_url, headers) if cover_url else None + + if foreground: + max_cover_width = 215 + max_cover_height = int(final_height * 3 / 5) + scale_w = max_cover_width / foreground.width + scale_h = max_cover_height / foreground.height + scale = min(scale_w, scale_h) + new_cover_width = int(foreground.width * scale) + new_cover_height = int(foreground.height * scale) + foreground = foreground.resize((new_cover_width, new_cover_height)) + foreground = self.add_rounded_corners(foreground, radius=20) + + fg_x = final_width - new_cover_width - 20 + fg_y = (final_height - new_cover_height) // 2 + + if background: + background.paste(foreground, (fg_x, fg_y), foreground) + else: + background = foreground + + if background: + background.save(box_path) diff --git a/RomM/models.py b/RomM/models.py index fab7557..26c72b8 100644 --- a/RomM/models.py +++ b/RomM/models.py @@ -5,7 +5,9 @@ [ "id", "name", + "summary", "fs_name", + "platform_id", "platform_slug", "fs_extension", "fs_size", @@ -15,6 +17,15 @@ "regions", "revision", "tags", + "path_cover_small", + "path_cover_large", + "merged_screenshots", + "first_release_date", + "average_rating", + "genres", + "franchises", + "companies", + "age_ratings", ], ) Collection = namedtuple("Collection", ["id", "name", "rom_count", "virtual"]) From 78584405f8dfcb00f8a33c34b00c6e8f259c281e Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Sun, 6 Jul 2025 18:25:41 -0400 Subject: [PATCH 2/4] bug fix --- RomM/api.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/RomM/api.py b/RomM/api.py index 2801ea9..5747f58 100644 --- a/RomM/api.py +++ b/RomM/api.py @@ -633,9 +633,12 @@ def download_rom(self) -> None: return filename = self._sanitize_filename(rom.fs_name).split(".")[0] - if rom.summary: + catalogue_path = self.file_system.get_catalogue_platform_path( + rom.platform_slug + ) + if rom.summary and catalogue_path: text_path = os.path.join( - self.file_system.get_catalogue_platform_path(rom.platform_slug), + catalogue_path, "text", f"{filename}.txt", ) @@ -667,16 +670,15 @@ def download_rom(self) -> None: if not self._download_assets: continue - box_path = os.path.join( - self.file_system.get_catalogue_platform_path(rom.platform_slug), - "box", - f"{filename}.png", - ) - preview_path = os.path.join( - self.file_system.get_catalogue_platform_path(rom.platform_slug), - "preview", - f"{filename}.png", + # Check if the catalogue path is set and valid + catalogue_path = self.file_system.get_catalogue_platform_path( + rom.platform_slug ) + if not catalogue_path: + continue + + box_path = os.path.join(catalogue_path, "box", f"{filename}.png") + preview_path = os.path.join(catalogue_path, "preview", f"{filename}.png") # Download cover and preview images os.makedirs(os.path.dirname(box_path), exist_ok=True) From b975f74c887b4a5c76a23593b43a4b91b92ba1b7 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Tue, 2 Dec 2025 13:16:23 -0500 Subject: [PATCH 3/4] update code to pull artwork --- RomM/api.py | 45 ++++++++++++++++++++++++++++--------------- RomM/filesystem.py | 2 +- RomM/imageutils.py | 2 +- RomM/models.py | 32 ++++++++++++++++++++---------- RomM/platform_maps.py | 6 +++--- RomM/romm.py | 2 +- 6 files changed, 57 insertions(+), 32 deletions(-) diff --git a/RomM/api.py b/RomM/api.py index 4713b65..fd6b26b 100644 --- a/RomM/api.py +++ b/RomM/api.py @@ -492,28 +492,40 @@ def fetch_roms(self) -> None: _roms.append( Rom( id=rom["id"], - name=rom["name"], - summary=rom["summary"], - fs_name=rom["fs_name"], platform_id=rom["platform_id"], platform_slug=rom["platform_slug"], + fs_name=rom["fs_name"], + fs_name_no_tags=rom["fs_name_no_tags"], + fs_name_no_ext=rom["fs_name_no_ext"], fs_extension=rom["fs_extension"], fs_size=self._human_readable_size(rom["fs_size_bytes"]), fs_size_bytes=rom["fs_size_bytes"], - multi=rom["multi"], - languages=rom["languages"], - regions=rom["regions"], + name=rom["name"], + slug=rom["slug"], + summary=rom["summary"], + youtube_video_id=rom["youtube_video_id"], + path_cover_small=rom["path_cover_small"], + path_cover_large=rom["path_cover_large"], + is_identified=rom["is_identified"], revision=rom["revision"], + regions=rom["regions"], + languages=rom["languages"], tags=rom["tags"], - path_cover_small=rom.get("path_cover_small", ""), - path_cover_large=rom.get("path_cover_large", ""), + crc_hash=rom["crc_hash"], + md5_hash=rom["md5_hash"], + sha1_hash=rom["sha1_hash"], + has_simple_single_file=rom["has_simple_single_file"], + has_nested_single_file=rom["has_nested_single_file"], + has_multiple_files=rom["has_multiple_files"], merged_screenshots=rom["merged_screenshots"], - first_release_date=rom["first_release_date"], - average_rating=rom["average_rating"], - genres=rom["genres"], - franchises=rom["franchises"], - companies=rom["companies"], - age_ratings=rom["age_ratings"], + genres=rom["metadatum"]["genres"], + franchises=rom["metadatum"]["franchises"], + collections=rom["metadatum"]["collections"], + companies=rom["metadatum"]["companies"], + game_modes=rom["metadatum"]["game_modes"], + age_ratings=rom["metadatum"]["age_ratings"], + first_release_date=rom["metadatum"]["first_release_date"], + average_rating=rom["metadatum"]["average_rating"], ) ) @@ -586,10 +598,11 @@ def download_rom(self) -> None: self._reset_download_status(True, True) os.remove(dest_path) return + # Handle multi-file (ZIP) ROMs - if rom.multi: + if rom.has_multiple_files: self.status.extracting_rom = True - print("Multi file rom detected. Extracting...") + print("Multi-file rom detected. Extracting...") with zipfile.ZipFile(dest_path, "r") as zip_ref: total_size = sum(file.file_size for file in zip_ref.infolist()) extracted_size = 0 diff --git a/RomM/filesystem.py b/RomM/filesystem.py index 5b9b082..92832bd 100644 --- a/RomM/filesystem.py +++ b/RomM/filesystem.py @@ -182,6 +182,6 @@ def is_rom_in_device(self, rom: Rom) -> bool: """Check if a ROM exists in the storage path.""" rom_path = os.path.join( self.get_platforms_storage_path(rom.platform_slug), - rom.fs_name if not rom.multi else f"{rom.fs_name}.m3u", + rom.fs_name if not rom.has_multiple_files else f"{rom.fs_name}.m3u", ) return os.path.exists(rom_path) diff --git a/RomM/imageutils.py b/RomM/imageutils.py index 8d5bece..c97ae6c 100644 --- a/RomM/imageutils.py +++ b/RomM/imageutils.py @@ -52,7 +52,7 @@ def load_image_from_url(self, url: str, headers) -> Image.Image | None: from urllib.request import Request, urlopen try: - req = Request(url, headers=headers) + req = Request(url.split("?")[0], headers=headers) with urlopen(req, timeout=60) as response: # trunk-ignore(bandit/B310) data = response.read() return Image.open(BytesIO(data)).convert("RGBA") diff --git a/RomM/models.py b/RomM/models.py index 26c72b8..d40c682 100644 --- a/RomM/models.py +++ b/RomM/models.py @@ -4,28 +4,40 @@ "Rom", [ "id", - "name", - "summary", - "fs_name", "platform_id", "platform_slug", + "fs_name", + "fs_name_no_tags", + "fs_name_no_ext", "fs_extension", "fs_size", "fs_size_bytes", - "multi", - "languages", - "regions", - "revision", - "tags", + "name", + "slug", + "summary", + "youtube_video_id", "path_cover_small", "path_cover_large", + "is_identified", + "revision", + "regions", + "languages", + "tags", + "crc_hash", + "md5_hash", + "sha1_hash", + "has_simple_single_file", + "has_nested_single_file", + "has_multiple_files", "merged_screenshots", - "first_release_date", - "average_rating", "genres", "franchises", + "collections", "companies", + "game_modes", "age_ratings", + "first_release_date", + "average_rating", ], ) Collection = namedtuple("Collection", ["id", "name", "rom_count", "virtual"]) diff --git a/RomM/platform_maps.py b/RomM/platform_maps.py index 2881ac5..3326039 100644 --- a/RomM/platform_maps.py +++ b/RomM/platform_maps.py @@ -10,9 +10,9 @@ # "slug": ("es-system", "icon"), "ngc": ("gamecube", "ngc"), # Nintendo GameCube "n3ds": ("3ds", "3ds"), # Nintendo 3DS - "genesis": ("genesis", "genesis-slash-megadrive"), # Sega Genesis / Megadrive - "megadrive": ("megadrive", "genesis-slash-megadrive"), # Sega Genesis / Megadrive - "mastersystem": ("mastersystem", "sega-master-system"), # Sega Mastersystem + "genesis": ("genesis", "genesis"), # Sega Genesis / Megadrive + "megadrive": ("megadrive", "genesis"), # Sega Genesis / Megadrive + "mastersystem": ("mastersystem", "sms"), # Sega Mastersystem } # Manual mapping of RomM slugs for MuOS default platforms diff --git a/RomM/romm.py b/RomM/romm.py index da68be7..c7f4df8 100644 --- a/RomM/romm.py +++ b/RomM/romm.py @@ -875,7 +875,7 @@ def update(self): def _remove_rom_files(self, rom: Rom): storage_path = self.fs.get_platforms_storage_path(rom.platform_slug) - if rom.multi: + if rom.has_multiple_files: # Read the m3u file to get the list of ROMs under the .hidden folder rom_list_path = os.path.join(storage_path, rom.fs_name + ".m3u") if os.path.isfile(rom_list_path): From cd8643697de707afd04b1b27936f37c7af0eb987 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Tue, 2 Dec 2025 13:50:14 -0500 Subject: [PATCH 4/4] changes from bot review --- RomM/api.py | 60 ++++++++++++++++++++++------------------------ RomM/filesystem.py | 16 +------------ RomM/imageutils.py | 28 ++++++++++++---------- 3 files changed, 45 insertions(+), 59 deletions(-) diff --git a/RomM/api.py b/RomM/api.py index fd6b26b..d4a790d 100644 --- a/RomM/api.py +++ b/RomM/api.py @@ -489,6 +489,7 @@ def fetch_roms(self) -> None: if view == View.PLATFORMS and platform_slug != selected_platform_slug: continue + metadatum = rom.get("metadatum", {}) _roms.append( Rom( id=rom["id"], @@ -503,29 +504,29 @@ def fetch_roms(self) -> None: name=rom["name"], slug=rom["slug"], summary=rom["summary"], - youtube_video_id=rom["youtube_video_id"], + youtube_video_id=rom.get("youtube_video_id", None), path_cover_small=rom["path_cover_small"], path_cover_large=rom["path_cover_large"], is_identified=rom["is_identified"], - revision=rom["revision"], - regions=rom["regions"], - languages=rom["languages"], - tags=rom["tags"], - crc_hash=rom["crc_hash"], - md5_hash=rom["md5_hash"], - sha1_hash=rom["sha1_hash"], - has_simple_single_file=rom["has_simple_single_file"], - has_nested_single_file=rom["has_nested_single_file"], - has_multiple_files=rom["has_multiple_files"], - merged_screenshots=rom["merged_screenshots"], - genres=rom["metadatum"]["genres"], - franchises=rom["metadatum"]["franchises"], - collections=rom["metadatum"]["collections"], - companies=rom["metadatum"]["companies"], - game_modes=rom["metadatum"]["game_modes"], - age_ratings=rom["metadatum"]["age_ratings"], - first_release_date=rom["metadatum"]["first_release_date"], - average_rating=rom["metadatum"]["average_rating"], + revision=rom.get("revision", None), + regions=rom.get("regions", []), + languages=rom.get("languages", []), + tags=rom.get("tags", []), + crc_hash=rom.get("crc_hash", ""), + md5_hash=rom.get("md5_hash", ""), + sha1_hash=rom.get("sha1_hash", ""), + has_simple_single_file=rom.get("has_simple_single_file", False), + has_nested_single_file=rom.get("has_nested_single_file", False), + has_multiple_files=rom.get("has_multiple_files", False), + merged_screenshots=rom.get("merged_screenshots", []), + genres=metadatum.get("genres", []), + franchises=metadatum.get("franchises", []), + collections=metadatum.get("collections", []), + companies=metadatum.get("companies", []), + game_modes=metadatum.get("game_modes", []), + age_ratings=metadatum.get("age_ratings", []), + first_release_date=metadatum.get("first_release_date", None), + average_rating=metadatum.get("average_rating", None), ) ) @@ -645,11 +646,15 @@ def download_rom(self) -> None: self._reset_download_status(valid_host=True) return - filename = self._sanitize_filename(rom.fs_name).split(".")[0] + # Check if the catalogue path is set and valid catalogue_path = self.file_system.get_catalogue_platform_path( rom.platform_slug ) - if rom.summary and catalogue_path: + if not catalogue_path: + continue + + filename = self._sanitize_filename(rom.fs_name_no_ext) + if rom.summary: text_path = os.path.join( catalogue_path, "text", @@ -683,13 +688,6 @@ def download_rom(self) -> None: if not self._download_assets: continue - # Check if the catalogue path is set and valid - catalogue_path = self.file_system.get_catalogue_platform_path( - rom.platform_slug - ) - if not catalogue_path: - continue - box_path = os.path.join(catalogue_path, "box", f"{filename}.png") preview_path = os.path.join(catalogue_path, "preview", f"{filename}.png") @@ -699,8 +697,8 @@ def download_rom(self) -> None: self.image_utils.process_assets( fullscreen=self._fullscreen_assets, - cover_url=f"{self.host}{rom.path_cover_small}", - screenshot_url=f"{self.host}{rom.merged_screenshots[0]}", + cover_url=rom.path_cover_small, + screenshot_urls=rom.merged_screenshots, box_path=box_path, preview_path=preview_path, headers=self.headers, diff --git a/RomM/filesystem.py b/RomM/filesystem.py index 92832bd..4bf9f7f 100644 --- a/RomM/filesystem.py +++ b/RomM/filesystem.py @@ -75,14 +75,6 @@ def _get_sd2_roms_storage_path(self) -> Optional[str]: """Return the secondary ROMs storage path if available.""" return self._sd2_roms_storage_path - def _get_sd1_catalogue_path(self) -> Optional[str]: - """Return the catalogue path for SD1.""" - return self._sd1_catalogue_path - - def _get_sd2_catalogue_path(self) -> Optional[str]: - """Return the catalogue path for SD2.""" - return self._sd2_catalogue_path - def _get_platform_storage_dir_from_mapping(self, platform: str) -> str: """ Return the platform-specific storage path, @@ -165,14 +157,8 @@ def get_platforms_storage_path(self, platform: str) -> str: return self._get_sd1_platforms_storage_path(platform) - def get_catalogue_path(self, platform: str) -> str | None: - """Return the catalogue path for a specific platform.""" - if self._current_sd == 2: - return self._get_sd2_catalogue_path() - - return self._get_sd1_catalogue_path() - def get_catalogue_platform_path(self, platform: str) -> str: + """Return the catalogue path for a specific platform.""" if self._current_sd == 2: return self.get_sd2_catalogue_platform_path(platform) diff --git a/RomM/imageutils.py b/RomM/imageutils.py index c97ae6c..cc09853 100644 --- a/RomM/imageutils.py +++ b/RomM/imageutils.py @@ -1,4 +1,8 @@ +import os +from io import BytesIO from typing import Optional +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen from PIL import Image, ImageDraw @@ -19,6 +23,7 @@ def __init__(self) -> None: if self._initialized: return + self.host = os.getenv("HOST", "").strip("/") self.fade_mask = self.generate_fade_mask() self._initialized = True @@ -38,7 +43,7 @@ def generate_fade_mask(self) -> Image.Image: return fade_mask - def add_rounded_corners(self, image, radius): + def add_rounded_corners(self, image: Image.Image, radius: int = 20): rounded_mask = Image.new("L", image.size, 0) draw = ImageDraw.Draw(rounded_mask) draw.rounded_rectangle( @@ -47,36 +52,33 @@ def add_rounded_corners(self, image, radius): image.putalpha(rounded_mask) return image - def load_image_from_url(self, url: str, headers) -> Image.Image | None: - from io import BytesIO - from urllib.request import Request, urlopen - + def load_image_from_url(self, url: str, headers: dict) -> Image.Image | None: try: req = Request(url.split("?")[0], headers=headers) with urlopen(req, timeout=60) as response: # trunk-ignore(bandit/B310) data = response.read() return Image.open(BytesIO(data)).convert("RGBA") - except Exception as e: + except (URLError, HTTPError, IOError) as e: print(f"Error loading image from URL {url}: {e}") return None def process_assets( self, fullscreen: bool, - cover_url: str, - screenshot_url: str, + cover_url: str | None, + screenshot_urls: list[str], box_path: str, preview_path: str, - headers, + headers: dict, ) -> None: - if not cover_url and not screenshot_url: + if not cover_url and not screenshot_urls: return final_width, final_height = self.screen_width, self.screen_height background = None preview = ( - self.load_image_from_url(screenshot_url, headers) - if screenshot_url + self.load_image_from_url(screenshot_urls[0], headers) + if len(screenshot_urls) > 0 else None ) @@ -104,7 +106,7 @@ def process_assets( new_cover_width = int(foreground.width * scale) new_cover_height = int(foreground.height * scale) foreground = foreground.resize((new_cover_width, new_cover_height)) - foreground = self.add_rounded_corners(foreground, radius=20) + foreground = self.add_rounded_corners(foreground) fg_x = final_width - new_cover_width - 20 fg_y = (final_height - new_cover_height) // 2