[FEATURE] TV Show prebuilt presets that require no config (#788)

Introduces 3 new presets + 1 helper preset to download TV show-based subscriptions without the need of a config

```
Plex TV Show by Date
Jellyfin TV Show by Date
Kodi TV Show by Date
```
and
```
Only Recent
```

Example usage can be found in https://github.com/jmbannon/ytdl-sub/blob/master/examples/tv_show_subscriptions.yaml
This commit is contained in:
Jesse Bannon 2023-11-01 22:39:32 -07:00 committed by GitHub
parent 9f408d2196
commit 5fb5a18abc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 394 additions and 416 deletions

View file

@ -1,7 +1,13 @@
# Example Configurations # Example Configurations
This directory shows how you can use ytdl-sub for various use cases. These This directory shows how use ytdl-sub's built-in presets to start downloading immediately with no
are the configs I personally use and have incorporated as part of the e2e tests. configuration required. Simply run:
Each example has a `config.yaml` and `subscription.yaml`. The config defines ### Unix
_how_ you format your media, whereas the subscription defines _what_ you ```commandline
download plus some additional configuring if needed. ytdl-sub sub tv_show_subscriptions.yaml
```
### Windows
```commandline
./ytdl-sub.exe sub tv_show_subscriptions.yaml
```

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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__: __preset__:
overrides: overrides:
tv_show_directory: "/tv_shows" # Root folder of ytdl-sub TV Shows tv_show_directory: "/tv_shows" # Root folder of all ytdl-sub TV Shows
download_range: "2months" # Range to keep for only recent 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 # Sets genre tag to "Documentaries"
TV Show Full Archive: = Documentaries:
# Sets "Music" for genre "NOVA PBS": "https://www.youtube.com/@novapbs"
= Music: "National Geographic": "https://www.youtube.com/@NatGeo"
"Rick A": "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw" "Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U"
"Opeth": "https://www.youtube.com/channel/UCmQSJTFZaXN85gYk6W3XbdQ"
# Sets "Kids" for genre, "TV-Y" for content rating # Sets genre tag to "Kids", "TV-Y" for content rating
= Kids | = TV-Y: = Kids | = TV-Y:
"Jake Trains": "https://www.youtube.com/@JakeTrains" "Jake Trains": "https://www.youtube.com/@JakeTrains"
"Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel" "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 # Set "News" for genre, use `Only Recent` preset to only store videos uploaded recently
TV Show Only Recent: = News | Only Recent:
= News | = TV-14: "BBC News": "https://www.youtube.com/@BBCNews"
"BBC": "https://www.youtube.com/@BBCNews"

View file

@ -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()

View file

@ -17,4 +17,17 @@ presets:
max_downloads: 20 max_downloads: 20
playlistreverse: True playlistreverse: True
break_on_existing: False break_on_existing: False
break_on_reject: True 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"

View file

@ -2,6 +2,14 @@ from ytdl_sub.prebuilt_presets import PrebuiltPresets
class TvShowByDatePresets(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 TV Show by Date presets create a TV show from a single URL using upload dates as season/episode
numbers. numbers.

View file

@ -61,26 +61,45 @@ presets:
- url: "{url20}" - url: "{url20}"
overrides: overrides:
subscription_value: "" 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}" url: "{subscription_value}"
url2: "" url2: "{subscription_value_2}"
url3: "" url3: "{subscription_value_3}"
url4: "" url4: "{subscription_value_4}"
url5: "" url5: "{subscription_value_5}"
url6: "" url6: "{subscription_value_6}"
url7: "" url7: "{subscription_value_7}"
url8: "" url8: "{subscription_value_8}"
url9: "" url9: "{subscription_value_9}"
url10: "" url10: "{subscription_value_10}"
url11: "" url11: "{subscription_value_11}"
url12: "" url12: "{subscription_value_12}"
url13: "" url13: "{subscription_value_13}"
url14: "" url14: "{subscription_value_14}"
url15: "" url15: "{subscription_value_15}"
url16: "" url16: "{subscription_value_16}"
url17: "" url17: "{subscription_value_17}"
url18: "" url18: "{subscription_value_18}"
url19: "" url19: "{subscription_value_19}"
url20: "" url20: "{subscription_value_20}"
#################################################################################################### ####################################################################################################

View file

@ -13,4 +13,20 @@ presets:
plex_tv_show_by_date: plex_tv_show_by_date:
preset: preset:
- "_plex_tv_show" - "_plex_tv_show"
- "_tv_show_by_date" - "_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"

View file

@ -169,10 +169,13 @@ def music_video_subscription_path() -> Path:
@pytest.fixture() @pytest.fixture()
def channel_as_tv_show_config(working_directory) -> ConfigFile: def tv_show_config_path() -> str:
return _load_config( return "examples/advanced/tv_show_config.yaml"
config_path=Path("examples/tv_show_config.yaml"), working_directory=working_directory
)
@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() @pytest.fixture()
@ -180,6 +183,11 @@ def tv_show_subscriptions_path() -> Path:
return Path("examples/tv_show_subscriptions.yaml") 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() @pytest.fixture()
def default_config(working_directory) -> ConfigFile: def default_config(working_directory) -> ConfigFile:
return ConfigFile.from_dict({"configuration": {"working_directory": working_directory}}) return ConfigFile.from_dict({"configuration": {"working_directory": working_directory}})

View file

@ -43,12 +43,12 @@ class TestDateRange:
def test_recent_channel_download( def test_recent_channel_download(
self, self,
recent_preset_dict, recent_preset_dict,
channel_as_tv_show_config, tv_show_config,
output_directory, output_directory,
dry_run, dry_run,
): ):
recent_channel_subscription = Subscription.from_dict( recent_channel_subscription = Subscription.from_dict(
config=channel_as_tv_show_config, config=tv_show_config,
preset_name="recent", preset_name="recent",
preset_dict=recent_preset_dict, preset_dict=recent_preset_dict,
) )
@ -86,7 +86,7 @@ class TestDateRange:
@pytest.mark.parametrize("dry_run", [True, False]) @pytest.mark.parametrize("dry_run", [True, False])
def test_recent_channel_download__no_vids_in_range( def test_recent_channel_download__no_vids_in_range(
self, self,
channel_as_tv_show_config, tv_show_config,
recent_preset_dict, recent_preset_dict,
output_directory, output_directory,
dry_run, dry_run,
@ -94,7 +94,7 @@ class TestDateRange:
recent_preset_dict["date_range"]["after"] = "21000101" recent_preset_dict["date_range"]["after"] = "21000101"
recent_channel_no_vids_in_range_subscription = Subscription.from_dict( recent_channel_no_vids_in_range_subscription = Subscription.from_dict(
config=channel_as_tv_show_config, config=tv_show_config,
preset_name="recent", preset_name="recent",
preset_dict=recent_preset_dict, preset_dict=recent_preset_dict,
) )
@ -118,19 +118,19 @@ class TestDateRange:
@pytest.mark.parametrize("dry_run", [True, False]) @pytest.mark.parametrize("dry_run", [True, False])
def test_rolling_recent_channel_download( def test_rolling_recent_channel_download(
self, self,
channel_as_tv_show_config, tv_show_config,
recent_preset_dict, recent_preset_dict,
rolling_recent_channel_preset_dict, rolling_recent_channel_preset_dict,
output_directory, output_directory,
dry_run, dry_run,
): ):
recent_channel_subscription = Subscription.from_dict( recent_channel_subscription = Subscription.from_dict(
config=channel_as_tv_show_config, config=tv_show_config,
preset_name="recent", preset_name="recent",
preset_dict=recent_preset_dict, preset_dict=recent_preset_dict,
) )
rolling_recent_channel_subscription = Subscription.from_dict( rolling_recent_channel_subscription = Subscription.from_dict(
config=channel_as_tv_show_config, config=tv_show_config,
preset_name="recent", preset_name="recent",
preset_dict=rolling_recent_channel_preset_dict, preset_dict=rolling_recent_channel_preset_dict,
) )

View file

@ -43,16 +43,16 @@ class TestChannel:
expected md5 file hashes. expected md5 file hashes.
""" """
@pytest.mark.parametrize("dry_run", [False]) @pytest.mark.parametrize("dry_run", [True, False])
def test_full_channel_download( def test_full_channel_download(
self, self,
channel_as_tv_show_config, tv_show_config,
channel_preset_dict, channel_preset_dict,
output_directory, output_directory,
dry_run, dry_run,
): ):
full_channel_subscription = Subscription.from_dict( 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) transaction_log = full_channel_subscription.download(dry_run=dry_run)
assert_transaction_log_matches( assert_transaction_log_matches(

View file

@ -208,12 +208,12 @@ class TestYoutubeVideo:
def test_single_video_nulled_values( def test_single_video_nulled_values(
self, self,
channel_as_tv_show_config, tv_show_config,
single_tv_show_video_nulled_values_preset_dict, single_tv_show_video_nulled_values_preset_dict,
output_directory, output_directory,
): ):
single_video_subscription = Subscription.from_dict( single_video_subscription = Subscription.from_dict(
config=channel_as_tv_show_config, config=tv_show_config,
preset_name="tv_video_nulled_values", preset_name="tv_video_nulled_values",
preset_dict=single_tv_show_video_nulled_values_preset_dict, preset_dict=single_tv_show_video_nulled_values_preset_dict,
) )

View file

@ -6,7 +6,7 @@
"Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer.nfo": "ee5bee94667ed4f1d47af08384418d3c", "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-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.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/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.nfo": "c2f06f9eb5b42c777808a35587e5941e",
"Project Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee", "Project Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee",
"Project Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73", "Project Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73",

View file

@ -2,7 +2,7 @@
"Project Zombie/.ytdl-sub-recent-download-archive.json": "46170edaf7cc51e5f59ef5d4c0476200", "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-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.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/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.nfo": "c2f06f9eb5b42c777808a35587e5941e",
"Project Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee", "Project Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee",
"Project Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73", "Project Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73",

View file

@ -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-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.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.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/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.nfo": "a9b7ac3cacbda7fed9d12b0519945292",
"Project Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee", "Project Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee",
"Project Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73", "Project Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73",

View file

@ -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-thumb.jpg
s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.info.json s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.info.json
s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.mp4 s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.mp4
Embedded subtitles with lang(s) en
Video Tags: Video Tags:
contentRating: TV-14 contentRating: TV-14
date: 2018-11-02 date: 2018-11-02

View file

@ -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.en.srt
s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.info.json s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.info.json
s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.mp4 s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.mp4
Embedded subtitles with lang(s) en
Video Tags: Video Tags:
contentRating: TV-14 contentRating: TV-14
date: 2018-11-02 date: 2018-11-02

View file

@ -489,16 +489,14 @@ def test_subscription_file_invalid_form(config_file: ConfigFile):
_ = Subscription.from_file_path(config=config_file, subscription_path="mocked") _ = Subscription.from_file_path(config=config_file, subscription_path="mocked")
def test_tv_show_subscriptions( def test_tv_show_subscriptions(config_file: ConfigFile, tv_show_subscriptions_path: Path):
channel_as_tv_show_config: ConfigFile, tv_show_subscriptions_path: Path
):
subs = Subscription.from_file_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 len(subs) == 7
assert subs[2].name == "Jake Trains" assert subs[3].name == "Jake Trains"
jake_train_overrides = subs[2].overrides.dict_with_format_strings jake_train_overrides = subs[3].overrides.dict_with_format_strings
assert jake_train_overrides["subscription_name"] == "Jake Trains" assert jake_train_overrides["subscription_name"] == "Jake Trains"
assert jake_train_overrides["subscription_name_sanitized"] == "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" 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): def test_music_subscriptions(default_config: ConfigFile, music_subscriptions_path: Path):
subs = Subscription.from_file_path( subs = Subscription.from_file_path(
config=default_config, subscription_path=music_subscriptions_path config=default_config, subscription_path=music_subscriptions_path

View file

@ -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" 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( with mock_sys_exit(expected_exit_code=0), patch.object(
sys, sys,
"argv", "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: ), patch("ytdl_sub.cli.entrypoint._download_subscriptions_from_yaml_files") as mock_sub:
main() main()
assert mock_sub.call_count == 1 assert mock_sub.call_count == 1
assert mock_sub.call_args.kwargs["subscription_paths"] == ["subscriptions.yaml"] 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 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) 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( with mock_sys_exit(expected_exit_code=1), patch.object(
sys, sys,
"argv", "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: ), patch.object(logging.Logger, "error") as mock_error:
main() main()

View file

@ -1,6 +1,8 @@
import copy import copy
from typing import Dict from typing import Dict
from typing import List from typing import List
from typing import Optional
from typing import Tuple
import pytest import pytest
from expected_download import assert_expected_downloads 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 import MusicPresets
from ytdl_sub.prebuilt_presets.music_videos import MusicVideoPresets 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 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 TvShowCollectionEpisodeFormattingPresets
from ytdl_sub.prebuilt_presets.tv_show import TvShowCollectionPresets from ytdl_sub.prebuilt_presets.tv_show import TvShowCollectionPresets
from ytdl_sub.prebuilt_presets.tv_show import TvShowCollectionSeasonPresets 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 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( @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: class TestPrebuiltTVShowPresets:
def test_compilation( def test_compilation(
self, self,
config, config,
all_in_one_preset: Optional[str],
media_player_preset: str, media_player_preset: str,
tv_show_structure_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( _ = Subscription.from_dict(
config=config, config=config,
preset_name="preset_test", preset_name="preset_test",
@ -45,10 +85,14 @@ class TestPrebuiltTVShowPresets:
def test_compilation_many_urls( def test_compilation_many_urls(
self, self,
config, config,
all_in_one_preset: Optional[str],
media_player_preset: str, media_player_preset: str,
tv_show_structure_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( _ = Subscription.from_dict(
config=config, config=config,
preset_name="preset_test", preset_name="preset_test",
@ -66,10 +110,14 @@ class TestPrebuiltTVShowPresets:
def test_compilation_errors_missing_one( def test_compilation_errors_missing_one(
self, self,
config, config,
all_in_one_preset: Optional[str],
media_player_preset: str, media_player_preset: str,
tv_show_structure_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: for parent_preset in parent_presets:
parent_presets_missing_one = copy.deepcopy(parent_presets).remove(parent_preset) parent_presets_missing_one = copy.deepcopy(parent_presets).remove(parent_preset)
@ -95,6 +143,7 @@ class TestPrebuiltTVShowPresets:
subscription_name, subscription_name,
output_directory, output_directory,
mock_download_collection_entries, mock_download_collection_entries,
all_in_one_preset: Optional[str],
media_player_preset: str, media_player_preset: str,
tv_show_structure_preset: str, tv_show_structure_preset: str,
is_youtube_channel: bool, is_youtube_channel: bool,
@ -106,7 +155,9 @@ class TestPrebuiltTVShowPresets:
int(is_youtube_channel), int(is_youtube_channel),
"_many_urls" if is_many_urls else "", "_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_dict = {
"preset": parent_presets, "preset": parent_presets,