diff --git a/README.md b/README.md index eae9d50f..2fe75402 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,18 @@ Automate downloading and adding metadata with YoutubeDL. -This package strives to download media via +This package downloads media via [yt-dlp](https://github.com/yt-dlp/yt-dlp) -and prepare it for consumption in your favorite media player +and prepares it for consumption in your favorite media player ([Kodi](https://github.com/xbmc/xbmc), [Jellyfin](https://github.com/jellyfin/jellyfin), [Plex](https://github.com/plexinc/pms-docker), [Emby](https://github.com/plexinc/pms-docker), -modern music players) -in the most hassle-free way possible. We recognize that everyone stores their +modern music players). +We recognize that everyone stores their media differently. Our approach for file and metadata formatting is to provide maximum flexibility while maintaining simplicity. Read more about it -[here](https://ytdl-sub.readthedocs.io/en/latest/). +[here](https://ytdl-sub.readthedocs.io/en/latest/getting_started.html). ## Supported Features Below lists supported download schemes. You can see our diff --git a/docs/conf.py b/docs/conf.py index d603eaa7..47a478be 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,3 +61,7 @@ add_module_names = False python_use_unqualified_type_names = False napoleon_numpy_docstrings = True napoleon_use_rtype = False + +# -- Options for autodocs ------------------------------------------------- + +autodoc_default_options = {"autodoc_typehints_format": "short"} diff --git a/docs/config.rst b/docs/config.rst index 056b8abe..f311bdd0 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -4,6 +4,9 @@ ytdl-sub is configured using a ``config.yaml`` file. You can view our :doc:`examples ` and read detailed documentation for every configurable field below. +config.yaml +----------- + The ``config.yaml`` is made up of two sections: .. code-block:: yaml @@ -15,159 +18,129 @@ You can jump to any section and subsection of the config using the navigation section to the left. configuration -------------- +^^^^^^^^^^^^^ The ``configuration`` section contains app-wide configs applied to all presets and subscriptions. presets -------- +^^^^^^^ ``presets`` define a `formula` for how to format downloaded media and metadata. download_strategy -^^^^^^^^^^^^^^^^^ +""""""""""""""""" Download strategies dictate what is getting downloaded from a source. Each download strategy has its own set of parameters. youtube -""""""" +''''''' +Download strategies for downloading videos (or audio if you configure `ytdl_options`_ correctly) from Youtube. See +Download strategies for downloading music from Soundcloud. See +:class:`Youtube Variables ` +for available source variables to use. channel -''''''' -.. code-block:: yaml - - presets: - my_example_preset: - youtube: - download_strategy: "channel" - -channel_id -__________ - .. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubeChannelDownloaderOptions.channel_id - -channel_avatar_path -___________________ - .. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubeChannelDownloaderOptions.channel_avatar_path - -channel_banner_path -___________________ - .. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubeChannelDownloaderOptions.channel_banner_path - -before -______ - .. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubeChannelDownloaderOptions.before - -after -_____ - .. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubeChannelDownloaderOptions.after +_______ +.. autoclass:: ytdl_sub.downloaders.youtube_downloader.YoutubeChannelDownloaderOptions() + :members: + :member-order: bysource + :inherited-members: + :exclude-members: get_date_range ------------------------------------------------------------------------------- playlist -'''''''' -.. code-block:: yaml - - presets: - my_example_preset: - youtube: - download_strategy: "playlist" - -playlist_id -___________ - .. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubePlaylistDownloaderOptions.playlist_id +________ +.. autoclass:: ytdl_sub.downloaders.youtube_downloader.YoutubePlaylistDownloaderOptions() + :members: + :member-order: bysource + :inherited-members: ------------------------------------------------------------------------------- video -''''' -.. code-block:: yaml - - presets: - my_example_preset: - youtube: - download_strategy: "video" - -video_id -________ - .. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubeVideoDownloaderOptions.video_id +_____ +.. autoclass:: ytdl_sub.downloaders.youtube_downloader.YoutubeVideoDownloaderOptions() + :members: + :member-order: bysource + :inherited-members: ------------------------------------------------------------------------------- soundcloud -"""""""""" +'''''''''' +Download strategies for downloading music from Soundcloud. See +:class:`Soundcloud Variables ` +for available source variables to use. albums_and_singles -'''''''''''''''''' -.. code-block:: yaml - - presets: - my_example_preset: - soundcloud: - download_strategy: "albums_and_singles" - -username -________ - .. autoproperty:: ytdl_sub.downloaders.soundcloud_downloader.SoundcloudAlbumsAndSinglesDownloadOptions.username - -skip_premiere_tracks -____________________ - .. autoproperty:: ytdl_sub.downloaders.soundcloud_downloader.SoundcloudAlbumsAndSinglesDownloadOptions.skip_premiere_tracks +__________________ +.. autoclass:: ytdl_sub.downloaders.soundcloud_downloader.SoundcloudAlbumsAndSinglesDownloadOptions() + :members: + :member-order: bysource + :inherited-members: ------------------------------------------------------------------------------- output_options -^^^^^^^^^^^^^^ - -output_directory -"""""""""""""""" - .. autoproperty:: ytdl_sub.config.preset_options.OutputOptions.output_directory - -file_name -""""""""" - .. autoproperty:: ytdl_sub.config.preset_options.OutputOptions.file_name - -thumbnail_name """""""""""""" - .. autoproperty:: ytdl_sub.config.preset_options.OutputOptions.thumbnail_name -maintain_download_archive -""""""""""""""""""""""""" - .. autoproperty:: ytdl_sub.config.preset_options.OutputOptions.maintain_download_archive - -keep_files -"""""""""" - .. autoproperty:: ytdl_sub.config.preset_options.OutputOptions.keep_files +.. autoclass:: ytdl_sub.config.preset_options.OutputOptions() + :members: + :member-order: bysource + :exclude-members: get_upload_date_range_to_keep ------------------------------------------------------------------------------- ytdl_options -^^^^^^^^^^^^ +"""""""""""" .. autoclass:: ytdl_sub.config.preset_options.YTDLOptions() ------------------------------------------------------------------------------- +.. _overrides: + overrides -^^^^^^^^^ +""""""""" .. autoclass:: ytdl_sub.config.preset_options.Overrides() +------------------------------------------------------------------------------- + + Plugins -^^^^^^^ +""""""" +Plugins are used to perform any type of post-processing to the already downloaded files. music_tags -"""""""""" +'''''''''' .. autoclass:: ytdl_sub.plugins.music_tags.MusicTagsOptions() + :members: + +------------------------------------------------------------------------------- nfo -""" +''' .. autoclass:: ytdl_sub.plugins.nfo_tags.NfoTagsOptions() + :members: + :member-order: bysource + +------------------------------------------------------------------------------- nfo_output_directory -"""""""""""""""""""" +'''''''''''''''''''' .. autoclass:: ytdl_sub.plugins.output_directory_nfo_tags.OutputDirectoryNfoTagsOptions() + :members: + :member-order: bysource + +------------------------------------------------------------------------------- + +.. _source-variables: Source Variables ---------------- -Source variables are ``{variables}`` that contain metadata from downloaded media. -These variables can be used in StringFormatters, but not OverrideFormatters. + +.. autoclass:: ytdl_sub.entries.variables.entry_variables.SourceVariables + +.. _youtube-variables: Youtube Variables ^^^^^^^^^^^^^^^^^ @@ -176,6 +149,8 @@ Youtube Variables :inherited-members: :undoc-members: +.. _soundcloud-variables: + Soundcloud Variables ^^^^^^^^^^^^^^^^^^^^ .. automodule:: ytdl_sub.entries.variables.soundcloud_variables @@ -183,16 +158,18 @@ Soundcloud Variables :inherited-members: :undoc-members: -Formatters ----------- -Formatters are strings that can contain ``{variables}`` that are overwritten at -run-time with values assigned to that variable. There are two different types of -formatters. +------------------------------------------------------------------------------- + +Config Types +------------ +The `config.yaml`_ uses various types for its configurable fields. Below is a definition for each type. -String Formatter -^^^^^^^^^^^^^^^^ .. autoclass:: ytdl_sub.validators.string_formatter_validators.StringFormatterValidator() -Overrides Formatter -^^^^^^^^^^^^^^^^^^^ .. autoclass:: ytdl_sub.validators.string_formatter_validators.OverridesStringFormatterValidator() + +.. autoclass:: ytdl_sub.validators.string_datetime.StringDatetimeValidator() + +.. autoclass:: ytdl_sub.validators.string_formatter_validators.DictFormatterValidator() + +.. autoclass:: ytdl_sub.validators.string_formatter_validators.OverridesDictFormatterValidator() \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 8b3be2de..5c7b5e4b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,20 +2,20 @@ ytdl-sub: to enjoy media in your favorite player ================================================ ytdl-sub is an `open-source `_ -python app that strives to automate downloading media and preparing it for +python app that automates downloading media and preparing it for consumption in your favorite media player in the most hassle-free way possible. If you are new to ytdl-sub, head over to the :doc:`Getting Started ` -page. This guide shows how you can install ytdl-sub, configure it to download +page. This guide explains how you can use ytdl-sub, install, configure it to download various types of media, and start downloading. Contents ======== .. toctree:: - :maxdepth: 10 + :maxdepth: 2 getting_started config diff --git a/examples/kodi_tv_shows_subscriptions.yaml b/examples/kodi_tv_shows_subscriptions.yaml index d46ff789..c79de12c 100644 --- a/examples/kodi_tv_shows_subscriptions.yaml +++ b/examples/kodi_tv_shows_subscriptions.yaml @@ -70,7 +70,7 @@ john_smith_recent_archive: # only keep the last 14-days worth of videos, and delete the rest. # # The only difference between this example and the one above is -# - output_options.keep_files.after +# - output_options.keep_files_after # This is saying "only keep files if they were uploaded in the last 14 days". # All other files that this subscription had previously downloaded will be # deleted. @@ -85,5 +85,4 @@ john_smith_rolling_archive: break_on_reject: True break_on_existing: True output_options: - keep_files: - after: today-2weeks \ No newline at end of file + keep_files_after: today-2weeks \ No newline at end of file diff --git a/src/ytdl_sub/config/preset_options.py b/src/ytdl_sub/config/preset_options.py index ca827c97..62ecb339 100644 --- a/src/ytdl_sub/config/preset_options.py +++ b/src/ytdl_sub/config/preset_options.py @@ -1,10 +1,11 @@ from typing import Optional +from yt_dlp.utils import DateRange from yt_dlp.utils import sanitize_filename from ytdl_sub.entries.entry import Entry -from ytdl_sub.validators.date_range_validator import DateRangeValidator from ytdl_sub.validators.strict_dict_validator import StrictDictValidator +from ytdl_sub.validators.string_datetime import StringDatetimeValidator from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator @@ -86,13 +87,32 @@ class Overrides(DictFormatterValidator): class OutputOptions(StrictDictValidator): - """Where to output the final files and thumbnails""" + """ + Defines where to output files and thumbnails after all post-processing has completed. + + Usage: + + .. code-block:: yaml + + presets: + my_example_preset: + output_options: + # required + output_directory: "/path/to/videos_or_music" + file_name: "{sanitized_title}.{ext}" + # optional + thumbnail_name: "{sanitized_title}.{thumbnail_ext}" + maintain_download_archive: True + keep_files_before: now + keep_files_after: 19000101 + """ _required_keys = {"output_directory", "file_name"} _optional_keys = { "thumbnail_name", "maintain_download_archive", - "keep_files", + "keep_files_before", + "keep_files_after", } def __init__(self, name, value): @@ -115,11 +135,17 @@ class OutputOptions(StrictDictValidator): self._maintain_download_archive = self._validate_key_if_present( key="maintain_download_archive", validator=BoolValidator, default=False ) - self._keep_files = self._validate_key_if_present( - key="keep_files", validator=DateRangeValidator + + self._keep_files_before = self._validate_key_if_present( + "keep_files_before", StringDatetimeValidator + ) + self._keep_files_after = self._validate_key_if_present( + "keep_files_after", StringDatetimeValidator ) - if self._keep_files and not self.maintain_download_archive: + if ( + self._keep_files_before or self._keep_files_after + ) and not self.maintain_download_archive: raise self._validation_exception( "keep_files requires maintain_download_archive set to True" ) @@ -164,34 +190,34 @@ class OutputOptions(StrictDictValidator): return self._maintain_download_archive.value @property - def keep_files(self) -> DateRangeValidator: + def keep_files_before(self) -> Optional[StringDatetimeValidator]: """ Optional. Requires ``maintain_download_archive`` set to True. - Only keeps files that are uploaded in the defined range. Should be formatted as: - - .. code-block:: yaml - - presets: - my_example_preset: - output_options: - keep_files: - before: - after: - - where ``before`` and ``after`` are date-times. A common usage of this option is to only - fill in the after, such as: - - .. code-block:: yaml - - presets: - my_example_preset: - output_options: - keep_files: - after: today-2weeks - - Which translates to 'keep files uploaded in the last two weeks'. - - By default, ytdl-sub will keep all files. + Only keeps files that are uploaded before this datetime. By default, ytdl-sub will keep + files before ``now``, which implies all files. """ - return self._keep_files + return self._keep_files_before + + @property + def keep_files_after(self) -> Optional[StringDatetimeValidator]: + """ + Optional. Requires ``maintain_download_archive`` set to True. + + Only keeps files that are uploaded after this datetime. By default, ytdl-sub will keep + files after ``19000101``, which implies all files. + """ + return self._keep_files_after + + def get_upload_date_range_to_keep(self) -> Optional[DateRange]: + """ + Returns + ------- + Date range if the 'before' or 'after' is defined. None otherwise. + """ + if self.keep_files_before or self.keep_files_after: + return DateRange( + start=self.keep_files_after.datetime_str if self.keep_files_after else None, + end=self.keep_files_before.datetime_str if self.keep_files_before else None, + ) + return None diff --git a/src/ytdl_sub/downloaders/soundcloud_downloader.py b/src/ytdl_sub/downloaders/soundcloud_downloader.py index 23f1a2e6..89bea5c2 100644 --- a/src/ytdl_sub/downloaders/soundcloud_downloader.py +++ b/src/ytdl_sub/downloaders/soundcloud_downloader.py @@ -71,6 +71,25 @@ class SoundcloudDownloader( class SoundcloudAlbumsAndSinglesDownloadOptions(SoundcloudDownloaderOptions): + """ + Downloads a soundcloud user's entire discography. Groups together album tracks and considers + any track not in an album as a single. Also includes any collaboration tracks. + + Usage: + + .. code-block:: yaml + + presets: + my_example_preset: + soundcloud: + # required + download_strategy: "albums_and_singles" + username: soundcloud_username_from_url + # optional + skip_premiere_tracks: True + + """ + _required_keys = {"username"} def __init__(self, name, value): diff --git a/src/ytdl_sub/downloaders/youtube_downloader.py b/src/ytdl_sub/downloaders/youtube_downloader.py index 8de267cb..694f20f5 100644 --- a/src/ytdl_sub/downloaders/youtube_downloader.py +++ b/src/ytdl_sub/downloaders/youtube_downloader.py @@ -55,6 +55,29 @@ class YoutubeDownloader( class YoutubeVideoDownloaderOptions(YoutubeDownloaderOptions): + """ + Downloads a single youtube video. This download strategy is intended for CLI usage performing + a one-time download of a video, not a subscription. + + Usage: + + .. code-block:: yaml + + presets: + my_example_preset: + youtube: + # required + download_strategy: "video" + video_id: "VMAPTo7RVDo" + + CLI usage: + + .. code-block:: bash + + ytdl-sub dl --preset "my_example_preset" --youtube.video_id "VMAPTo7RVDo" + + """ + _required_keys = {"video_id"} def __init__(self, name, value): @@ -64,7 +87,7 @@ class YoutubeVideoDownloaderOptions(YoutubeDownloaderOptions): @property def video_id(self) -> str: """ - Required. The ID of the video. Looks like the ``VMAPTo7RVDo`` in + Required. The ID of the video. Looks like the ``VMAPTo7RVDo`` from ``youtube.com/watch?v=VMAPTo7RVDo``. """ return self._video_id.value @@ -92,6 +115,21 @@ class YoutubeVideoDownloader(YoutubeDownloader[YoutubeVideoDownloaderOptions, Yo class YoutubePlaylistDownloaderOptions(YoutubeDownloaderOptions): + """ + Downloads all videos from a youtube playlist. + + Usage: + + .. code-block:: yaml + + presets: + my_example_preset: + youtube: + # required + download_strategy: "playlist" + playlist_id: "UCsvn_Po0SmunchJYtttWpOxMg" + """ + _required_keys = {"playlist_id"} def __init__(self, name, value): @@ -101,7 +139,8 @@ class YoutubePlaylistDownloaderOptions(YoutubeDownloaderOptions): @property def playlist_id(self) -> str: """ - Required. The playlist's ID. + Required. The playlist's ID. Looks like "UCsvn_Po0SmunchJYtttWpOxMg" + from ``https://www.youtube.com/playlist?list=UCsvn_Po0SmunchJYtttWpOxMg``. """ return self._playlist_id.value @@ -141,6 +180,26 @@ class YoutubePlaylistDownloader( class YoutubeChannelDownloaderOptions(YoutubeDownloaderOptions, DateRangeValidator): + """ + Downloads all videos from a youtube channel. + + Usage: + + .. code-block:: yaml + + presets: + my_example_preset: + youtube: + # required + download_strategy: "channel" + channel_id: "UCsvn_Po0SmunchJYtttWpOxMg" + # optional + channel_avatar_path: "poster.jpg" + channel_banner_path: "fanart.jpg" + before: "now" + after: "today-2weeks" + """ + _required_keys = {"channel_id"} _optional_keys = {"before", "after", "channel_avatar_path", "channel_banner_path"} @@ -159,8 +218,10 @@ class YoutubeChannelDownloaderOptions(YoutubeDownloaderOptions, DateRangeValidat def channel_id(self) -> str: """ Required. The channel's ID. Not to be confused with the username. It should look something - like `UCsvn_Po0SmunchJYOWpOxMg`. You can get this by opening a video and clicking on the - channel's avatar image to take you to their channel, then check the url. + like ``UCsvn_Po0SmunchJYOWpOxMg`` from + ``https://www.youtube.com/channel/UCsvn_Po0SmunchJYOWpOxMg``. You can get this by opening a + video and clicking on the channel's avatar image to take you to their channel, then check + the url. """ return self._channel_id.value diff --git a/src/ytdl_sub/entries/variables/entry_variables.py b/src/ytdl_sub/entries/variables/entry_variables.py index 6ddc7a75..5e79bc00 100644 --- a/src/ytdl_sub/entries/variables/entry_variables.py +++ b/src/ytdl_sub/entries/variables/entry_variables.py @@ -9,9 +9,13 @@ from ytdl_sub.entries.base_entry import BaseEntry # pylint: disable=no-member -class BaseEntryVariables: +class SourceVariables: """ - Abstract entry object to represent usable variables for formatting presets and subscriptions + Source variables are ``{variables}`` that contain metadata from downloaded media. + These variables can be used with fields that expect + :class:`~ytdl_sub.validators.string_formatter_validators.StringFormatterValidator`, + but not + :class:`~ytdl_sub.validators.string_formatter_validators.OverridesStringFormatterValidator`. """ @property @@ -45,7 +49,7 @@ class BaseEntryVariables: return {property_name: getattr(self, property_name) for property_name in property_names} -class EntryVariables(BaseEntryVariables): +class EntryVariables(SourceVariables): @property def title(self: BaseEntry) -> str: """ diff --git a/src/ytdl_sub/plugins/music_tags.py b/src/ytdl_sub/plugins/music_tags.py index 9d61ca3e..c2b13b91 100644 --- a/src/ytdl_sub/plugins/music_tags.py +++ b/src/ytdl_sub/plugins/music_tags.py @@ -8,18 +8,47 @@ from ytdl_sub.validators.validators import StringValidator class MusicTagsOptions(PluginOptions): + """ + Adds tags to every download audio file using + `MediaFile `_, + the same audio file tagging package used by + `beets `_. + It supports basic tags like ``title``, ``album``, ``artist`` and ``albumartist``. You can find + a full list of tags for various file types in MediaFile's + `source code `_. + + Usage: + + .. code-block:: yaml + + presets: + my_example_preset: + music_tags: + tags: + artist: "{artist}" + album: "{album}" + genre: "ytdl downloaded music" + """ + _required_keys = {"tags"} _optional_keys = {"multi_value_separator"} def __init__(self, name, value): super().__init__(name, value) - self.tags = self._validate_key(key="tags", validator=DictFormatterValidator) + self._tags = self._validate_key(key="tags", validator=DictFormatterValidator) self.multi_value_separator = self._validate_key_if_present( key="multi_value_separator", validator=StringValidator ) + @property + def tags(self) -> DictFormatterValidator: + """ + Key/values of tag names/tag values. Supports source and override variables. + """ + return self._tags + class MusicTagsPlugin(Plugin[MusicTagsOptions]): plugin_options_type = MusicTagsOptions diff --git a/src/ytdl_sub/plugins/nfo_tags.py b/src/ytdl_sub/plugins/nfo_tags.py index e006148a..abce398a 100644 --- a/src/ytdl_sub/plugins/nfo_tags.py +++ b/src/ytdl_sub/plugins/nfo_tags.py @@ -11,14 +11,69 @@ from ytdl_sub.validators.string_formatter_validators import StringFormatterValid class NfoTagsOptions(PluginOptions): + """ + Adds an NFO file for every download file. An NFO file is simply an XML file + with a ``.nfo`` extension. You can add any values into the NFO. + + Usage: + + .. code-block:: yaml + + presets: + my_example_preset: + nfo: + nfo_name: "{sanitized_title}.nfo" + nfo_root: "episodedetails" + tags: + title: "{title}" + season: "{upload_year}" + episode: "{upload_month}{upload_day_padded}" + """ + _required_keys = {"nfo_name", "nfo_root", "tags"} def __init__(self, name, value): super().__init__(name, value) - self.nfo_name = self._validate_key(key="nfo_name", validator=StringFormatterValidator) - self.nfo_root = self._validate_key(key="nfo_root", validator=StringFormatterValidator) - self.tags = self._validate_key(key="tags", validator=DictFormatterValidator) + self._nfo_name = self._validate_key(key="nfo_name", validator=StringFormatterValidator) + self._nfo_root = self._validate_key(key="nfo_root", validator=StringFormatterValidator) + self._tags = self._validate_key(key="tags", validator=DictFormatterValidator) + + @property + def nfo_name(self) -> StringFormatterValidator: + """ + The NFO file name. + """ + return self._nfo_name + + @property + def nfo_root(self) -> StringFormatterValidator: + """ + The root tag of the NFO's XML. In the usage above, it would look like + + .. code-block:: xml + + + + + """ + return self._nfo_root + + @property + def tags(self) -> DictFormatterValidator: + """ + Tags within the nfo_root tag. In the usage above, it would look like + + .. code-block:: xml + + + + Awesome Youtube Video + 2022 + 502 + + """ + return self._tags class NfoTagsPlugin(Plugin[NfoTagsOptions]): diff --git a/src/ytdl_sub/plugins/output_directory_nfo_tags.py b/src/ytdl_sub/plugins/output_directory_nfo_tags.py index 3d1f3be4..24123f8a 100644 --- a/src/ytdl_sub/plugins/output_directory_nfo_tags.py +++ b/src/ytdl_sub/plugins/output_directory_nfo_tags.py @@ -11,7 +11,20 @@ from ytdl_sub.validators.string_formatter_validators import OverridesStringForma class OutputDirectoryNfoTagsOptions(PluginOptions): """ - Validates config values for the output directory nfo tags plugin. + Adds a single NFO file in the output directory. An NFO file is simply an XML file with a + ``.nfo`` extension. You can add any values into the NFO. + + Usage: + + .. code-block:: yaml + + presets: + my_example_preset: + output_directory_nfo_tags: + nfo_name: "tvshow.nfo" + nfo_root: "tvshow" + tags: + title: "Sweet youtube TV show" """ _required_keys = {"nfo_name", "nfo_root", "tags"} @@ -20,13 +33,47 @@ class OutputDirectoryNfoTagsOptions(PluginOptions): super().__init__(name, value) # Since this does not create NFOs for entries, we must use the overrides formatter classes # to ensure we are only using values defined as string literals or in overrides - self.nfo_name = self._validate_key( + self._nfo_name = self._validate_key( key="nfo_name", validator=OverridesStringFormatterValidator ) - self.nfo_root = self._validate_key( + self._nfo_root = self._validate_key( key="nfo_root", validator=OverridesStringFormatterValidator ) - self.tags = self._validate_key(key="tags", validator=OverridesDictFormatterValidator) + self._tags = self._validate_key(key="tags", validator=OverridesDictFormatterValidator) + + @property + def nfo_name(self) -> OverridesStringFormatterValidator: + """ + The NFO file name. + """ + return self._nfo_name + + @property + def nfo_root(self) -> OverridesStringFormatterValidator: + """ + The root tag of the NFO's XML. In the usage above, it would look like + + .. code-block:: xml + + + + + """ + return self._nfo_root + + @property + def tags(self) -> OverridesDictFormatterValidator: + """ + Tags within the nfo_root tag. In the usage above, it would look like + + .. code-block:: xml + + + + Sweet youtube TV show + + """ + return self._tags class OutputDirectoryNfoTagsPlugin(Plugin[OutputDirectoryNfoTagsOptions]): diff --git a/src/ytdl_sub/subscriptions/subscription.py b/src/ytdl_sub/subscriptions/subscription.py index 11333731..2d939091 100644 --- a/src/ytdl_sub/subscriptions/subscription.py +++ b/src/ytdl_sub/subscriptions/subscription.py @@ -202,10 +202,9 @@ class Subscription: # If output options maintains stale file deletion, perform the delete here prior to saving # the download archive if self.output_options.maintain_download_archive: - if self.output_options.keep_files: - self._enhanced_download_archive.remove_stale_files( - date_range=self.output_options.keep_files.get_date_range() - ) + date_range_to_keep = self.output_options.get_upload_date_range_to_keep() + if date_range_to_keep: + self._enhanced_download_archive.remove_stale_files(date_range=date_range_to_keep) self._enhanced_download_archive.save_download_mappings() diff --git a/src/ytdl_sub/validators/string_datetime.py b/src/ytdl_sub/validators/string_datetime.py index 391e0ae6..5e85c581 100644 --- a/src/ytdl_sub/validators/string_datetime.py +++ b/src/ytdl_sub/validators/string_datetime.py @@ -5,7 +5,14 @@ from ytdl_sub.validators.validators import Validator class StringDatetimeValidator(Validator): """ - Validates a ytdl datetime string value + String that contains a yt-dlp datetime. From their docs: + + .. code-block:: Markdown + + A string in the format YYYYMMDD or + (now|today|yesterday|date)[+-][0-9](microsecond|second|minute|hour|day|week|month|year)(s) + + Valid examples are ``now-2weeks`` or ``20200101``. """ _expected_value_type = str diff --git a/src/ytdl_sub/validators/string_formatter_validators.py b/src/ytdl_sub/validators/string_formatter_validators.py index e1b7df97..b33db5b6 100644 --- a/src/ytdl_sub/validators/string_formatter_validators.py +++ b/src/ytdl_sub/validators/string_formatter_validators.py @@ -13,7 +13,28 @@ from ytdl_sub.validators.validators import Validator class StringFormatterValidator(Validator): """ - Ensures user-created formatter strings are valid + String that can use + :class:`source variables ` + and + :class:`overrides ` + for populating things like file paths and metadata. + + .. code-block:: python + + "{tv_show_file_name}.s{upload_year}.e{upload_month}{upload_day_padded}.{ext}" + + is valid when using + :class:`youtube variables ` + with the following overrides: + + .. code-block:: yaml + + presets: + my_example_preset: + overrides: + tv_show_file_name: "sweet_tv_show" + + and would resolve to something like ``sweet_tv_show.s2022.e502.mp4``. """ _expected_value_type = str @@ -132,15 +153,26 @@ class StringFormatterValidator(Validator): return formatter.format_string +# pylint: disable=line-too-long class OverridesStringFormatterValidator(StringFormatterValidator): """ - A string formatter that should strictly use overrides that resolve without any entry variables. + String that can `only` use :class:`overrides `. + + Used in fields that do not touch the downloaded files themselves, but instead, `single` + things like + :func:`output_directory ` + or the fields in + :class:`nfo_output_directory ` """ +# pylint: enable=line-too-long + + class DictFormatterValidator(LiteralDictValidator): """ - Validates a dictionary made up of key: string_formatters + A dict made up of + :class:`~ytdl_sub.validators.string_formatter_validators.StringFormatterValidator`. """ _key_validator = StringFormatterValidator @@ -164,8 +196,8 @@ class DictFormatterValidator(LiteralDictValidator): class OverridesDictFormatterValidator(DictFormatterValidator): """ - Validates a dictionary made up of key: string_formatters, that must be resolved by overrides - only. + A dict made up of + :class:`~ytdl_sub.validators.string_formatter_validators.OverridesStringFormatterValidator`. """ _key_validator = OverridesStringFormatterValidator diff --git a/tests/e2e/youtube/test_channel_as_kodi_tv_show.py b/tests/e2e/youtube/test_channel_as_kodi_tv_show.py index 03c1e22e..0f637992 100644 --- a/tests/e2e/youtube/test_channel_as_kodi_tv_show.py +++ b/tests/e2e/youtube/test_channel_as_kodi_tv_show.py @@ -183,7 +183,7 @@ def expected_recent_channel_download(): @pytest.fixture def rolling_recent_channel_subscription_dict(recent_channel_subscription_dict): return mergedeep.merge( - recent_channel_subscription_dict, {"output_options": {"keep_files": {"after": "20181101"}}} + recent_channel_subscription_dict, {"output_options": {"keep_files_after": "20181101"}} )