From 27893bcbf930c83ddc03d129043f5f2457d0ba2a Mon Sep 17 00:00:00 2001 From: arabcoders Date: Tue, 16 Sep 2025 16:39:50 +0300 Subject: [PATCH] Add desc & sidecar to item data --- app/library/DownloadQueue.py | 9 +++++-- app/library/ItemDTO.py | 35 ++++++++++++++++++++++++--- app/library/Utils.py | 12 ++++++--- app/tests/test_itemdto.py | 47 +++++++++++++++++++++++++++++++++--- 4 files changed, 91 insertions(+), 12 deletions(-) diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index 749b96c5..1ec7fe5d 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -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 diff --git a/app/library/ItemDTO.py b/app/library/ItemDTO.py index 8cf2a788..d638d45c 100644 --- a/app/library/ItemDTO.py +++ b/app/library/ItemDTO.py @@ -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() diff --git a/app/library/Utils.py b/app/library/Utils.py index b2cf8074..afa6d54c 100644 --- a/app/library/Utils.py +++ b/app/library/Utils.py @@ -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 diff --git a/app/tests/test_itemdto.py b/app/tests/test_itemdto.py index c6ec0d40..796bbbbe 100644 --- a/app/tests/test_itemdto.py +++ b/app/tests/test_itemdto.py @@ -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