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>
This commit is contained in:
parent
1dfe2decaf
commit
1d5137f74f
4 changed files with 143 additions and 4 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
6
ui/app/types/store.d.ts
vendored
6
ui/app/types/store.d.ts
vendored
|
|
@ -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<string, unknown>;
|
||||
options?: Record<string, unknown>;
|
||||
/** Sidecar associated with the item. */
|
||||
sidecar: {
|
||||
Unknown?: Array<SideCar>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue