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:
parent
5fabd40a47
commit
66bfc4cf99
7 changed files with 98 additions and 9 deletions
23
API.md
23
API.md
|
|
@ -2446,10 +2446,6 @@ or an error:
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paused": false,
|
"paused": false,
|
||||||
"folders": [
|
|
||||||
{"name": "folder1", "path": "folder1"},
|
|
||||||
{"name": "folder2", "path": "folder2"}
|
|
||||||
],
|
|
||||||
"history_count": 150,
|
"history_count": 150,
|
||||||
"queue": [
|
"queue": [
|
||||||
{
|
{
|
||||||
|
|
@ -2464,11 +2460,28 @@ or an error:
|
||||||
|
|
||||||
**Notes**:
|
**Notes**:
|
||||||
- This endpoint combines multiple data sources into a single response for efficient initialization
|
- 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
|
- 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
|
### POST /api/system/terminal
|
||||||
**Purpose**: Stream yt-dlp CLI output via Server-Sent Events (SSE). Requires `YTP_CONSOLE_ENABLED=true`.
|
**Purpose**: Stream yt-dlp CLI output via Server-Sent Events (SSE). Requires `YTP_CONSOLE_ENABLED=true`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,33 @@ async def system_config(queue: DownloadQueue, config: Config, encoder: Encoder)
|
||||||
"presets": Presets.get_instance().get_all(),
|
"presets": Presets.get_instance().get_all(),
|
||||||
"dl_fields": await DLFields.get_instance().get_all_serialized(),
|
"dl_fields": await DLFields.get_instance().get_all_serialized(),
|
||||||
"paused": queue.is_paused(),
|
"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(
|
"folders": list_folders(
|
||||||
path=Path(config.download_path),
|
path=Path(config.download_path),
|
||||||
base=Path(config.download_path),
|
base=Path(config.download_path),
|
||||||
depth_limit=config.download_path_depth - 1,
|
depth_limit=config.download_path_depth - 1,
|
||||||
),
|
),
|
||||||
"history_count": await queue.done.get_total_count(),
|
|
||||||
},
|
},
|
||||||
status=web.HTTPOk.status_code,
|
status=web.HTTPOk.status_code,
|
||||||
dumps=encoder.encode,
|
dumps=encoder.encode,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
from app.library.config import Config
|
from app.library.config import Config
|
||||||
from app.library.encoder import Encoder
|
from app.library.encoder import Encoder
|
||||||
from app.library.UpdateChecker import UpdateChecker
|
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:
|
class TestSystemConfigEndpoint:
|
||||||
|
|
@ -31,7 +31,6 @@ class TestSystemConfigEndpoint:
|
||||||
with (
|
with (
|
||||||
patch("app.routes.api.system.Presets") as mock_presets_cls,
|
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.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_presets_cls.get_instance.return_value.get_all.return_value = []
|
||||||
mock_dl_fields_cls.get_instance.return_value.get_all_serialized = AsyncMock(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 "history_count" in body, "Configuration response should include history_count"
|
||||||
assert "presets" in body, "Configuration response should include presets"
|
assert "presets" in body, "Configuration response should include presets"
|
||||||
assert "dl_fields" in body, "Configuration response should include dl_fields"
|
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:
|
class TestCheckUpdatesEndpoint:
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@
|
||||||
:placeholder="getDefault('folder', '/')"
|
:placeholder="getDefault('folder', '/')"
|
||||||
:disabled="addInProgress"
|
:disabled="addInProgress"
|
||||||
list="folders"
|
list="folders"
|
||||||
|
@focus="config.loadFolders()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,7 @@
|
||||||
v-model="form.folder"
|
v-model="form.folder"
|
||||||
:disabled="addInProgress"
|
:disabled="addInProgress"
|
||||||
list="folders"
|
list="folders"
|
||||||
|
@focus="config.loadFolders()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -319,6 +319,7 @@
|
||||||
v-model="form.folder"
|
v-model="form.folder"
|
||||||
:disabled="addInProgress"
|
:disabled="addInProgress"
|
||||||
list="folders"
|
list="folders"
|
||||||
|
@focus="config.loadFolders()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
const add = (key: string, value: any) => {
|
||||||
if (key.includes('.')) {
|
if (key.includes('.')) {
|
||||||
const [parentKey, subKey] = key.split('.') as [keyof ConfigState, string];
|
const [parentKey, subKey] = key.split('.') as [keyof ConfigState, string];
|
||||||
|
|
@ -209,6 +224,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||||
isLoaded,
|
isLoaded,
|
||||||
patch,
|
patch,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
|
loadFolders,
|
||||||
} as { [K in keyof ConfigState]: Ref<ConfigState[K]> } & {
|
} as { [K in keyof ConfigState]: Ref<ConfigState[K]> } & {
|
||||||
add: typeof add;
|
add: typeof add;
|
||||||
get: typeof get;
|
get: typeof get;
|
||||||
|
|
@ -218,5 +234,6 @@ export const useConfigStore = defineStore('config', () => {
|
||||||
patch: typeof patch;
|
patch: typeof patch;
|
||||||
isLoaded: typeof isLoaded;
|
isLoaded: typeof isLoaded;
|
||||||
loadConfig: typeof loadConfig;
|
loadConfig: typeof loadConfig;
|
||||||
|
loadFolders: typeof loadFolders;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue