perf: lazy-load folders on demand; remove queue from config endpoint
Two startup cost reductions: 1. Folder enumeration is moved from /api/system/configuration to a new GET /api/system/folders endpoint. The frontend fetches it only when the user focuses a folder input field, and caches the result for the session. On large directory trees this was a noticeable startup delay. 2. The queue array is removed from /api/system/configuration. The queue is already loaded separately via /api/history/live on mount; including it in the config response was a double-load that inflated the initial payload for users with large queues.
This commit is contained in:
parent
e0dbc2d15d
commit
38c120ce85
6 changed files with 102 additions and 5 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@
|
|||
class="w-full"
|
||||
size="lg"
|
||||
:ui="{ root: 'w-full', base: 'bg-default/90' }"
|
||||
@focus="config.loadFolders()"
|
||||
/>
|
||||
</div>
|
||||
</UFormField>
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@
|
|||
:disabled="addInProgress"
|
||||
class="w-full"
|
||||
:ui="inputUi"
|
||||
@focus="config.loadFolders()"
|
||||
/>
|
||||
</div>
|
||||
</UFormField>
|
||||
|
|
|
|||
|
|
@ -300,6 +300,7 @@
|
|||
size="lg"
|
||||
class="w-full"
|
||||
:ui="inputUi"
|
||||
@focus="config.loadFolders()"
|
||||
/>
|
||||
</div>
|
||||
</UFormField>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { request } from '~/utils';
|
|||
|
||||
let last_reload = 0;
|
||||
const CONFIG_TTL = 10;
|
||||
let isLoadingFolders = false;
|
||||
|
||||
const state = reactive<ConfigState>({
|
||||
showForm: useStorage('showForm', true),
|
||||
|
|
@ -205,6 +206,23 @@ const patch = (feature: ConfigFeature, action: ConfigUpdateAction, data: unknown
|
|||
}
|
||||
};
|
||||
|
||||
const loadFolders = async (): Promise<void> => {
|
||||
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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue