Add desc & sidecar to item data

This commit is contained in:
arabcoders 2025-09-16 16:39:50 +03:00
parent 816fd55397
commit 27893bcbf9
4 changed files with 91 additions and 12 deletions

View file

@ -465,6 +465,7 @@ class DownloadQueue(metaclass=Singleton):
dl = ItemDTO(
id=str(entry.get("id")),
title=str(entry.get("title")),
description=str(entry.get("description", "")),
url=str(entry.get("webpage_url") or entry.get("url")),
preset=item.preset,
folder=item.folder,
@ -982,6 +983,7 @@ class DownloadQueue(metaclass=Singleton):
items["queue"][k] = self._active[k].info if k in self._active else v
for k, v in self.done.saved_items():
v.get_file_sidecar()
items["done"][k] = v
for k, v in self.queue.items():
@ -989,8 +991,11 @@ class DownloadQueue(metaclass=Singleton):
items["queue"][k] = self._active[k].info if k in self._active else v
for k, v in self.done.items():
if k not in items["done"]:
items["done"][k] = v.info
if k in items["done"]:
continue
v.info.get_file_sidecar()
items["done"][k] = v.info
return items

View file

@ -8,7 +8,15 @@ from pathlib import Path
from typing import Any
from app.library.encoder import Encoder
from app.library.Utils import archive_add, archive_delete, archive_read, clean_item, get_archive_id, get_file
from app.library.Utils import (
archive_add,
archive_delete,
archive_read,
clean_item,
get_archive_id,
get_file,
get_file_sidecar,
)
from app.library.YTDLPOpts import YTDLPOpts
LOG: logging.Logger = logging.getLogger("ItemDTO")
@ -305,6 +313,8 @@ class ItemDTO:
""" The ID of the item yt-dlp """
title: str
""" The title of the item. """
description: str = ""
""" The description of the item. """
url: str
""" The URL of the item. """
preset: str = "default"
@ -347,6 +357,8 @@ class ItemDTO:
""" If the item has been archived. """
archive_id: str | None = None
""" The archive ID of the item. """
sidecar: dict = field(default_factory=dict)
""" Sidecar data associated with the item. """
# yt-dlp injected fields.
tmpfilename: str | None = None
@ -370,8 +382,11 @@ class ItemDTO:
dict: The serialized item.
"""
if "finished" == self.status and not self._recomputed:
self.archive_status()
if "finished" == self.status:
if not self._recomputed:
self.archive_status()
self.get_file_sidecar()
item, _ = clean_item(self.__dict__.copy(), ItemDTO.removed_fields())
return item
@ -576,6 +591,19 @@ class ItemDTO:
return True
def get_file_sidecar(self) -> dict:
"""
Get sidecar files associated with the item.
Returns:
dict: A dictionary with sidecar files categorized by type.
"""
if filename := self.get_file():
self.sidecar = get_file_sidecar(filename)
return self.sidecar
@staticmethod
def removed_fields() -> tuple:
"""
@ -606,3 +634,4 @@ class ItemDTO:
self.get_archive_id()
self.get_archive_file()
self.archive_status()
self.get_file_sidecar()

View file

@ -138,7 +138,7 @@ def timed_lru_cache(ttl_seconds: int, max_size: int = 128):
# Limit cache size
if len(cache) > max_size:
# Remove oldest entries
oldest_keys = sorted(cache_expiry.keys(), key=lambda k: cache_expiry[k])[:len(cache) - max_size]
oldest_keys = sorted(cache_expiry.keys(), key=lambda k: cache_expiry[k])[: len(cache) - max_size]
for old_key in oldest_keys:
cache.pop(old_key, None)
cache_expiry.pop(old_key, None)
@ -153,6 +153,7 @@ def timed_lru_cache(ttl_seconds: int, max_size: int = 128):
def cache_info():
# Use functools._CacheInfo for compatibility with standard lru_cache
from functools import _CacheInfo
return _CacheInfo(hits=0, misses=len(cache), maxsize=max_size, currsize=len(cache))
async_wrapper.cache_clear = cache_clear
@ -626,19 +627,22 @@ def validate_uuid(uuid_str: str, version: int = 4) -> bool:
@timed_lru_cache(ttl_seconds=60, max_size=256)
def get_file_sidecar(file: Path) -> list[dict]:
def get_file_sidecar(file: Path | None = None) -> dict[dict]:
"""
Get sidecar files for the given file.
Args:
file (Path): The video file.
file (Path|None): The video file.
Returns:
list: List of sidecar files.
dict: A dictionary with sidecar files categorized by type.
"""
files: dict = {}
if not file:
return files
for i, f in enumerate(file.parent.glob(f"{glob.escape(file.stem)}.*")):
if f == file or f.is_file() is False or f.stem.startswith("."):
continue

View file

@ -1,4 +1,5 @@
import json
from pathlib import Path
from unittest.mock import patch
import pytest
@ -168,9 +169,6 @@ class TestItemDTO:
def test_get_file_method(self):
"""Test ItemDTO get_file method returns correct path."""
from pathlib import Path
from unittest.mock import patch
# Create ItemDTO with filename but no folder
dto = ItemDTO(
id="test-id",
@ -238,3 +236,46 @@ class TestItemDTO:
result = dto.get_file(download_path=Path("/custom"))
assert result == Path("/custom/test_video.mp4")
def test_get_file_sidecar_populates_from_utils(self):
with patch.object(ItemDTO, "__post_init__", lambda _: None):
dto = ItemDTO(id="sidecar", title="Title", url="u", folder="f")
expected_sidecar = {
"subtitle": [
{
"file": Path("/downloads/video.en.srt"),
"lang": "en",
"name": "SRT (0) - en",
}
]
}
with (
patch("app.library.ItemDTO.ItemDTO.get_file", autospec=True, return_value=Path("/downloads/video.mp4")) as mock_get_file,
patch("app.library.ItemDTO.get_file_sidecar", return_value=expected_sidecar) as mock_utils_sidecar,
):
result = dto.get_file_sidecar()
mock_get_file.assert_called_once_with(dto)
mock_utils_sidecar.assert_called_once_with(Path("/downloads/video.mp4"))
assert result is expected_sidecar
assert dto.sidecar is expected_sidecar
def test_get_file_sidecar_returns_existing_when_no_file(self):
with patch.object(ItemDTO, "__post_init__", lambda _: None):
dto = ItemDTO(id="sidecar-none", title="Title", url="u", folder="f")
existing = {"existing": []}
dto.sidecar = existing
with (
patch("app.library.ItemDTO.ItemDTO.get_file", autospec=True, return_value=None) as mock_get_file,
patch("app.library.ItemDTO.get_file_sidecar") as mock_utils_sidecar,
):
result = dto.get_file_sidecar()
mock_get_file.assert_called_once_with(dto)
mock_utils_sidecar.assert_not_called()
assert result is existing
assert dto.sidecar is existing