Remove folders from config endpoint, add lazy-loaded /api/system/folders endpoint

Co-authored-by: jbatesy <51190172+jbatesy@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-03-09 05:27:45 +00:00
parent 5fabd40a47
commit 66bfc4cf99
7 changed files with 98 additions and 9 deletions

23
API.md
View file

@ -2446,10 +2446,6 @@ or an error:
}
],
"paused": false,
"folders": [
{"name": "folder1", "path": "folder1"},
{"name": "folder2", "path": "folder2"}
],
"history_count": 150,
"queue": [
{
@ -2464,11 +2460,28 @@ or an error:
**Notes**:
- This endpoint combines multiple data sources into a single response for efficient initialization
- The `folders` array includes available download folders up to the configured depth limit
- The `queue` array contains active download items
---
### GET /api/system/folders
**Purpose**: Retrieve available download folders. This endpoint is designed to be called lazily when the user interacts with the download path input.
**Response**:
```json
{
"folders": [
"folder1",
"folder2"
]
}
```
**Notes**:
- The `folders` array includes available download folders up to the configured depth limit
---
### POST /api/system/terminal
**Purpose**: Stream yt-dlp CLI output via Server-Sent Events (SSE). Requires `YTP_CONSOLE_ENABLED=true`.

View file

@ -49,12 +49,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(),
},
status=web.HTTPOk.status_code,
dumps=encoder.encode,

View file

@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
from app.library.config import Config
from app.library.encoder import Encoder
from app.library.UpdateChecker import UpdateChecker
from app.routes.api.system import check_updates, system_config
from app.routes.api.system import check_updates, system_config, system_folders
class TestSystemConfigEndpoint:
@ -31,7 +31,6 @@ class TestSystemConfigEndpoint:
with (
patch("app.routes.api.system.Presets") as mock_presets_cls,
patch("app.routes.api.system.DLFields") as mock_dl_fields_cls,
patch("app.routes.api.system.list_folders", return_value=[]),
):
mock_presets_cls.get_instance.return_value.get_all.return_value = []
mock_dl_fields_cls.get_instance.return_value.get_all_serialized = AsyncMock(return_value=[])
@ -46,7 +45,43 @@ class TestSystemConfigEndpoint:
assert "history_count" in body, "Configuration response should include history_count"
assert "presets" in body, "Configuration response should include presets"
assert "dl_fields" in body, "Configuration response should include dl_fields"
assert "folders" in body, "Configuration response should include folders"
assert "folders" not in body, "Configuration response should not include folders"
class TestSystemFoldersEndpoint:
"""Tests for the system folders endpoint."""
def setup_method(self):
"""Reset singletons before each test."""
Config._reset_singleton()
@pytest.mark.asyncio
async def test_system_folders_returns_folders(self):
"""Test that the folders endpoint returns the folders list."""
config = Config.get_instance()
encoder = Encoder()
with patch("app.routes.api.system.list_folders", return_value=["folder1", "subfolder/nested"]):
response = await system_folders(config, encoder)
assert 200 == response.status
body = json.loads(response.body.decode("utf-8"))
assert "folders" in body, "Folders response should include folders"
assert ["folder1", "subfolder/nested"] == body["folders"]
@pytest.mark.asyncio
async def test_system_folders_returns_empty_list(self):
"""Test that the folders endpoint returns an empty list when no folders exist."""
config = Config.get_instance()
encoder = Encoder()
with patch("app.routes.api.system.list_folders", return_value=[]):
response = await system_folders(config, encoder)
assert 200 == response.status
body = json.loads(response.body.decode("utf-8"))
assert "folders" in body, "Folders response should include folders"
assert [] == body["folders"]
class TestCheckUpdatesEndpoint:

View file

@ -128,6 +128,7 @@
:placeholder="getDefault('folder', '/')"
:disabled="addInProgress"
list="folders"
@focus="config.loadFolders()"
/>
</div>
</div>

View file

@ -189,6 +189,7 @@
v-model="form.folder"
:disabled="addInProgress"
list="folders"
@focus="config.loadFolders()"
/>
</div>
</div>

View file

@ -319,6 +319,7 @@
v-model="form.folder"
:disabled="addInProgress"
list="folders"
@focus="config.loadFolders()"
/>
</div>
</div>

View file

@ -95,6 +95,21 @@ export const useConfigStore = defineStore('config', () => {
}
};
const loadFolders = async () => {
try {
const resp = await request('/api/system/folders', { timeout: 10 });
if (!resp.ok) {
return;
}
const data = await resp.json();
if (Array.isArray(data.folders)) {
state.folders = data.folders;
}
} catch (e: any) {
console.error(`Failed to load folders: ${e}`);
}
};
const add = (key: string, value: any) => {
if (key.includes('.')) {
const [parentKey, subKey] = key.split('.') as [keyof ConfigState, string];
@ -209,6 +224,7 @@ export const useConfigStore = defineStore('config', () => {
isLoaded,
patch,
loadConfig,
loadFolders,
} as { [K in keyof ConfigState]: Ref<ConfigState[K]> } & {
add: typeof add;
get: typeof get;
@ -218,5 +234,6 @@ export const useConfigStore = defineStore('config', () => {
patch: typeof patch;
isLoaded: typeof isLoaded;
loadConfig: typeof loadConfig;
loadFolders: typeof loadFolders;
};
});