Overhaul docs for readthedocs (#20)
This commit is contained in:
parent
b37a138f65
commit
ab2df88299
21 changed files with 520 additions and 157 deletions
|
|
@ -49,6 +49,8 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||||
#
|
#
|
||||||
html_theme = "sphinx_rtd_theme"
|
html_theme = "sphinx_rtd_theme"
|
||||||
|
|
||||||
|
html_theme_options = {"navigation_depth": 10}
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
|
@ -56,6 +58,6 @@ html_theme = "sphinx_rtd_theme"
|
||||||
|
|
||||||
# Do not show full module path in api docs
|
# Do not show full module path in api docs
|
||||||
add_module_names = False
|
add_module_names = False
|
||||||
python_use_unqualified_type_names = True
|
python_use_unqualified_type_names = False
|
||||||
napoleon_numpy_docstrings = True
|
napoleon_numpy_docstrings = True
|
||||||
napoleon_use_rtype = False
|
napoleon_use_rtype = False
|
||||||
|
|
|
||||||
198
docs/config.rst
Normal file
198
docs/config.rst
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
Config
|
||||||
|
======
|
||||||
|
ytdl-sub is configured using a ``config.yaml`` file. You can view our
|
||||||
|
:doc:`examples <examples>` and read detailed documentation for every configurable
|
||||||
|
field below.
|
||||||
|
|
||||||
|
The ``config.yaml`` is made up of two sections:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
configuration:
|
||||||
|
presets:
|
||||||
|
|
||||||
|
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
|
||||||
|
"""""""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
playlist
|
||||||
|
''''''''
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
presets:
|
||||||
|
my_example_preset:
|
||||||
|
youtube:
|
||||||
|
download_strategy: "playlist"
|
||||||
|
|
||||||
|
playlist_id
|
||||||
|
___________
|
||||||
|
.. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubePlaylistDownloaderOptions.playlist_id
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
video
|
||||||
|
'''''
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
presets:
|
||||||
|
my_example_preset:
|
||||||
|
youtube:
|
||||||
|
download_strategy: "video"
|
||||||
|
|
||||||
|
video_id
|
||||||
|
________
|
||||||
|
.. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubeVideoDownloaderOptions.video_id
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
soundcloud
|
||||||
|
""""""""""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ytdl_options
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
.. autoclass:: ytdl_sub.config.preset_options.YTDLOptions()
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
overrides
|
||||||
|
^^^^^^^^^
|
||||||
|
.. autoclass:: ytdl_sub.config.preset_options.Overrides()
|
||||||
|
|
||||||
|
Plugins
|
||||||
|
^^^^^^^
|
||||||
|
|
||||||
|
music_tags
|
||||||
|
""""""""""
|
||||||
|
.. autoclass:: ytdl_sub.plugins.music_tags.MusicTagsOptions()
|
||||||
|
|
||||||
|
nfo
|
||||||
|
"""
|
||||||
|
.. autoclass:: ytdl_sub.plugins.nfo_tags.NfoTagsOptions()
|
||||||
|
|
||||||
|
nfo_output_directory
|
||||||
|
""""""""""""""""""""
|
||||||
|
.. autoclass:: ytdl_sub.plugins.output_directory_nfo_tags.OutputDirectoryNfoTagsOptions()
|
||||||
|
|
||||||
|
Source Variables
|
||||||
|
----------------
|
||||||
|
Source variables are ``{variables}`` that contain metadata from downloaded media.
|
||||||
|
These variables can be used in StringFormatters, but not OverrideFormatters.
|
||||||
|
|
||||||
|
Youtube Variables
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
.. automodule:: ytdl_sub.entries.variables.youtube_variables
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
Soundcloud Variables
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
.. automodule:: ytdl_sub.entries.variables.soundcloud_variables
|
||||||
|
:members:
|
||||||
|
: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.
|
||||||
|
|
||||||
|
String Formatter
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
.. autoclass:: ytdl_sub.validators.string_formatter_validators.StringFormatterValidator()
|
||||||
|
|
||||||
|
Overrides Formatter
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
.. autoclass:: ytdl_sub.validators.string_formatter_validators.OverridesStringFormatterValidator()
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
Examples
|
|
||||||
========
|
|
||||||
|
|
||||||
Hello, WIP
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:titlesonly:
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
kodi_tv_shows
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
Kodi/Jellyfin TV Shows
|
|
||||||
======================
|
|
||||||
|
|
||||||
Config
|
|
||||||
|
|
||||||
.. include:: ../../../examples/kodi_tv_shows_config.yaml
|
|
||||||
:literal:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. include:: ../../../examples/kodi_tv_shows_subscriptions.yaml
|
|
||||||
:literal:
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
Format Variables
|
|
||||||
================
|
|
||||||
|
|
||||||
Hello, WIP
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:titlesonly:
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
soundcloud
|
|
||||||
youtube
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
Soundcloud Track
|
|
||||||
----------------
|
|
||||||
|
|
||||||
.. automodule:: ytdl_sub.entries.variables.soundcloud_variables
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
Youtube Video
|
|
||||||
-------------
|
|
||||||
|
|
||||||
.. automodule:: ytdl_sub.entries.variables.youtube_variables
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
Hello, WIP
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:titlesonly:
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
format_variables/index
|
|
||||||
examples/index
|
|
||||||
48
docs/examples.rst
Normal file
48
docs/examples.rst
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
Examples
|
||||||
|
========
|
||||||
|
|
||||||
|
This page shows how you can use ytdl-sub for various use cases. These
|
||||||
|
are the configs I personally use and have incorporated as part of the e2e tests.
|
||||||
|
|
||||||
|
Each example has a ``config.yaml`` and ``subscription.yaml``. The config defines
|
||||||
|
`how` you format your media, whereas the subscription defines `what` you
|
||||||
|
download plus some additional configuring if needed.
|
||||||
|
|
||||||
|
Also note that ``kodi`` examples are applicable for ``jellyfin``, ``emby``, and
|
||||||
|
``plex`` with the
|
||||||
|
`XBMC Movies <https://github.com/gboudreau/XBMCnfoMoviesImporter.bundle>`_
|
||||||
|
or
|
||||||
|
`XBMC TV Show <https://github.com/gboudreau/XBMCnfoTVImporter.bundle>`_
|
||||||
|
Plex importer. We would like to improve Plex support, please chime in
|
||||||
|
`here <https://github.com/jmbannon/ytdl-sub/issues/6>`_
|
||||||
|
if you have experience with importing custom videos with metadata.
|
||||||
|
|
||||||
|
Kodi/Jellyfin TV Shows
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
config.yaml
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. include:: ../examples/kodi_tv_shows_config.yaml
|
||||||
|
:literal:
|
||||||
|
|
||||||
|
subscriptions.yaml
|
||||||
|
^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. include:: ../examples/kodi_tv_shows_subscriptions.yaml
|
||||||
|
:literal:
|
||||||
|
|
||||||
|
Kodi/Jellyfin Music Videos
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
config.yaml
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. include:: ../examples/kodi_music_videos_config.yaml
|
||||||
|
:literal:
|
||||||
|
|
||||||
|
subscriptions.yaml
|
||||||
|
^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. include:: ../examples/kodi_music_videos_subscriptions.yaml
|
||||||
|
:literal:
|
||||||
58
docs/getting_started.rst
Normal file
58
docs/getting_started.rst
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
Getting Started
|
||||||
|
===============
|
||||||
|
|
||||||
|
The goal of this app is automate the downloading and metadata creation of audio and video files, then place the files
|
||||||
|
in a directory that gets read by your media player/server. Everyone stores and watches their media differently, so
|
||||||
|
we strive for comprehensive and simplistic customization to fit all self-hosting needs.
|
||||||
|
|
||||||
|
Audio metadata is just a matter of adding tags to the audio file. The backend plugin we use supports practically every
|
||||||
|
common audio file type, so there should (hopefully) be no issues getting audio recognized by your media player.
|
||||||
|
|
||||||
|
Video metadata on the other hand, is currently geared toward generating Kodi/Jellyfin/Emby NFO files. Plex uses metadata
|
||||||
|
written within MP4 containers - we do not support that currently, but could be added as a plugin in the future.
|
||||||
|
|
||||||
|
Usage Ideas
|
||||||
|
-----------
|
||||||
|
Below is a list ways you can use ytdl-sub to download and consume different kinds of media in the representation you
|
||||||
|
prefer.
|
||||||
|
|
||||||
|
* Download a Youtube channel
|
||||||
|
* Store the channel as its own TV Show
|
||||||
|
* Use the channel's avatar as the TV Show poster, banner as the fanart
|
||||||
|
* Format the season and episodes as ``Season YYYY/sYYYY.eMMDD - Video Title.mp4`` to easily navigate videos by upload date
|
||||||
|
* Store the channel as a single season under a TV Show shared with other channels
|
||||||
|
* Only download audio, store it as a Podcast
|
||||||
|
* Only keep video/audio uploaded in the last `N` days
|
||||||
|
* Great for news or podcast based channels
|
||||||
|
|
||||||
|
* Download a Youtube playlist
|
||||||
|
* Download an artist's music videos playlist, store each video as a Kodi/Jellyfin/Emby Music Video
|
||||||
|
* Only download the audio, store the playlist as an album
|
||||||
|
|
||||||
|
* Manually download a single Youtube video
|
||||||
|
* Store it as a Movie
|
||||||
|
* Download a one-hit wonder and store it as a Kodi/Jellyfin/Emby Music Video
|
||||||
|
|
||||||
|
* Download a soundcloud artist's discography
|
||||||
|
* Add tags and album cover images so it shows up nicely in your music player
|
||||||
|
|
||||||
|
If you want to jump the gun to see how ytdl-sub can be configured to do these things, head over to the
|
||||||
|
:doc:`examples <examples>`.
|
||||||
|
|
||||||
|
Install
|
||||||
|
-------
|
||||||
|
Once we are ready for our first release, we will add this package to pypi. Then,
|
||||||
|
we plan to create a docker image that uses the
|
||||||
|
`LinuxServer.io <https://www.linuxserver.io/>`_
|
||||||
|
base image, and hopefully become a part of their fleet someday.
|
||||||
|
|
||||||
|
Until then, you will have to clone this repo and run it using python 3.10
|
||||||
|
|
||||||
|
.. code-block:: Bash
|
||||||
|
|
||||||
|
git clone https://github.com/jmbannon/ytdl-sub.git
|
||||||
|
cd ytdl-sub
|
||||||
|
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
Getting Started
|
|
||||||
===============
|
|
||||||
|
|
||||||
The goal of this app is automate the downloading and metadata creation of audio and video files, then place the files
|
|
||||||
in a directory that gets read by your media player/server. Everyone stores and watches their media differently, so
|
|
||||||
we strive for comprehensive and simplistic customization to fit all self-hosting needs.
|
|
||||||
|
|
||||||
Audio metadata is just a matter of adding tags to the audio file. The backend plugin we use supports practically every
|
|
||||||
common audio file type, so there should (hopefully) be no issues getting audio recognized by your media player.
|
|
||||||
|
|
||||||
Video metadata on the other hand, is currently geared toward generating Kodi/Jellyfin/Emby NFO files. Plex uses metadata
|
|
||||||
written within MP4 containers - we do not support that currently, but could be added as a plugin in the future.
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:titlesonly:
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
usage_ideas
|
|
||||||
install
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
Install
|
|
||||||
=======
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
Usage Ideas
|
|
||||||
===========
|
|
||||||
Below is a list ways you can use ytdl-sub to download and consume different kinds of media in the representation you
|
|
||||||
prefer.
|
|
||||||
|
|
||||||
* Download a Youtube channel
|
|
||||||
* Store the channel as its own TV Show
|
|
||||||
* Use the channel's avatar as the TV Show poster, banner as the fanart
|
|
||||||
* Format the season and episodes as ``Season YYYY/sYYYY.eMMDD - Video Title.mp4`` to easily navigate videos by upload date
|
|
||||||
* Store the channel as a single season under a TV Show shared with other channels
|
|
||||||
* Only download audio, store it as a Podcast
|
|
||||||
* Only keep video/audio uploaded in the last `N` days
|
|
||||||
* Great for news or podcast based channels
|
|
||||||
|
|
||||||
* Download a Youtube playlist
|
|
||||||
* Download an artist's music videos playlist, store each video as a Kodi/Jellyfin/Emby Music Video
|
|
||||||
* Only download the audio, store the playlist as an album
|
|
||||||
|
|
||||||
* Manually download a single Youtube video
|
|
||||||
* Store it as a Movie
|
|
||||||
* Download a one-hit wonder and store it as a Kodi/Jellyfin/Emby Music Video
|
|
||||||
|
|
||||||
* Download a soundcloud artist's discography
|
|
||||||
* Add tags and album cover images so it shows up nicely in your music player
|
|
||||||
|
|
||||||
If you want to jump the gun to see how ytdl-sub can be configured to do these things, head over to the
|
|
||||||
:doc:`examples <../config/examples/index>`.
|
|
||||||
|
|
||||||
|
|
@ -6,16 +6,17 @@ python app that strives to automate 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 Getting Started page. This guide
|
If you are new to ytdl-sub, head over to the
|
||||||
shows how you can install ytdl-sub, configure it to download various types
|
:doc:`Getting Started <getting_started>`
|
||||||
of media, and start downloading.
|
page. This guide shows how you can install ytdl-sub, configure it to download
|
||||||
|
various types of media, and start downloading.
|
||||||
|
|
||||||
Contents
|
Contents
|
||||||
========
|
========
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:titlesonly:
|
:maxdepth: 10
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
getting_started/index
|
getting_started
|
||||||
config/index
|
config
|
||||||
|
examples
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,52 @@ from ytdl_sub.validators.validators import LiteralDictValidator
|
||||||
|
|
||||||
|
|
||||||
class YTDLOptions(LiteralDictValidator):
|
class YTDLOptions(LiteralDictValidator):
|
||||||
"""Ensures `ytdl_options` is a dict"""
|
"""
|
||||||
|
Optional. This section allows you to add any ytdl argument to ytdl-sub's downloader.
|
||||||
|
The argument names can differ slightly from the command-line argument names. See
|
||||||
|
`this docstring <https://github.com/yt-dlp/yt-dlp/blob/2022.04.08/yt_dlp/YoutubeDL.py#L197>`_
|
||||||
|
for more details.
|
||||||
|
|
||||||
|
ytdl_options should be formatted like:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
presets:
|
||||||
|
my_example_preset:
|
||||||
|
ytdl_options:
|
||||||
|
ignoreerrors: True
|
||||||
|
|
||||||
|
where each key is a ytdl argument.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Disable for proper docstring formatting
|
||||||
|
# pylint: disable=line-too-long
|
||||||
class Overrides(DictFormatterValidator):
|
class Overrides(DictFormatterValidator):
|
||||||
"""Ensures `overrides` is a dict"""
|
"""
|
||||||
|
Optional. This section allows you to define variables that can be used in any string formatter.
|
||||||
|
For example, if you want your file and thumbnail files to match without copy-pasting a large
|
||||||
|
format string, you can define something like:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
presets:
|
||||||
|
my_example_preset:
|
||||||
|
overrides:
|
||||||
|
output_directory: "/path/to/media"
|
||||||
|
custom_file_name: "{upload_year}.{upload_month_padded}.{upload_day_padded}.{sanitized_title}"
|
||||||
|
|
||||||
|
# Then use the override variables in the output options
|
||||||
|
output_options:
|
||||||
|
output_directory: "{output_directory}"
|
||||||
|
file_name: "{custom_file_name}.{ext}"
|
||||||
|
thumbnail_name: "{custom_file_name}.{thumbnail_ext}"
|
||||||
|
|
||||||
|
Override variables can contain explicit values and other variables, including both override
|
||||||
|
and source variables.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: enable=line-too-long
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
for key in self._keys:
|
for key in self._keys:
|
||||||
|
|
@ -60,26 +100,98 @@ class OutputOptions(StrictDictValidator):
|
||||||
|
|
||||||
# Output directory should resolve without any entry variables.
|
# Output directory should resolve without any entry variables.
|
||||||
# This is to check the directory for any download-archives before any downloads begin
|
# This is to check the directory for any download-archives before any downloads begin
|
||||||
self.output_directory: OverridesStringFormatterValidator = self._validate_key(
|
self._output_directory: OverridesStringFormatterValidator = self._validate_key(
|
||||||
key="output_directory", validator=OverridesStringFormatterValidator
|
key="output_directory", validator=OverridesStringFormatterValidator
|
||||||
)
|
)
|
||||||
|
|
||||||
# file name and thumbnails however can use entry variables
|
# file name and thumbnails however can use entry variables
|
||||||
self.file_name: StringFormatterValidator = self._validate_key(
|
self._file_name: StringFormatterValidator = self._validate_key(
|
||||||
key="file_name", validator=StringFormatterValidator
|
key="file_name", validator=StringFormatterValidator
|
||||||
)
|
)
|
||||||
self.thumbnail_name = self._validate_key_if_present(
|
self._thumbnail_name = self._validate_key_if_present(
|
||||||
key="thumbnail_name", validator=StringFormatterValidator
|
key="thumbnail_name", validator=StringFormatterValidator
|
||||||
)
|
)
|
||||||
|
|
||||||
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.delete_stale_files = self._validate_key_if_present(
|
self._keep_files = self._validate_key_if_present(
|
||||||
key="keep_files", validator=DateRangeValidator
|
key="keep_files", validator=DateRangeValidator
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.delete_stale_files and not self.maintain_download_archive:
|
if self._keep_files 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output_directory(self) -> OverridesStringFormatterValidator:
|
||||||
|
"""
|
||||||
|
Required. The output directory to store all media files downloaded.
|
||||||
|
"""
|
||||||
|
return self._output_directory
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file_name(self) -> StringFormatterValidator:
|
||||||
|
"""
|
||||||
|
Required. The file name for the media file. This can include directories such as
|
||||||
|
``"Season {upload_year}/{title}.{ext}"``, and will be placed in the output directory.
|
||||||
|
"""
|
||||||
|
return self._file_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thumbnail_name(self) -> Optional[StringFormatterValidator]:
|
||||||
|
"""
|
||||||
|
Optional. The file name for the media's thumbnail image. This can include directories such
|
||||||
|
as ``"Season {upload_year}/{title}.{thumbnail_ext}"``, and will be placed in the output
|
||||||
|
directory.
|
||||||
|
"""
|
||||||
|
return self._thumbnail_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def maintain_download_archive(self) -> bool:
|
||||||
|
"""
|
||||||
|
Optional. Maintains a download archive file in the output directory for a subscription.
|
||||||
|
It is named ``.ytdl-sub-{subscription_name}-download-archive.json``, stored in the
|
||||||
|
output directory.
|
||||||
|
|
||||||
|
The download archive contains a mapping of ytdl IDs to downloaded files. This is used to
|
||||||
|
create a ytdl download-archive file when invoking a download on a subscription. This will
|
||||||
|
prevent ytdl from redownloading media already downloaded.
|
||||||
|
|
||||||
|
Defaults to False.
|
||||||
|
"""
|
||||||
|
return self._maintain_download_archive.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def keep_files(self) -> DateRangeValidator:
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
return self._keep_files
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,17 @@ class SoundcloudDownloaderOptions(DownloaderValidator, ABC):
|
||||||
|
|
||||||
def __init__(self, name: str, value: dict):
|
def __init__(self, name: str, value: dict):
|
||||||
super().__init__(name=name, value=value)
|
super().__init__(name=name, value=value)
|
||||||
self.skip_premiere_tracks = self._validate_key(
|
self._skip_premiere_tracks = self._validate_key(
|
||||||
"skip_premiere_tracks", BoolValidator, default=True
|
"skip_premiere_tracks", BoolValidator, default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def skip_premiere_tracks(self) -> bool:
|
||||||
|
"""
|
||||||
|
Optional. True to skip tracks that require purchasing. False otherwise. Defaults to True.
|
||||||
|
"""
|
||||||
|
return self._skip_premiere_tracks.value
|
||||||
|
|
||||||
|
|
||||||
SoundcloudDownloaderOptionsT = TypeVar(
|
SoundcloudDownloaderOptionsT = TypeVar(
|
||||||
"SoundcloudDownloaderOptionsT", bound=SoundcloudDownloaderOptions
|
"SoundcloudDownloaderOptionsT", bound=SoundcloudDownloaderOptions
|
||||||
|
|
@ -68,7 +75,14 @@ class SoundcloudAlbumsAndSinglesDownloadOptions(SoundcloudDownloaderOptions):
|
||||||
|
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
self.username = self._validate_key(key="username", validator=StringValidator)
|
self._username = self._validate_key(key="username", validator=StringValidator)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def username(self) -> str:
|
||||||
|
"""
|
||||||
|
Required. The Soundcloud username found in the url of their page.
|
||||||
|
"""
|
||||||
|
return self._username.value
|
||||||
|
|
||||||
|
|
||||||
class SoundcloudAlbumsAndSinglesDownloader(
|
class SoundcloudAlbumsAndSinglesDownloader(
|
||||||
|
|
@ -132,7 +146,7 @@ class SoundcloudAlbumsAndSinglesDownloader(
|
||||||
"""
|
"""
|
||||||
Soundcloud subscription to download albums and tracks as singles.
|
Soundcloud subscription to download albums and tracks as singles.
|
||||||
"""
|
"""
|
||||||
artist_url = self.artist_url(artist_name=self.download_options.username.value)
|
artist_url = self.artist_url(artist_name=self.download_options.username)
|
||||||
entry_dicts = self.extract_info_via_info_json(url=artist_url)
|
entry_dicts = self.extract_info_via_info_json(url=artist_url)
|
||||||
|
|
||||||
# Get all of the artist's albums
|
# Get all of the artist's albums
|
||||||
|
|
@ -146,7 +160,7 @@ class SoundcloudAlbumsAndSinglesDownloader(
|
||||||
tracks += album.album_tracks()
|
tracks += album.album_tracks()
|
||||||
|
|
||||||
# Filter any premiere tracks if specified
|
# Filter any premiere tracks if specified
|
||||||
if self.download_options.skip_premiere_tracks.value:
|
if self.download_options.skip_premiere_tracks:
|
||||||
tracks = [track for track in tracks if not track.is_premiere()]
|
tracks = [track for track in tracks if not track.is_premiere()]
|
||||||
|
|
||||||
return tracks
|
return tracks
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,15 @@ class YoutubeVideoDownloaderOptions(YoutubeDownloaderOptions):
|
||||||
|
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
self.video_id = self._validate_key("video_id", StringValidator)
|
self._video_id = self._validate_key("video_id", StringValidator)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def video_id(self) -> str:
|
||||||
|
"""
|
||||||
|
Required. The ID of the video. Looks like the ``VMAPTo7RVDo`` in
|
||||||
|
``youtube.com/watch?v=VMAPTo7RVDo``.
|
||||||
|
"""
|
||||||
|
return self._video_id.value
|
||||||
|
|
||||||
|
|
||||||
class YoutubeVideoDownloader(YoutubeDownloader[YoutubeVideoDownloaderOptions, YoutubeVideo]):
|
class YoutubeVideoDownloader(YoutubeDownloader[YoutubeVideoDownloaderOptions, YoutubeVideo]):
|
||||||
|
|
@ -73,8 +81,7 @@ class YoutubeVideoDownloader(YoutubeDownloader[YoutubeVideoDownloaderOptions, Yo
|
||||||
|
|
||||||
def download(self) -> List[YoutubeVideo]:
|
def download(self) -> List[YoutubeVideo]:
|
||||||
"""Download a single Youtube video"""
|
"""Download a single Youtube video"""
|
||||||
video_id = self.download_options.video_id.value
|
video_url = self.video_url(video_id=self.download_options.video_id)
|
||||||
video_url = self.video_url(video_id=video_id)
|
|
||||||
|
|
||||||
entry_dict = self.extract_info(url=video_url)
|
entry_dict = self.extract_info(url=video_url)
|
||||||
return [YoutubeVideo(entry_dict=entry_dict, working_directory=self.working_directory)]
|
return [YoutubeVideo(entry_dict=entry_dict, working_directory=self.working_directory)]
|
||||||
|
|
@ -89,7 +96,14 @@ class YoutubePlaylistDownloaderOptions(YoutubeDownloaderOptions):
|
||||||
|
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
self.playlist_id = self._validate_key("playlist_id", StringValidator)
|
self._playlist_id = self._validate_key("playlist_id", StringValidator)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def playlist_id(self) -> str:
|
||||||
|
"""
|
||||||
|
Required. The playlist's ID.
|
||||||
|
"""
|
||||||
|
return self._playlist_id.value
|
||||||
|
|
||||||
|
|
||||||
class YoutubePlaylistDownloader(
|
class YoutubePlaylistDownloader(
|
||||||
|
|
@ -107,8 +121,7 @@ class YoutubePlaylistDownloader(
|
||||||
"""
|
"""
|
||||||
Downloads all videos in a Youtube playlist
|
Downloads all videos in a Youtube playlist
|
||||||
"""
|
"""
|
||||||
playlist_id = self.download_options.playlist_id.value
|
playlist_url = self.playlist_url(playlist_id=self.download_options.playlist_id)
|
||||||
playlist_url = self.playlist_url(playlist_id=playlist_id)
|
|
||||||
playlist_videos: List[YoutubePlaylistVideo] = []
|
playlist_videos: List[YoutubePlaylistVideo] = []
|
||||||
|
|
||||||
entry_dicts = self.extract_info_via_info_json(url=playlist_url)
|
entry_dicts = self.extract_info_via_info_json(url=playlist_url)
|
||||||
|
|
@ -134,14 +147,37 @@ class YoutubeChannelDownloaderOptions(YoutubeDownloaderOptions, DateRangeValidat
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
YoutubeDownloaderOptions.__init__(self, name, value)
|
YoutubeDownloaderOptions.__init__(self, name, value)
|
||||||
DateRangeValidator.__init__(self, name, value)
|
DateRangeValidator.__init__(self, name, value)
|
||||||
self.channel_id = self._validate_key("channel_id", StringValidator)
|
self._channel_id = self._validate_key("channel_id", StringValidator)
|
||||||
self.channel_avatar_path = self._validate_key_if_present(
|
self._channel_avatar_path = self._validate_key_if_present(
|
||||||
"channel_avatar_path", OverridesStringFormatterValidator
|
"channel_avatar_path", OverridesStringFormatterValidator
|
||||||
)
|
)
|
||||||
self.channel_banner_path = self._validate_key_if_present(
|
self._channel_banner_path = self._validate_key_if_present(
|
||||||
"channel_banner_path", OverridesStringFormatterValidator
|
"channel_banner_path", OverridesStringFormatterValidator
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
return self._channel_id.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channel_avatar_path(self) -> Optional[OverridesStringFormatterValidator]:
|
||||||
|
"""
|
||||||
|
Optional. Path to store the channel's avatar thumbnail image to.
|
||||||
|
"""
|
||||||
|
return self._channel_avatar_path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channel_banner_path(self) -> Optional[OverridesStringFormatterValidator]:
|
||||||
|
"""
|
||||||
|
Optional. Path to store the channel's banner image to.
|
||||||
|
"""
|
||||||
|
return self._channel_banner_path
|
||||||
|
|
||||||
|
|
||||||
class YoutubeChannelDownloader(YoutubeDownloader[YoutubeChannelDownloaderOptions, YoutubeVideo]):
|
class YoutubeChannelDownloader(YoutubeDownloader[YoutubeChannelDownloaderOptions, YoutubeVideo]):
|
||||||
downloader_options_type = YoutubeChannelDownloaderOptions
|
downloader_options_type = YoutubeChannelDownloaderOptions
|
||||||
|
|
@ -167,20 +203,11 @@ class YoutubeChannelDownloader(YoutubeDownloader[YoutubeChannelDownloaderOptions
|
||||||
"""Returns full channel url"""
|
"""Returns full channel url"""
|
||||||
return f"https://youtube.com/channel/{channel_id}"
|
return f"https://youtube.com/channel/{channel_id}"
|
||||||
|
|
||||||
@property
|
|
||||||
def channel_id(self) -> str:
|
|
||||||
"""
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
Channel ID
|
|
||||||
"""
|
|
||||||
return self.download_options.channel_id.value
|
|
||||||
|
|
||||||
def download(self) -> List[YoutubeVideo]:
|
def download(self) -> List[YoutubeVideo]:
|
||||||
"""
|
"""
|
||||||
Downloads all videos from a channel
|
Downloads all videos from a channel
|
||||||
"""
|
"""
|
||||||
channel_url = self.channel_url(channel_id=self.channel_id)
|
channel_url = self.channel_url(channel_id=self.download_options.channel_id)
|
||||||
channel_videos: List[YoutubeVideo] = []
|
channel_videos: List[YoutubeVideo] = []
|
||||||
ytdl_options_overrides = {}
|
ytdl_options_overrides = {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -202,9 +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.delete_stale_files:
|
if self.output_options.keep_files:
|
||||||
self._enhanced_download_archive.remove_stale_files(
|
self._enhanced_download_archive.remove_stale_files(
|
||||||
date_range=self.output_options.delete_stale_files.get_date_range()
|
date_range=self.output_options.keep_files.get_date_range()
|
||||||
)
|
)
|
||||||
|
|
||||||
self._enhanced_download_archive.save_download_mappings()
|
self._enhanced_download_archive.save_download_mappings()
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,18 @@ class DateRangeValidator(StrictDictValidator):
|
||||||
|
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
self.before = self._validate_key_if_present("before", StringDatetimeValidator)
|
self._before = self._validate_key_if_present("before", StringDatetimeValidator)
|
||||||
self.after = self._validate_key_if_present("after", StringDatetimeValidator)
|
self._after = self._validate_key_if_present("after", StringDatetimeValidator)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def before(self) -> Optional[StringDatetimeValidator]:
|
||||||
|
"""Optional. Only download videos before this datetime."""
|
||||||
|
return self._before
|
||||||
|
|
||||||
|
@property
|
||||||
|
def after(self) -> Optional[StringDatetimeValidator]:
|
||||||
|
"""Optional. Only download videos after this datetime."""
|
||||||
|
return self._after
|
||||||
|
|
||||||
def get_date_range(self) -> Optional[DateRange]:
|
def get_date_range(self) -> Optional[DateRange]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -20,9 +30,9 @@ class DateRangeValidator(StrictDictValidator):
|
||||||
-------
|
-------
|
||||||
Date range if the 'before' or 'after' is defined. None otherwise.
|
Date range if the 'before' or 'after' is defined. None otherwise.
|
||||||
"""
|
"""
|
||||||
if self.before or self.after:
|
if self._before or self._after:
|
||||||
return DateRange(
|
return DateRange(
|
||||||
start=self.after.datetime_str if self.after else None,
|
start=self._after.datetime_str if self._after else None,
|
||||||
end=self.before.datetime_str if self.before else None,
|
end=self._before.datetime_str if self._before else None,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -355,7 +355,7 @@ class EnhancedDownloadArchive:
|
||||||
-------
|
-------
|
||||||
The download mapping's file name (no path)
|
The download mapping's file name (no path)
|
||||||
"""
|
"""
|
||||||
return f".ytdl-subscribe-{self.subscription_name}-download-mapping.json"
|
return f".ytdl-sub-{self.subscription_name}-download-archive.json"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _mapping_output_file_path(self):
|
def _mapping_output_file_path(self):
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ def expected_full_channel_download():
|
||||||
return ExpectedDownload(
|
return ExpectedDownload(
|
||||||
expected_md5_file_hashes={
|
expected_md5_file_hashes={
|
||||||
# Download mapping
|
# Download mapping
|
||||||
Path("pz/.ytdl-subscribe-pz-download-mapping.json"): "add71021318bf87a3facb965fd38bd7f",
|
Path("pz/.ytdl-sub-pz-download-archive.json"): "add71021318bf87a3facb965fd38bd7f",
|
||||||
|
|
||||||
# Output directory files
|
# Output directory files
|
||||||
Path("pz/fanart.jpg"): "e6e323373c8902568e96e374817179cf",
|
Path("pz/fanart.jpg"): "e6e323373c8902568e96e374817179cf",
|
||||||
|
|
@ -158,7 +158,7 @@ def expected_recent_channel_download():
|
||||||
return ExpectedDownload(
|
return ExpectedDownload(
|
||||||
expected_md5_file_hashes={
|
expected_md5_file_hashes={
|
||||||
# Download mapping
|
# Download mapping
|
||||||
Path("pz/.ytdl-subscribe-pz-download-mapping.json"): "a133d9ea8a63e239cd41b799b9031fd5",
|
Path("pz/.ytdl-sub-pz-download-archive.json"): "a133d9ea8a63e239cd41b799b9031fd5",
|
||||||
|
|
||||||
# Output directory files
|
# Output directory files
|
||||||
Path("pz/fanart.jpg"): "e6e323373c8902568e96e374817179cf",
|
Path("pz/fanart.jpg"): "e6e323373c8902568e96e374817179cf",
|
||||||
|
|
@ -205,7 +205,7 @@ def expected_rolling_recent_channel_download():
|
||||||
return ExpectedDownload(
|
return ExpectedDownload(
|
||||||
expected_md5_file_hashes={
|
expected_md5_file_hashes={
|
||||||
# Download mapping
|
# Download mapping
|
||||||
Path("pz/.ytdl-subscribe-pz-download-mapping.json"): "8013b4d2ba6921c9347c014ac915e3f6",
|
Path("pz/.ytdl-sub-pz-download-archive.json"): "8013b4d2ba6921c9347c014ac915e3f6",
|
||||||
|
|
||||||
# Output directory files
|
# Output directory files
|
||||||
Path("pz/fanart.jpg"): "e6e323373c8902568e96e374817179cf",
|
Path("pz/fanart.jpg"): "e6e323373c8902568e96e374817179cf",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue