Add documentation for plugins + improve docs overall (#23)

This commit is contained in:
Jesse Bannon 2022-05-10 22:22:03 -07:00 committed by GitHub
parent f67a4dc7dc
commit 29ef377cc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 434 additions and 175 deletions

View file

@ -8,18 +8,18 @@
Automate downloading and adding metadata with YoutubeDL. 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) [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), ([Kodi](https://github.com/xbmc/xbmc),
[Jellyfin](https://github.com/jellyfin/jellyfin), [Jellyfin](https://github.com/jellyfin/jellyfin),
[Plex](https://github.com/plexinc/pms-docker), [Plex](https://github.com/plexinc/pms-docker),
[Emby](https://github.com/plexinc/pms-docker), [Emby](https://github.com/plexinc/pms-docker),
modern music players) modern music players).
in the most hassle-free way possible. We recognize that everyone stores their We recognize that everyone stores their
media differently. Our approach for file and metadata formatting is to provide media differently. Our approach for file and metadata formatting is to provide
maximum flexibility while maintaining simplicity. Read more about it 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 ## Supported Features
Below lists supported download schemes. You can see our Below lists supported download schemes. You can see our

View file

@ -61,3 +61,7 @@ add_module_names = False
python_use_unqualified_type_names = False python_use_unqualified_type_names = False
napoleon_numpy_docstrings = True napoleon_numpy_docstrings = True
napoleon_use_rtype = False napoleon_use_rtype = False
# -- Options for autodocs -------------------------------------------------
autodoc_default_options = {"autodoc_typehints_format": "short"}

View file

@ -4,6 +4,9 @@ ytdl-sub is configured using a ``config.yaml`` file. You can view our
:doc:`examples <examples>` and read detailed documentation for every configurable :doc:`examples <examples>` and read detailed documentation for every configurable
field below. field below.
config.yaml
-----------
The ``config.yaml`` is made up of two sections: The ``config.yaml`` is made up of two sections:
.. code-block:: yaml .. 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. section to the left.
configuration configuration
------------- ^^^^^^^^^^^^^
The ``configuration`` section contains app-wide configs applied to all presets The ``configuration`` section contains app-wide configs applied to all presets
and subscriptions. and subscriptions.
presets presets
------- ^^^^^^^
``presets`` define a `formula` for how to format downloaded media and metadata. ``presets`` define a `formula` for how to format downloaded media and metadata.
download_strategy download_strategy
^^^^^^^^^^^^^^^^^ """""""""""""""""
Download strategies dictate what is getting downloaded from a source. Each Download strategies dictate what is getting downloaded from a source. Each
download strategy has its own set of parameters. download strategy has its own set of parameters.
youtube 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 <ytdl_sub.entries.variables.youtube_variables>`
for available source variables to use.
channel channel
''''''' _______
.. code-block:: yaml .. autoclass:: ytdl_sub.downloaders.youtube_downloader.YoutubeChannelDownloaderOptions()
:members:
presets: :member-order: bysource
my_example_preset: :inherited-members:
youtube: :exclude-members: get_date_range
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
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
playlist playlist
'''''''' ________
.. code-block:: yaml .. autoclass:: ytdl_sub.downloaders.youtube_downloader.YoutubePlaylistDownloaderOptions()
:members:
presets: :member-order: bysource
my_example_preset: :inherited-members:
youtube:
download_strategy: "playlist"
playlist_id
___________
.. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubePlaylistDownloaderOptions.playlist_id
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
video video
''''' _____
.. code-block:: yaml .. autoclass:: ytdl_sub.downloaders.youtube_downloader.YoutubeVideoDownloaderOptions()
:members:
presets: :member-order: bysource
my_example_preset: :inherited-members:
youtube:
download_strategy: "video"
video_id
________
.. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubeVideoDownloaderOptions.video_id
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
soundcloud soundcloud
"""""""""" ''''''''''
Download strategies for downloading music from Soundcloud. See
:class:`Soundcloud Variables <ytdl_sub.entries.variables.soundcloud_variables>`
for available source variables to use.
albums_and_singles albums_and_singles
'''''''''''''''''' __________________
.. code-block:: yaml .. autoclass:: ytdl_sub.downloaders.soundcloud_downloader.SoundcloudAlbumsAndSinglesDownloadOptions()
:members:
presets: :member-order: bysource
my_example_preset: :inherited-members:
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
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
output_options 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 .. autoclass:: ytdl_sub.config.preset_options.OutputOptions()
""""""""""""""""""""""""" :members:
.. autoproperty:: ytdl_sub.config.preset_options.OutputOptions.maintain_download_archive :member-order: bysource
:exclude-members: get_upload_date_range_to_keep
keep_files
""""""""""
.. autoproperty:: ytdl_sub.config.preset_options.OutputOptions.keep_files
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
ytdl_options ytdl_options
^^^^^^^^^^^^ """"""""""""
.. autoclass:: ytdl_sub.config.preset_options.YTDLOptions() .. autoclass:: ytdl_sub.config.preset_options.YTDLOptions()
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
.. _overrides:
overrides overrides
^^^^^^^^^ """""""""
.. autoclass:: ytdl_sub.config.preset_options.Overrides() .. autoclass:: ytdl_sub.config.preset_options.Overrides()
-------------------------------------------------------------------------------
Plugins Plugins
^^^^^^^ """""""
Plugins are used to perform any type of post-processing to the already downloaded files.
music_tags music_tags
"""""""""" ''''''''''
.. autoclass:: ytdl_sub.plugins.music_tags.MusicTagsOptions() .. autoclass:: ytdl_sub.plugins.music_tags.MusicTagsOptions()
:members:
-------------------------------------------------------------------------------
nfo nfo
""" '''
.. autoclass:: ytdl_sub.plugins.nfo_tags.NfoTagsOptions() .. autoclass:: ytdl_sub.plugins.nfo_tags.NfoTagsOptions()
:members:
:member-order: bysource
-------------------------------------------------------------------------------
nfo_output_directory nfo_output_directory
"""""""""""""""""""" ''''''''''''''''''''
.. autoclass:: ytdl_sub.plugins.output_directory_nfo_tags.OutputDirectoryNfoTagsOptions() .. autoclass:: ytdl_sub.plugins.output_directory_nfo_tags.OutputDirectoryNfoTagsOptions()
:members:
:member-order: bysource
-------------------------------------------------------------------------------
.. _source-variables:
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 Youtube Variables
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
@ -176,6 +149,8 @@ Youtube Variables
:inherited-members: :inherited-members:
:undoc-members: :undoc-members:
.. _soundcloud-variables:
Soundcloud Variables Soundcloud Variables
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
.. automodule:: ytdl_sub.entries.variables.soundcloud_variables .. automodule:: ytdl_sub.entries.variables.soundcloud_variables
@ -183,16 +158,18 @@ Soundcloud Variables
:inherited-members: :inherited-members:
:undoc-members: :undoc-members:
Formatters -------------------------------------------------------------------------------
----------
Formatters are strings that can contain ``{variables}`` that are overwritten at Config Types
run-time with values assigned to that variable. There are two different types of ------------
formatters. 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() .. autoclass:: ytdl_sub.validators.string_formatter_validators.StringFormatterValidator()
Overrides Formatter
^^^^^^^^^^^^^^^^^^^
.. autoclass:: ytdl_sub.validators.string_formatter_validators.OverridesStringFormatterValidator() .. 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()

View file

@ -2,20 +2,20 @@ ytdl-sub: to enjoy media in your favorite player
================================================ ================================================
ytdl-sub is an `open-source <https://github.com/jmbannon/ytdl-sub>`_ ytdl-sub is an `open-source <https://github.com/jmbannon/ytdl-sub>`_
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 consumption in your favorite media player in the most hassle-free way
possible. possible.
If you are new to ytdl-sub, head over to the If you are new to ytdl-sub, head over to the
:doc:`Getting Started <getting_started>` :doc:`Getting Started <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. various types of media, and start downloading.
Contents Contents
======== ========
.. toctree:: .. toctree::
:maxdepth: 10 :maxdepth: 2
getting_started getting_started
config config

View file

@ -70,7 +70,7 @@ john_smith_recent_archive:
# only keep the last 14-days worth of videos, and delete the rest. # only keep the last 14-days worth of videos, and delete the rest.
# #
# The only difference between this example and the one above is # 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". # 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 # All other files that this subscription had previously downloaded will be
# deleted. # deleted.
@ -85,5 +85,4 @@ john_smith_rolling_archive:
break_on_reject: True break_on_reject: True
break_on_existing: True break_on_existing: True
output_options: output_options:
keep_files: keep_files_after: today-2weeks
after: today-2weeks

View file

@ -1,10 +1,11 @@
from typing import Optional from typing import Optional
from yt_dlp.utils import DateRange
from yt_dlp.utils import sanitize_filename from yt_dlp.utils import sanitize_filename
from ytdl_sub.entries.entry import Entry 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.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 DictFormatterValidator
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
@ -86,13 +87,32 @@ class Overrides(DictFormatterValidator):
class OutputOptions(StrictDictValidator): 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"} _required_keys = {"output_directory", "file_name"}
_optional_keys = { _optional_keys = {
"thumbnail_name", "thumbnail_name",
"maintain_download_archive", "maintain_download_archive",
"keep_files", "keep_files_before",
"keep_files_after",
} }
def __init__(self, name, value): def __init__(self, name, value):
@ -115,11 +135,17 @@ class OutputOptions(StrictDictValidator):
self._maintain_download_archive = self._validate_key_if_present( self._maintain_download_archive = self._validate_key_if_present(
key="maintain_download_archive", validator=BoolValidator, default=False 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( raise self._validation_exception(
"keep_files requires maintain_download_archive set to True" "keep_files requires maintain_download_archive set to True"
) )
@ -164,34 +190,34 @@ class OutputOptions(StrictDictValidator):
return self._maintain_download_archive.value return self._maintain_download_archive.value
@property @property
def keep_files(self) -> DateRangeValidator: def keep_files_before(self) -> Optional[StringDatetimeValidator]:
""" """
Optional. Requires ``maintain_download_archive`` set to True. Optional. Requires ``maintain_download_archive`` set to True.
Only keeps files that are uploaded in the defined range. Should be formatted as: Only keeps files that are uploaded before this datetime. By default, ytdl-sub will keep
files before ``now``, which implies all files.
.. 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.
""" """
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

View file

@ -71,6 +71,25 @@ class SoundcloudDownloader(
class SoundcloudAlbumsAndSinglesDownloadOptions(SoundcloudDownloaderOptions): 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"} _required_keys = {"username"}
def __init__(self, name, value): def __init__(self, name, value):

View file

@ -55,6 +55,29 @@ class YoutubeDownloader(
class YoutubeVideoDownloaderOptions(YoutubeDownloaderOptions): 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"} _required_keys = {"video_id"}
def __init__(self, name, value): def __init__(self, name, value):
@ -64,7 +87,7 @@ class YoutubeVideoDownloaderOptions(YoutubeDownloaderOptions):
@property @property
def video_id(self) -> str: 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``. ``youtube.com/watch?v=VMAPTo7RVDo``.
""" """
return self._video_id.value return self._video_id.value
@ -92,6 +115,21 @@ class YoutubeVideoDownloader(YoutubeDownloader[YoutubeVideoDownloaderOptions, Yo
class YoutubePlaylistDownloaderOptions(YoutubeDownloaderOptions): 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"} _required_keys = {"playlist_id"}
def __init__(self, name, value): def __init__(self, name, value):
@ -101,7 +139,8 @@ class YoutubePlaylistDownloaderOptions(YoutubeDownloaderOptions):
@property @property
def playlist_id(self) -> str: 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 return self._playlist_id.value
@ -141,6 +180,26 @@ class YoutubePlaylistDownloader(
class YoutubeChannelDownloaderOptions(YoutubeDownloaderOptions, DateRangeValidator): 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"} _required_keys = {"channel_id"}
_optional_keys = {"before", "after", "channel_avatar_path", "channel_banner_path"} _optional_keys = {"before", "after", "channel_avatar_path", "channel_banner_path"}
@ -159,8 +218,10 @@ class YoutubeChannelDownloaderOptions(YoutubeDownloaderOptions, DateRangeValidat
def channel_id(self) -> str: def channel_id(self) -> str:
""" """
Required. The channel's ID. Not to be confused with the username. It should look something 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 like ``UCsvn_Po0SmunchJYOWpOxMg`` from
channel's avatar image to take you to their channel, then check the url. ``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 return self._channel_id.value

View file

@ -9,9 +9,13 @@ from ytdl_sub.entries.base_entry import BaseEntry
# pylint: disable=no-member # 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 @property
@ -45,7 +49,7 @@ class BaseEntryVariables:
return {property_name: getattr(self, property_name) for property_name in property_names} return {property_name: getattr(self, property_name) for property_name in property_names}
class EntryVariables(BaseEntryVariables): class EntryVariables(SourceVariables):
@property @property
def title(self: BaseEntry) -> str: def title(self: BaseEntry) -> str:
""" """

View file

@ -8,18 +8,47 @@ from ytdl_sub.validators.validators import StringValidator
class MusicTagsOptions(PluginOptions): class MusicTagsOptions(PluginOptions):
"""
Adds tags to every download audio file using
`MediaFile <https://mediafile.readthedocs.io/en/latest/>`_,
the same audio file tagging package used by
`beets <https://beets.readthedocs.io/en/stable/>`_.
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 <https://github.com/beetbox/mediafile/blob/v0.9.0/mediafile.py#L1770>`_.
Usage:
.. code-block:: yaml
presets:
my_example_preset:
music_tags:
tags:
artist: "{artist}"
album: "{album}"
genre: "ytdl downloaded music"
"""
_required_keys = {"tags"} _required_keys = {"tags"}
_optional_keys = {"multi_value_separator"} _optional_keys = {"multi_value_separator"}
def __init__(self, name, value): def __init__(self, name, value):
super().__init__(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( self.multi_value_separator = self._validate_key_if_present(
key="multi_value_separator", validator=StringValidator 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]): class MusicTagsPlugin(Plugin[MusicTagsOptions]):
plugin_options_type = MusicTagsOptions plugin_options_type = MusicTagsOptions

View file

@ -11,14 +11,69 @@ from ytdl_sub.validators.string_formatter_validators import StringFormatterValid
class NfoTagsOptions(PluginOptions): 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"} _required_keys = {"nfo_name", "nfo_root", "tags"}
def __init__(self, name, value): def __init__(self, name, value):
super().__init__(name, value) super().__init__(name, value)
self.nfo_name = self._validate_key(key="nfo_name", validator=StringFormatterValidator) self._nfo_name = self._validate_key(key="nfo_name", validator=StringFormatterValidator)
self.nfo_root = self._validate_key(key="nfo_root", validator=StringFormatterValidator) self._nfo_root = self._validate_key(key="nfo_root", validator=StringFormatterValidator)
self.tags = self._validate_key(key="tags", validator=DictFormatterValidator) 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
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<episodedetails>
</episodedetails>
"""
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
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<episodedetails>
<title>Awesome Youtube Video</title>
<season>2022</season>
<episode>502</episode>
</episodedetails>
"""
return self._tags
class NfoTagsPlugin(Plugin[NfoTagsOptions]): class NfoTagsPlugin(Plugin[NfoTagsOptions]):

View file

@ -11,7 +11,20 @@ from ytdl_sub.validators.string_formatter_validators import OverridesStringForma
class OutputDirectoryNfoTagsOptions(PluginOptions): 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"} _required_keys = {"nfo_name", "nfo_root", "tags"}
@ -20,13 +33,47 @@ class OutputDirectoryNfoTagsOptions(PluginOptions):
super().__init__(name, value) super().__init__(name, value)
# Since this does not create NFOs for entries, we must use the overrides formatter classes # 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 # 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 key="nfo_name", validator=OverridesStringFormatterValidator
) )
self.nfo_root = self._validate_key( self._nfo_root = self._validate_key(
key="nfo_root", validator=OverridesStringFormatterValidator 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
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<tvshow>
</tvshow>
"""
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
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<tvshow>
<title>Sweet youtube TV show</title>
</tvshow>
"""
return self._tags
class OutputDirectoryNfoTagsPlugin(Plugin[OutputDirectoryNfoTagsOptions]): class OutputDirectoryNfoTagsPlugin(Plugin[OutputDirectoryNfoTagsOptions]):

View file

@ -202,10 +202,9 @@ class Subscription:
# If output options maintains stale file deletion, perform the delete here prior to saving # If output options maintains stale file deletion, perform the delete here prior to saving
# the download archive # the download archive
if self.output_options.maintain_download_archive: if self.output_options.maintain_download_archive:
if self.output_options.keep_files: date_range_to_keep = self.output_options.get_upload_date_range_to_keep()
self._enhanced_download_archive.remove_stale_files( if date_range_to_keep:
date_range=self.output_options.keep_files.get_date_range() self._enhanced_download_archive.remove_stale_files(date_range=date_range_to_keep)
)
self._enhanced_download_archive.save_download_mappings() self._enhanced_download_archive.save_download_mappings()

View file

@ -5,7 +5,14 @@ from ytdl_sub.validators.validators import Validator
class StringDatetimeValidator(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 _expected_value_type = str

View file

@ -13,7 +13,28 @@ from ytdl_sub.validators.validators import Validator
class StringFormatterValidator(Validator): class StringFormatterValidator(Validator):
""" """
Ensures user-created formatter strings are valid String that can use
:class:`source variables <ytdl_sub.entries.variables.entry_variables.SourceVariables>`
and
:class:`overrides <ytdl_sub.config.preset_options.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 <ytdl_sub.entries.variables.youtube_variables.YoutubeVideoVariables>`
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 _expected_value_type = str
@ -132,15 +153,26 @@ class StringFormatterValidator(Validator):
return formatter.format_string return formatter.format_string
# pylint: disable=line-too-long
class OverridesStringFormatterValidator(StringFormatterValidator): class OverridesStringFormatterValidator(StringFormatterValidator):
""" """
A string formatter that should strictly use overrides that resolve without any entry variables. String that can `only` use :class:`overrides <ytdl_sub.config.preset_options.Overrides>`.
Used in fields that do not touch the downloaded files themselves, but instead, `single`
things like
:func:`output_directory <ytdl_sub.config.preset_options.OutputOptions.output_directory>`
or the fields in
:class:`nfo_output_directory <ytdl_sub.plugins.output_directory_nfo_tags.OutputDirectoryNfoTagsOptions>`
""" """
# pylint: enable=line-too-long
class DictFormatterValidator(LiteralDictValidator): 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 _key_validator = StringFormatterValidator
@ -164,8 +196,8 @@ class DictFormatterValidator(LiteralDictValidator):
class OverridesDictFormatterValidator(DictFormatterValidator): class OverridesDictFormatterValidator(DictFormatterValidator):
""" """
Validates a dictionary made up of key: string_formatters, that must be resolved by overrides A dict made up of
only. :class:`~ytdl_sub.validators.string_formatter_validators.OverridesStringFormatterValidator`.
""" """
_key_validator = OverridesStringFormatterValidator _key_validator = OverridesStringFormatterValidator

View file

@ -183,7 +183,7 @@ def expected_recent_channel_download():
@pytest.fixture @pytest.fixture
def rolling_recent_channel_subscription_dict(recent_channel_subscription_dict): def rolling_recent_channel_subscription_dict(recent_channel_subscription_dict):
return mergedeep.merge( return mergedeep.merge(
recent_channel_subscription_dict, {"output_options": {"keep_files": {"after": "20181101"}}} recent_channel_subscription_dict, {"output_options": {"keep_files_after": "20181101"}}
) )