From 739d8e702f7728c311e300f2dce961c06a7480be Mon Sep 17 00:00:00 2001 From: arabcoders Date: Thu, 13 Nov 2025 19:28:10 +0300 Subject: [PATCH] Feat: Paginate history items to improve performance --- API.md | 27 ++++-- FAQ.md | 1 + app/library/config.py | 5 + app/routes/api/history.py | 8 +- app/routes/socket/connection.py | 3 +- ui/app/components/FloatingImage.vue | 32 ++++--- ui/app/components/History.vue | 138 ++++++++++++++++++++++++++-- ui/app/components/Message.vue | 82 +++++++++++------ ui/app/components/Pager.vue | 70 ++++++++++++++ ui/app/stores/ConfigStore.ts | 1 + ui/app/stores/SocketStore.ts | 20 +++- ui/app/stores/StateStore.ts | 125 ++++++++++++++++++++++++- ui/app/types/config.d.ts | 2 + 13 files changed, 448 insertions(+), 66 deletions(-) create mode 100644 ui/app/components/Pager.vue diff --git a/API.md b/API.md index d9f8053a..b71649e4 100644 --- a/API.md +++ b/API.md @@ -81,6 +81,7 @@ This document describes the available endpoints and their usage. All endpoints r - [Connection Events](#connection-events) - [`connect` (Built-in)](#connect-built-in) - [`disconnect` (Built-in)](#disconnect-built-in) + - [`configuration` (Server → Client)](#configuration-server--client) - [`connected` (Server → Client)](#connected-server--client) - [Subscription Events](#subscription-events) - [`subscribe` (Client → Server)](#subscribe-client--server) @@ -450,7 +451,7 @@ or an error: - `queue`: Returns only queue items (with pagination) - `done`: Returns only history items (with pagination) - `page` (optional): Page number (1-indexed). Default: `1`. Only used when `type != all` -- `per_page` (optional): Items per page. Default: `50`, Max: `200`. Only used when `type != all` +- `per_page` (optional): Items per page. Default: `config.default_pagination`, Max: `200`. Only used when `type != all` - `order` (optional): Sort order. Default: `DESC`. Only used when `type != all` - `DESC`: Newest items first (descending by creation date) - `ASC`: Oldest items first (ascending by creation date) @@ -1733,25 +1734,35 @@ Fired when WebSocket connection is closed. No data payload. socket.on('disconnect', (reason: string) => console.log('WebSocket disconnected:', reason)); ``` -##### `connected` (Server → Client) -Initial connection event with full application state. +##### `configuration` (Server → Client) +Sends the current application configuration. **Data Fields**: - `config`: Global configuration object -- `queue`: Current download queue (array of items) -- `done`: Download history (array of completed items) -- `tasks`: Scheduled tasks - `presets`: Available download presets - `dl_fields`: Available download fields -- `folders`: Directory structure for downloads - `paused`: Queue pause status (boolean) +**Example**: +```typescript +socket.on('connected', (data: string) => { + const json = JSON.parse(data); + console.log('Current configuration:', json.data.config); +}); +``` + +##### `connected` (Server → Client) +When a client connects, this events sends the folder and current queue. + +**Data Fields**: +- `queue`: Current download queue (array of items) +- `folders`: Directory structure for downloads + **Example**: ```typescript socket.on('connected', (data: string) => { const json = JSON.parse(data); const queueItems = json.data.queue || {}; - const historyItems = json.data.done || {}; console.log('Connected with', Object.keys(queueItems).length, 'queued downloads'); }); ``` diff --git a/FAQ.md b/FAQ.md index 5ba877b9..acb74aa0 100644 --- a/FAQ.md +++ b/FAQ.md @@ -48,6 +48,7 @@ or the `environment:` section in `compose.yaml` file. | YTP_SIMPLE_MODE | Switch default interface to Simple mode. | `false` | | YTP_STATIC_UI_PATH | Path to custom static UI files. | `(not_set)` | | YTP_AUTO_CLEAR_HISTORY_DAYS | Number of days after which completed download history is cleared. | `0` | +| YTP_DEFAULT_PAGINATION | The default number of items per page for history. | `50` | > [!NOTE] > To raise the maximum workers for specific extractor, you need to add a ENV variable that follows the pattern `YTP_MAX_WORKERS_FOR_`. diff --git a/app/library/config.py b/app/library/config.py index 05abc8f2..ad78464c 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -195,6 +195,9 @@ class Config(metaclass=Singleton): auto_clear_history_days: int = 0 """Number of days after which completed download history is automatically cleared. 0 to disable.""" + default_pagination: int = 50 + """The default number of items per page for pagination.""" + pictures_backends: list[str] = [ "https://unsplash.it/1920/1080?random", "https://picsum.photos/1920/1080", @@ -234,6 +237,7 @@ class Config(metaclass=Singleton): "download_path_depth", "download_info_expires", "auto_clear_history_days", + "default_pagination", ) "The variables that are integers." @@ -281,6 +285,7 @@ class Config(metaclass=Singleton): "app_commit_sha", "app_build_date", "app_branch", + "default_pagination", ) "The variables that are relevant to the frontend." diff --git a/app/routes/api/history.py b/app/routes/api/history.py index 9e162823..a1920ecf 100644 --- a/app/routes/api/history.py +++ b/app/routes/api/history.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING from aiohttp import web from aiohttp.web import Request, Response +from app.library.config import Config from app.library.Download import Download from app.library.DownloadQueue import DownloadQueue from app.library.encoder import Encoder @@ -20,7 +21,7 @@ LOG: logging.Logger = logging.getLogger(__name__) @route("GET", r"api/history/", "items_list") -async def items_list(request: Request, queue: DownloadQueue, encoder: Encoder) -> Response: +async def items_list(request: Request, queue: DownloadQueue, encoder: Encoder, config: Config) -> Response: """ Get the history with optional pagination support. @@ -28,6 +29,7 @@ async def items_list(request: Request, queue: DownloadQueue, encoder: Encoder) - request (Request): The request object. queue (DownloadQueue): The download queue instance. encoder (Encoder): The encoder instance. + config (Config): The configuration instance. Returns: Response: The response object. @@ -63,7 +65,7 @@ async def items_list(request: Request, queue: DownloadQueue, encoder: Encoder) - try: page = int(request.query.get("page", 1)) - per_page = int(request.query.get("per_page", 50)) + per_page = int(request.query.get("per_page", config.default_pagination)) order = request.query.get("order", "DESC").upper() except ValueError: return web.json_response( @@ -74,7 +76,7 @@ async def items_list(request: Request, queue: DownloadQueue, encoder: Encoder) - if page < 1: return web.json_response(data={"error": "page must be >= 1."}, status=web.HTTPBadRequest.status_code) - if per_page < 1 or per_page > 200: + if per_page < 1 or per_page > 1000: return web.json_response( data={"error": "per_page must be between 1 and 1000."}, status=web.HTTPBadRequest.status_code, diff --git a/app/routes/socket/connection.py b/app/routes/socket/connection.py index 3364386d..d7a51c61 100644 --- a/app/routes/socket/connection.py +++ b/app/routes/socket/connection.py @@ -44,7 +44,8 @@ async def connect(config: Config, queue: DownloadQueue, notify: EventBus, sid: s base=Path(config.download_path), depth_limit=config.download_path_depth - 1, ), - **queue.get(), + "history_count": queue.done.get_total_count(), + **queue.get()["queue"], }, title="Sending initial download data", message=f"Sending initial download data to client '{sid}'.", diff --git a/ui/app/components/FloatingImage.vue b/ui/app/components/FloatingImage.vue index 6db9b82e..bd311c45 100644 --- a/ui/app/components/FloatingImage.vue +++ b/ui/app/components/FloatingImage.vue @@ -2,19 +2,26 @@ @@ -36,6 +43,7 @@ const thumbnail_ratio = useStorage<'is-16by9' | 'is-3by1'>('thumbnail_ratio', 'i const url = ref(null) const error = ref(false) const isPreloading = ref(false) +const error_msg = ref(null) const loadTimer: ReturnType | null = null const cancelRequest = new AbortController() @@ -54,7 +62,7 @@ const defaultLoader = async (): Promise => { const response = await request(props.image, { signal: cancelRequest.signal }) if (!response.ok) { - toast.error(`ImageView Request error. ${response.status}: ${response.statusText}`) + error_msg.value = `ImageView Request error. ${response.status}: ${response.statusText}` return } diff --git a/ui/app/components/History.vue b/ui/app/components/History.vue index 6ebe6517..0831f571 100644 --- a/ui/app/components/History.vue +++ b/ui/app/components/History.vue @@ -5,7 +5,8 @@ - History ({{ stateStore.count('history') }}) + History + ({{ stateStore.count('history') }})  - Selected: {{ selectedElms.length }} @@ -77,6 +78,34 @@ +
+ +
+ + +
+
+
+
+ + + Loading history... + +
+
+
+
+ +
+
+ +
@@ -410,19 +439,30 @@
-
+
- + No results found for '{{ query }}'. - + + The current page has no items. Try navigating to a different page. + +
+
+
+ +
+
+