From 90694133c635bbc8dcc5e89a548509f778eeb6e1 Mon Sep 17 00:00:00 2001 From: Cobaltboy Date: Tue, 3 Feb 2026 19:42:45 +0530 Subject: [PATCH 1/8] Added Automatic Gamelist export support --- backend/config/config_manager.py | 13 +++++++++++++ backend/endpoints/sockets/scan.py | 23 +++++++++++++++++++++++ backend/utils/gamelist_exporter.py | 10 +++++++--- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/backend/config/config_manager.py b/backend/config/config_manager.py index 06206bc63..863b1f49d 100644 --- a/backend/config/config_manager.py +++ b/backend/config/config_manager.py @@ -76,6 +76,7 @@ class Config: EXCLUDED_MULTI_FILES: list[str] EXCLUDED_MULTI_PARTS_EXT: list[str] EXCLUDED_MULTI_PARTS_FILES: list[str] + GAMELIST_AUTO_EXPORT_ON_SCAN: bool PLATFORMS_BINDING: dict[str, str] PLATFORMS_VERSIONS: dict[str, str] ROMS_FOLDER_NAME: str @@ -222,6 +223,9 @@ def _parse_config(self): ), FIRMWARE_FOLDER_NAME=pydash.get( self._raw_config, "filesystem.firmware_folder", "bios" + ), + GAMELIST_AUTO_EXPORT_ON_SCAN=pydash.get( + self._raw_config, "gamelist.auto_export_on_scan", False ), SKIP_HASH_CALCULATION=pydash.get( self._raw_config, "filesystem.skip_hash_calculation", False @@ -364,6 +368,12 @@ def _validate_config(self): "Invalid config.yml: exclude.roms.multi_file.parts.names must be a list" ) sys.exit(3) + + if not isinstance(self.config.GAMELIST_AUTO_EXPORT_ON_SCAN, bool): + log.critical( + "Invalid config.yml: gamelist.auto_export_on_scan must be a boolean" + ) + sys.exit(3) if not isinstance(self.config.PLATFORMS_BINDING, dict): log.critical("Invalid config.yml: system.platforms must be a dictionary") @@ -571,6 +581,9 @@ def _update_config_file(self) -> None: }, "media": self.config.SCAN_MEDIA, }, + "gamelist": { + "auto_export_on_scan": self.config.GAMELIST_AUTO_EXPORT_ON_SCAN, + }, } try: diff --git a/backend/endpoints/sockets/scan.py b/backend/endpoints/sockets/scan.py index 81bf69770..56a2c2e2d 100644 --- a/backend/endpoints/sockets/scan.py +++ b/backend/endpoints/sockets/scan.py @@ -22,6 +22,7 @@ RomsNotFoundException, ) from exceptions.socket_exceptions import ScanStoppedException +from utils.gamelist_exporter import GamelistExporter from handler.database import db_firmware_handler, db_platform_handler, db_rom_handler from handler.filesystem import ( fs_firmware_handler, @@ -683,6 +684,28 @@ async def stop_scan(): log.warning(f" - {p.slug} ({p.fs_slug})") log.info(f"{emoji.EMOJI_CHECK_MARK} Scan completed") + + # Export gamelist.xml if enabled in config + if cm.get_config().GAMELIST_AUTO_EXPORT_ON_SCAN: + log.info("Auto-exporting gamelist.xml for all platforms...") + gamelist_exporter = GamelistExporter(local_export=True) + for platform_slug in platform_list: + platform = db_platform_handler.get_platform_by_fs_slug(platform_slug) + if platform: + try: + await gamelist_exporter.export_platform_to_file( + platform.id, + request=None, + ) + log.info( + f"Auto-exported gamelist.xml for platform {platform.name} after scan" + ) + except Exception as e: + log.error( + f"Failed to auto-export gamelist.xml for platform {platform.name}: {e}" + ) + log.info("Gamelist.xml auto-export completed.") + await socket_manager.emit("scan:done", scan_stats.to_dict()) except ScanStoppedException: await stop_scan() diff --git a/backend/utils/gamelist_exporter.py b/backend/utils/gamelist_exporter.py index 66987cce8..d091c2ff9 100644 --- a/backend/utils/gamelist_exporter.py +++ b/backend/utils/gamelist_exporter.py @@ -25,7 +25,7 @@ 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: Request) -> Element: + def _create_game_element(self, rom: Rom, request: Request | None) -> Element: """Create a element for a ROM""" game = Element("game") @@ -33,6 +33,10 @@ def _create_game_element(self, rom: Rom, request: Request) -> Element: if self.local_export: SubElement(game, "path").text = f"./{rom.fs_name}" else: + if request is None: + raise ValueError( + "Request object must be provided for non-local exports" + ) SubElement(game, "path").text = str( request.url_for( "get_rom_content", @@ -155,7 +159,7 @@ def _create_game_element(self, rom: Rom, request: Request) -> Element: return game - def export_platform_to_xml(self, platform_id: int, request: Request) -> str: + def export_platform_to_xml(self, platform_id: int, request: Request | None) -> str: """Export a platform's ROMs to gamelist.xml format Args: @@ -188,7 +192,7 @@ def export_platform_to_xml(self, platform_id: int, request: Request) -> str: async def export_platform_to_file( self, platform_id: int, - request: Request, + request: Request | None, ) -> bool: """Export platform ROMs to gamelist.xml file in the platform's directory From d681ca1f8662f3c19dd333ff1310564b9c77b572 Mon Sep 17 00:00:00 2001 From: Cobaltboy Date: Wed, 4 Feb 2026 12:15:31 +0530 Subject: [PATCH 2/8] Optimise database query for auto_export Pulls database call out of the loop Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- backend/endpoints/sockets/scan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/endpoints/sockets/scan.py b/backend/endpoints/sockets/scan.py index 56a2c2e2d..a13b4b9f4 100644 --- a/backend/endpoints/sockets/scan.py +++ b/backend/endpoints/sockets/scan.py @@ -689,8 +689,9 @@ async def stop_scan(): if cm.get_config().GAMELIST_AUTO_EXPORT_ON_SCAN: log.info("Auto-exporting gamelist.xml for all platforms...") gamelist_exporter = GamelistExporter(local_export=True) + platforms_by_slug = {p.fs_slug: p for p in db_platform_handler.get_platforms()} for platform_slug in platform_list: - platform = db_platform_handler.get_platform_by_fs_slug(platform_slug) + platform = platforms_by_slug.get(platform_slug) if platform: try: await gamelist_exporter.export_platform_to_file( From 0c2f27d6690f2de2b65214cdbafe01b5c2709f76 Mon Sep 17 00:00:00 2001 From: Cobaltboy Date: Wed, 4 Feb 2026 13:39:18 +0530 Subject: [PATCH 3/8] formatting cleanup Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- backend/config/config_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/config/config_manager.py b/backend/config/config_manager.py index 863b1f49d..7dc46115c 100644 --- a/backend/config/config_manager.py +++ b/backend/config/config_manager.py @@ -368,7 +368,6 @@ def _validate_config(self): "Invalid config.yml: exclude.roms.multi_file.parts.names must be a list" ) sys.exit(3) - if not isinstance(self.config.GAMELIST_AUTO_EXPORT_ON_SCAN, bool): log.critical( "Invalid config.yml: gamelist.auto_export_on_scan must be a boolean" From 5464b3748c5ca71de06392644b22e3b185a03b38 Mon Sep 17 00:00:00 2001 From: Cobaltboy Date: Wed, 4 Feb 2026 13:39:51 +0530 Subject: [PATCH 4/8] formatting cleanup Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- backend/config/config_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/config/config_manager.py b/backend/config/config_manager.py index 7dc46115c..95f45eb8a 100644 --- a/backend/config/config_manager.py +++ b/backend/config/config_manager.py @@ -223,7 +223,7 @@ def _parse_config(self): ), FIRMWARE_FOLDER_NAME=pydash.get( self._raw_config, "filesystem.firmware_folder", "bios" - ), + ), GAMELIST_AUTO_EXPORT_ON_SCAN=pydash.get( self._raw_config, "gamelist.auto_export_on_scan", False ), From 2944ad9b35a496170ec45f38b2cf5fd510831f39 Mon Sep 17 00:00:00 2001 From: Cobaltboy Date: Wed, 4 Feb 2026 13:40:39 +0530 Subject: [PATCH 5/8] improved logging conditioning Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- backend/endpoints/sockets/scan.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/endpoints/sockets/scan.py b/backend/endpoints/sockets/scan.py index a13b4b9f4..3e1a12e56 100644 --- a/backend/endpoints/sockets/scan.py +++ b/backend/endpoints/sockets/scan.py @@ -693,17 +693,17 @@ async def stop_scan(): for platform_slug in platform_list: platform = platforms_by_slug.get(platform_slug) if platform: - try: - await gamelist_exporter.export_platform_to_file( - platform.id, - request=None, - ) + export_success = await gamelist_exporter.export_platform_to_file( + platform.id, + request=None, + ) + if export_success: log.info( f"Auto-exported gamelist.xml for platform {platform.name} after scan" ) - except Exception as e: - log.error( - f"Failed to auto-export gamelist.xml for platform {platform.name}: {e}" + else: + log.warning( + f"Failed to auto-export gamelist.xml for platform {platform.name} after scan" ) log.info("Gamelist.xml auto-export completed.") From 0d2ddb1b8b3fa5334b3339befefb30cf37268d34 Mon Sep 17 00:00:00 2001 From: Cobaltboy Date: Wed, 4 Feb 2026 13:41:19 +0530 Subject: [PATCH 6/8] minor config optimization Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- backend/endpoints/sockets/scan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/endpoints/sockets/scan.py b/backend/endpoints/sockets/scan.py index 3e1a12e56..39fe1cfe9 100644 --- a/backend/endpoints/sockets/scan.py +++ b/backend/endpoints/sockets/scan.py @@ -686,7 +686,8 @@ async def stop_scan(): log.info(f"{emoji.EMOJI_CHECK_MARK} Scan completed") # Export gamelist.xml if enabled in config - if cm.get_config().GAMELIST_AUTO_EXPORT_ON_SCAN: + config = cm.get_config() + if config.GAMELIST_AUTO_EXPORT_ON_SCAN: log.info("Auto-exporting gamelist.xml for all platforms...") gamelist_exporter = GamelistExporter(local_export=True) platforms_by_slug = {p.fs_slug: p for p in db_platform_handler.get_platforms()} From d3ededffd63648465dfae9b52c49b26fb7a38852 Mon Sep 17 00:00:00 2001 From: Cobaltboy Date: Thu, 5 Feb 2026 14:05:00 +0530 Subject: [PATCH 7/8] renamed configuration --- backend/config/config_manager.py | 9 ++++----- backend/endpoints/sockets/scan.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/backend/config/config_manager.py b/backend/config/config_manager.py index 95f45eb8a..6c0b415b6 100644 --- a/backend/config/config_manager.py +++ b/backend/config/config_manager.py @@ -225,7 +225,7 @@ def _parse_config(self): self._raw_config, "filesystem.firmware_folder", "bios" ), GAMELIST_AUTO_EXPORT_ON_SCAN=pydash.get( - self._raw_config, "gamelist.auto_export_on_scan", False + self._raw_config, "scan.export_gamelist", False ), SKIP_HASH_CALCULATION=pydash.get( self._raw_config, "filesystem.skip_hash_calculation", False @@ -370,7 +370,7 @@ def _validate_config(self): sys.exit(3) if not isinstance(self.config.GAMELIST_AUTO_EXPORT_ON_SCAN, bool): log.critical( - "Invalid config.yml: gamelist.auto_export_on_scan must be a boolean" + "Invalid config.yml: scan.export_gamelist must be a boolean" ) sys.exit(3) @@ -579,10 +579,9 @@ def _update_config_file(self) -> None: "language": self.config.SCAN_LANGUAGE_PRIORITY, }, "media": self.config.SCAN_MEDIA, + "export_gamelist": self.config.GAMELIST_AUTO_EXPORT_ON_SCAN, }, - "gamelist": { - "auto_export_on_scan": self.config.GAMELIST_AUTO_EXPORT_ON_SCAN, - }, + } try: diff --git a/backend/endpoints/sockets/scan.py b/backend/endpoints/sockets/scan.py index 39fe1cfe9..8cfb76aa8 100644 --- a/backend/endpoints/sockets/scan.py +++ b/backend/endpoints/sockets/scan.py @@ -786,4 +786,4 @@ async def cancel_job(job: Job): ): return await cancel_job(current_job) - log.info(f"{emoji.EMOJI_STOP_BUTTON} No running scan to stop") + log.info(f"{emoji.EMOJI_STOP_BUTTON} No running scan to stop") \ No newline at end of file From 5712f651d39747da0b6d41716719b5a69933984b Mon Sep 17 00:00:00 2001 From: Cobaltboy Date: Thu, 5 Feb 2026 19:57:39 +0530 Subject: [PATCH 8/8] Formatted files --- backend/config/config_manager.py | 5 +---- backend/endpoints/sockets/scan.py | 10 ++++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/backend/config/config_manager.py b/backend/config/config_manager.py index 6c0b415b6..a98bd14e2 100644 --- a/backend/config/config_manager.py +++ b/backend/config/config_manager.py @@ -369,9 +369,7 @@ def _validate_config(self): ) 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.export_gamelist must be a boolean") sys.exit(3) if not isinstance(self.config.PLATFORMS_BINDING, dict): @@ -581,7 +579,6 @@ def _update_config_file(self) -> None: "media": self.config.SCAN_MEDIA, "export_gamelist": self.config.GAMELIST_AUTO_EXPORT_ON_SCAN, }, - } try: diff --git a/backend/endpoints/sockets/scan.py b/backend/endpoints/sockets/scan.py index 8cfb76aa8..a2ff80cf5 100644 --- a/backend/endpoints/sockets/scan.py +++ b/backend/endpoints/sockets/scan.py @@ -22,7 +22,6 @@ RomsNotFoundException, ) from exceptions.socket_exceptions import ScanStoppedException -from utils.gamelist_exporter import GamelistExporter from handler.database import db_firmware_handler, db_platform_handler, db_rom_handler from handler.filesystem import ( fs_firmware_handler, @@ -51,6 +50,7 @@ from tasks.tasks import update_job_meta from utils import emoji from utils.context import initialize_context +from utils.gamelist_exporter import GamelistExporter STOP_SCAN_FLAG: Final = "scan:stop" @@ -690,7 +690,9 @@ async def stop_scan(): if config.GAMELIST_AUTO_EXPORT_ON_SCAN: log.info("Auto-exporting gamelist.xml for all platforms...") gamelist_exporter = GamelistExporter(local_export=True) - platforms_by_slug = {p.fs_slug: p for p in db_platform_handler.get_platforms()} + platforms_by_slug = { + p.fs_slug: p for p in db_platform_handler.get_platforms() + } for platform_slug in platform_list: platform = platforms_by_slug.get(platform_slug) if platform: @@ -707,7 +709,7 @@ async def stop_scan(): f"Failed to auto-export gamelist.xml for platform {platform.name} after scan" ) log.info("Gamelist.xml auto-export completed.") - + await socket_manager.emit("scan:done", scan_stats.to_dict()) except ScanStoppedException: await stop_scan() @@ -786,4 +788,4 @@ async def cancel_job(job: Job): ): return await cancel_job(current_job) - log.info(f"{emoji.EMOJI_STOP_BUTTON} No running scan to stop") \ No newline at end of file + log.info(f"{emoji.EMOJI_STOP_BUTTON} No running scan to stop")