diff --git a/config_page.py b/config_page.py index 9b8c394..d21d2cb 100644 --- a/config_page.py +++ b/config_page.py @@ -757,9 +757,10 @@ CONFIG_HTML = r""" const rootGroup = document.createElement("div"); rootGroup.className = "browser-roots"; for (const rootPath of roots) { + const active = data.path === rootPath || String(data.path || "").startsWith(`${rootPath}/`); const button = document.createElement("button"); button.type = "button"; - button.className = `ghost${rootPath === data.path ? " active" : ""}`; + button.className = `ghost${active ? " active" : ""}`; button.textContent = rootPath; button.addEventListener("click", () => loadPathBrowser(rootPath)); rootGroup.appendChild(button); diff --git a/http_handler.py b/http_handler.py index 95732f5..9b8b56c 100644 --- a/http_handler.py +++ b/http_handler.py @@ -629,14 +629,17 @@ class Handler(BaseHTTPRequestHandler): Handler.update_config(self, Handler.require_json_object(self, self.body_json())) elif parsed.path == "/api/config/jellyfin/sync": payload = Handler.require_json_object(self, self.body_json()) + current = Handler._context(self).get_config_snapshot() + merged = dict(current) + merged.update(payload) for key, label in ( ("download_dir", "download directory"), ("jellyfin_tv_dir", "Jellyfin TV directory"), ("jellyfin_movie_dir", "Jellyfin movie directory"), ): - if str(payload.get(key) or "").strip(): - Handler.validate_remote_path(self, payload[key], label) - self.json(Handler._context(self).run_jellyfin_sync(payload)) + if str(merged.get(key) or "").strip(): + Handler.validate_remote_path(self, merged[key], label) + self.json(Handler._context(self).run_jellyfin_sync(merged)) elif parsed.path == "/api/config/webhook/test": payload = Handler.require_json_object(self, self.body_json()) validate_discord_webhook_url(payload.get("discord_webhook_url")) diff --git a/test_app.py b/test_app.py index db35d0a..a551e4f 100644 --- a/test_app.py +++ b/test_app.py @@ -3023,11 +3023,45 @@ class HandlerRouteTests(unittest.TestCase): body = b'{"download_dir":"/tmp/example","jellyfin_sync_enabled":true,"jellyfin_tv_dir":"/tmp/jellyfin-tv","jellyfin_movie_dir":"/tmp/jellyfin-movies"}' handler = DummyHandler("/api/config/jellyfin/sync", body=body, content_length=len(body)) payload = {"message": "Moved 1 watchlist entry to Jellyfin libraries.", "moved": 1, "total": 2, "items": []} - handler.handler_context = mock.Mock(run_jellyfin_sync=mock.Mock(return_value=payload)) + handler.handler_context = mock.Mock( + get_config_snapshot=mock.Mock(return_value={}), + run_jellyfin_sync=mock.Mock(return_value=payload), + http_error=APP.HttpError, + ) APP.Handler.do_POST(handler) self.assertEqual(handler.json_payload, payload) self.assertEqual(handler.json_status, HTTPStatus.OK) + def test_jellyfin_sync_post_merges_partial_payload_with_saved_config(self): + body = b'{"jellyfin_sync_enabled":true}' + handler = DummyHandler("/api/config/jellyfin/sync", body=body, content_length=len(body)) + payload = {"message": "Moved 0 watchlist entries to Jellyfin libraries.", "moved": 0, "total": 0, "items": []} + handler.handler_context = mock.Mock( + get_config_snapshot=mock.Mock( + return_value={ + "download_dir": "/tmp/example", + "mode": "sub", + "quality": "best", + "jellyfin_sync_enabled": False, + "jellyfin_tv_dir": "/tmp/jellyfin-tv", + "jellyfin_movie_dir": "/tmp/jellyfin-movies", + } + ), + run_jellyfin_sync=mock.Mock(return_value=payload), + ) + APP.Handler.do_POST(handler) + self.assertEqual(handler.json_status, HTTPStatus.OK) + handler.handler_context.run_jellyfin_sync.assert_called_once_with( + { + "download_dir": "/tmp/example", + "mode": "sub", + "quality": "best", + "jellyfin_sync_enabled": True, + "jellyfin_tv_dir": "/tmp/jellyfin-tv", + "jellyfin_movie_dir": "/tmp/jellyfin-movies", + } + ) + def test_generic_handler_exception_skips_traceback_when_debug_disabled(self): handler = DummyHandler("/api/config") with mock.patch.object(http_handler, "debug_enabled", return_value=False), mock.patch.object( @@ -3105,6 +3139,7 @@ class TemplateHelperTests(unittest.TestCase): self.assertIn('id="pathBrowserOverlay"', APP.CONFIG_HTML) self.assertIn('/api/fs/browse?path=${encodeURIComponent(path || state.pathBrowser.path || "")}', APP.CONFIG_HTML) self.assertIn('rootGroup.className = "browser-roots";', APP.CONFIG_HTML) + self.assertIn('const active = data.path === rootPath || String(data.path || "").startsWith(`${rootPath}/`);', APP.CONFIG_HTML) def test_search_page_sends_watchlist_auto_download_series(self): self.assertIn('auto_download_series: $("seasonInput").value || "1"', APP.INDEX_HTML)