From 429f97a60c95093afbd7f0f4d74553b38a78d9e9 Mon Sep 17 00:00:00 2001 From: Jesse Bate Date: Wed, 10 Jun 2026 20:09:13 +0930 Subject: [PATCH] perf: exclude unused fields from ItemDTO list and WebSocket serialization Every item_updated WebSocket event and every history/queue list response was serializing the full ItemDTO including fields the UI never reads. Add ItemDTO.serialize_for_list() which excludes: options, template_chapter, temp_dir, total_bytes, total_bytes_estimate, tmpfilename, archive_id. The global Encoder now uses serialize_for_list() for all list/event contexts; the full serialize() remains available for detail endpoints (GET /api/history/{id}). Mark the excluded fields optional in the TypeScript StoreItem type. --- app/library/ItemDTO.py | 33 +++++++++++++++++++++++++++++++++ app/library/encoder.py | 2 +- app/tests/test_itemdto.py | 29 +++++++++++++++++++++++++++++ ui/app/types/store.d.ts | 6 +++--- 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/app/library/ItemDTO.py b/app/library/ItemDTO.py index e768d66c..448e0972 100644 --- a/app/library/ItemDTO.py +++ b/app/library/ItemDTO.py @@ -661,6 +661,39 @@ class ItemDTO: "_archive_file", ) + @staticmethod + def list_excluded_fields() -> tuple: + """ + Fields excluded from list and WebSocket serialization as they are not used by the UI. + + Returns: + tuple: A tuple of field names to exclude from list/event responses. + + """ + return ( + "options", + "template_chapter", + "temp_dir", + "total_bytes", + "total_bytes_estimate", + "tmpfilename", + "archive_id", + ) + + def serialize_for_list(self) -> dict: + """ + Serialize the item to a dictionary, excluding fields not needed for list/WebSocket payloads. + + Returns: + dict: The serialized item with list-unused fields removed. + + """ + if "finished" == self.status and not self._recomputed: + self.archive_status() + + item, _ = clean_item(self.__dict__.copy(), ItemDTO.removed_fields() + ItemDTO.list_excluded_fields()) + return item + def __post_init__(self): """ Post-initialization to compute archive status if applicable. diff --git a/app/library/encoder.py b/app/library/encoder.py index 72ba8e0b..6261d6cb 100644 --- a/app/library/encoder.py +++ b/app/library/encoder.py @@ -23,7 +23,7 @@ class Encoder(json.JSONEncoder): return str(o) if isinstance(o, ItemDTO): - return o.serialize() + return o.serialize_for_list() if isinstance(o, object): if hasattr(o, "serialize"): diff --git a/app/tests/test_itemdto.py b/app/tests/test_itemdto.py index a7c63498..2e8e179e 100644 --- a/app/tests/test_itemdto.py +++ b/app/tests/test_itemdto.py @@ -425,3 +425,32 @@ class TestItemAddExtras: assert item.extras["boolean"] is True assert item.extras["list"] == [1, 2, 3] assert item.extras["dict"] == {"nested": "data"} + + +@pytest.fixture +def sample_item() -> ItemDTO: + with patch.object(ItemDTO, "__post_init__", lambda _: None): + return ItemDTO(id="test", title="Test Video", url="http://example.com/test", folder="/downloads") + + +class TestSerializeForList: + def test_excludes_listed_fields(self, sample_item: ItemDTO) -> None: + result = sample_item.serialize_for_list() + for field in ItemDTO.list_excluded_fields(): + assert field not in result, f"Field '{field}' should be excluded from serialize_for_list" + + def test_includes_required_ui_fields(self, sample_item: ItemDTO) -> None: + result = sample_item.serialize_for_list() + for field in ("_id", "url", "title", "status", "download_dir"): + assert field in result, f"Field '{field}' should be present in serialize_for_list" + + def test_list_excluded_fields_is_tuple(self) -> None: + assert isinstance(ItemDTO.list_excluded_fields(), tuple) + + def test_serialize_for_list_is_subset_of_serialize(self, sample_item: ItemDTO) -> None: + full = sample_item.serialize() + slim = sample_item.serialize_for_list() + for key in slim: + assert key in full, f"serialize_for_list key '{key}' not found in full serialize" + for excluded in ItemDTO.list_excluded_fields(): + assert excluded not in slim diff --git a/ui/app/types/store.d.ts b/ui/app/types/store.d.ts index ad2f7f18..d5017610 100644 --- a/ui/app/types/store.d.ts +++ b/ui/app/types/store.d.ts @@ -39,7 +39,7 @@ type StoreItem = { /** Download directory */ download_dir: string; /** Temporary directory for the item */ - temp_dir: string; + temp_dir?: string; /** Status of the item */ status: ItemStatus; /** If the item has cookies */ @@ -47,7 +47,7 @@ type StoreItem = { /** If the item has custom output_template */ template: string; /** If the item has custom output_template for chapters */ - template_chapter: string; + template_chapter?: string; /** When the item was created */ timestamp: number; /** If the item is a live stream */ @@ -63,7 +63,7 @@ type StoreItem = { /** If the item is auto-started */ auto_start: boolean; /** Options for the item */ - options: Record; + options?: Record; /** Sidecar associated with the item. */ sidecar: { Unknown?: Array;