diff --git a/examples/README.md b/examples/README.md index b74a4d44..8b5ace4d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,7 +1,13 @@ # Example Configurations -This directory shows how you can use ytdl-sub for various use cases. These -are the configs I personally use and have incorporated as part of the e2e tests. +This directory shows how use ytdl-sub's built-in presets to start downloading immediately with no +configuration required. Simply run: -Each example has a `config.yaml` and `subscription.yaml`. The config defines -_how_ you format your media, whereas the subscription defines _what_ you -download plus some additional configuring if needed. \ No newline at end of file +### Unix +```commandline +ytdl-sub sub tv_show_subscriptions.yaml +``` + +### Windows +```commandline +./ytdl-sub.exe sub tv_show_subscriptions.yaml +``` \ No newline at end of file diff --git a/examples/advanced/tv_show_config.yaml b/examples/advanced/tv_show_config.yaml new file mode 100644 index 00000000..b2d0c694 --- /dev/null +++ b/examples/advanced/tv_show_config.yaml @@ -0,0 +1,100 @@ +############################################################################### +# Top-level configurations to apply umask and persist error logs +configuration: + umask: "002" + persist_logs: + logs_directory: './logs' + keep_successful_logs: False + +presets: + + ############################################################################### + # Set tv_show_directory here instead of in the subscriptions file + tv_show_paths: + overrides: + tv_show_directory: "/tv_shows" + + ############################################################################### + # Filter out any YouTube shorts + no_shorts: + match_filters: + filters: + - "original_url!*=/shorts/" + + ############################################################################### + # Remove all the following sponsorblock sections + sponsorblock: + chapters: + sponsorblock_categories: + - "outro" + - "selfpromo" + - "preview" + - "interaction" + - "sponsor" + - "music_offtopic" + - "intro" + remove_sponsorblock_categories: "all" + force_key_frames: False + + ############################################################################### + # Wait 2 days before downloading in hopes to get more accurate sponsorblock + sponsorblock_wait: + # Import the sponsorblock preset defined above + preset: + - "sponsorblock" + + date_range: + before: "today-2days" + + + ############################################################################### + # base preset to use on all TV Show-based subscriptions + base: + preset: + - "Kodi TV Show by Date" # Set intended player + - "best_video_quality" # prebuilt preset to get best quality + - "tv_show_paths" + + # Embed chapters into video files + chapters: + embed_chapters: True + + # Embed English subtitles into video files (supports more) + subtitles: + embed_subtitles: True + languages: + - "en" + allow_auto_generated_subtitles: True + + # ytdl_options lets you pass any arg into yt-dlp's Python API + ytdl_options: + # Set the cookie file + # cookiefile: "/config/youtube_cookies.txt" + + # For YouTube, get English metadata if multiple languages are present + extractor_args: + youtube: + lang: + - "en" + + ############################################################################### + # Custom preset to archive an entire channel + TV Show Full Archive: + preset: + - "base" + - "sponsorblock_wait" # wait for sponsorblock when full-archiving + + ############################################################################### + # Custom preset to only fetch and keep recent videos. + # Format the videos in reverse order, so the first video is the most recent + # Also include the prebuilt "Only Recent" preset + TV Show Only Recent: + preset: + - "base" + - "sponsorblock" + - "no_shorts" + - "season_by_year__episode_by_month_day_reversed" + - "Only Recent" + + overrides: + date_range: "2months" diff --git a/examples/advanced/tv_show_subscriptions.yaml b/examples/advanced/tv_show_subscriptions.yaml new file mode 100644 index 00000000..52e1728c --- /dev/null +++ b/examples/advanced/tv_show_subscriptions.yaml @@ -0,0 +1,29 @@ +# Subscriptions using custom presets made in `tv_show_config.yaml` + +TV Show Full Archive: + = Documentaries | = TV-PG: + "NOVA PBS": "https://www.youtube.com/@novapbs" + "National Geographic": "https://www.youtube.com/@NatGeo" + "Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U" + + = Kids | = TV-Y: + "Jake Trains": "https://www.youtube.com/@JakeTrains" + "Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel" + + = Gardening | = TV-Y: + # TV Show presets support multiple URLs as a list + "Gardening with Ciscoe": + - "https://www.youtube.com/@gardeningwithciscoe4430" + - "https://www.youtube.com/playlist?list=PLi8V8UemxeG6lo5if5H5g5EbsteELcb0_" + - "https://www.youtube.com/playlist?list=PLsJlQSR-KjmaQqqJ9jq18cF6XXXAR4kyn" + - "https://www.youtube.com/watch?v=2vq-vPubS5I" + +TV Show Only Recent: + = News | = TV-14: + # Subscriptions can prefix a tilda to specify override variables + # to set only for that subscriptions + "~BBC News": + url: "https://www.youtube.com/@BBCNews" # use url2, url3, ... for multi-url in this form + date_range: "2weeks" + "Frontline PBS": "https://www.youtube.com/@frontline" + "Whitehouse": "https://www.bitchute.com/channel/zWsYVmCOu4JA/" # Supports non-YT sites \ No newline at end of file diff --git a/examples/tv_show_config.yaml b/examples/tv_show_config.yaml deleted file mode 100644 index 3843f2c8..00000000 --- a/examples/tv_show_config.yaml +++ /dev/null @@ -1,118 +0,0 @@ -# This config uses prebuilt presets included with ytdl-sub to download and format -# channels from YouTube or other sites supported by yt-dlp into a TV show for -# your favorite player. The directory format will look something like -# -# /tv_shows -# /Season 2021 -# s2021.e0317 - Pattys Day Video-thumb.jpg -# s2021.e0317 - Pattys Day Video.mp4 -# s2021.e0317 - Pattys Day Video.nfo -# /Season 2022 -# s2022.e1225 - Merry Christmas-thumb.jpg -# s2022.e1225 - Merry Christmas.mp4 -# s2022.e1225 - Merry Christmas.nfo -# poster.jpg -# fanart.jpg -# tvshow.nfo -# -# The idea is to use dates as numerics to represent season and episode numbers. -configuration: - working_directory: '.ytdl-sub-downloads' - -presets: - - # Your main TV show preset - all your tv show subscriptions will use this. - "TV Show Full Archive": - preset: - # Choose one of the following player types: - # - "kodi_tv_show_by_date" - # - "jellyfin_tv_show_by_date" - # - "plex_tv_show_by_date" - - - "kodi_tv_show_by_date" # replace with desired player type - - # Choose one of the following season/episode formats: - # - "season_by_year__episode_by_month_day" - # - "season_by_year_month__episode_by_day" - # - "season_by_year__episode_by_month_day_reversed" - # - "season_by_year__episode_by_download_index" - - - "season_by_year__episode_by_month_day" # replace with desired season/episode format - - # Include any of the presets listed below in your 'main preset' if you want - # it applied to every TV show. Or, use them on the individual subscriptions. - # - "add_subtitles" - # - "sponsorblock" - - # To download age-restricted videos, you will need to uncomment and set your cookie - # file here as a ytdl parameter. For more info, see - # https://ytdl-sub.readthedocs.io/en/latest/faq.html#download-age-restricted-youtube-videos - # - # ytdl_options: - # cookiefile: "/config/cookie_file.txt" # replace with actual cookie file path - - overrides: - tv_show_directory: "/tv_shows" # replace with path to tv show directory - # Fields in the prebuilt preset that can be changed: - # - # episode_title: "{upload_date_standardized} - {title}" - # episode_plot: "{webpage_url}" # source variable for the video description is {description} - -#################################################################################################### - - # A secondary TV Show preset that only keeps recent videos - "TV Show Only Recent": - - # Inherit the `TV Show Full Archive` preset - preset: - - "TV Show Full Archive" - - # Only download videos within the download_range - date_range: - after: "today-{download_range}" - - # Deletes any videos older than download_range. WARNING: do not use - # "season_by_year__episode_by_download_index" if you plan to delete older videos - output_options: - keep_files_after: "today-{download_range}" - - # Set the duration of download_range, defaults to 2 months - overrides: - download_range: "2months" - -#################################################################################################### - - # Preset to download subtitles (either by file or embedded) - add_subtitles: - subtitles: - # Embed subtitles into the video - embed_subtitles: True - # And/or download them as a file. Uncomment to download as file: - # subtitles_name: "{episode_file_path}.{lang}.{subtitles_ext}" - # subtitles_type: "srt" - - languages: - - "en" - allow_auto_generated_subtitles: True # allow auto subtitles - -#################################################################################################### - - # Preset to cut sponsor segments from videos - sponsorblock: - # If you download using cron, it is wise to add a delay before downloading ad-filled content to - # give folks time to submit sponsor segments. Uncomment to wait 2 days before download a video. - # date_range: - # before: "today-2days" - - chapters: - # Remove all of these sponsorblock categories - sponsorblock_categories: - - "intro" - - "outro" - - "selfpromo" - - "preview" - - "interaction" - - "sponsor" - - "music_offtopic" - remove_sponsorblock_categories: "all" - force_key_frames: False diff --git a/examples/tv_show_subscriptions.yaml b/examples/tv_show_subscriptions.yaml index c043cc4f..e8fea998 100644 --- a/examples/tv_show_subscriptions.yaml +++ b/examples/tv_show_subscriptions.yaml @@ -1,24 +1,55 @@ -# Global overrides for all subscriptions. Can either be set here or in your config.yaml +# This example downloads the entirety of a channel (not limited to YouTube). +# Files will be stored in the form of: +# +# /tv_shows +# /Season 2021 +# s2021.e031701 - Pattys Day Video-thumb.jpg +# s2021.e031701 - Pattys Day Video.mp4 +# s2021.e031701 - Pattys Day Video.nfo +# s2021.e031702 - Second Pattys Day Video-thumb.jpg +# s2021.e031702 - Second Pattys Day Video.mp4 +# s2021.e031702 - Second Pattys Day Video.nfo +# /Season 2022 +# s2022.e122501 - Merry Christmas-thumb.jpg +# s2022.e122501 - Merry Christmas.mp4 +# s2022.e122501 - Merry Christmas.nfo +# poster.jpg +# fanart.jpg +# tvshow.nfo +# +# The idea is to use dates as numerics to represent season and episode numbers. + +# Overrides to the prebuilt presets __preset__: overrides: - tv_show_directory: "/tv_shows" # Root folder of ytdl-sub TV Shows - download_range: "2months" # Range to keep for only recent + tv_show_directory: "/tv_shows" # Root folder of all ytdl-sub TV Shows + date_range: "2months" # For 'Only Recent' preset, only keep vids uploaded in this range +# Choose the player you intend to use by setting the top-level key to be either: +# - Plex TV Show by Date: +# - Jellyfin TV Show by Date: +# - Kodi TV Show by Date: +Plex TV Show by Date: -# All subscriptions under this will use the `TV Show Full Archive` preset -TV Show Full Archive: - # Sets "Music" for genre - = Music: - "Rick A": "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw" - "Opeth": "https://www.youtube.com/channel/UCmQSJTFZaXN85gYk6W3XbdQ" + # Sets genre tag to "Documentaries" + = Documentaries: + "NOVA PBS": "https://www.youtube.com/@novapbs" + "National Geographic": "https://www.youtube.com/@NatGeo" + "Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U" - # Sets "Kids" for genre, "TV-Y" for content rating + # Sets genre tag to "Kids", "TV-Y" for content rating = Kids | = TV-Y: "Jake Trains": "https://www.youtube.com/@JakeTrains" "Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel" + # Sets genre tag to "Music" + = Music: + # Subscriptions can support multiple urls and store in the same + # TV Show + "Rick Beato": + - "https://www.youtube.com/@RickBeato" + - "https://www.youtube.com/@rickbeato240" -# All subscriptions under this will use the `TV Show Only Recent` preset -TV Show Only Recent: - = News | = TV-14: - "BBC": "https://www.youtube.com/@BBCNews" + # Set "News" for genre, use `Only Recent` preset to only store videos uploaded recently + = News | Only Recent: + "BBC News": "https://www.youtube.com/@BBCNews" \ No newline at end of file diff --git a/src/ytdl_sub/prebuilt_presets/helpers/__init__.py b/src/ytdl_sub/prebuilt_presets/helpers/__init__.py deleted file mode 100644 index 598e8fa7..00000000 --- a/src/ytdl_sub/prebuilt_presets/helpers/__init__.py +++ /dev/null @@ -1,220 +0,0 @@ -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Set - -from ytdl_sub.entries.variables.entry_variables import pad - -Preset = Dict[str, Any] - -#################################################### -# TV SHOW TYPES - -KODI_TV_SHOW = "_kodi_tv_show" -JELLYFIN_TV_SHOW = "_jellyfin_tv_show" -PLEX_TV_SHOW = "_plex_tv_show" - -##################################################### -# TV SHOW URL PRESETS - -TV_SHOW_BY_DATE = "_tv_show_by_date" - -SEASON_YEAR__EPISODE_MONTH_DAY = "season_by_year__episode_by_month_day" -SEASON_YEAR__EPISODE_MONTH_DAY_REVERSED = "season_by_year__episode_by_month_day_reversed" -SEASON_YEAR_MONTH__EPISODE_DAY = "season_by_year_month__episode_by_day" - -##################################################### -# TV SHOW COLLECTION PRESETS - -TV_SHOW_COLLECTION = "_tv_show_collection" - -SEASON_COLLECTION__EPISODE_Y_M_D = "season_by_collection__episode_by_year_month_day" -SEASON_COLLECTION__EPISODE_Y_M_D_REV = "season_by_collection__episode_by_year_month_day_reversed" - - -class PrebuiltPresets: - @classmethod - def _build_preset(cls, name: str, parent_presets: List[str]) -> Preset: - return {"presets": {name: {"preset": parent_presets}}} - - @classmethod - def _document_preset(cls) -> Preset: - return {} - - @classmethod - def get_preset_names(cls) -> Set[str]: - """ - Returns - ------- - Preset names in the set - """ - preset_names = [prop for prop in dir(cls) if isinstance(getattr(cls, prop), property)] - return set(preset_names) - - @classmethod - def get_presets(cls) -> List[Preset]: - """ - Returns - ------- - Preset dicts in the set if they are not added in other yamls - """ - return [getattr(cls(), preset_name) for preset_name in cls.get_preset_names()] - - -class TvShowByDatePresets(PrebuiltPresets): - """ - TV Show by Date presets create a TV show from a single URL using upload dates as season/episode - numbers. - """ - - @property - def kodi_tv_show_by_date(self): - """ - Formats a TV show organized by date for Kodi - """ - return self._build_preset( - name="kodi_tv_show_by_date", parent_presets=[KODI_TV_SHOW, TV_SHOW_BY_DATE] - ) - - @property - def jellyfin_tv_show_by_date(self): - """ - Formats a TV show organized by date for Jellyfin - """ - return self._build_preset( - name="jellyfin_tv_show_by_date", parent_presets=[JELLYFIN_TV_SHOW, TV_SHOW_BY_DATE] - ) - - @property - def plex_tv_show_by_date(self): - """ - Formats a TV show organized by date for Plex - """ - return self._build_preset( - name="plex_tv_show_by_date", parent_presets=[PLEX_TV_SHOW, TV_SHOW_BY_DATE] - ) - - -class TvShowByDateEpisodeFormattingPresets(PrebuiltPresets): - @property - def season_by_year__episode_by_month_day(self) -> Preset: - """ - DOC - """ - return self._document_preset() - - @property - def season_by_year__episode_by_month_day_reversed(self) -> Preset: - """ - DOC - """ - return self._document_preset() - - @property - def season_by_year_month__episode_by_day(self) -> Preset: - """ - DOC - """ - return self._document_preset() - - -class TvShowCollectionPresets(PrebuiltPresets): - """ - Docstring for all TV SHOW URL presets - """ - - @property - def kodi_tv_show_collection(self) -> Preset: - """ - Kodi TV Show with seasons as years, episodes ordered by upload date - """ - return self._build_preset( - name="kodi_tv_show_collection", parent_presets=[KODI_TV_SHOW, TV_SHOW_COLLECTION] - ) - - @property - def jellyfin_tv_show_collection(self) -> Preset: - """ - Kodi TV Show with seasons as years, episodes ordered by upload date - """ - return self._build_preset( - name="jellyfin_tv_show_collection", - parent_presets=[JELLYFIN_TV_SHOW, TV_SHOW_COLLECTION], - ) - - @property - def plex_tv_show_collection(self) -> Preset: - """ - Kodi TV Show with seasons as years, episodes ordered by upload date - """ - return self._build_preset( - name="plex_tv_show_collection", parent_presets=[PLEX_TV_SHOW, TV_SHOW_COLLECTION] - ) - - -class TvShowCollectionEpisodeFormattingPresets(PrebuiltPresets): - @property - def season_by_collection__episode_by_year_month_day(self) -> Preset: - """ - DOC - """ - return self._document_preset() - - @property - def season_by_collection__episode_by_year_month_day_reversed(self) -> Preset: - """ - DOC - """ - return self._document_preset() - - @property - def season_by_collection__episode_by_playlist_index(self) -> Preset: - """ - DOC - """ - return self._document_preset() - - @property - def season_by_collection__episode_by_playlist_index_reversed(self) -> Preset: - """ - DOC - """ - return self._document_preset() - - -class TvShowCollectionSeasonPresets(PrebuiltPresets): - @property - def collection_season_1(self): - """ - DOC - """ - return self._document_preset() - - @property - def collection_season_2(self): - """ - DOC - """ - return self._document_preset() - - @property - def collection_season_3(self): - """ - DOC - """ - return self._document_preset() - - @property - def collection_season_4(self): - """ - DOC - """ - return self._document_preset() - - @property - def collection_season_5(self): - """ - DOC - """ - return self._document_preset() diff --git a/src/ytdl_sub/prebuilt_presets/helpers/common.yaml b/src/ytdl_sub/prebuilt_presets/helpers/common.yaml index 0e00f73d..031b2964 100644 --- a/src/ytdl_sub/prebuilt_presets/helpers/common.yaml +++ b/src/ytdl_sub/prebuilt_presets/helpers/common.yaml @@ -17,4 +17,17 @@ presets: max_downloads: 20 playlistreverse: True break_on_existing: False - break_on_reject: True \ No newline at end of file + break_on_reject: True + + "Only Recent": + # Only fetch videos after today minus date_range + date_range: + after: "today-{date_range}" + + # Only keep files uploaded after date_range + output_options: + keep_files_after: "today-{date_range}" + + # Set the default date_range to 2 months + overrides: + date_range: "2months" \ No newline at end of file diff --git a/src/ytdl_sub/prebuilt_presets/tv_show/__init__.py b/src/ytdl_sub/prebuilt_presets/tv_show/__init__.py index b2604726..fa87eaa1 100644 --- a/src/ytdl_sub/prebuilt_presets/tv_show/__init__.py +++ b/src/ytdl_sub/prebuilt_presets/tv_show/__init__.py @@ -2,6 +2,14 @@ from ytdl_sub.prebuilt_presets import PrebuiltPresets class TvShowByDatePresets(PrebuiltPresets): + preset_names = { + "Kodi TV Show by Date", + "Jellyfin TV Show by Date", + "Plex TV Show by Date", + } + + +class TvShowByDateOldPresets(PrebuiltPresets): """ TV Show by Date presets create a TV show from a single URL using upload dates as season/episode numbers. diff --git a/src/ytdl_sub/prebuilt_presets/tv_show/tv_show.yaml b/src/ytdl_sub/prebuilt_presets/tv_show/tv_show.yaml index 9ec8c5b8..45a59c3d 100644 --- a/src/ytdl_sub/prebuilt_presets/tv_show/tv_show.yaml +++ b/src/ytdl_sub/prebuilt_presets/tv_show/tv_show.yaml @@ -61,26 +61,45 @@ presets: - url: "{url20}" overrides: subscription_value: "" + subscription_value_2: "" + subscription_value_3: "" + subscription_value_4: "" + subscription_value_5: "" + subscription_value_6: "" + subscription_value_7: "" + subscription_value_8: "" + subscription_value_9: "" + subscription_value_10: "" + subscription_value_11: "" + subscription_value_12: "" + subscription_value_13: "" + subscription_value_14: "" + subscription_value_15: "" + subscription_value_16: "" + subscription_value_17: "" + subscription_value_18: "" + subscription_value_19: "" + subscription_value_20: "" url: "{subscription_value}" - url2: "" - url3: "" - url4: "" - url5: "" - url6: "" - url7: "" - url8: "" - url9: "" - url10: "" - url11: "" - url12: "" - url13: "" - url14: "" - url15: "" - url16: "" - url17: "" - url18: "" - url19: "" - url20: "" + url2: "{subscription_value_2}" + url3: "{subscription_value_3}" + url4: "{subscription_value_4}" + url5: "{subscription_value_5}" + url6: "{subscription_value_6}" + url7: "{subscription_value_7}" + url8: "{subscription_value_8}" + url9: "{subscription_value_9}" + url10: "{subscription_value_10}" + url11: "{subscription_value_11}" + url12: "{subscription_value_12}" + url13: "{subscription_value_13}" + url14: "{subscription_value_14}" + url15: "{subscription_value_15}" + url16: "{subscription_value_16}" + url17: "{subscription_value_17}" + url18: "{subscription_value_18}" + url19: "{subscription_value_19}" + url20: "{subscription_value_20}" #################################################################################################### diff --git a/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_by_date.yaml b/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_by_date.yaml index 096c2d0a..275216fe 100644 --- a/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_by_date.yaml +++ b/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_by_date.yaml @@ -13,4 +13,20 @@ presets: plex_tv_show_by_date: preset: - "_plex_tv_show" - - "_tv_show_by_date" \ No newline at end of file + - "_tv_show_by_date" + + # All-in-one presets that require no config + "Kodi TV Show by Date": + preset: + - "kodi_tv_show_by_date" + - "season_by_year__episode_by_month_day" + + "Jellyfin TV Show by Date": + preset: + - "jellyfin_tv_show_by_date" + - "season_by_year__episode_by_month_day" + + "Plex TV Show by Date": + preset: + - "plex_tv_show_by_date" + - "season_by_year__episode_by_month_day" \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 3b88c827..18582577 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -169,10 +169,13 @@ def music_video_subscription_path() -> Path: @pytest.fixture() -def channel_as_tv_show_config(working_directory) -> ConfigFile: - return _load_config( - config_path=Path("examples/tv_show_config.yaml"), working_directory=working_directory - ) +def tv_show_config_path() -> str: + return "examples/advanced/tv_show_config.yaml" + + +@pytest.fixture() +def tv_show_config(working_directory, tv_show_config_path) -> ConfigFile: + return _load_config(config_path=Path(tv_show_config_path), working_directory=working_directory) @pytest.fixture() @@ -180,6 +183,11 @@ def tv_show_subscriptions_path() -> Path: return Path("examples/tv_show_subscriptions.yaml") +@pytest.fixture() +def advanced_tv_show_subscriptions_path() -> Path: + return Path("examples/advanced/tv_show_subscriptions.yaml") + + @pytest.fixture() def default_config(working_directory) -> ConfigFile: return ConfigFile.from_dict({"configuration": {"working_directory": working_directory}}) diff --git a/tests/e2e/plugins/test_date_range.py b/tests/e2e/plugins/test_date_range.py index 6d0dd483..4196d567 100644 --- a/tests/e2e/plugins/test_date_range.py +++ b/tests/e2e/plugins/test_date_range.py @@ -43,12 +43,12 @@ class TestDateRange: def test_recent_channel_download( self, recent_preset_dict, - channel_as_tv_show_config, + tv_show_config, output_directory, dry_run, ): recent_channel_subscription = Subscription.from_dict( - config=channel_as_tv_show_config, + config=tv_show_config, preset_name="recent", preset_dict=recent_preset_dict, ) @@ -86,7 +86,7 @@ class TestDateRange: @pytest.mark.parametrize("dry_run", [True, False]) def test_recent_channel_download__no_vids_in_range( self, - channel_as_tv_show_config, + tv_show_config, recent_preset_dict, output_directory, dry_run, @@ -94,7 +94,7 @@ class TestDateRange: recent_preset_dict["date_range"]["after"] = "21000101" recent_channel_no_vids_in_range_subscription = Subscription.from_dict( - config=channel_as_tv_show_config, + config=tv_show_config, preset_name="recent", preset_dict=recent_preset_dict, ) @@ -118,19 +118,19 @@ class TestDateRange: @pytest.mark.parametrize("dry_run", [True, False]) def test_rolling_recent_channel_download( self, - channel_as_tv_show_config, + tv_show_config, recent_preset_dict, rolling_recent_channel_preset_dict, output_directory, dry_run, ): recent_channel_subscription = Subscription.from_dict( - config=channel_as_tv_show_config, + config=tv_show_config, preset_name="recent", preset_dict=recent_preset_dict, ) rolling_recent_channel_subscription = Subscription.from_dict( - config=channel_as_tv_show_config, + config=tv_show_config, preset_name="recent", preset_dict=rolling_recent_channel_preset_dict, ) diff --git a/tests/e2e/youtube/test_channel.py b/tests/e2e/youtube/test_channel.py index 8910ea65..585abaa4 100644 --- a/tests/e2e/youtube/test_channel.py +++ b/tests/e2e/youtube/test_channel.py @@ -43,16 +43,16 @@ class TestChannel: expected md5 file hashes. """ - @pytest.mark.parametrize("dry_run", [False]) + @pytest.mark.parametrize("dry_run", [True, False]) def test_full_channel_download( self, - channel_as_tv_show_config, + tv_show_config, channel_preset_dict, output_directory, dry_run, ): full_channel_subscription = Subscription.from_dict( - config=channel_as_tv_show_config, preset_name="pz", preset_dict=channel_preset_dict + config=tv_show_config, preset_name="pz", preset_dict=channel_preset_dict ) transaction_log = full_channel_subscription.download(dry_run=dry_run) assert_transaction_log_matches( diff --git a/tests/e2e/youtube/test_video.py b/tests/e2e/youtube/test_video.py index e2f23970..2b63d6db 100644 --- a/tests/e2e/youtube/test_video.py +++ b/tests/e2e/youtube/test_video.py @@ -208,12 +208,12 @@ class TestYoutubeVideo: def test_single_video_nulled_values( self, - channel_as_tv_show_config, + tv_show_config, single_tv_show_video_nulled_values_preset_dict, output_directory, ): single_video_subscription = Subscription.from_dict( - config=channel_as_tv_show_config, + config=tv_show_config, preset_name="tv_video_nulled_values", preset_dict=single_tv_show_video_nulled_values_preset_dict, ) diff --git a/tests/resources/expected_downloads_summaries/plugins/date_range/test_channel_recent.json b/tests/resources/expected_downloads_summaries/plugins/date_range/test_channel_recent.json index a570473a..9c301b18 100644 --- a/tests/resources/expected_downloads_summaries/plugins/date_range/test_channel_recent.json +++ b/tests/resources/expected_downloads_summaries/plugins/date_range/test_channel_recent.json @@ -6,7 +6,7 @@ "Project ⧸ Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server | Teaser Trailer.nfo": "ee5bee94667ed4f1d47af08384418d3c", "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id-thumb.jpg": "28d852ede73b879b9ebf9a061cfc7d46", "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.info.json": "INFO_JSON", - "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.mp4": "77aca1c8f8ef32c282a8a871d80bec10", + "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.mp4": "3725eaeaae4a400599070a56a1dccf9d", "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.nfo": "c2f06f9eb5b42c777808a35587e5941e", "Project ⧸ Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee", "Project ⧸ Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73", diff --git a/tests/resources/expected_downloads_summaries/plugins/date_range/test_channel_rolling_recent.json b/tests/resources/expected_downloads_summaries/plugins/date_range/test_channel_rolling_recent.json index 9d743de4..ac3d9fdf 100644 --- a/tests/resources/expected_downloads_summaries/plugins/date_range/test_channel_rolling_recent.json +++ b/tests/resources/expected_downloads_summaries/plugins/date_range/test_channel_rolling_recent.json @@ -2,7 +2,7 @@ "Project ⧸ Zombie/.ytdl-sub-recent-download-archive.json": "46170edaf7cc51e5f59ef5d4c0476200", "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id-thumb.jpg": "28d852ede73b879b9ebf9a061cfc7d46", "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.info.json": "INFO_JSON", - "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.mp4": "77aca1c8f8ef32c282a8a871d80bec10", + "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.mp4": "3725eaeaae4a400599070a56a1dccf9d", "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.nfo": "c2f06f9eb5b42c777808a35587e5941e", "Project ⧸ Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee", "Project ⧸ Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73", diff --git a/tests/resources/expected_downloads_summaries/youtube/test_channel_full.json b/tests/resources/expected_downloads_summaries/youtube/test_channel_full.json index 852adc14..8350d840 100644 --- a/tests/resources/expected_downloads_summaries/youtube/test_channel_full.json +++ b/tests/resources/expected_downloads_summaries/youtube/test_channel_full.json @@ -47,7 +47,7 @@ "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id-thumb.jpg": "28d852ede73b879b9ebf9a061cfc7d46", "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.en.srt": "3d2c4e7f65d2ca5e96da38ce7eecfc4e", "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.info.json": "INFO_JSON", - "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.mp4": "77aca1c8f8ef32c282a8a871d80bec10", + "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.mp4": "3725eaeaae4a400599070a56a1dccf9d", "Project ⧸ Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.nfo": "a9b7ac3cacbda7fed9d12b0519945292", "Project ⧸ Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee", "Project ⧸ Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73", diff --git a/tests/resources/transaction_log_summaries/plugins/date_range/test_channel_recent.txt b/tests/resources/transaction_log_summaries/plugins/date_range/test_channel_recent.txt index 4d2f7afd..496901d5 100644 --- a/tests/resources/transaction_log_summaries/plugins/date_range/test_channel_recent.txt +++ b/tests/resources/transaction_log_summaries/plugins/date_range/test_channel_recent.txt @@ -59,6 +59,7 @@ Files created: s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id-thumb.jpg s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.info.json s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.mp4 + Embedded subtitles with lang(s) en Video Tags: contentRating: TV-14 date: 2018-11-02 diff --git a/tests/resources/transaction_log_summaries/youtube/test_channel_full.txt b/tests/resources/transaction_log_summaries/youtube/test_channel_full.txt index 04baa854..9c48de45 100644 --- a/tests/resources/transaction_log_summaries/youtube/test_channel_full.txt +++ b/tests/resources/transaction_log_summaries/youtube/test_channel_full.txt @@ -579,6 +579,7 @@ Files created: s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.en.srt s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.info.json s2018.e110201 - Jesse's Minecraft Server | IP mc.jesse.id.mp4 + Embedded subtitles with lang(s) en Video Tags: contentRating: TV-14 date: 2018-11-02 diff --git a/tests/unit/config/test_subscription.py b/tests/unit/config/test_subscription.py index a419a8d6..7649ef4e 100644 --- a/tests/unit/config/test_subscription.py +++ b/tests/unit/config/test_subscription.py @@ -489,16 +489,14 @@ def test_subscription_file_invalid_form(config_file: ConfigFile): _ = Subscription.from_file_path(config=config_file, subscription_path="mocked") -def test_tv_show_subscriptions( - channel_as_tv_show_config: ConfigFile, tv_show_subscriptions_path: Path -): +def test_tv_show_subscriptions(config_file: ConfigFile, tv_show_subscriptions_path: Path): subs = Subscription.from_file_path( - config=channel_as_tv_show_config, subscription_path=tv_show_subscriptions_path + config=config_file, subscription_path=tv_show_subscriptions_path ) - assert len(subs) == 5 - assert subs[2].name == "Jake Trains" - jake_train_overrides = subs[2].overrides.dict_with_format_strings + assert len(subs) == 7 + assert subs[3].name == "Jake Trains" + jake_train_overrides = subs[3].overrides.dict_with_format_strings assert jake_train_overrides["subscription_name"] == "Jake Trains" assert jake_train_overrides["subscription_name_sanitized"] == "Jake Trains" @@ -507,6 +505,41 @@ def test_tv_show_subscriptions( assert jake_train_overrides["subscription_indent_2"] == "TV-Y" +def test_advanced_tv_show_subscriptions( + tv_show_config: ConfigFile, advanced_tv_show_subscriptions_path: Path +): + subs = Subscription.from_file_path( + config=tv_show_config, subscription_path=advanced_tv_show_subscriptions_path + ) + + assert len(subs) == 9 + assert subs[3].name == "Jake Trains" + jake_train_overrides = subs[3].overrides.dict_with_format_strings + + assert jake_train_overrides["subscription_name"] == "Jake Trains" + assert jake_train_overrides["subscription_name_sanitized"] == "Jake Trains" + assert jake_train_overrides["subscription_value"] == "https://www.youtube.com/@JakeTrains" + assert jake_train_overrides["subscription_indent_1"] == "Kids" + assert jake_train_overrides["subscription_indent_2"] == "TV-Y" + + assert subs[5].name == "Gardening with Ciscoe" + overrides = subs[5].overrides + + assert overrides.apply_formatter(overrides.dict["subscription_name"]) == "Gardening with Ciscoe" + assert ( + overrides.apply_formatter(overrides.dict["subscription_name_sanitized"]) + == "Gardening with Ciscoe" + ) + assert ( + overrides.apply_formatter(overrides.dict["url"]) + == "https://www.youtube.com/@gardeningwithciscoe4430" + ) + assert ( + overrides.apply_formatter(overrides.dict["url2"]) + == "https://www.youtube.com/playlist?list=PLi8V8UemxeG6lo5if5H5g5EbsteELcb0_" + ) + + def test_music_subscriptions(default_config: ConfigFile, music_subscriptions_path: Path): subs = Subscription.from_file_path( config=default_config, subscription_path=music_subscriptions_path diff --git a/tests/unit/main/test_main.py b/tests/unit/main/test_main.py index 5f4b2d5a..a42e21a5 100644 --- a/tests/unit/main/test_main.py +++ b/tests/unit/main/test_main.py @@ -96,17 +96,17 @@ def test_main_permission_error(capsys, mock_sys_exit, expected_uncaught_error_me assert mock_error.call_args.args[1] == "test" -def test_args_after_sub_work(mock_sys_exit): +def test_args_after_sub_work(mock_sys_exit, tv_show_config_path): with mock_sys_exit(expected_exit_code=0), patch.object( sys, "argv", - ["ytdl-sub", "-c", "examples/tv_show_config.yaml", "sub", "--log-level", "verbose"], + ["ytdl-sub", "-c", tv_show_config_path, "sub", "--log-level", "verbose"], ), patch("ytdl_sub.cli.entrypoint._download_subscriptions_from_yaml_files") as mock_sub: main() assert mock_sub.call_count == 1 assert mock_sub.call_args.kwargs["subscription_paths"] == ["subscriptions.yaml"] - assert mock_sub.call_args.kwargs["config"]._name == "examples/tv_show_config.yaml" + assert mock_sub.call_args.kwargs["config"]._name == tv_show_config_path assert Logger._LOGGER_LEVEL == LoggerLevels.VERBOSE @@ -151,11 +151,11 @@ def test_uses_default_config_if_present(mock_sys_exit): FileHandler.delete(DEFAULT_CONFIG_FILE_NAME) -def test_no_positional_arg_command(mock_sys_exit): +def test_no_positional_arg_command(mock_sys_exit, tv_show_config_path): with mock_sys_exit(expected_exit_code=1), patch.object( sys, "argv", - ["ytdl-sub", "-c", "examples/tv_show_config.yaml", "--log-level", "verbose"], + ["ytdl-sub", "-c", tv_show_config_path, "--log-level", "verbose"], ), patch.object(logging.Logger, "error") as mock_error: main() diff --git a/tests/unit/prebuilt_presets/test_prebuilt_presets.py b/tests/unit/prebuilt_presets/test_prebuilt_presets.py index 155f7cc8..90b0b993 100644 --- a/tests/unit/prebuilt_presets/test_prebuilt_presets.py +++ b/tests/unit/prebuilt_presets/test_prebuilt_presets.py @@ -1,6 +1,8 @@ import copy from typing import Dict from typing import List +from typing import Optional +from typing import Tuple import pytest from expected_download import assert_expected_downloads @@ -9,7 +11,7 @@ from expected_transaction_log import assert_transaction_log_matches from ytdl_sub.prebuilt_presets.music import MusicPresets from ytdl_sub.prebuilt_presets.music_videos import MusicVideoPresets from ytdl_sub.prebuilt_presets.tv_show import TvShowByDateEpisodeFormattingPresets -from ytdl_sub.prebuilt_presets.tv_show import TvShowByDatePresets +from ytdl_sub.prebuilt_presets.tv_show import TvShowByDateOldPresets from ytdl_sub.prebuilt_presets.tv_show import TvShowCollectionEpisodeFormattingPresets from ytdl_sub.prebuilt_presets.tv_show import TvShowCollectionPresets from ytdl_sub.prebuilt_presets.tv_show import TvShowCollectionSeasonPresets @@ -17,18 +19,56 @@ from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.utils.exceptions import ValidationException -@pytest.mark.parametrize("media_player_preset", TvShowByDatePresets.preset_names) +def _tv_show_by_date_combos() -> List[Tuple[None, str, str]]: + combos: List[Tuple[None, str, str]] = [] + + for media_player_preset in TvShowByDateOldPresets.preset_names: + for tv_show_structure_preset in TvShowByDateEpisodeFormattingPresets.preset_names: + combos.append((None, media_player_preset, tv_show_structure_preset)) + + return combos + + +def _tv_show_by_date_parent_presets( + all_in_one_preset: Optional[str], + media_player_preset: str, + tv_show_structured_preset: str, +) -> List[str]: + """Hack to include the all_in_one presets in this test suite""" + if all_in_one_preset is not None: + return [all_in_one_preset] + return [ + media_player_preset, + tv_show_structured_preset, + ] + + @pytest.mark.parametrize( - "tv_show_structure_preset", TvShowByDateEpisodeFormattingPresets.preset_names + "all_in_one_preset, media_player_preset, tv_show_structure_preset", + [ + # Test that the all-in-ones are equivalent to using the two underlying ones + ("Kodi TV Show by Date", "kodi_tv_show_by_date", "season_by_year__episode_by_month_day"), + ( + "Jellyfin TV Show by Date", + "jellyfin_tv_show_by_date", + "season_by_year__episode_by_month_day", + ), + ("Plex TV Show by Date", "plex_tv_show_by_date", "season_by_year__episode_by_month_day"), + *_tv_show_by_date_combos(), + ], ) class TestPrebuiltTVShowPresets: def test_compilation( self, config, + all_in_one_preset: Optional[str], media_player_preset: str, tv_show_structure_preset: str, ): - parent_presets: List[str] = [media_player_preset, tv_show_structure_preset] + parent_presets = _tv_show_by_date_parent_presets( + all_in_one_preset, media_player_preset, tv_show_structure_preset + ) + _ = Subscription.from_dict( config=config, preset_name="preset_test", @@ -45,10 +85,14 @@ class TestPrebuiltTVShowPresets: def test_compilation_many_urls( self, config, + all_in_one_preset: Optional[str], media_player_preset: str, tv_show_structure_preset: str, ): - parent_presets: List[str] = [media_player_preset, tv_show_structure_preset] + parent_presets = _tv_show_by_date_parent_presets( + all_in_one_preset, media_player_preset, tv_show_structure_preset + ) + _ = Subscription.from_dict( config=config, preset_name="preset_test", @@ -66,10 +110,14 @@ class TestPrebuiltTVShowPresets: def test_compilation_errors_missing_one( self, config, + all_in_one_preset: Optional[str], media_player_preset: str, tv_show_structure_preset: str, ): - parent_presets: List[str] = [media_player_preset, tv_show_structure_preset] + parent_presets = _tv_show_by_date_parent_presets( + all_in_one_preset, media_player_preset, tv_show_structure_preset + ) + for parent_preset in parent_presets: parent_presets_missing_one = copy.deepcopy(parent_presets).remove(parent_preset) @@ -95,6 +143,7 @@ class TestPrebuiltTVShowPresets: subscription_name, output_directory, mock_download_collection_entries, + all_in_one_preset: Optional[str], media_player_preset: str, tv_show_structure_preset: str, is_youtube_channel: bool, @@ -106,7 +155,9 @@ class TestPrebuiltTVShowPresets: int(is_youtube_channel), "_many_urls" if is_many_urls else "", ) - parent_presets = [media_player_preset, tv_show_structure_preset] + parent_presets = _tv_show_by_date_parent_presets( + all_in_one_preset, media_player_preset, tv_show_structure_preset + ) preset_dict = { "preset": parent_presets,