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.
This commit is contained in:
Jesse Bate 2026-06-10 20:09:13 +09:30
parent 38c120ce85
commit 429f97a60c
4 changed files with 66 additions and 4 deletions

View file

@ -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.

View file

@ -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"):

View file

@ -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

View file

@ -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>;