diff --git a/src/ytdl_sub/config/preset.py b/src/ytdl_sub/config/preset.py index d046d62b..543801c7 100644 --- a/src/ytdl_sub/config/preset.py +++ b/src/ytdl_sub/config/preset.py @@ -22,6 +22,7 @@ from ytdl_sub.downloaders.downloader import DownloaderValidator from ytdl_sub.entries.entry import Entry from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.plugin import PluginOptions +from ytdl_sub.utils.yaml import dump_yaml from ytdl_sub.utils.yaml import load_yaml from ytdl_sub.validators.strict_dict_validator import StrictDictValidator from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator @@ -328,8 +329,8 @@ class Preset(StrictDictValidator): ) # Merge all presets - self._value = mergedeep.merge( - {}, *reversed(presets_to_merge), strategy=mergedeep.Strategy.ADDITIVE + self._value = dict( + mergedeep.merge({}, *reversed(presets_to_merge), strategy=mergedeep.Strategy.ADDITIVE) ) def __init__(self, config: ConfigFile, name: str, value: Any): @@ -411,3 +412,7 @@ class Preset(StrictDictValidator): ) return subscriptions + + @property + def yaml(self) -> str: + return dump_yaml({"presets": {self._name: self._value}}) diff --git a/src/ytdl_sub/prebuilt_presets/__init__.py b/src/ytdl_sub/prebuilt_presets/__init__.py index 9503d275..f91ee5e1 100644 --- a/src/ytdl_sub/prebuilt_presets/__init__.py +++ b/src/ytdl_sub/prebuilt_presets/__init__.py @@ -5,6 +5,7 @@ from typing import Dict import mergedeep +from ytdl_sub.prebuilt_presets.tv_show.out import PrebuiltJellyfinTVShowPresets from ytdl_sub.prebuilt_presets.tv_show.out import PrebuiltKodiTVShowPresets from ytdl_sub.utils.yaml import load_yaml @@ -18,7 +19,11 @@ def _merge_presets() -> Dict[str, Any]: mergedeep.merge(merged_configs, load_yaml(file)) # Get all presets from published preset configs - mergedeep.merge(merged_configs, *PrebuiltKodiTVShowPresets.get_presets()) + mergedeep.merge( + merged_configs, + *PrebuiltKodiTVShowPresets.get_presets(), + *PrebuiltJellyfinTVShowPresets.get_presets() + ) return merged_configs["presets"] diff --git a/src/ytdl_sub/prebuilt_presets/tv_show/download_strategy.yaml b/src/ytdl_sub/prebuilt_presets/tv_show/download_strategy.yaml index 2008fdd6..976620c4 100644 --- a/src/ytdl_sub/prebuilt_presets/tv_show/download_strategy.yaml +++ b/src/ytdl_sub/prebuilt_presets/tv_show/download_strategy.yaml @@ -32,6 +32,28 @@ presets: season_number: "{collection_season_number}" season_number_padded: "{collection_season_number_padded}" + tv-show-collection-season-1-youtube-channel: + generic: + urls: + - url: "{collection_season_1_url}" + variables: + collection_season_number: "1" + collection_season_number_padded: "01" + playlist_thumbnails: + - name: "{season_poster_file_name}" + uid: "latest_entry" + - name: "{tv_show_poster_file_name}" + uid: "avatar_uncropped" + - name: "{tv_show_fanart_file_name}" + uid: "banner_uncropped" + + output_directory_nfo_tags: + tags: + namedseason: + - tag: "{collection_season_1_name}" + attributes: + number: "1" + tv-show-collection-season-1: generic: urls: diff --git a/src/ytdl_sub/prebuilt_presets/tv_show/out.py b/src/ytdl_sub/prebuilt_presets/tv_show/out.py index f291a330..a96333d4 100644 --- a/src/ytdl_sub/prebuilt_presets/tv_show/out.py +++ b/src/ytdl_sub/prebuilt_presets/tv_show/out.py @@ -1,6 +1,7 @@ from typing import Any from typing import Dict from typing import List +from typing import Optional Preset = Dict[str, Any] @@ -40,8 +41,19 @@ class PrebuiltPresets: return preset_names @classmethod - def get_presets(cls) -> List[Preset]: - return [getattr(cls(), preset_name) for preset_name in cls.get_preset_names()] + def get_collection_preset_names(cls) -> List[str]: + return [name for name in cls.get_preset_names() if "collection" in name] + + @classmethod + def get_non_collection_preset_names(cls) -> List[str]: + return [name for name in cls.get_preset_names() if "collection" not in name] + + @classmethod + def get_presets(cls, preset_names: Optional[List[str]] = None) -> List[Preset]: + if preset_names is None: + preset_names = cls.get_preset_names() + + return [getattr(cls(), preset_name) for preset_name in preset_names] class PrebuiltKodiTVShowPresets(PrebuiltPresets): @@ -90,3 +102,53 @@ class PrebuiltKodiTVShowPresets(PrebuiltPresets): Kodi TV Show from a collection of multiple URLs. TODO: finish docstring """ return self._tv_show_collection_reversed(name="kodi_tv_show_collection_reversed") + + +class PrebuiltJellyfinTVShowPresets(PrebuiltPresets): + BASE_PRESET = "jellyfin-tv-show" + + @property + def jellyfin_tv_show_url(self) -> Preset: + """ + Kodi TV Show with seasons as years, episodes ordered by upload date + """ + return self._tv_show_url(name="jellyfin_tv_show_url") + + @property + def jellyfin_tv_show_url_reversed(self) -> Preset: + """ + Kodi TV Show with seasons as years, episodes ordered by upload date in descending order + (more recent uploads have smaller episode number) + """ + return self._tv_show_url_reversed(name="jellyfin_tv_show_url_reversed") + + @property + def jellyfin_tv_show_youtube_channel(self) -> Preset: + """ + Kodi TV Show from a YouTube channel with seasons as years, episodes ordered by upload date + """ + return self._tv_show_youtube_channel(name="jellyfin_tv_show_youtube_channel") + + @property + def jellyfin_tv_show_youtube_channel_reversed(self) -> Preset: + """ + Kodi TV Show from a YouTube channel with seasons as years, episodes ordered by upload date + in descending order (more recent uploads have smaller episode number) + """ + return self._tv_show_youtube_channel_reversed( + name="jellyfin_tv_show_youtube_channel_reversed" + ) + + @property + def jellyfin_tv_show_collection(self) -> Preset: + """ + Kodi TV Show from a collection of multiple URLs. TODO: finish docstring + """ + return self._tv_show_collection(name="jellyfin_tv_show_collection") + + @property + def jellyfin_tv_show_collection_reversed(self) -> Preset: + """ + Kodi TV Show from a collection of multiple URLs. TODO: finish docstring + """ + return self._tv_show_collection_reversed(name="jellyfin_tv_show_collection_reversed") diff --git a/src/ytdl_sub/utils/yaml.py b/src/ytdl_sub/utils/yaml.py index 7410efbe..f072ed4f 100644 --- a/src/ytdl_sub/utils/yaml.py +++ b/src/ytdl_sub/utils/yaml.py @@ -1,4 +1,5 @@ import os.path +from io import StringIO from pathlib import Path from typing import Dict @@ -37,3 +38,9 @@ def load_yaml(file_path: str | Path) -> Dict: raise InvalidYamlException( f"'{file_path}' has invalid YAML, copy-paste it into a YAML checker to find the issue." ) from yaml_exception + + +def dump_yaml(to_dump: Dict) -> str: + string_io = StringIO() + yaml.safe_dump(to_dump, string_io, indent=2, allow_unicode=True, sort_keys=True) + return string_io.getvalue() diff --git a/tests/unit/prebuilt_presets/test_prebuilt_presets.py b/tests/unit/prebuilt_presets/test_prebuilt_presets.py index 6bc6e415..2f53592c 100644 --- a/tests/unit/prebuilt_presets/test_prebuilt_presets.py +++ b/tests/unit/prebuilt_presets/test_prebuilt_presets.py @@ -1,3 +1,4 @@ +from typing import Dict from typing import List import pytest @@ -5,6 +6,7 @@ import pytest from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.config.preset import Preset from ytdl_sub.prebuilt_presets import PrebuiltKodiTVShowPresets +from ytdl_sub.prebuilt_presets.tv_show.out import PrebuiltJellyfinTVShowPresets @pytest.fixture @@ -15,9 +17,15 @@ def config() -> ConfigFile: class TestPrebuiltTVShowPresets: - @pytest.mark.parametrize("preset_name", PrebuiltKodiTVShowPresets.get_preset_names()) - def test_presets_compile(self, config, preset_name: str): - Preset.from_dict( + @pytest.mark.parametrize( + "preset_name", + [ + *PrebuiltKodiTVShowPresets.get_non_collection_preset_names(), + *PrebuiltJellyfinTVShowPresets.get_non_collection_preset_names(), + ], + ) + def test_non_collection_presets_compile(self, config, preset_name: str): + preset = Preset.from_dict( config=config, preset_name=f"{preset_name}-test", preset_dict={ @@ -29,3 +37,49 @@ class TestPrebuiltTVShowPresets: }, }, ) + assert preset + + @pytest.mark.parametrize( + "preset_name", + [ + *PrebuiltKodiTVShowPresets.get_collection_preset_names(), + *PrebuiltJellyfinTVShowPresets.get_collection_preset_names(), + ], + ) + @pytest.mark.parametrize( + "season_indices", [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5], [4, 1]] + ) + @pytest.mark.parametrize("is_season_1_youtube_channel", [True, False]) + def test_collection_presets_compile( + self, config, preset_name: str, season_indices: List[int], is_season_1_youtube_channel: bool + ): + parent_presets: List[str] = [preset_name] + overrides: Dict[str, str] = {} + for season_index in season_indices: + parent_presets.append(f"tv-show-collection-season-{season_index}") + if season_index == 1 and is_season_1_youtube_channel: + parent_presets[-1] += "-youtube-channel" + + overrides = dict( + overrides, + **{ + f"collection_season_{season_index}_name": f"Season {season_index}", + f"collection_season_{season_index}_url": f"https://season.{season_index}.com", + }, + ) + + preset = Preset.from_dict( + config=config, + preset_name=f"{preset_name}-test", + preset_dict={ + "preset": parent_presets, + "overrides": dict( + overrides, + **{ + "tv_show_name": "test tv show", + "tv_show_directory": "output_path", + }, + ), + }, + ) + assert preset