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:
parent
38c120ce85
commit
429f97a60c
4 changed files with 66 additions and 4 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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