diff --git a/backend/config/config_manager.py b/backend/config/config_manager.py index cca771065..53637bea7 100644 --- a/backend/config/config_manager.py +++ b/backend/config/config_manager.py @@ -116,8 +116,6 @@ class Config: ROMS_FOLDER_NAME: str FIRMWARE_FOLDER_NAME: str SKIP_HASH_CALCULATION: bool - STRUCTURE_PATH_A: str - STRUCTURE_PATH_B: str EJS_DEBUG: bool EJS_CACHE_LIMIT: int | None EJS_DISABLE_AUTO_UNLOAD: bool @@ -136,15 +134,16 @@ class Config: def __init__(self, **entries): self.__dict__.update(entries) - self.STRUCTURE_PATH_A = f"{LIBRARY_BASE_PATH}/{self.ROMS_FOLDER_NAME}/*" - self.STRUCTURE_PATH_B = f"{LIBRARY_BASE_PATH}/*/{self.ROMS_FOLDER_NAME}" @functools.cached_property - def has_structure_b(self) -> bool: + def has_structure_path_b(self) -> bool: """True when any `///` directory exists.""" - for match in glob.iglob(self.STRUCTURE_PATH_B): - if os.path.isdir(match): - return True + try: + for match in glob.iglob(f"{LIBRARY_BASE_PATH}/*/{self.ROMS_FOLDER_NAME}"): + if os.path.isdir(match): + return True + except OSError: + pass return False diff --git a/backend/handler/filesystem/firmware_handler.py b/backend/handler/filesystem/firmware_handler.py index c0ac7daf4..8422e0220 100644 --- a/backend/handler/filesystem/firmware_handler.py +++ b/backend/handler/filesystem/firmware_handler.py @@ -18,7 +18,7 @@ class FSFirmwareHandler(FSHandler): cnfg = cm.get_config() return ( f"{fs_slug}/{cnfg.FIRMWARE_FOLDER_NAME}" - if cnfg.has_structure_b + if cnfg.has_structure_path_b else f"{cnfg.FIRMWARE_FOLDER_NAME}/{fs_slug}" ) diff --git a/backend/handler/filesystem/platforms_handler.py b/backend/handler/filesystem/platforms_handler.py index c20c3632f..eed0e969a 100644 --- a/backend/handler/filesystem/platforms_handler.py +++ b/backend/handler/filesystem/platforms_handler.py @@ -34,35 +34,28 @@ class FSPlatformsHandler(FSHandler): """Detects the library structure type. Returns: - "LibraryStructure.A" for Structure A (roms/{platform}) - "LibraryStructure.B" for Structure B ({platform}/roms) - None if no structure detected + "LibraryStructure.B" for Structure B ({platform}/roms) when any + platform has a roms subfolder. + "LibraryStructure.A" for Structure A (roms/{platform}) when the + top-level roms folder exists. + None if no structure detected. """ cnfg = cm.get_config() - # Check if the roms folder exists (Structure A indicator) + if cnfg.has_structure_path_b: + return LibraryStructure.B + roms_path = os.path.join(LIBRARY_BASE_PATH, cnfg.ROMS_FOLDER_NAME) if os.path.exists(roms_path): return LibraryStructure.A - # Check if any platform folders with roms subfolders exist (Structure B) - try: - library_contents = os.listdir(LIBRARY_BASE_PATH) - for item in library_contents: - item_path = os.path.join(LIBRARY_BASE_PATH, item) - roms_subfolder = os.path.join(item_path, cnfg.ROMS_FOLDER_NAME) - if os.path.isdir(item_path) and os.path.exists(roms_subfolder): - return LibraryStructure.B - except (OSError, FileNotFoundError): - pass - return None def get_platforms_directory(self) -> str: cnfg = cm.get_config() # Fallback to config hint when detection is inconclusive - return "" if cnfg.has_structure_b else cnfg.ROMS_FOLDER_NAME + return "" if cnfg.has_structure_path_b else cnfg.ROMS_FOLDER_NAME def get_platform_fs_structure(self, fs_slug: str) -> str: cnfg = cm.get_config() @@ -70,7 +63,7 @@ class FSPlatformsHandler(FSHandler): # Fallback to config hint when detection is inconclusive return ( f"{fs_slug}/{cnfg.ROMS_FOLDER_NAME}" - if cnfg.has_structure_b + if cnfg.has_structure_path_b else f"{cnfg.ROMS_FOLDER_NAME}/{fs_slug}" ) diff --git a/backend/handler/filesystem/roms_handler.py b/backend/handler/filesystem/roms_handler.py index af48b9a58..8145a1f1e 100644 --- a/backend/handler/filesystem/roms_handler.py +++ b/backend/handler/filesystem/roms_handler.py @@ -314,7 +314,7 @@ class FSRomsHandler(FSHandler): cnfg = cm.get_config() return ( f"{fs_slug}/{cnfg.ROMS_FOLDER_NAME}" - if cnfg.has_structure_b + if cnfg.has_structure_path_b else f"{cnfg.ROMS_FOLDER_NAME}/{fs_slug}" ) diff --git a/backend/tests/handler/filesystem/test_firmware_handler.py b/backend/tests/handler/filesystem/test_firmware_handler.py index 0e0f7523c..28472933c 100644 --- a/backend/tests/handler/filesystem/test_firmware_handler.py +++ b/backend/tests/handler/filesystem/test_firmware_handler.py @@ -143,7 +143,7 @@ class TestFSFirmwareHandler: ): """Test that firmware paths are constructed correctly for Structure A""" platform_fs_slug = "n64" - config.has_structure_b = False + config.has_structure_path_b = False with patch( "handler.filesystem.firmware_handler.cm.get_config", return_value=config @@ -156,7 +156,7 @@ class TestFSFirmwareHandler: ): """Test that firmware paths are constructed correctly for Structure B""" platform_fs_slug = "n64" - config.has_structure_b = True + config.has_structure_path_b = True with patch( "handler.filesystem.firmware_handler.cm.get_config", return_value=config diff --git a/backend/tests/handler/filesystem/test_platforms_handler.py b/backend/tests/handler/filesystem/test_platforms_handler.py index 24c5302ce..fdcc12b1f 100644 --- a/backend/tests/handler/filesystem/test_platforms_handler.py +++ b/backend/tests/handler/filesystem/test_platforms_handler.py @@ -90,7 +90,7 @@ class TestFSPlatformsHandler: self, handler: FSPlatformsHandler, config ): """Test get_platforms_directory with Structure A (roms/{platform})""" - config.has_structure_b = False + config.has_structure_path_b = False with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): @@ -101,7 +101,7 @@ class TestFSPlatformsHandler: self, handler: FSPlatformsHandler, config ): """Test get_platforms_directory with Structure B ({platform}/roms)""" - config.has_structure_b = True + config.has_structure_path_b = True with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): @@ -113,7 +113,7 @@ class TestFSPlatformsHandler: ): """Test get_platform_fs_structure with Structure A (roms/{platform})""" fs_slug = "n64" - config.has_structure_b = False + config.has_structure_path_b = False with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): @@ -125,7 +125,7 @@ class TestFSPlatformsHandler: ): """Test get_platform_fs_structure with Structure B ({platform}/roms)""" fs_slug = "n64" - config.has_structure_b = True + config.has_structure_path_b = True with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): @@ -137,7 +137,7 @@ class TestFSPlatformsHandler: ): """Test get_platform_fs_structure with custom folder name (Structure B)""" fs_slug = "psx" - config_custom_folder.has_structure_b = True + config_custom_folder.has_structure_path_b = True with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config_custom_folder, @@ -151,7 +151,7 @@ class TestFSPlatformsHandler: """Test that add_platform creates the correct directory (Structure A)""" fs_slug = "gba" expected_path = f"{config.ROMS_FOLDER_NAME}/{fs_slug}" - config.has_structure_b = False + config.has_structure_path_b = False with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config @@ -166,7 +166,7 @@ class TestFSPlatformsHandler: """Test that add_platform creates directory with Structure B""" fs_slug = "gba" expected_path = f"{fs_slug}/{config.ROMS_FOLDER_NAME}" - config.has_structure_b = True + config.has_structure_path_b = True with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config @@ -203,7 +203,7 @@ class TestFSPlatformsHandler: self, handler: FSPlatformsHandler, config ): """Test that get_platforms calls list_directories with correct path""" - config.has_structure_b = False + config.has_structure_path_b = False with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): @@ -316,6 +316,7 @@ class TestFSPlatformsHandler: ): """Test detect_library_structure detects Structure A (roms/{platform})""" roms_path = f"{LIBRARY_BASE_PATH}/{config.ROMS_FOLDER_NAME}" + config.has_structure_path_b = False with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config @@ -331,64 +332,47 @@ class TestFSPlatformsHandler: self, handler: FSPlatformsHandler, config ): """Test detect_library_structure detects Structure B ({platform}/roms)""" + config.has_structure_path_b = True + with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): - with patch("os.path.exists") as mock_exists: - # ROMs folder doesn't exist at base level - mock_exists.return_value = False + result = handler.detect_library_structure() + assert result == LibraryStructure.B - with patch("os.listdir") as mock_listdir: - mock_listdir.return_value = ["n64", "psx", "other_folder"] + def test_detect_library_structure_b_takes_priority_over_a( + self, handler: FSPlatformsHandler, config + ): + """Structure B is reported even when the top-level roms folder exists.""" + config.has_structure_path_b = True - with patch("os.path.isdir") as mock_isdir: - # n64 and psx are directories with roms subfolders - def isdir_side_effect(path): - return "n64" in path or "psx" in path - - def exists_side_effect(path): - # n64/roms and psx/roms exist - return ( - f"n64/{config.ROMS_FOLDER_NAME}" in path - or f"psx/{config.ROMS_FOLDER_NAME}" in path - ) - - mock_isdir.side_effect = isdir_side_effect - mock_exists.side_effect = exists_side_effect - - result = handler.detect_library_structure() - assert result == LibraryStructure.B + with patch( + "handler.filesystem.platforms_handler.cm.get_config", return_value=config + ): + with patch("os.path.exists", return_value=True): + result = handler.detect_library_structure() + assert result == LibraryStructure.B def test_detect_library_structure_none(self, handler: FSPlatformsHandler, config): """Test detect_library_structure returns None when no structure detected""" - with patch( - "handler.filesystem.platforms_handler.cm.get_config", return_value=config - ): - with patch("os.path.exists", return_value=False): - with patch("os.listdir", return_value=[]): - result = handler.detect_library_structure() - assert result is None + config.has_structure_path_b = False - def test_detect_library_structure_handles_os_errors( - self, handler: FSPlatformsHandler, config - ): - """Test detect_library_structure handles OS errors gracefully""" with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): with patch("os.path.exists", return_value=False): - with patch("os.listdir", side_effect=OSError("Permission denied")): - result = handler.detect_library_structure() - assert result is None + result = handler.detect_library_structure() + assert result is None def test_detect_library_structure_empty_library( self, handler: FSPlatformsHandler, config ): """Test detect_library_structure with empty library directory""" + config.has_structure_path_b = False + with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): with patch("os.path.exists", return_value=False): - with patch("os.listdir", return_value=[]): - result = handler.detect_library_structure() - assert result is None + result = handler.detect_library_structure() + assert result is None diff --git a/backend/tests/handler/filesystem/test_roms_handler.py b/backend/tests/handler/filesystem/test_roms_handler.py index 4153ac5b0..7026dd97b 100644 --- a/backend/tests/handler/filesystem/test_roms_handler.py +++ b/backend/tests/handler/filesystem/test_roms_handler.py @@ -117,7 +117,7 @@ class TestFSRomsHandler: ROMS_FOLDER_NAME="roms", FIRMWARE_FOLDER_NAME="bios", ) - cfg.has_structure_b = True + cfg.has_structure_path_b = True with pytest.MonkeyPatch.context() as m: m.setattr("handler.filesystem.roms_handler.cm.get_config", lambda: cfg) @@ -140,7 +140,7 @@ class TestFSRomsHandler: ROMS_FOLDER_NAME="roms", FIRMWARE_FOLDER_NAME="bios", ) - cfg.has_structure_b = False + cfg.has_structure_path_b = False with pytest.MonkeyPatch.context() as m: m.setattr("handler.filesystem.roms_handler.cm.get_config", lambda: cfg) @@ -513,7 +513,7 @@ class TestFSRomsHandler: non_hashable_platform.fs_slug = "n64" non_hashable_platform.slug = "nintendo-64" - config.has_structure_b = True + config.has_structure_path_b = True with pytest.MonkeyPatch.context() as m: m.setattr("handler.filesystem.roms_handler.cm.get_config", lambda: config) @@ -551,12 +551,12 @@ class TestFSRomsHandler: m.setattr("handler.filesystem.roms_handler.cm.get_config", lambda: config) # Test with Structure B - config.has_structure_b = True + config.has_structure_path_b = True structure = handler.get_roms_fs_structure(fs_slug) assert structure == f"{fs_slug}/roms" # Test with Structure A - config.has_structure_b = False + config.has_structure_path_b = False structure = handler.get_roms_fs_structure(fs_slug) assert structure == f"roms/{fs_slug}" diff --git a/backend/watcher.py b/backend/watcher.py index 9234dea43..c3bf3d913 100644 --- a/backend/watcher.py +++ b/backend/watcher.py @@ -50,7 +50,7 @@ sentry_sdk.init( tracer = trace.get_tracer(__name__) cfg = cm.get_config() -structure_level = 1 if cfg.has_structure_b else 2 +structure_level = 1 if cfg.has_structure_path_b else 2 @enum.unique diff --git a/env.template b/env.template index fa9079aeb..74f9bf52b 100644 --- a/env.template +++ b/env.template @@ -1,7 +1,7 @@ # Core Application ROMM_BASE_PATH=/romm # Base folder path for library, resources and assets ROMM_TMP_PATH= # Custom temporary directory path -ROMM_BASE_URL=http://0.0.0.0 # Public URL of this instance (CORS origin, OIDC redirect base, links in container logs) +ROMM_BASE_URL=http://0.0.0.0 # Public URL of this instance ROMM_PORT=8080 # Port on which the application listens KIOSK_MODE=false # Read-only mode for public displays or kiosks