diff --git a/app/routes/api/system.py b/app/routes/api/system.py index a9b604ad..42527d55 100644 --- a/app/routes/api/system.py +++ b/app/routes/api/system.py @@ -30,7 +30,7 @@ DIAGNOSTICS_CACHE_TTL = 5.0 @route("GET", "api/system/configuration", "system.configuration") async def system_config(queue: DownloadQueue, config: Config, encoder: Encoder) -> Response: """ - Pause non-active downloads. + Get the system configuration. Args: queue (DownloadQueue): The download queue instance. @@ -47,13 +47,33 @@ async def system_config(queue: DownloadQueue, config: Config, encoder: Encoder) "presets": Presets.get_instance().get_all(), "dl_fields": await DLFields.get_instance().get_all_serialized(), "paused": queue.is_paused(), + "history_count": await queue.done.get_total_count(), + }, + status=web.HTTPOk.status_code, + dumps=encoder.encode, + ) + + +@route("GET", "api/system/folders", "system.folders") +async def system_folders(config: Config, encoder: Encoder) -> Response: + """ + Get the list of folders available for downloads. + + Args: + config (Config): The config instance. + encoder (Encoder): The encoder instance. + + Returns: + Response: The response object. + + """ + return web.json_response( + data={ "folders": list_folders( path=Path(config.download_path), base=Path(config.download_path), depth_limit=config.download_path_depth - 1, ), - "history_count": await queue.done.get_total_count(), - "queue": (await queue.get("queue"))["queue"], }, status=web.HTTPOk.status_code, dumps=encoder.encode, diff --git a/app/tests/test_system_routes.py b/app/tests/test_system_routes.py index cde68baf..5aba2502 100644 --- a/app/tests/test_system_routes.py +++ b/app/tests/test_system_routes.py @@ -1,7 +1,7 @@ import json from dataclasses import dataclass from pathlib import Path -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -9,7 +9,7 @@ from app.library.config import Config from app.library.cache import Cache from app.library.encoder import Encoder from app.library.UpdateChecker import UpdateChecker -from app.routes.api.system import check_updates, system_diagnostics, system_limits +from app.routes.api.system import check_updates, system_config, system_diagnostics, system_folders, system_limits @dataclass @@ -395,3 +395,58 @@ class TestSystemDiagnosticsEndpoint: ) assert check.status == "skip" + + +class TestSystemFoldersEndpoint: + def setup_method(self): + Config._reset_singleton() + + @pytest.mark.asyncio + async def test_system_folders_returns_folder_list(self, tmp_path: Path) -> None: + """GET /api/system/folders returns the folders list.""" + config = Config.get_instance() + config.download_path = str(tmp_path) + config.download_path_depth = 1 + encoder = Encoder() + + response = await system_folders(config, encoder) + + assert response.status == 200 + data = json.loads(response.body.decode("utf-8")) + assert "folders" in data + assert isinstance(data["folders"], list) + + +class TestSystemConfigEndpoint: + def setup_method(self): + Config._reset_singleton() + + @pytest.mark.asyncio + async def test_system_configuration_excludes_queue_and_folders(self, tmp_path: Path) -> None: + """GET /api/system/configuration no longer includes queue or folders.""" + config = Config.get_instance() + config.download_path = str(tmp_path) + config.download_path_depth = 1 + encoder = Encoder() + + done_mock = MagicMock() + done_mock.get_total_count = AsyncMock(return_value=0) + queue = DummyQueue() + queue.done = done_mock + + with patch("app.features.dl_fields.service.DLFields.get_instance") as mock_dlfields, \ + patch("app.features.presets.service.Presets.get_instance") as mock_presets: + mock_dlfields_instance = MagicMock() + mock_dlfields_instance.get_all_serialized = AsyncMock(return_value=[]) + mock_dlfields.return_value = mock_dlfields_instance + + mock_presets_instance = MagicMock() + mock_presets_instance.get_all.return_value = [] + mock_presets.return_value = mock_presets_instance + + response = await system_config(queue, config, encoder) + + assert response.status == 200 + data = json.loads(response.body.decode("utf-8")) + assert "queue" not in data, "queue should be removed from config response" + assert "folders" not in data, "folders should be moved to /api/system/folders" diff --git a/ui/app/components/NewDownload.vue b/ui/app/components/NewDownload.vue index d1d8f54d..94cf7154 100644 --- a/ui/app/components/NewDownload.vue +++ b/ui/app/components/NewDownload.vue @@ -151,6 +151,7 @@ class="w-full" size="lg" :ui="{ root: 'w-full', base: 'bg-default/90' }" + @focus="config.loadFolders()" /> diff --git a/ui/app/components/PresetForm.vue b/ui/app/components/PresetForm.vue index 951ee1e3..9cb542f3 100644 --- a/ui/app/components/PresetForm.vue +++ b/ui/app/components/PresetForm.vue @@ -166,6 +166,7 @@ :disabled="addInProgress" class="w-full" :ui="inputUi" + @focus="config.loadFolders()" /> diff --git a/ui/app/components/TaskForm.vue b/ui/app/components/TaskForm.vue index 1099cb8f..55738f40 100644 --- a/ui/app/components/TaskForm.vue +++ b/ui/app/components/TaskForm.vue @@ -300,6 +300,7 @@ size="lg" class="w-full" :ui="inputUi" + @focus="config.loadFolders()" /> diff --git a/ui/app/composables/useYtpConfig.ts b/ui/app/composables/useYtpConfig.ts index 522475e2..eec3eff1 100644 --- a/ui/app/composables/useYtpConfig.ts +++ b/ui/app/composables/useYtpConfig.ts @@ -9,6 +9,7 @@ import { request } from '~/utils'; let last_reload = 0; const CONFIG_TTL = 10; +let isLoadingFolders = false; const state = reactive({ showForm: useStorage('showForm', true), @@ -205,6 +206,23 @@ const patch = (feature: ConfigFeature, action: ConfigUpdateAction, data: unknown } }; +const loadFolders = async (): Promise => { + if (state.folders.length > 0 || isLoadingFolders) return; + isLoadingFolders = true; + try { + const resp = await request('/api/system/folders', { timeout: 10 }); + if (!resp.ok) { + return; + } + const data = await resp.json(); + state.folders = data.folders ?? []; + } catch (e: any) { + console.error(`Failed to load folders: ${e}`); + } finally { + isLoadingFolders = false; + } +}; + const ytpConfigApi = proxyRefs({ ...toRefs(state), add, @@ -215,6 +233,7 @@ const ytpConfigApi = proxyRefs({ isLoaded, patch, loadConfig, + loadFolders, }); export const useYtpConfig = () => ytpConfigApi;