From 1d5137f74f43671713673d0360f3d13e5b91dab5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 05:59:42 +0000 Subject: [PATCH] Reduce API response data by excluding unused fields from ItemDTO serialization - Add serialize_for_list() to ItemDTO that excludes fields never used by the UI: options, template_chapter, temp_dir, total_bytes, total_bytes_estimate, tmpfilename, archive_id - Update Encoder to use serialize_for_list() for ItemDTO objects - Full serialize() still available for detail endpoint (GET /api/history/{id}) - Update TypeScript types to mark excluded fields as optional - Add tests for serialize_for_list() Co-authored-by: jbatesy <51190172+jbatesy@users.noreply.github.com> --- app/library/ItemDTO.py | 34 ++++++++++++ app/library/encoder.py | 2 +- app/tests/test_itemdto.py | 105 ++++++++++++++++++++++++++++++++++++++ ui/app/types/store.d.ts | 6 +-- 4 files changed, 143 insertions(+), 4 deletions(-) diff --git a/app/library/ItemDTO.py b/app/library/ItemDTO.py index f4ef3d85..91f7b831 100644 --- a/app/library/ItemDTO.py +++ b/app/library/ItemDTO.py @@ -659,6 +659,40 @@ class ItemDTO: "_archive_file", ) + @staticmethod + def list_excluded_fields() -> tuple: + """ + Fields excluded from list/event serialization because they are not used by the UI. + + Returns: + tuple: A tuple of field names excluded from list serialization. + + """ + return ( + "options", + "template_chapter", + "temp_dir", + "total_bytes", + "total_bytes_estimate", + "tmpfilename", + "archive_id", + ) + + def serialize_for_list(self) -> dict: + """ + Serialize the item for list views and WebSocket events, + excluding fields not needed by the UI. + + Returns: + dict: The serialized item with only fields needed by the UI. + + """ + 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 72a27a9a..8533a3f0 100644 --- a/app/tests/test_itemdto.py +++ b/app/tests/test_itemdto.py @@ -125,6 +125,111 @@ class TestItemDTO: for key in ItemDTO.removed_fields(): assert key not in data + @patch("app.library.ItemDTO.archive_read") + def test_serialize_for_list_excludes_unused_fields(self, mock_read): + """Test that serialize_for_list excludes fields not needed by the UI.""" + dto = ItemDTO(id="vid", title="t", url="u", folder="f") + dto.archive_id = "arch" + dto._archive_file = "/tmp/a.txt" + dto.status = "finished" + dto.options = {"format": "best", "verbose": True} + dto.template_chapter = "%(chapter)s.%(ext)s" + dto.temp_dir = "/tmp/temp" + dto.total_bytes = 1000000 + dto.total_bytes_estimate = 2000000 + dto.tmpfilename = "video.tmp" + mock_read.return_value = ["arch"] + + data = dto.serialize_for_list() + + # list_excluded_fields must not be present + for key in ItemDTO.list_excluded_fields(): + assert key not in data, f"Field '{key}' should be excluded from list serialization" + + # removed_fields must not be present either + for key in ItemDTO.removed_fields(): + assert key not in data, f"Removed field '{key}' should not be present" + + # Essential fields must still be present + assert data["_id"] == dto._id + assert data["id"] == "vid" + assert data["title"] == "t" + assert data["url"] == "u" + assert data["folder"] == "f" + assert data["status"] == "finished" + assert data["is_archived"] is True + + def test_serialize_for_list_keeps_ui_required_fields(self): + """Test that serialize_for_list retains all fields needed by the UI.""" + with patch.object(ItemDTO, "__post_init__", lambda _: None): + dto = ItemDTO(id="vid", title="Title", url="https://example.com", folder="media") + dto.preset = "custom" + dto.status = "downloading" + dto.datetime = "Mon, 01 Jan 2024 00:00:00 GMT" + dto.auto_start = True + dto.downloaded_bytes = 5000 + dto.percent = 50 + dto.speed = 1024 + dto.eta = 30 + dto.msg = "Downloading" + dto.postprocessor = "ffmpeg" + dto.is_live = False + dto.description = "A video" + dto.sidecar = {"image": []} + dto.extras = {"duration": 120} + dto.error = None + dto.filename = "video.mp4" + dto.file_size = 10000 + dto.live_in = None + dto.is_archivable = True + dto.is_archived = False + dto.cli = "--embed-metadata" + dto.cookies = "session=abc" + dto.template = "%(title)s.%(ext)s" + dto.download_dir = "/downloads/media" + dto.timestamp = 1704067200 + + data = dto.serialize_for_list() + + # All UI-required fields must be present + ui_fields = [ + "_id", "id", "title", "url", "preset", "status", "datetime", + "auto_start", "downloaded_bytes", "percent", "speed", "eta", + "msg", "postprocessor", "is_live", "description", "sidecar", + "extras", "error", "filename", "file_size", "live_in", + "is_archivable", "is_archived", "cli", "cookies", "template", + "download_dir", "timestamp", "folder", + ] + + for field in ui_fields: + assert field in data, f"UI-required field '{field}' is missing from serialize_for_list()" + + def test_list_excluded_fields_not_empty(self): + """Test that list_excluded_fields returns a non-empty tuple.""" + excluded = ItemDTO.list_excluded_fields() + assert isinstance(excluded, tuple) + assert len(excluded) > 0 + + def test_serialize_full_includes_all_fields(self): + """Test that full serialize() still includes fields excluded from list.""" + with patch.object(ItemDTO, "__post_init__", lambda _: None): + dto = ItemDTO(id="vid", title="t", url="u", folder="f") + dto.options = {"format": "best"} + dto.template_chapter = "%(chapter)s.%(ext)s" + dto.temp_dir = "/tmp/temp" + dto.total_bytes = 1000000 + dto.total_bytes_estimate = 2000000 + dto.tmpfilename = "video.tmp" + dto.archive_id = "arch" + + full_data = dto.serialize() + list_data = dto.serialize_for_list() + + # Full serialization should include list-excluded fields + for key in ItemDTO.list_excluded_fields(): + assert key in full_data, f"Field '{key}' should be in full serialize()" + assert key not in list_data, f"Field '{key}' should NOT be in serialize_for_list()" + @patch("app.library.ItemDTO.YTDLPOpts") def test_get_ytdlp_opts_uses_preset_and_cli(self, mock_opts): mock_opts.get_instance.return_value.preset.return_value = mock_opts.get_instance.return_value diff --git a/ui/app/types/store.d.ts b/ui/app/types/store.d.ts index 268758fd..34629164 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;