Merge branch 'master' into prebuilt_presets

This commit is contained in:
Qualis Svagtlys 2023-12-30 07:46:44 -06:00
commit 1b2fe5c7ab
78 changed files with 4567 additions and 2632 deletions

2
.gitignore vendored
View file

@ -149,3 +149,5 @@ docker/testing/volumes
ffmpeg.exe ffmpeg.exe
ffprobe.exe ffprobe.exe
tools/docgen/out

View file

View file

@ -53,6 +53,7 @@ html_theme_options = {
} }
html_static_path = ["_static"] html_static_path = ["_static"]
html_css_files = ["custom.css"]
# Make sure the all autosectionlabel targets are unique # Make sure the all autosectionlabel targets are unique

View file

@ -53,51 +53,7 @@ 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 This section is work-in-progress!
"""""""""""""""""
Download strategies dictate what is getting downloaded from a source. Each
download strategy has its own set of parameters.
.. _url:
url
'''
.. autoclass:: ytdl_sub.downloaders.url.url.UrlDownloadOptions()
:members: url, playlist_thumbnails, source_thumbnails, download_reverse
:member-order: bysource
multi_url
'''''''''
.. autoclass:: ytdl_sub.downloaders.url.multi_url.MultiUrlDownloadOptions()
:members: urls, variables
-------------------------------------------------------------------------------
output_options
""""""""""""""
.. autoclass:: ytdl_sub.config.preset_options.OutputOptions()
:members:
:member-order: bysource
:exclude-members: get_upload_date_range_to_keep, partial_validate
-------------------------------------------------------------------------------
.. _ytdl_options:
ytdl_options
""""""""""""
.. autoclass:: ytdl_sub.config.preset_options.YTDLOptions()
-------------------------------------------------------------------------------
.. _overrides:
overrides
"""""""""
.. autoclass:: ytdl_sub.config.overrides.Overrides()
.. _parent preset:
preset preset
"""""" """"""

File diff suppressed because it is too large Load diff

View file

@ -1,17 +0,0 @@
==================
Config Field Types
==================
The ``config.yaml`` uses various types for its configurable fields. Below is a definition for each type.
.. autoclass:: ytdl_sub.validators.string_formatter_validators.StringFormatterValidator()
.. autoclass:: ytdl_sub.validators.string_formatter_validators.OverridesStringFormatterValidator()
.. autoclass:: ytdl_sub.validators.file_path_validators.StringFormatterFileNameValidator()
.. 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

@ -1,3 +1,4 @@
Entry Variables Entry Variables
=============== ===============
@ -6,135 +7,203 @@ Entry Variables
channel channel
~~~~~~~ ~~~~~~~
:type: ``String``
:description:
The channel name if it exists, otherwise returns the uploader. The channel name if it exists, otherwise returns the uploader.
channel_id channel_id
~~~~~~~~~~ ~~~~~~~~~~
:type: ``String``
:description:
The channel id if it exists, otherwise returns the entry uploader ID. The channel id if it exists, otherwise returns the entry uploader ID.
chapters chapters
~~~~~~~~ ~~~~~~~~
:type: ``Array``
:description:
Chapters if they exist Chapters if they exist
comments comments
~~~~~~~~ ~~~~~~~~
:type: ``Array``
:description:
Comments if they are requested Comments if they are requested
creator creator
~~~~~~~ ~~~~~~~
:type: ``String``
:description:
The creator name if it exists, otherwise returns the channel. The creator name if it exists, otherwise returns the channel.
description description
~~~~~~~~~~~ ~~~~~~~~~~~
:type: ``String``
:description:
The description if it exists. Otherwise, returns an emtpy string. The description if it exists. Otherwise, returns an emtpy string.
duration duration
~~~~~~~~ ~~~~~~~~
The duration of the entry in seconds :type: ``Integer``
:description:
The duration of the entry in seconds if it exists. Defaults to zero otherwise.
epoch epoch
~~~~~ ~~~~~
:type: ``Integer``
:description:
The unix epoch of when the metadata was scraped by yt-dlp. The unix epoch of when the metadata was scraped by yt-dlp.
epoch_date epoch_date
~~~~~~~~~~ ~~~~~~~~~~
:type: ``String``
:description:
The epoch's date, in YYYYMMDD format. The epoch's date, in YYYYMMDD format.
epoch_hour epoch_hour
~~~~~~~~~~ ~~~~~~~~~~
:type: ``String``
:description:
The epoch's hour The epoch's hour
ext ext
~~~ ~~~
:type: ``String``
:description:
The downloaded entry's file extension The downloaded entry's file extension
extractor extractor
~~~~~~~~~ ~~~~~~~~~
:type: ``String``
:description:
The yt-dlp extractor name The yt-dlp extractor name
extractor_key extractor_key
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
:type: ``String``
:description:
The yt-dlp extractor key The yt-dlp extractor key
ie_key ie_key
~~~~~~ ~~~~~~
The ie_key, used in legacy yt-dlp things as the 'info-extractor key' :type: ``String``
:description:
The ie_key, used in legacy yt-dlp things as the 'info-extractor key'.
If it does not exist, return ``extractor_key``
info_json_ext info_json_ext
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
:type: ``String``
:description:
The "info.json" extension The "info.json" extension
requested_subtitles requested_subtitles
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
:type: ``Map``
:description:
Subtitles if they are requested and exist Subtitles if they are requested and exist
sponsorblock_chapters sponsorblock_chapters
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
:type: ``Array``
:description:
Sponsorblock Chapters if they are requested and exist Sponsorblock Chapters if they are requested and exist
thumbnail_ext thumbnail_ext
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
:type: ``String``
:description:
The download entry's thumbnail extension. Will always return 'jpg'. Until there is a The download entry's thumbnail extension. Will always return 'jpg'. Until there is a
need to support other image types, we always convert to jpg. need to support other image types, we always convert to jpg.
title title
~~~~~ ~~~~~
:type: ``String``
:description:
The title of the entry. If a title does not exist, returns its unique ID. The title of the entry. If a title does not exist, returns its unique ID.
title_sanitized_plex title_sanitized_plex
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The sanitized title with additional sanitizing for Plex. It replaces numbers with The sanitized title with additional sanitizing for Plex. It replaces numbers with
fixed-width numbers so Plex does not recognize them as season or episode numbers. fixed-width numbers so Plex does not recognize them as season or episode numbers.
uid uid
~~~ ~~~
:type: ``String``
:description:
The entry's unique ID The entry's unique ID
uid_sanitized_plex uid_sanitized_plex
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The sanitized uid with additional sanitizing for Plex. Replaces numbers with The sanitized uid with additional sanitizing for Plex. Replaces numbers with
fixed-width numbers so Plex does not recognize them as season or episode numbers. fixed-width numbers so Plex does not recognize them as season or episode numbers.
uploader uploader
~~~~~~~~ ~~~~~~~~
:type: ``String``
:description:
The uploader if it exists, otherwise return the uploader ID. The uploader if it exists, otherwise return the uploader ID.
uploader_id uploader_id
~~~~~~~~~~~ ~~~~~~~~~~~
:type: ``String``
:description:
The uploader id if it exists, otherwise return the unique ID. The uploader id if it exists, otherwise return the unique ID.
uploader_url uploader_url
~~~~~~~~~~~~ ~~~~~~~~~~~~
:type: ``String``
:description:
The uploader url if it exists, otherwise returns the webpage_url. The uploader url if it exists, otherwise returns the webpage_url.
webpage_url webpage_url
~~~~~~~~~~~ ~~~~~~~~~~~
:type: ``String``
:description:
The url to the webpage. The url to the webpage.
----------------------------------------------------------------------------------------------------
Metadata Variables Metadata Variables
------------------ ------------------
entry_metadata entry_metadata
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
:type: ``Map``
:description:
The entry's info.json The entry's info.json
playlist_metadata playlist_metadata
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
:type: ``Map``
:description:
Metadata from the playlist (i.e. the parent metadata, like playlist -> entry) Metadata from the playlist (i.e. the parent metadata, like playlist -> entry)
sibling_metadata sibling_metadata
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
:type: ``Array``
:description:
Metadata from any sibling entries that reside in the same playlist as this entry. Metadata from any sibling entries that reside in the same playlist as this entry.
source_metadata source_metadata
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
Metadata from the source (i.e. the grandparent metadata, like channel -> playlist -> entry) :type: ``Map``
:description:
Metadata from the source
(i.e. the grandparent metadata, like channel -> playlist -> entry)
----------------------------------------------------------------------------------------------------
Playlist Variables Playlist Variables
------------------ ------------------
playlist_count playlist_count
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
:type: ``Integer``
:description:
Playlist count if it exists, otherwise returns ``1``. Playlist count if it exists, otherwise returns ``1``.
Note that for channels/playlists, any change (i.e. adding or removing a video) will make Note that for channels/playlists, any change (i.e. adding or removing a video) will make
@ -142,10 +211,14 @@ this value change. Use with caution.
playlist_description playlist_description
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The playlist description if it exists, otherwise returns the entry's description. The playlist description if it exists, otherwise returns the entry's description.
playlist_index playlist_index
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
:type: ``Integer``
:description:
Playlist index if it exists, otherwise returns ``1``. Playlist index if it exists, otherwise returns ``1``.
Note that for channels/playlists, any change (i.e. adding or removing a video) will make Note that for channels/playlists, any change (i.e. adding or removing a video) will make
@ -153,151 +226,222 @@ this value change. Use with caution.
playlist_index_padded playlist_index_padded
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
playlist_index padded two digits playlist_index padded two digits
playlist_index_padded6 playlist_index_padded6
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
playlist_index padded six digits. playlist_index padded six digits.
playlist_index_reversed playlist_index_reversed
~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
Playlist index reversed via ``playlist_count - playlist_index + 1`` Playlist index reversed via ``playlist_count - playlist_index + 1``
playlist_index_reversed_padded playlist_index_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
playlist_index_reversed padded two digits playlist_index_reversed padded two digits
playlist_index_reversed_padded6 playlist_index_reversed_padded6
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
playlist_index_reversed padded six digits. playlist_index_reversed padded six digits.
playlist_max_upload_date playlist_max_upload_date
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
Max upload_date for all entries in this entry's playlist if it exists, otherwise returns Max upload_date for all entries in this entry's playlist if it exists, otherwise returns
``upload_date`` ``upload_date``
playlist_max_upload_year playlist_max_upload_year
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
Max upload_year for all entries in this entry's playlist if it exists, otherwise returns Max upload_year for all entries in this entry's playlist if it exists, otherwise returns
``upload_year`` ``upload_year``
playlist_max_upload_year_truncated playlist_max_upload_year_truncated
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The max playlist truncated upload year for all entries in this entry's playlist if it The max playlist truncated upload year for all entries in this entry's playlist if it
exists, otherwise returns ``upload_year_truncated``. exists, otherwise returns ``upload_year_truncated``.
playlist_title playlist_title
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
:type: ``String``
:description:
Name of its parent playlist/channel if it exists, otherwise returns its title. Name of its parent playlist/channel if it exists, otherwise returns its title.
playlist_uid playlist_uid
~~~~~~~~~~~~ ~~~~~~~~~~~~
:type: ``String``
:description:
The playlist unique ID if it exists, otherwise return the entry unique ID. The playlist unique ID if it exists, otherwise return the entry unique ID.
playlist_uploader playlist_uploader
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The playlist uploader if it exists, otherwise return the entry uploader. The playlist uploader if it exists, otherwise return the entry uploader.
playlist_uploader_id playlist_uploader_id
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The playlist uploader id if it exists, otherwise returns the entry uploader ID. The playlist uploader id if it exists, otherwise returns the entry uploader ID.
playlist_uploader_url playlist_uploader_url
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The playlist uploader url if it exists, otherwise returns the playlist webpage_url. The playlist uploader url if it exists, otherwise returns the playlist webpage_url.
playlist_webpage_url playlist_webpage_url
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The playlist webpage url if it exists. Otherwise, returns the entry webpage url. The playlist webpage url if it exists. Otherwise, returns the entry webpage url.
----------------------------------------------------------------------------------------------------
Release Date Variables Release Date Variables
---------------------- ----------------------
release_date release_date
~~~~~~~~~~~~ ~~~~~~~~~~~~
:type: ``String``
:description:
The entrys release date, in YYYYMMDD format. If not present, return the upload date. The entrys release date, in YYYYMMDD format. If not present, return the upload date.
release_date_standardized release_date_standardized
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
The release date formatted as YYYY-MM-DD :type: ``String``
:description:
The uploaded date formatted as YYYY-MM-DD
release_day release_day
~~~~~~~~~~~ ~~~~~~~~~~~
The release day as an integer (no padding). :type: ``Integer``
:description:
The upload day as an integer (no padding).
release_day_of_year release_day_of_year
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The day of the year, i.e. February 1st returns ``32`` The day of the year, i.e. February 1st returns ``32``
release_day_of_year_padded release_day_of_year_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~
The release day of year, but padded i.e. February 1st returns "032" :type: ``String``
:description:
The upload day of year, but padded i.e. February 1st returns "032"
release_day_of_year_reversed release_day_of_year_reversed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The release day, but reversed using ``{total_days_in_year} + 1 - {release_day}``, :type: ``Integer``
:description:
The upload day, but reversed using ``{total_days_in_year} + 1 - {release_day}``,
i.e. February 2nd would have release_day_of_year_reversed of ``365 + 1 - 32`` = ``334`` i.e. February 2nd would have release_day_of_year_reversed of ``365 + 1 - 32`` = ``334``
release_day_of_year_reversed_padded release_day_of_year_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The reversed release day of year, but padded i.e. December 31st returns "001" :type: ``String``
:description:
The reversed upload day of year, but padded i.e. December 31st returns "001"
release_day_padded release_day_padded
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
The entry's release day padded to two digits, i.e. the fifth returns "05" :type: ``String``
:description:
The entry's upload day padded to two digits, i.e. the fifth returns "05"
release_day_reversed release_day_reversed
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
The release day, but reversed using ``{total_days_in_month} + 1 - {release_day}``, :type: ``Integer``
:description:
The upload day, but reversed using ``{total_days_in_month} + 1 - {release_day}``,
i.e. August 8th would have release_day_reversed of ``31 + 1 - 8`` = ``24`` i.e. August 8th would have release_day_reversed of ``31 + 1 - 8`` = ``24``
release_day_reversed_padded release_day_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
The reversed release day, but padded. i.e. August 30th returns "02". :type: ``String``
:description:
The reversed upload day, but padded. i.e. August 30th returns "02".
release_month release_month
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
The release month as an integer (no padding). :type: ``Integer``
:description:
The upload month as an integer (no padding).
release_month_padded release_month_padded
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
The entry's release month padded to two digits, i.e. March returns "03" :type: ``String``
:description:
The entry's upload month padded to two digits, i.e. March returns "03"
release_month_reversed release_month_reversed
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
The release month, but reversed :type: ``Integer``
using ``13 - {release_month}``, i.e. March returns ``10`` :description:
The upload month, but reversed using ``13 - {release_month}``, i.e. March returns ``10``
release_month_reversed_padded release_month_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The reversed release month, but padded. i.e. November returns "02" :type: ``String``
:description:
The reversed upload month, but padded. i.e. November returns "02"
release_year release_year
~~~~~~~~~~~~ ~~~~~~~~~~~~
The entry's release year :type: ``Integer``
:description:
The entry's upload year
release_year_truncated release_year_truncated
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
The last two digits of the release year, i.e. 22 in 2022 :type: ``Integer``
:description:
The last two digits of the upload year, i.e. 22 in 2022
release_year_truncated_reversed release_year_truncated_reversed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The release year truncated, but reversed using ``100 - {release_year_truncated}``, i.e. :type: ``Integer``
:description:
The upload year truncated, but reversed using ``100 - {release_year_truncated}``, i.e.
2022 returns ``100 - 22`` = ``78`` 2022 returns ``100 - 22`` = ``78``
----------------------------------------------------------------------------------------------------
Source Variables Source Variables
---------------- ----------------
source_count source_count
~~~~~~~~~~~~ ~~~~~~~~~~~~
The source count if it exists, otherwise returns the playlist count. :type: ``Integer``
:description:
The source count if it exists, otherwise returns ``1``.
source_description source_description
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The source description if it exists, otherwise returns the playlist description. The source description if it exists, otherwise returns the playlist description.
source_index source_index
~~~~~~~~~~~~ ~~~~~~~~~~~~
:type: ``Integer``
:description:
Source index if it exists, otherwise returns ``1``. Source index if it exists, otherwise returns ``1``.
It is recommended to not use this unless you know the source will never add new content It is recommended to not use this unless you know the source will never add new content
@ -305,135 +449,201 @@ It is recommended to not use this unless you know the source will never add new
source_index_padded source_index_padded
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
The source index, padded. :type: ``String``
:description:
The source index, padded two digits.
source_title source_title
~~~~~~~~~~~~ ~~~~~~~~~~~~
:type: ``String``
:description:
Name of the source (i.e. channel with multiple playlists) if it exists, otherwise Name of the source (i.e. channel with multiple playlists) if it exists, otherwise
returns its playlist_title. returns its playlist_title.
source_uid source_uid
~~~~~~~~~~ ~~~~~~~~~~
:type: ``String``
:description:
The source unique id if it exists, otherwise returns the playlist unique ID. The source unique id if it exists, otherwise returns the playlist unique ID.
source_uploader source_uploader
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
:type: ``String``
:description:
The source uploader if it exists, otherwise return the playlist_uploader The source uploader if it exists, otherwise return the playlist_uploader
source_uploader_id source_uploader_id
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The source uploader id if it exists, otherwise returns the playlist_uploader_id The source uploader id if it exists, otherwise returns the playlist_uploader_id
source_uploader_url source_uploader_url
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The source uploader url if it exists, otherwise returns the source webpage_url. The source uploader url if it exists, otherwise returns the source webpage_url.
source_webpage_url source_webpage_url
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The source webpage url if it exists, otherwise returns the playlist webpage url. The source webpage url if it exists, otherwise returns the playlist webpage url.
----------------------------------------------------------------------------------------------------
Upload Date Variables Upload Date Variables
--------------------- ---------------------
upload_date upload_date
~~~~~~~~~~~ ~~~~~~~~~~~
:type: ``String``
:description:
The entrys uploaded date, in YYYYMMDD format. If not present, return todays date. The entrys uploaded date, in YYYYMMDD format. If not present, return todays date.
upload_date_standardized upload_date_standardized
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The uploaded date formatted as YYYY-MM-DD The uploaded date formatted as YYYY-MM-DD
upload_day upload_day
~~~~~~~~~~ ~~~~~~~~~~
:type: ``Integer``
:description:
The upload day as an integer (no padding). The upload day as an integer (no padding).
upload_day_of_year upload_day_of_year
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The day of the year, i.e. February 1st returns ``32`` The day of the year, i.e. February 1st returns ``32``
upload_day_of_year_padded upload_day_of_year_padded
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The upload day of year, but padded i.e. February 1st returns "032" The upload day of year, but padded i.e. February 1st returns "032"
upload_day_of_year_reversed upload_day_of_year_reversed
~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload day, but reversed using ``{total_days_in_year} + 1 - {upload_day}``, The upload day, but reversed using ``{total_days_in_year} + 1 - {upload_day}``,
i.e. February 2nd would have upload_day_of_year_reversed of ``365 + 1 - 32`` = ``334`` i.e. February 2nd would have upload_day_of_year_reversed of ``365 + 1 - 32`` = ``334``
upload_day_of_year_reversed_padded upload_day_of_year_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The reversed upload day of year, but padded i.e. December 31st returns "001" The reversed upload day of year, but padded i.e. December 31st returns "001"
upload_day_padded upload_day_padded
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The entry's upload day padded to two digits, i.e. the fifth returns "05" The entry's upload day padded to two digits, i.e. the fifth returns "05"
upload_day_reversed upload_day_reversed
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload day, but reversed using ``{total_days_in_month} + 1 - {upload_day}``, The upload day, but reversed using ``{total_days_in_month} + 1 - {upload_day}``,
i.e. August 8th would have upload_day_reversed of ``31 + 1 - 8`` = ``24`` i.e. August 8th would have upload_day_reversed of ``31 + 1 - 8`` = ``24``
upload_day_reversed_padded upload_day_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The reversed upload day, but padded. i.e. August 30th returns "02". The reversed upload day, but padded. i.e. August 30th returns "02".
upload_month upload_month
~~~~~~~~~~~~ ~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload month as an integer (no padding). The upload month as an integer (no padding).
upload_month_padded upload_month_padded
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The entry's upload month padded to two digits, i.e. March returns "03" The entry's upload month padded to two digits, i.e. March returns "03"
upload_month_reversed upload_month_reversed
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload month, but reversed using ``13 - {upload_month}``, i.e. March returns ``10`` The upload month, but reversed using ``13 - {upload_month}``, i.e. March returns ``10``
upload_month_reversed_padded upload_month_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The reversed upload month, but padded. i.e. November returns "02" The reversed upload month, but padded. i.e. November returns "02"
upload_year upload_year
~~~~~~~~~~~ ~~~~~~~~~~~
:type: ``Integer``
:description:
The entry's upload year The entry's upload year
upload_year_truncated upload_year_truncated
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The last two digits of the upload year, i.e. 22 in 2022 The last two digits of the upload year, i.e. 22 in 2022
upload_year_truncated_reversed upload_year_truncated_reversed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload year truncated, but reversed using ``100 - {upload_year_truncated}``, i.e. The upload year truncated, but reversed using ``100 - {upload_year_truncated}``, i.e.
2022 returns ``100 - 22`` = ``78`` 2022 returns ``100 - 22`` = ``78``
----------------------------------------------------------------------------------------------------
Ytdl-Sub Variables Ytdl-Sub Variables
------------------ ------------------
download_index download_index
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The i'th entry downloaded. NOTE that this is fetched dynamically from the download The i'th entry downloaded. NOTE that this is fetched dynamically from the download
archive. archive.
download_index_padded6 download_index_padded6
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The download_index padded six digits The download_index padded six digits
upload_date_index upload_date_index
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The i'th entry downloaded with this upload date. The i'th entry downloaded with this upload date.
upload_date_index_padded upload_date_index_padded
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The upload_date_index padded two digits The upload_date_index padded two digits
upload_date_index_reversed upload_date_index_reversed
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
100 - upload_date_index 100 - upload_date_index
upload_date_index_reversed_padded upload_date_index_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The upload_date_index padded two digits The upload_date_index padded two digits
ytdl_sub_input_url ytdl_sub_input_url
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The input URL used in ytdl-sub to create this entry. The input URL used in ytdl-sub to create this entry.

View file

@ -2,8 +2,171 @@
Scripting Scripting
========= =========
``ytdl-sub`` fields (file-names, tags, etc) are defined using variables and scripts. The links below
contain reference documentation for each built-in variable and scripting function.
.. toctree:: .. toctree::
:maxdepth: 1
entry_variables entry_variables
override_variables override_variables
scripting_functions scripting_functions
config_types scripting_types
How it Works
------------
Fields in the config that support ``formatters`` mean they support scripting, and will
*format* the field using its defined script.
In its most basic form, a script is a string comprised of variables and/or functions.
Static String
~~~~~~~~~~~~~
The following example sets ``ytdl-sub``'s output directory. It is
considered *static* because it does not depend on anything from an entry.
.. code-block:: yaml
output_options:
output_directory: "Custom YTDL-SUB TV Show"
Static Variables
~~~~~~~~~~~~~~~~
``ytdl-sub`` offers a few built-in static variables, including ``subscription_name``.
We can use this instead of hard-coding it above:
.. code-block:: yaml
output_options:
output_directory: "{subscription_name}"
The syntax for variable usage is curly-braces with the variable name within it. Assuming
our subscription is actually named "Custom YTDL-SUB TV Show", then ``ytdl-sub``
will actually write to that directory.
Entry Variables
~~~~~~~~~~~~~~~
For context, an *entry* is a video or audio file downloaded from ``yt-dlp``.
*Entry variables* are variables that are derived from an entry's ``info.json`` file. This file
comes from ``yt-dlp`` and contains every piece of metadata that it scraped.
These variables are not considered static since they change per entry download. There are a
few fields in ``ytdl-sub`` (i.e. ``output_directory``) that must be static. For others,
we are free to use values that derive from an entry.
Suppose we want to customize the name of an entry's output file and thumbnail to include its
title in its name. We can do that using entry variables:
.. code-block:: yaml
output_options:
output_directory: "{subscription_name}"
file_name: "{title}.{ext}"
thumbnail_name: "{title}.{thumbnail_ext}"
Creating Custom Variables
~~~~~~~~~~~~~~~~~~~~~~~~~
Suppose we want to include the date in our file names. This means we'd need to update
both the ``file_name`` and ``thumbnail_name`` fields to include it.
Instead, we can create a custom *override variable*. This is ``ytdl-sub``'s method
for creating and overriding custom variables.
These are created in the ``overrides`` section. Let's take our above example and create
a ``custom_file_name`` variable to use for the entry file and thumbnail fields:
.. code-block:: yaml
output_options:
output_directory: "{subscription_name}"
file_name: "{custom_file_name}.{ext}"
thumbnail_name: "{custom_file_name}.{thumbnail_ext}"
overrides:
custom_file_name: "{upload_date_standardized} {title}"
Sanitizing Variables
~~~~~~~~~~~~~~~~~~~~
For experienced ``yt-dlp`` scrapers, you may be thinking:
- What if the title has characters that do not play nice with my operating system?
``ytdl-sub`` is able to *sanitize* any variable, meaning it replaces any problematic characters
with safe alternatives that can be used in file names. We can ensure our file names and directories
are safe by using:
.. code-block:: yaml
output_options:
output_directory: "{subscription_name_sanitized}"
file_name: "{custom_file_name}.{ext}"
thumbnail_name: "{custom_file_name}.{thumbnail_ext}"
overrides:
custom_file_name: "{upload_date_standardized} {title_sanitized}"
Simply add a ``_sanitized`` suffix to any variable name to make it sanitized.
.. note::
Make sure you do not sanitize custom variables that intentionally create directories, otherwise
they will... be sanitized and not resolve to directories!
Using Scripting Functions
~~~~~~~~~~~~~~~~~~~~~~~~~
Let's suppose you are an avid command-line user, and like all of your file names to be
``snake_cased_with_no_spaces``. We can use the
`replace <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#replace>`_
*scripting function* to create and use a snake-cased title.
.. code-block:: yaml
output_options:
output_directory: "{subscription_name_sanitized}"
file_name: "{custom_file_name}.{ext}"
thumbnail_name: "{custom_file_name}.{thumbnail_ext}"
overrides:
snake_cased_title: >-
{
%replace( title, ' ', '_' )
}
custom_file_name: "{upload_date_standardized}_{snake_cased_title_sanitized}"
Scripting functions are similar to variables - they must be used within curly-braces.
It is good practice to use ``>-`` when defining variables that use functions. It is YAML's way of
saying:
- Allow a string to be multi-lined, and do not include newlines before or after it.
See for yourself `here <https://yaml-online-parser.appspot.com/?yaml=output_options%3A%0A%20%20output_directory%3A%20%22%7Bsubscription_name_sanitized%7D%22%0A%20%20file_name%3A%20%22%7Bcustom_file_name%7D.%7Bext%7D%22%0A%20%20thumbnail_name%3A%20%22%7Bcustom_file_name%7D.%7Bthumbnail_ext%7D%22%0A%0Aoverrides%3A%0A%20%20snake_cased_title%3A%20%3E-%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%25replace%28%20title%2C%20%27%20%27%2C%20%27_%27%20%29%0A%20%20%20%20%7D%0A%20%20custom_file_name%3A%20%22%7Bupload_date_standardized%7D%20%7Bsnake_cased_title_sanitized%7D%22&type=canonical_yaml>`_.
Any whitespace within curly-braces is okay since it will be parsed out. This is needed to make
scripting function usage readable.
.. important::
It is important to use ``>-`` over other YAML new-line directives like ``>`` because they
add newlines before or after curly-braces, and will be included in your variable's output string.
Advanced Scripting
------------------
Accessing ``info.json`` Fields
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
WIP
Creating Custom Functions
~~~~~~~~~~~~~~~~~~~~~~~~~
WIP
Parsing Maps and Arrays
~~~~~~~~~~~~~~~~~~~~~~~
WIP

View file

@ -1,3 +1,4 @@
Override Variables Override Variables
================== ==================
@ -14,6 +15,32 @@ For subscriptions in the form of
``subscription_indent_1`` and ``subscription_indent_2`` get set to ``subscription_indent_1`` and ``subscription_indent_2`` get set to
``Indent Value 1`` and ``Indent Value 2``. ``Indent Value 1`` and ``Indent Value 2``.
subscription_map
----------------
For subscriptions in the form of
.. code-block:: yaml
+ Subscription Name:
Music Videos:
- "https://url1.com/..."
Concerts:
- "https://url2.com/..."
Stores all the contents under the subscription name into the override variable
``subscription_map`` as a Map value. The above example is stored as:
.. code-block:: python
{
"Music Videos": [
"https://url1.com/..."
],
"Concerts: [
"https://url2.com/..."
]
}
subscription_name subscription_name
----------------- -----------------
Name of the subscription Name of the subscription
@ -41,4 +68,3 @@ For subscriptions in the form of
``subscription_value_1`` and ``subscription_value_2`` get set to ``https://url1.com/...`` ``subscription_value_1`` and ``subscription_value_2`` get set to ``https://url1.com/...``
and ``https://url2.com/...``. Note that ``subscription_value_1`` also gets set to and ``https://url2.com/...``. Note that ``subscription_value_1`` also gets set to
``subscription_value``. ``subscription_value``.

View file

@ -7,414 +7,571 @@ Array Functions
array array
~~~~~ ~~~~~
``array(maybe_array: AnyArgument) -> Array`` :spec: ``array(maybe_array: AnyArgument) -> Array``
:description:
Tries to cast an unknown variable type to an Array. Tries to cast an unknown variable type to an Array.
array_apply array_apply
~~~~~~~~~~~ ~~~~~~~~~~~
``array_apply(array: Array, lambda_function: Lambda) -> Array`` :spec: ``array_apply(array: Array, lambda_function: Lambda) -> Array``
:description:
Apply a lambda function on every element in the Array. Apply a lambda function on every element in the Array.
:usage:
.. code-block:: python
{
%array_apply( [1, 2, 3] , %string )
}
# ["1", "2", "3"]
array_apply_fixed
~~~~~~~~~~~~~~~~~
:spec: ``array_apply_fixed(array: Array, fixed_argument: AnyArgument, lambda2_function: LambdaTwo, reverse_args: Optional[Boolean]) -> Array``
:description:
Apply a lambda function on every element in the Array, with ``fixed_argument``
passed as a second argument to every invocation.
array_at array_at
~~~~~~~~ ~~~~~~~~
``array_at(array: Array, idx: Integer) -> AnyArgument`` :spec: ``array_at(array: Array, idx: Integer) -> AnyArgument``
:description:
Return the element in the Array at index ``idx``. Return the element in the Array at index ``idx``.
array_contains array_contains
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
``array_contains(array: Array, value: AnyArgument) -> Boolean`` :spec: ``array_contains(array: Array, value: AnyArgument) -> Boolean``
:description:
Return True if the value exists in the Array. False otherwise. Return True if the value exists in the Array. False otherwise.
array_enumerate array_enumerate
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
``array_enumerate(array: Array, lambda_function: LambdaTwo) -> Array`` :spec: ``array_enumerate(array: Array, lambda_function: LambdaTwo) -> Array``
:description:
Apply a lambda function on every element in the Array, where each arg Apply a lambda function on every element in the Array, where each arg
passed to the lambda function is ``idx, element`` as two separate args. passed to the lambda function is ``idx, element`` as two separate args.
array_extend array_extend
~~~~~~~~~~~~ ~~~~~~~~~~~~
``array_extend(arrays: Array, ...) -> Array`` :spec: ``array_extend(arrays: Array, ...) -> Array``
:description:
Combine multiple Arrays into a single Array. Combine multiple Arrays into a single Array.
array_first
~~~~~~~~~~~
:spec: ``array_first(array: Array, fallback: AnyArgument) -> AnyArgument``
:description:
Returns the first element whose boolean conversion is True. Returns fallback
if all elements evaluate to False.
array_flatten array_flatten
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
``array_flatten(array: Array) -> Array`` :spec: ``array_flatten(array: Array) -> Array``
:description:
Flatten any nested Arrays into a single-dimensional Array. Flatten any nested Arrays into a single-dimensional Array.
array_index array_index
~~~~~~~~~~~ ~~~~~~~~~~~
``array_index(array: Array, value: AnyArgument) -> Integer`` :spec: ``array_index(array: Array, value: AnyArgument) -> Integer``
:description:
Return the index of the value within the Array if it exists. If it does not, it will Return the index of the value within the Array if it exists. If it does not, it will
throw an error. throw an error.
array_overlay
~~~~~~~~~~~~~
:spec: ``array_overlay(array: Array, overlap: Array, only_missing: Optional[Boolean]) -> Array``
:description:
Overlaps ``overlap`` onto ``array``. Can optionally only overlay missing indices.
array_product array_product
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
``array_product(arrays: Array, ...) -> Array`` :spec: ``array_product(arrays: Array, ...) -> Array``
:description:
Returns the Cartesian product of elements from different arrays Returns the Cartesian product of elements from different arrays
array_reduce array_reduce
~~~~~~~~~~~~ ~~~~~~~~~~~~
``array_reduce(array: Array, lambda_reduce_function: LambdaReduce) -> AnyArgument`` :spec: ``array_reduce(array: Array, lambda_reduce_function: LambdaReduce) -> AnyArgument``
:description:
Apply a reduce function on pairs of elements in the Array, until one element remains. Apply a reduce function on pairs of elements in the Array, until one element remains.
Executes using the left-most and reduces in the right direction. Executes using the left-most and reduces in the right direction.
array_reverse array_reverse
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
``array_reverse(array: Array) -> Array`` :spec: ``array_reverse(array: Array) -> Array``
:description:
Reverse an Array. Reverse an Array.
array_size array_size
~~~~~~~~~~ ~~~~~~~~~~
``array_size(array: Array) -> Integer`` :spec: ``array_size(array: Array) -> Integer``
:description:
Returns the size of an Array. Returns the size of an Array.
array_slice array_slice
~~~~~~~~~~~ ~~~~~~~~~~~
``array_slice(array: Array, start: Integer, end: Optional[Integer]) -> Array`` :spec: ``array_slice(array: Array, start: Integer, end: Optional[Integer]) -> Array``
:description:
Returns the slice of the Array. Returns the slice of the Array.
----------------------------------------------------------------------------------------------------
Boolean Functions Boolean Functions
----------------- -----------------
and and
~~~ ~~~
``and(values: AnyArgument, ...) -> Boolean`` :spec: ``and(values: AnyArgument, ...) -> Boolean``
:description:
``and`` operator. Returns True if all values evaluate to True. False otherwise. ``and`` operator. Returns True if all values evaluate to True. False otherwise.
bool bool
~~~~ ~~~~
``bool(value: AnyArgument) -> Boolean`` :spec: ``bool(value: AnyArgument) -> Boolean``
:description:
Cast any type to a Boolean. Cast any type to a Boolean.
eq eq
~~ ~~
``eq(left: AnyArgument, right: AnyArgument) -> Boolean`` :spec: ``eq(left: AnyArgument, right: AnyArgument) -> Boolean``
:description:
``==`` operator. Returns True if left == right. False otherwise. ``==`` operator. Returns True if left == right. False otherwise.
gt gt
~~ ~~
``gt(left: AnyArgument, right: AnyArgument) -> Boolean`` :spec: ``gt(left: AnyArgument, right: AnyArgument) -> Boolean``
:description:
``>`` operator. Returns True if left > right. False otherwise. ``>`` operator. Returns True if left > right. False otherwise.
gte gte
~~~ ~~~
``gte(left: AnyArgument, right: AnyArgument) -> Boolean`` :spec: ``gte(left: AnyArgument, right: AnyArgument) -> Boolean``
:description:
``>=`` operator. Returns True if left >= right. False otherwise. ``>=`` operator. Returns True if left >= right. False otherwise.
is_null
~~~~~~~
:spec: ``is_null(value: AnyArgument) -> Boolean``
:description:
Returns True if a value is null (i.e. an empty string). False otherwise.
lt lt
~~ ~~
``lt(left: AnyArgument, right: AnyArgument) -> Boolean`` :spec: ``lt(left: AnyArgument, right: AnyArgument) -> Boolean``
:description:
``<`` operator. Returns True if left < right. False otherwise. ``<`` operator. Returns True if left < right. False otherwise.
lte lte
~~~ ~~~
``lte(left: AnyArgument, right: AnyArgument) -> Boolean`` :spec: ``lte(left: AnyArgument, right: AnyArgument) -> Boolean``
:description:
``<=`` operator. Returns True if left <= right. False otherwise. ``<=`` operator. Returns True if left <= right. False otherwise.
ne ne
~~ ~~
``ne(left: AnyArgument, right: AnyArgument) -> Boolean`` :spec: ``ne(left: AnyArgument, right: AnyArgument) -> Boolean``
:description:
``!=`` operator. Returns True if left != right. False otherwise. ``!=`` operator. Returns True if left != right. False otherwise.
not not
~~~ ~~~
``not(value: Boolean) -> Boolean`` :spec: ``not(value: Boolean) -> Boolean``
:description:
``not`` operator. Returns the opposite of value. ``not`` operator. Returns the opposite of value.
or or
~~ ~~
``or(values: AnyArgument, ...) -> Boolean`` :spec: ``or(values: AnyArgument, ...) -> Boolean``
:description:
``or`` operator. Returns True if any value evaluates to True. False otherwise. ``or`` operator. Returns True if any value evaluates to True. False otherwise.
xor xor
~~~ ~~~
``xor(values: AnyArgument, ...) -> Boolean`` :spec: ``xor(values: AnyArgument, ...) -> Boolean``
:description:
``^`` operator. Returns True if exactly one value is set to True. False otherwise. ``^`` operator. Returns True if exactly one value is set to True. False otherwise.
----------------------------------------------------------------------------------------------------
Conditional Functions Conditional Functions
--------------------- ---------------------
if if
~~ ~~
``if(condition: Boolean, true: ReturnableArgumentA, false: ReturnableArgumentB) -> Union[ReturnableArgumentA, ReturnableArgumentB]`` :spec: ``if(condition: Boolean, true: ReturnableArgumentA, false: ReturnableArgumentB) -> Union[ReturnableArgumentA, ReturnableArgumentB]``
:description:
Conditional ``if`` statement that returns the ``true`` or ``false`` parameter Conditional ``if`` statement that returns the ``true`` or ``false`` parameter
depending on the ``condition`` value. depending on the ``condition`` value.
if_passthrough if_passthrough
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
``if_passthrough(maybe_true_arg: ReturnableArgumentA, else_arg: ReturnableArgumentB) -> Union[ReturnableArgumentA, ReturnableArgumentB]`` :spec: ``if_passthrough(maybe_true_arg: ReturnableArgumentA, else_arg: ReturnableArgumentB) -> Union[ReturnableArgumentA, ReturnableArgumentB]``
:description:
Conditional ``if`` statement that returns the ``maybe_true_arg`` if it evaluates to True, Conditional ``if`` statement that returns the ``maybe_true_arg`` if it evaluates to True,
otherwise returns ``else_arg``. otherwise returns ``else_arg``.
----------------------------------------------------------------------------------------------------
Date Functions Date Functions
-------------- --------------
datetime_strftime datetime_strftime
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
``datetime_strftime(posix_timestamp: Integer, date_format: String) -> String`` :spec: ``datetime_strftime(posix_timestamp: Integer, date_format: String) -> String``
:description:
Converts a posix timestamp to a date using strftime formatting. Converts a posix timestamp to a date using strftime formatting.
----------------------------------------------------------------------------------------------------
Error Functions Error Functions
--------------- ---------------
assert assert
~~~~~~ ~~~~~~
``assert(value: ReturnableArgument, assert_message: String) -> ReturnableArgument`` :spec: ``assert(value: ReturnableArgument, assert_message: String) -> ReturnableArgument``
Explicitly throw an error with the provided assert message if ``value`` evaluates to False. :description:
If it evaluates to True, it will return ``value``. Explicitly throw an error with the provided assert message if ``value`` evaluates to
False. If it evaluates to True, it will return ``value``.
assert_eq
~~~~~~~~~
:spec: ``assert_eq(value: ReturnableArgument, equals: AnyArgument, assert_message: String) -> ReturnableArgument``
:description:
Explicitly throw an error with the provided assert message if ``value`` does not equal
``equals``. If they do equal, then return ``value``.
assert_ne
~~~~~~~~~
:spec: ``assert_ne(value: ReturnableArgument, equals: AnyArgument, assert_message: String) -> ReturnableArgument``
:description:
Explicitly throw an error with the provided assert message if ``value`` equals
``equals``. If they do equal, then return ``value``.
assert_then
~~~~~~~~~~~
:spec: ``assert_then(value: AnyArgument, ret: ReturnableArgument, assert_message: String) -> ReturnableArgument``
:description:
Explicitly throw an error with the provided assert message if ``value`` evaluates to
False. If it evaluates to True, it will return ``ret``.
throw throw
~~~~~ ~~~~~
``throw(error_message: String) -> AnyArgument`` :spec: ``throw(error_message: String) -> AnyArgument``
:description:
Explicitly throw an error with the provided error message. Explicitly throw an error with the provided error message.
----------------------------------------------------------------------------------------------------
Json Functions Json Functions
-------------- --------------
from_json from_json
~~~~~~~~~ ~~~~~~~~~
``from_json(argument: String) -> AnyArgument`` :spec: ``from_json(argument: String) -> AnyArgument``
:description:
Converts a JSON string into an actual type. Converts a JSON string into an actual type.
----------------------------------------------------------------------------------------------------
Map Functions Map Functions
------------- -------------
map map
~~~ ~~~
``map(maybe_mapping: AnyArgument) -> Map`` :spec: ``map(maybe_mapping: AnyArgument) -> Map``
:description:
Tries to cast an unknown variable type to a Map. Tries to cast an unknown variable type to a Map.
map_apply map_apply
~~~~~~~~~ ~~~~~~~~~
``map_apply(mapping: Map, lambda_function: LambdaTwo) -> Array`` :spec: ``map_apply(mapping: Map, lambda_function: LambdaTwo) -> Array``
:description:
Apply a lambda function on the Map, where each arg Apply a lambda function on the Map, where each arg
passed to the lambda function is ``key, value`` as two separate args. passed to the lambda function is ``key, value`` as two separate args.
map_contains map_contains
~~~~~~~~~~~~ ~~~~~~~~~~~~
``map_contains(mapping: Map, key: AnyArgument) -> Boolean`` :spec: ``map_contains(mapping: Map, key: AnyArgument) -> Boolean``
:description:
Returns True if the key is in the Map. False otherwise. Returns True if the key is in the Map. False otherwise.
map_enumerate map_enumerate
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
``map_enumerate(mapping: Map, lambda_function: LambdaThree) -> Array`` :spec: ``map_enumerate(mapping: Map, lambda_function: LambdaThree) -> Array``
:description:
Apply a lambda function on the Map, where each arg Apply a lambda function on the Map, where each arg
passed to the lambda function is ``idx, key, value`` as three separate args. passed to the lambda function is ``idx, key, value`` as three separate args.
map_get map_get
~~~~~~~ ~~~~~~~
``map_get(mapping: Map, key: AnyArgument, default: Optional[AnyArgument]) -> AnyArgument`` :spec: ``map_get(mapping: Map, key: AnyArgument, default: Optional[AnyArgument]) -> AnyArgument``
:description:
Return ``key``'s value within the Map. If ``key`` does not exist, and ``default`` is Return ``key``'s value within the Map. If ``key`` does not exist, and ``default`` is
provided, it will return ``default``. Otherwise, will error. provided, it will return ``default``. Otherwise, will error.
map_get_non_empty map_get_non_empty
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
``map_get_non_empty(mapping: Map, key: AnyArgument, default: AnyArgument) -> AnyArgument`` :spec: ``map_get_non_empty(mapping: Map, key: AnyArgument, default: AnyArgument) -> AnyArgument``
:description:
Return ``key``'s value within the Map. If ``key`` does not exist or is an empty string, Return ``key``'s value within the Map. If ``key`` does not exist or is an empty string,
return ``default``. Otherwise, will error. return ``default``. Otherwise, will error.
map_size map_size
~~~~~~~~ ~~~~~~~~
``map_size(mapping: Map) -> Integer`` :spec: ``map_size(mapping: Map) -> Integer``
:description:
Returns the size of a Map. Returns the size of a Map.
----------------------------------------------------------------------------------------------------
Numeric Functions Numeric Functions
----------------- -----------------
add add
~~~ ~~~
``add(values: Numeric, ...) -> Numeric`` :spec: ``add(values: Numeric, ...) -> Numeric``
:description:
``+`` operator. Returns the sum of all values. ``+`` operator. Returns the sum of all values.
div div
~~~ ~~~
``div(left: Numeric, right: Numeric) -> Numeric`` :spec: ``div(left: Numeric, right: Numeric) -> Numeric``
:description:
``/`` operator. Returns ``left / right``. ``/`` operator. Returns ``left / right``.
float float
~~~~~ ~~~~~
``float(value: AnyArgument) -> Float`` :spec: ``float(value: AnyArgument) -> Float``
:description:
Cast to Float. Cast to Float.
int int
~~~ ~~~
``int(value: AnyArgument) -> Integer`` :spec: ``int(value: AnyArgument) -> Integer``
:description:
Cast to Integer. Cast to Integer.
max max
~~~ ~~~
``max(values: Numeric, ...) -> Numeric`` :spec: ``max(values: Numeric, ...) -> Numeric``
:description:
Returns max of all values. Returns max of all values.
min min
~~~ ~~~
``min(values: Numeric, ...) -> Numeric`` :spec: ``min(values: Numeric, ...) -> Numeric``
:description:
Returns min of all values. Returns min of all values.
mod mod
~~~ ~~~
``mod(left: Numeric, right: Numeric) -> Numeric`` :spec: ``mod(left: Numeric, right: Numeric) -> Numeric``
:description:
``%`` operator. Returns ``left % right``. ``%`` operator. Returns ``left % right``.
mul mul
~~~ ~~~
``mul(values: Numeric, ...) -> Numeric`` :spec: ``mul(values: Numeric, ...) -> Numeric``
:description:
``*`` operator. Returns the product of all values. ``*`` operator. Returns the product of all values.
pow pow
~~~ ~~~
``pow(base: Numeric, exponent: Numeric) -> Numeric`` :spec: ``pow(base: Numeric, exponent: Numeric) -> Numeric``
:description:
``**`` operator. Returns the exponential of the base and exponent value. ``**`` operator. Returns the exponential of the base and exponent value.
sub sub
~~~ ~~~
``sub(values: Numeric, ...) -> Numeric`` :spec: ``sub(values: Numeric, ...) -> Numeric``
:description:
``-`` operator. Subtracts all values from left to right. ``-`` operator. Subtracts all values from left to right.
----------------------------------------------------------------------------------------------------
Regex Functions Regex Functions
--------------- ---------------
regex_capture_groups
~~~~~~~~~~~~~~~~~~~~
:spec: ``regex_capture_groups(regex: String) -> Integer``
:description:
Returns number of capture groups in regex
regex_fullmatch regex_fullmatch
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
``regex_fullmatch(regex: String, string: String) -> Array`` :spec: ``regex_fullmatch(regex: String, string: String) -> Array``
:description:
Checks for entire string to be a match. If a match exists, returns Checks for entire string to be a match. If a match exists, returns
the string as the first element of the Array. If there are capture groups, returns each the string as the first element of the Array. If there are capture groups, returns each
group as a subsequent element in the Array. group as a subsequent element in the Array.
regex_match regex_match
~~~~~~~~~~~ ~~~~~~~~~~~
``regex_match(regex: String, string: String) -> Array`` :spec: ``regex_match(regex: String, string: String) -> Array``
:description:
Checks for a match only at the beginning of the string. If a match exists, returns Checks for a match only at the beginning of the string. If a match exists, returns
the string as the first element of the Array. If there are capture groups, returns each the string as the first element of the Array. If there are capture groups, returns each
group as a subsequent element in the Array. group as a subsequent element in the Array.
regex_search regex_search
~~~~~~~~~~~~ ~~~~~~~~~~~~
``regex_search(regex: String, string: String) -> Array`` :spec: ``regex_search(regex: String, string: String) -> Array``
:description:
Checks for a match anywhere in the string. If a match exists, returns Checks for a match anywhere in the string. If a match exists, returns
the string as the first element of the Array. If there are capture groups, returns each the string as the first element of the Array. If there are capture groups, returns each
group as a subsequent element in the Array. group as a subsequent element in the Array.
----------------------------------------------------------------------------------------------------
String Functions String Functions
---------------- ----------------
capitalize capitalize
~~~~~~~~~~ ~~~~~~~~~~
``capitalize(string: String) -> String`` :spec: ``capitalize(string: String) -> String``
:description:
Capitalize the first character in the string. Capitalize the first character in the string.
concat concat
~~~~~~ ~~~~~~
``concat(values: String, ...) -> String`` :spec: ``concat(values: String, ...) -> String``
:description:
Concatenate multiple Strings into a single String. Concatenate multiple Strings into a single String.
contains
~~~~~~~~
:spec: ``contains(string: String, contains: String) -> Boolean``
:description:
Returns True if ``contains`` is in ``string``. False otherwise.
lower lower
~~~~~ ~~~~~
``lower(string: String) -> String`` :spec: ``lower(string: String) -> String``
:description:
Lower-case the entire String. Lower-case the entire String.
pad pad
~~~ ~~~
``pad(string: String, length: Integer, char: String) -> String`` :spec: ``pad(string: String, length: Integer, char: String) -> String``
:description:
Pads the string to the given length Pads the string to the given length
pad_zero pad_zero
~~~~~~~~ ~~~~~~~~
``pad_zero(numeric: Numeric, length: Integer) -> String`` :spec: ``pad_zero(numeric: Numeric, length: Integer) -> String``
:description:
Pads a numeric with zeros to the given length Pads a numeric with zeros to the given length
replace replace
~~~~~~~ ~~~~~~~
``replace(string: String, old: String, new: String, count: Optional[Integer]) -> String`` :spec: ``replace(string: String, old: String, new: String, count: Optional[Integer]) -> String``
:description:
Replace the ``old`` part of the String with the ``new``. Optionally only replace it Replace the ``old`` part of the String with the ``new``. Optionally only replace it
``count`` number of times. ``count`` number of times.
slice slice
~~~~~ ~~~~~
``slice(string: String, start: Integer, end: Optional[Integer]) -> String`` :spec: ``slice(string: String, start: Integer, end: Optional[Integer]) -> String``
:description:
Returns the slice of the Array. Returns the slice of the Array.
string string
~~~~~~ ~~~~~~
``string(value: AnyArgument) -> String`` :spec: ``string(value: AnyArgument) -> String``
:description:
Cast to String. Cast to String.
titlecase titlecase
~~~~~~~~~ ~~~~~~~~~
``titlecase(string: String) -> String`` :spec: ``titlecase(string: String) -> String``
:description:
Capitalize each word in the string. Capitalize each word in the string.
upper upper
~~~~~ ~~~~~
``upper(string: String) -> String`` :spec: ``upper(string: String) -> String``
:description:
Upper-case the entire String. Upper-case the entire String.
----------------------------------------------------------------------------------------------------
Ytdl-Sub Functions Ytdl-Sub Functions
------------------ ------------------
legacy_bracket_safety legacy_bracket_safety
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
``legacy_bracket_safety(value: ReturnableArgument) -> ReturnableArgument`` :spec: ``legacy_bracket_safety(value: ReturnableArgument) -> ReturnableArgument``
ytdl-sub used to replace brackets ('{', '}') with unicode brackets ('', '') to not ytdl-sub used to replace brackets ('{', '}') with unicode brackets ('', '') to not
interfere with its legacy variable scripting system. This function replicates that interfere with its legacy variable scripting system. This function replicates that
@ -422,14 +579,14 @@ behavior.
sanitize sanitize
~~~~~~~~ ~~~~~~~~
``sanitize(value: AnyArgument) -> String`` :spec: ``sanitize(value: AnyArgument) -> String``
Sanitize a string using yt-dlp's ``sanitize_filename`` method to ensure it's safe to use Sanitize a string using yt-dlp's ``sanitize_filename`` method to ensure it's safe to use
for file/directory names on any OS. for file/directory names on any OS.
sanitize_plex_episode sanitize_plex_episode
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
``sanitize_plex_episode(string: String) -> String`` :spec: ``sanitize_plex_episode(string: String) -> String``
Sanitize a string using ``sanitize`` and replace numerics with their respective fixed-width Sanitize a string using ``sanitize`` and replace numerics with their respective fixed-width
numbers. This is used to have Plex avoid scraping numbers like ``4x4`` as the numbers. This is used to have Plex avoid scraping numbers like ``4x4`` as the
@ -437,7 +594,7 @@ season and/or episode.
to_date_metadata to_date_metadata
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
``to_date_metadata(yyyymmdd: String) -> Map`` :spec: ``to_date_metadata(yyyymmdd: String) -> Map``
Takes a date in the form of YYYYMMDD and returns a Map containing: Takes a date in the form of YYYYMMDD and returns a Map containing:
@ -446,7 +603,7 @@ Takes a date in the form of YYYYMMDD and returns a Map containing:
- year (Integer) - year (Integer)
- month (Integer) - month (Integer)
- day (Integer) - day (Integer)
- year_truncated (String, YY from YY[YY]) - year_truncated (Integer, YY from YY[YY])
- month_padded (String) - month_padded (String)
- day_padded (String) - day_padded (String)
- year_truncated_reversed (Integer, 100 - year_truncated) - year_truncated_reversed (Integer, 100 - year_truncated)
@ -461,14 +618,14 @@ Takes a date in the form of YYYYMMDD and returns a Map containing:
to_native_filepath to_native_filepath
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
``to_native_filepath(filepath: String) -> String`` :spec: ``to_native_filepath(filepath: String) -> String``
Convert any unix-based path separators ('/') with the OS's native Convert any unix-based path separators ('/') with the OS's native
separator. separator.
truncate_filepath_if_too_long truncate_filepath_if_too_long
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``truncate_filepath_if_too_long(filepath: String) -> String`` :spec: ``truncate_filepath_if_too_long(filepath: String) -> String``
If a file-path is too long for the OS, this function will truncate it while preserving If a file-path is too long for the OS, this function will truncate it while preserving
the extension. the extension.

View file

@ -0,0 +1,319 @@
Scripting Types
===============
Types
-----
String
~~~~~~
Strings are a series of characters surrounded by quotes and can be defined in a few ways, including:
.. tab-set::
.. tab-item:: Literal
.. code-block:: yaml
string_variable: "This is a String variable"
.. tab-item:: In-Line
.. code-block:: yaml
string_variable: "{ %string('This is a String variable') }"
.. tab-item:: Single Quote
.. code-block:: yaml
string_variable: >-
{
%string('This is a String variable')
}
.. tab-item:: Double Quote
.. code-block:: yaml
string_variable: >-
{
%string("This is a String variable")
}
.. tab-item:: Triple Quote
.. code-block:: yaml
string_variable: >-
{
%string('''This is a String variable''')
}
.. tab-item:: Triple-Double Quote
.. code-block:: yaml
string_variable: >-
{
%string("""This is a String variable""")
}
.. note::
For non-String types, they must be defined as parameters to scripting functions. This is because
anything in a variable definition that is not within curly-braces gets evaluated as a String.
Integer
~~~~~~~
Integers are whole numbers with no decimal.
.. tab-set::
.. tab-item:: Literal
.. code-block:: yaml
int_variable: >-
{
%int(2022)
}
.. tab-item:: In-Line
.. code-block:: yaml
int_variable: "{ %int(2022) }"
Float
~~~~~
Floats are floating-point decimals numbers.
.. tab-set::
.. tab-item:: Literal
.. code-block:: yaml
float_variable: >-
{
%float(3.14)
}
.. tab-item:: In-Line
.. code-block:: yaml
float_variable: "{ %float(3.14) }"
Boolean
~~~~~~~
A type is considered boolean if it spells out ``True`` or ``False``, case-insensitive.
.. tab-set::
.. tab-item:: Literal
.. code-block:: yaml
bool_variable: >-
{
%bool(True)
}
.. tab-item:: In-Line
.. code-block:: yaml
bool_variable: "{ %bool(FALSE) }"
Array
~~~~~
An Array contains multiple types of any kind, including nested Arrays and Maps.
Arrays are defined using brackets (``[ ]``), and are accessed using zero-based indexing.
.. tab-set::
.. tab-item:: Literal
.. code-block:: yaml
array_variable: >-
{
[
"element with index 0",
1,
2.0,
[ "Nested Array 3" ]
]
}
element_0: >-
{
%array_at(array_variable, 0)
}
.. tab-item:: In-Line
.. code-block:: yaml
array_variable: "{ ['element with index 0', 1, 2.0, ['Nested Array 3' ]] }"
element_0: "{ %array_at(array_variable, 0) }"
Map
~~~
A Map is a key-value store, containing mappings between keys and values.
Maps are defined using curley-braces (``{ }``), and are accessed using their keys.
.. tab-set::
.. tab-item:: Literal
.. code-block:: yaml
map_variable: >-
{
{
"string_key": "string_value",
1: "int_key",
"list_value": [ "elem0", 1, 2.0 ]
}
}
string_value: >-
{
%map_get(map_variable, "string_key")
}
.. tab-item:: In-Line
.. code-block:: yaml
map_variable: "{ {'string_key': 'string_value', 1: 'int_key', 'list_value': [ 'elem0', 1, 2.0 ]} }"
string_value: "{ %map_get(map_variable, 'string_key') }"
Null
~~~~
Null is represented by an empty String, and can be conveyed by spelling out ``null``,
case-insensitive.
.. tab-set::
.. tab-item:: Literal
.. code-block:: yaml
null_variable: ""
.. tab-item:: In-Line
.. code-block:: yaml
null_variable: "{ %string(null) }"
Function Type-Hints
-------------------
AnyArgument
~~~~~~~~~~~
AnyArgument means any of the above Types are valid as input or output to a scripting function.
.. note::
Strict typing is enforced. For functions that return ``AnyArgument`` need to be casted before
passing into functions that expect a particular type.
Numeric
~~~~~~~
Numeric refers to either an Integer or Float.
Optional
~~~~~~~~
Optional means a particular scripting function argument can be either provided or not included.
For example, the function
`map_get <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#map-get>`_
has an optional default value. Both of these usages are valid:
.. tab-set::
.. tab-item:: Map Get
.. code-block:: yaml
will_throw_key_does_not_exist_error: "{ %map_get( {}, 'key' ) }"
.. tab-item:: Map Get with Optional Default Value
.. code-block:: yaml
will_return_default: "{ %map_get( {}, 'key', 'default value' ) }"
Lambda
~~~~~~
Lambda parameters are a reference to a function, and will call that lambda function
on the input. In this example,
.. code-block:: yaml
lambda_array_numeric_to_string: >-
{
%array_apply( [ 1, 2, 3, 4], %string )
}
We apply ``%string`` as a lambda function to
`array_apply <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#array-apply>`_,
which is called on every element in the input array. The output becomes ``["1", "2", "3", "4"]``.
This example has one input-argument being passed into the lambda. For other lambda-based functions
like `array_enumerate <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#array-enumerate>`_,
it expects the lambda function to have two input arguments. These are denoted using
``LambdaTwo``, ``LambdaThree``, etc within the function spec.
LambdaReduce
~~~~~~~~~~~~
LambdaReduce is special type of lambda that reduces an Array to a single value by calling the
LabmdaReduce function repeatedly on two elements in the Array until it is reduced to a single value.
In this example,
.. code-block:: yaml
lambda_reduce_sum: >-
{
%array_reduce( [ 1, 2, 3, 4], %add )
}
We call
`array_reduce <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#array-reduce>`_
on the input array, using
`add <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#add>`_
as the LambdaReduce function. This will reduce the Array to a single value by internally calling
.. code-block::
- %add(1, 2) = 3
- %add(3, 3) = 6
- %add(6, 4) = 10
And evaluate to ``10``.
ReturnableArguments
~~~~~~~~~~~~~~~~~~~
Returnable arguments are used in conditional functions like
`if <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#if>`_,
which implies the argument passed into the function is the function's output. For example,
.. code-block:: yaml
conditional_function: >-
{
%if( True, "Return this if True", "Return this if False" )
}
is going to return ``"Return this if True"`` since the condition parameter is ``True``.

View file

@ -18,7 +18,7 @@ Jellyfin
Kodi Kodi
~~~~ ~~~~
* Everything that the Jellyfin version does * Everything that the Jellyfin version does
* Turns on :ref:`config_reference/plugins:kodi_safe`, replacing characters that would break kodi with safer characters * Enables ``kodi_safe`` NFOs, replacing 4-byte unicode characters that break kodi with ````
Plex Plex
~~~~ ~~~~

View file

@ -7,7 +7,7 @@ import mergedeep
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.entries.script.variable_definitions import VARIABLES
from ytdl_sub.entries.variables.override_variables import SUBSCRIPTION_NAME from ytdl_sub.entries.variables.override_variables import OverrideHelpers
from ytdl_sub.entries.variables.override_variables import OverrideVariables from ytdl_sub.entries.variables.override_variables import OverrideVariables
from ytdl_sub.script.parser import parse from ytdl_sub.script.parser import parse
from ytdl_sub.script.script import Script from ytdl_sub.script.script import Script
@ -23,9 +23,9 @@ from ytdl_sub.validators.string_formatter_validators import StringFormatterValid
class Overrides(DictFormatterValidator, Scriptable): class Overrides(DictFormatterValidator, Scriptable):
""" """
Optional. This section allows you to define variables that can be used in any string formatter. Allows you to define variables that can be used in any EntryFormatter or OverridesFormatter.
For example, if you want your file and thumbnail files to match without copy-pasting a large
format string, you can define something like: :Usage:
.. code-block:: yaml .. code-block:: yaml
@ -89,7 +89,7 @@ class Overrides(DictFormatterValidator, Scriptable):
""" """
Ensures the variable name does not collide with any entry variables or built-in functions. Ensures the variable name does not collide with any entry variables or built-in functions.
""" """
if not OverrideVariables.is_valid_name(name): if not OverrideHelpers.is_valid_name(name):
override_type = "function" if name.startswith("%") else "variable" override_type = "function" if name.startswith("%") else "variable"
raise self._validation_exception( raise self._validation_exception(
f"Override {override_type} with name {name} is invalid. Names must be" f"Override {override_type} with name {name} is invalid. Names must be"
@ -97,14 +97,14 @@ class Overrides(DictFormatterValidator, Scriptable):
exception_class=InvalidVariableNameException, exception_class=InvalidVariableNameException,
) )
if OverrideVariables.is_entry_variable_name(name): if OverrideHelpers.is_entry_variable_name(name):
raise self._validation_exception( raise self._validation_exception(
f"Override variable with name {name} cannot be used since it is a" f"Override variable with name {name} cannot be used since it is a"
" built-in ytdl-sub entry variable name.", " built-in ytdl-sub entry variable name.",
exception_class=InvalidVariableNameException, exception_class=InvalidVariableNameException,
) )
if OverrideVariables.is_function_name(name): if OverrideHelpers.is_function_name(name):
raise self._validation_exception( raise self._validation_exception(
f"Override function definition with name {name} cannot be used since it is" f"Override function definition with name {name} cannot be used since it is"
" a built-in ytdl-sub function name.", " a built-in ytdl-sub function name.",
@ -124,14 +124,20 @@ class Overrides(DictFormatterValidator, Scriptable):
initial_variables, initial_variables,
self.dict_with_format_strings, self.dict_with_format_strings,
unresolved_variables if unresolved_variables else {}, unresolved_variables if unresolved_variables else {},
{SUBSCRIPTION_NAME: self.subscription_name},
) )
return ScriptUtils.add_sanitized_variables(initial_variables) return ScriptUtils.add_sanitized_variables(initial_variables)
def initialize_script(self, unresolved_variables: Set[str]) -> "Overrides": def initialize_script(
self, subscription_name: str, unresolved_variables: Set[str]
) -> "Overrides":
""" """
Initialize the override script with override variables + any unresolved variables Initialize the override script with override variables + any unresolved variables
""" """
self.script.add(
ScriptUtils.add_sanitized_variables(
{OverrideVariables.subscription_name(): subscription_name}
)
)
self.script.add( self.script.add(
self.initial_variables( self.initial_variables(
unresolved_variables={ unresolved_variables={
@ -144,15 +150,6 @@ class Overrides(DictFormatterValidator, Scriptable):
self.update_script() self.update_script()
return self return self
@property
def subscription_name(self) -> str:
"""
Returns
-------
Name of the subscription
"""
return self._root_name
def apply_formatter( def apply_formatter(
self, self,
formatter: StringFormatterValidator, formatter: StringFormatterValidator,

View file

@ -198,7 +198,9 @@ class Preset(_PresetShell):
downloader_options=self.downloader_options, downloader_options=self.downloader_options,
output_options=self.output_options, output_options=self.output_options,
plugins=self.plugins, plugins=self.plugins,
).initialize_overrides(overrides=self.overrides).ensure_proper_usage() ).initialize_overrides(
subscription_name=self.name, overrides=self.overrides
).ensure_proper_usage()
@property @property
def name(self) -> str: def name(self) -> str:

View file

@ -15,12 +15,12 @@ from ytdl_sub.validators.validators import LiteralDictValidator
class YTDLOptions(LiteralDictValidator): class YTDLOptions(LiteralDictValidator):
""" """
Optional. This section allows you to add any ytdl argument to ytdl-sub's downloader. 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 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>`_ `this docstring <https://github.com/yt-dlp/yt-dlp/blob/2022.04.08/yt_dlp/YoutubeDL.py#L197>`_
for more details. for more details.
ytdl_options should be formatted like: :Usage:
.. code-block:: yaml .. code-block:: yaml
@ -58,7 +58,7 @@ class OutputOptions(StrictDictValidator):
""" """
Defines where to output files and thumbnails after all post-processing has completed. Defines where to output files and thumbnails after all post-processing has completed.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
@ -156,14 +156,18 @@ class OutputOptions(StrictDictValidator):
@property @property
def output_directory(self) -> OverridesStringFormatterValidator: def output_directory(self) -> OverridesStringFormatterValidator:
""" """
Required. The output directory to store all media files downloaded. :expected type: OverridesFormatter
:description:
The output directory to store all media files downloaded.
""" """
return self._output_directory return self._output_directory
@property @property
def file_name(self) -> StringFormatterValidator: def file_name(self) -> StringFormatterValidator:
""" """
Required. The file name for the media file. This can include directories such as :expected type: EntryFormatter
:description:
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. ``"Season {upload_year}/{title}.{ext}"``, and will be placed in the output directory.
""" """
return self._file_name return self._file_name
@ -171,7 +175,9 @@ class OutputOptions(StrictDictValidator):
@property @property
def thumbnail_name(self) -> Optional[StringFormatterValidator]: def thumbnail_name(self) -> Optional[StringFormatterValidator]:
""" """
Optional. The file name for the media's thumbnail image. This can include directories such :expected type: Optional[EntryFormatter]
:description:
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 as ``"Season {upload_year}/{title}.{thumbnail_ext}"``, and will be placed in the output
directory. Can be set to empty string or `null` to disable thumbnail writes. directory. Can be set to empty string or `null` to disable thumbnail writes.
""" """
@ -180,7 +186,9 @@ class OutputOptions(StrictDictValidator):
@property @property
def info_json_name(self) -> Optional[StringFormatterValidator]: def info_json_name(self) -> Optional[StringFormatterValidator]:
""" """
Optional. The file name for the media's info json file. This can include directories such :expected type: Optional[EntryFormatter]
:description:
The file name for the media's info json file. This can include directories such
as ``"Season {upload_year}/{title}.{info_json_ext}"``, and will be placed in the output as ``"Season {upload_year}/{title}.{info_json_ext}"``, and will be placed in the output
directory. Can be set to empty string or `null` to disable info json writes. directory. Can be set to empty string or `null` to disable info json writes.
""" """
@ -189,7 +197,9 @@ class OutputOptions(StrictDictValidator):
@property @property
def download_archive_name(self) -> Optional[OverridesStringFormatterValidator]: def download_archive_name(self) -> Optional[OverridesStringFormatterValidator]:
""" """
Optional. The file name to store a subscriptions download archive placed relative to :expected type: Optional[OverridesFormatter]
:description:
The file name to store a subscriptions download archive placed relative to
the output directory. Defaults to ``.ytdl-sub-{subscription_name}-download-archive.json`` the output directory. Defaults to ``.ytdl-sub-{subscription_name}-download-archive.json``
""" """
return self._download_archive_name return self._download_archive_name
@ -197,9 +207,11 @@ class OutputOptions(StrictDictValidator):
@property @property
def migrated_download_archive_name(self) -> Optional[OverridesStringFormatterValidator]: def migrated_download_archive_name(self) -> Optional[OverridesStringFormatterValidator]:
""" """
Optional. Intended to be used if you are migrating a subscription with either a new :expected type: Optional[OverridesFormatter]
subscription name or output directory. It will try to load the archive file using this name :description:
first, and fallback to ``download_archive_name``. It will always save to this file Intended to be used if you are migrating a subscription with either a new
subscription name or output directory. It will try to load the archive file using this
name first, and fallback to ``download_archive_name``. It will always save to this file
and remove the original ``download_archive_name``. and remove the original ``download_archive_name``.
""" """
return self._migrated_download_archive_name return self._migrated_download_archive_name
@ -207,7 +219,9 @@ class OutputOptions(StrictDictValidator):
@property @property
def maintain_download_archive(self) -> bool: def maintain_download_archive(self) -> bool:
""" """
Optional. Maintains a download archive file in the output directory for a subscription. :expected type: Optional[Boolean]
:description:
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 It is named ``.ytdl-sub-{subscription_name}-download-archive.json``, stored in the
output directory. output directory.
@ -222,7 +236,10 @@ class OutputOptions(StrictDictValidator):
@property @property
def keep_files_before(self) -> Optional[StringDatetimeValidator]: def keep_files_before(self) -> Optional[StringDatetimeValidator]:
""" """
Optional. Requires ``maintain_download_archive`` set to True. :expected type: Optional[OverridesFormatter]
:description:
Requires ``maintain_download_archive`` set to True. Uses the same syntax as the
``date_range`` plugin.
Only keeps files that are uploaded before this datetime. By default, ytdl-sub will keep Only keeps files that are uploaded before this datetime. By default, ytdl-sub will keep
files before ``now``, which implies all files. Can be used in conjunction with files before ``now``, which implies all files. Can be used in conjunction with
@ -233,7 +250,10 @@ class OutputOptions(StrictDictValidator):
@property @property
def keep_files_after(self) -> Optional[StringDatetimeValidator]: def keep_files_after(self) -> Optional[StringDatetimeValidator]:
""" """
Optional. Requires ``maintain_download_archive`` set to True. :expected type: Optional[OverridesFormatter]
:description:
Requires ``maintain_download_archive`` set to True. Uses the same syntax as the
``date_range`` plugin.
Only keeps files that are uploaded after this datetime. By default, ytdl-sub will keep Only keeps files that are uploaded after this datetime. By default, ytdl-sub will keep
files after ``19000101``, which implies all files. Can be used in conjunction with files after ``19000101``, which implies all files. Can be used in conjunction with
@ -244,7 +264,9 @@ class OutputOptions(StrictDictValidator):
@property @property
def keep_max_files(self) -> Optional[OverridesIntegerFormatterValidator]: def keep_max_files(self) -> Optional[OverridesIntegerFormatterValidator]:
""" """
Optional. Requires ``maintain_download_archive`` set to True. :expected type: Optional[OverridesFormatter]
:description:
Requires ``maintain_download_archive`` set to True.
Only keeps N most recently uploaded videos. If set to <= 0, ``keep_max_files`` will not be Only keeps N most recently uploaded videos. If set to <= 0, ``keep_max_files`` will not be
applied. Can be used in conjunction with ``keep_files_before`` and ``keep_files_after``. applied. Can be used in conjunction with ``keep_files_before`` and ``keep_files_after``.

View file

@ -13,7 +13,8 @@ from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
from ytdl_sub.config.preset_options import OutputOptions from ytdl_sub.config.preset_options import OutputOptions
from ytdl_sub.config.validators.options import OptionsValidator from ytdl_sub.config.validators.options import OptionsValidator
from ytdl_sub.downloaders.url.validators import MultiUrlValidator from ytdl_sub.downloaders.url.validators import MultiUrlValidator
from ytdl_sub.entries.script.variable_scripts import VARIABLE_SCRIPTS from ytdl_sub.entries.script.variable_definitions import VARIABLE_SCRIPTS
from ytdl_sub.entries.variables.override_variables import OverrideVariables
from ytdl_sub.script.script import Script from ytdl_sub.script.script import Script
from ytdl_sub.validators.string_formatter_validators import validate_formatters from ytdl_sub.validators.string_formatter_validators import validate_formatters
@ -66,7 +67,7 @@ def _get_added_and_modified_variables(
def _override_variables(overrides: Overrides) -> Set[str]: def _override_variables(overrides: Overrides) -> Set[str]:
return set(list(overrides.initial_variables().keys())) return set(list(overrides.initial_variables().keys())) | {OverrideVariables.subscription_name()}
def _entry_variables() -> Set[str]: def _entry_variables() -> Set[str]:
@ -88,7 +89,9 @@ class VariableValidation:
self.resolved_variables: Set[str] = set() self.resolved_variables: Set[str] = set()
self.unresolved_variables: Set[str] = set() self.unresolved_variables: Set[str] = set()
def initialize_overrides(self, overrides: Overrides) -> "VariableValidation": def initialize_overrides(
self, subscription_name: str, overrides: Overrides
) -> "VariableValidation":
""" """
Do some gymnastics to initialize the Overrides script. Do some gymnastics to initialize the Overrides script.
""" """
@ -127,7 +130,9 @@ class VariableValidation:
# Initialize overrides with unresolved variables + modified variables to throw an error. # Initialize overrides with unresolved variables + modified variables to throw an error.
# For modified variables, this is to prevent a resolve(update=True) to setting any # For modified variables, this is to prevent a resolve(update=True) to setting any
# dependencies until it has been explicitly added # dependencies until it has been explicitly added
overrides = overrides.initialize_script(unresolved_variables=self.unresolved_variables) overrides = overrides.initialize_script(
subscription_name=subscription_name, unresolved_variables=self.unresolved_variables
)
# copy the script and mock entry variables # copy the script and mock entry variables
self.script = copy.deepcopy(overrides.script).add(_add_dummy_variables(entry_variables)) self.script = copy.deepcopy(overrides.script).add(_add_dummy_variables(entry_variables))

View file

@ -11,10 +11,9 @@ from ytdl_sub.config.validators.options import OptionsDictValidator
from ytdl_sub.downloaders.source_plugin import SourcePlugin from ytdl_sub.downloaders.source_plugin import SourcePlugin
from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.entries.script.variable_definitions import VARIABLE_SCRIPTS
from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.entries.script.variable_definitions import VARIABLES
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
from ytdl_sub.entries.script.variable_scripts import DOWNLOADER_INJECTED_VARIABLES
from ytdl_sub.entries.script.variable_scripts import VARIABLE_SCRIPTS
from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.exceptions import ValidationException
from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.file_handler import FileHandler
from ytdl_sub.utils.file_handler import get_file_extension from ytdl_sub.utils.file_handler import get_file_extension
@ -114,7 +113,7 @@ class InfoJsonDownloader(SourcePlugin[InfoJsonDownloaderOptions]):
inj.variable_name, inj.variable_name,
VARIABLE_SCRIPTS[inj.variable_name], VARIABLE_SCRIPTS[inj.variable_name],
) )
for inj in DOWNLOADER_INJECTED_VARIABLES for inj in v.injected_variables()
} }
) )
entries.append(entry) entries.append(entry)

View file

@ -1,37 +0,0 @@
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
# TODO: Remove later - keep for docstring
class MultiUrlDownloadOptions(MultiUrlValidator):
"""
Downloads from multiple URLs. If an entry is returned from more than one URL, it will
resolve to the bottom-most URL settings.
Usage:
.. code-block:: yaml
presets:
my_example_preset:
download:
# required
urls:
- url: "youtube.com/channel/UCsvn_Po0SmunchJYtttWpOxMg"
variables:
season_index: "1"
season_name: "Uploads"
playlist_thumbnails:
- name: "poster.jpg"
uid: "avatar_uncropped"
- name: "fanart.jpg"
uid: "banner_uncropped"
- name: "season{season_index}-poster.jpg"
uid: "latest_entry"
- url: "https://www.youtube.com/playlist?list=UCsvn_Po0SmunchJYtttWpOxMg"
variables:
season_index: "2"
season_name: "Playlist as Season"
playlist_thumbnails:
- name: "season{season_index}-poster.jpg"
uid: "latest_entry"
"""

View file

@ -1,25 +0,0 @@
from ytdl_sub.downloaders.url.validators import UrlValidator
# TODO: Remove later - keep for docstring
class UrlDownloadOptions(UrlValidator):
"""
Downloads from a single URL supported by yt-dlp.
Usage:
.. code-block:: yaml
presets:
my_example_preset:
download:
# required
url: "youtube.com/channel/UCsvn_Po0SmunchJYtttWpOxMg"
# optional
playlist_thumbnails:
- name: "poster.jpg"
uid: "avatar_uncropped"
- name: "fanart.jpg"
uid: "banner_uncropped"
download_reverse: True
"""

View file

@ -215,8 +215,53 @@ class UrlListValidator(ListValidator[UrlStringOrDictValidator]):
class MultiUrlValidator(OptionsValidator): class MultiUrlValidator(OptionsValidator):
""" """
Downloads from multiple URLs. If an entry is returned from more than one URL, it will Sets the URL(s) to download from. Can be used in many forms, including
resolve to the bottom-most URL settings.
:Single URL:
.. code-block:: yaml
download: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
:Multi URL:
.. code-block:: yaml
download:
- "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
- "https://www.youtube.com/watch?v=3BFTio5296w"
:Thumbnails + Variables:
All variables must be defined for the top-most url. All subsequent URL variables can be either
overwritten or default to the top-most value.
If an entry is returned from more than one URL, it will use the variables in the bottom-most
URL.
.. code-block:: yaml
download:
# required
urls:
- url: "youtube.com/channel/UCsvn_Po0SmunchJYtttWpOxMg"
variables:
season_index: "1"
season_name: "Uploads"
playlist_thumbnails:
- name: "poster.jpg"
uid: "avatar_uncropped"
- name: "fanart.jpg"
uid: "banner_uncropped"
- name: "season{season_index}-poster.jpg"
uid: "latest_entry"
- url: "https://www.youtube.com/playlist?list=UCsvn_Po0SmunchJYtttWpOxMg"
variables:
season_index: "2"
season_name: "Playlist as Season"
playlist_thumbnails:
- name: "season{season_index}-poster.jpg"
uid: "latest_entry"
""" """
@classmethod @classmethod

View file

@ -12,8 +12,10 @@ from typing import final
from ytdl_sub.entries.base_entry import BaseEntry from ytdl_sub.entries.base_entry import BaseEntry
from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.entries.script.variable_definitions import VARIABLES
from ytdl_sub.entries.script.variable_definitions import Variable
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
from ytdl_sub.entries.script.variable_types import ArrayVariable
from ytdl_sub.entries.script.variable_types import StringVariable
from ytdl_sub.entries.script.variable_types import Variable
from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
from ytdl_sub.utils.script import ScriptUtils from ytdl_sub.utils.script import ScriptUtils
from ytdl_sub.utils.scriptable import Scriptable from ytdl_sub.utils.scriptable import Scriptable
@ -23,8 +25,12 @@ from ytdl_sub.validators.audo_codec_validator import VIDEO_CODEC_EXTS
v: VariableDefinitions = VARIABLES v: VariableDefinitions = VARIABLES
_YTDL_SUB_ENTRY_VARIABLES_KWARG_KEY: str = "ytdl_sub_entry_variables" _YTDL_SUB_ENTRY_VARIABLES_KWARG_KEY: str = "ytdl_sub_entry_variables"
ytdl_sub_chapters_from_comments = Variable("ytdl_sub_chapters_from_comments") ytdl_sub_chapters_from_comments = ArrayVariable(
ytdl_sub_split_by_chapters_parent_uid = Variable("ytdl_sub_split_by_chapters_parent_uid") "ytdl_sub_chapters_from_comments", definition="{ [] }"
)
ytdl_sub_split_by_chapters_parent_uid = StringVariable(
"ytdl_sub_split_by_chapters_parent_uid", definition="{ %string('') }"
)
TypeT = TypeVar("TypeT") TypeT = TypeVar("TypeT")

View file

@ -3,15 +3,14 @@ from typing import Any
from typing import Dict from typing import Dict
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Set
from ytdl_sub.entries.base_entry import BaseEntry from ytdl_sub.entries.base_entry import BaseEntry
from ytdl_sub.entries.base_entry import TBaseEntry from ytdl_sub.entries.base_entry import TBaseEntry
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.entries.script.variable_definitions import VARIABLES
from ytdl_sub.entries.script.variable_definitions import MetadataVariable
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
from ytdl_sub.entries.script.variable_scripts import ENTRY_DEFAULT_VARIABLES from ytdl_sub.entries.script.variable_types import MetadataVariable
from ytdl_sub.entries.script.variable_scripts import ENTRY_REQUIRED_VARIABLES
v: VariableDefinitions = VARIABLES v: VariableDefinitions = VARIABLES
@ -53,8 +52,8 @@ class EntryParent(BaseEntry):
def _sibling_entry_metadata(self) -> List[Dict[str, Any]]: def _sibling_entry_metadata(self) -> List[Dict[str, Any]]:
sibling_entry_metadata: List[Dict[str, Any]] = [] sibling_entry_metadata: List[Dict[str, Any]] = []
variable_filter: List[MetadataVariable] = list(ENTRY_REQUIRED_VARIABLES.keys()) + list( variable_filter: Set[MetadataVariable] = (
ENTRY_DEFAULT_VARIABLES.keys() v.required_entry_variables() | v.default_entry_variables()
) )
for entry in self.entry_children(): for entry in self.entry_children():
sibling_entry_metadata.append( sibling_entry_metadata.append(

View file

@ -101,7 +101,7 @@ class CustomFunctions:
- year (Integer) - year (Integer)
- month (Integer) - month (Integer)
- day (Integer) - day (Integer)
- year_truncated (String, YY from YY[YY]) - year_truncated (Integer, YY from YY[YY])
- month_padded (String) - month_padded (String)
- day_padded (String) - day_padded (String)
- year_truncated_reversed (Integer, 100 - year_truncated) - year_truncated_reversed (Integer, 100 - year_truncated)
@ -149,7 +149,7 @@ class CustomFunctions:
String("year"): Integer(year), String("year"): Integer(year),
String("month"): Integer(month), String("month"): Integer(month),
String("day"): Integer(day), String("day"): Integer(day),
String("year_truncated"): String(year_truncated), String("year_truncated"): Integer(year_truncated),
String("month_padded"): String(month_padded), String("month_padded"): String(month_padded),
String("day_padded"): String(day_padded), String("day_padded"): String(day_padded),
String("year_truncated_reversed"): Integer(100 - year_truncated), String("year_truncated_reversed"): Integer(100 - year_truncated),

File diff suppressed because it is too large Load diff

View file

@ -1,296 +0,0 @@
from typing import Dict
from typing import List
from typing import Optional
from typing import Set
import mergedeep
from ytdl_sub.entries.script.custom_functions import CustomFunctions
from ytdl_sub.entries.script.variable_definitions import VARIABLES
from ytdl_sub.entries.script.variable_definitions import Metadata
from ytdl_sub.entries.script.variable_definitions import MetadataVariable
from ytdl_sub.entries.script.variable_definitions import Variable
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
###############################################################################################
# Helpers
v: VariableDefinitions = VARIABLES
def _pad_int(key: Variable, pad: int) -> str:
return f"{{%pad_zero({key.variable_name}, {pad})}}"
def _sanitized_plex(key: Variable) -> str:
return f"{{%sanitize_plex_episode({key.variable_name})}}"
def _date_metadata(date_key: Variable, metadata_key: str) -> str:
return f"{{%map_get(%to_date_metadata({date_key.variable_name}), '{metadata_key}')}}"
###############################################################################################
# Metadata Getters
def _get(
cast: str,
metadata: Metadata,
key: MetadataVariable,
default: Optional[Variable | str | int | Dict | List],
) -> str:
if default is None:
# TODO: assert with good error message if key DNE
out = f"%map_get({metadata.variable_name}, '{key.metadata_key}')"
elif isinstance(default, Variable):
args = f"{metadata.variable_name}, '{key.metadata_key}', {default.variable_name}"
out = f"%map_get_non_empty({args})"
elif isinstance(default, str):
out = f"%map_get_non_empty({metadata.variable_name}, '{key.metadata_key}', '{default}')"
elif isinstance(default, dict):
out = f"%map_get_non_empty({metadata.variable_name}, '{key.metadata_key}', {{}})"
elif isinstance(default, list):
out = f"%map_get_non_empty({metadata.variable_name}, '{key.metadata_key}', [])"
else:
out = f"%map_get_non_empty({metadata.variable_name}, '{key.metadata_key}', {default})"
return f"{{ %legacy_bracket_safety(%{cast}({out})) }}"
###############################################################################################
# Entry Getters
def _entry_get_str(key: MetadataVariable, default: Optional[Variable | str] = None) -> str:
return _get("string", metadata=v.entry_metadata, key=key, default=default)
def _entry_get_int(key: MetadataVariable, default: Optional[Variable | int] = None) -> str:
return _get("int", metadata=v.entry_metadata, key=key, default=default)
def _entry_get_map(key: MetadataVariable, default: Optional[Variable | Dict] = None):
return _get("map", metadata=v.entry_metadata, key=key, default=default)
def _entry_get_array(key: MetadataVariable, default: Optional[Variable | List] = None):
return _get("array", metadata=v.entry_metadata, key=key, default=default)
###############################################################################################
# Playlist Getters
def _playlist_get_str(key: MetadataVariable, default: Optional[Variable | str] = None) -> str:
return _get("string", metadata=v.playlist_metadata, key=key, default=default)
def _playlist_get_int(key: MetadataVariable, default: Optional[Variable | int] = None) -> str:
return _get("int", metadata=v.playlist_metadata, key=key, default=default)
###############################################################################################
# Source Getters
def _source_get_str(key: MetadataVariable, default: Optional[Variable | str] = None) -> str:
return _get("string", metadata=v.source_metadata, key=key, default=default)
###############################################################################################
# Scripts
ENTRY_EMPTY_METADATA: Dict[Variable, str] = {v.entry_metadata: "{ {} }"}
ENTRY_HARDCODED_VARIABLES: Dict[Variable, str] = {
v.info_json_ext: "info.json",
v.thumbnail_ext: "jpg",
}
ENTRY_RELATIVE_VARIABLES: Dict[MetadataVariable, str] = {
v.playlist_metadata: _entry_get_map(v.playlist_metadata, {}),
v.source_metadata: _entry_get_map(v.source_metadata, {}),
v.sibling_metadata: _entry_get_array(v.sibling_metadata, []),
}
ENTRY_REQUIRED_VARIABLES: Dict[MetadataVariable, str] = {
v.uid: _entry_get_str(v.uid),
v.extractor_key: _entry_get_str(v.extractor_key),
v.epoch: _entry_get_int(v.epoch),
v.webpage_url: _entry_get_str(v.webpage_url),
v.ext: _entry_get_str(v.ext),
}
ENTRY_DEFAULT_VARIABLES: Dict[MetadataVariable, str] = {
v.title: _entry_get_str(v.title, v.uid),
v.extractor: _entry_get_str(v.extractor, v.extractor_key),
v.description: _entry_get_str(v.description, ""),
v.ie_key: _entry_get_str(v.ie_key, v.extractor_key),
v.uploader_id: _entry_get_str(v.uploader_id, v.uid),
v.uploader: _entry_get_str(v.uploader, v.uploader_id),
v.uploader_url: _entry_get_str(v.uploader_url, v.webpage_url),
v.upload_date: _entry_get_str(v.upload_date, v.epoch_date),
v.release_date: _entry_get_str(v.release_date, v.upload_date),
v.channel: _entry_get_str(v.channel, v.uploader),
v.creator: _entry_get_str(v.creator, v.channel),
v.channel_id: _entry_get_str(v.channel_id, v.uploader_id),
v.duration: _entry_get_int(v.duration, 0),
v.playlist_index: _entry_get_int(v.playlist_index, 1),
v.playlist_count: _entry_get_int(v.playlist_count, 1),
v.playlist_uid: _entry_get_str(v.playlist_uid, v.uid),
v.playlist_title: _entry_get_str(v.playlist_title, v.title),
v.playlist_uploader_id: _entry_get_str(v.playlist_uploader_id, v.uploader_id),
}
# MARK AS UNRESOLVABLE UNTIL THEY ARE ADDED IN THE DOWNLOADER
DOWNLOADER_INJECTED_VARIABLES: Dict[Variable, str] = {
v.download_index: "{%int(1)}",
v.upload_date_index: "{%int(1)}",
v.comments: "{ [] }",
v.requested_subtitles: "{ {} }",
v.chapters: "{ [] }",
v.sponsorblock_chapters: "{ [] }",
v.ytdl_sub_input_url: f"{{{v.source_webpage_url.variable_name}}}",
}
ENTRY_DERIVED_VARIABLES: Dict[Variable, str] = {
v.uid_sanitized_plex: _sanitized_plex(v.uid),
v.title_sanitized_plex: _sanitized_plex(v.title),
v.epoch_date: f"{{%datetime_strftime({v.epoch.variable_name}, '%Y%m%d')}}",
v.epoch_hour: f"{{%datetime_strftime({v.epoch.variable_name}, '%H')}}",
v.download_index_padded6: _pad_int(v.download_index, 6),
v.upload_date_index_padded: _pad_int(v.upload_date_index, 2),
v.upload_date_index_reversed: f"{{%sub(100, {v.upload_date_index.variable_name})}}",
v.upload_date_index_reversed_padded: _pad_int(v.upload_date_index_reversed, 2),
v.playlist_index_reversed: (
f"{{%sub({v.playlist_count.variable_name}, {v.playlist_index.variable_name}, -1)}}"
),
v.playlist_index_padded: _pad_int(v.playlist_index, 2),
v.playlist_index_reversed_padded: _pad_int(v.playlist_index_reversed, 2),
v.playlist_index_padded6: _pad_int(v.playlist_index, 6),
v.playlist_index_reversed_padded6: _pad_int(v.playlist_index_reversed, 6),
}
ENTRY_UPLOAD_DATE_VARIABLES: Dict[Variable, str] = {
v.upload_year: _date_metadata(v.upload_date, "year"),
v.upload_year_truncated: _date_metadata(v.upload_date, "year_truncated"),
v.upload_year_truncated_reversed: _date_metadata(v.upload_date, "year_truncated_reversed"),
v.upload_month_reversed: _date_metadata(v.upload_date, "month_reversed"),
v.upload_month_reversed_padded: _date_metadata(v.upload_date, "month_reversed_padded"),
v.upload_month_padded: _date_metadata(v.upload_date, "month_padded"),
v.upload_day_padded: _date_metadata(v.upload_date, "day_padded"),
v.upload_month: _date_metadata(v.upload_date, "month"),
v.upload_day: _date_metadata(v.upload_date, "day"),
v.upload_day_reversed: _date_metadata(v.upload_date, "day_reversed"),
v.upload_day_reversed_padded: _date_metadata(v.upload_date, "day_reversed_padded"),
v.upload_day_of_year: _date_metadata(v.upload_date, "day_of_year"),
v.upload_day_of_year_padded: _date_metadata(v.upload_date, "day_of_year_padded"),
v.upload_day_of_year_reversed: _date_metadata(v.upload_date, "day_of_year_reversed"),
v.upload_day_of_year_reversed_padded: _date_metadata(
v.upload_date, "day_of_year_reversed_padded"
),
v.upload_date_standardized: _date_metadata(v.upload_date, "date_standardized"),
}
ENTRY_RELEASE_DATE_VARIABLES: Dict[Variable, str] = {
v.release_year: _date_metadata(v.release_date, "year"),
v.release_year_truncated: _date_metadata(v.release_date, "year_truncated"),
v.release_year_truncated_reversed: _date_metadata(v.release_date, "year_truncated_reversed"),
v.release_month_reversed: _date_metadata(v.release_date, "month_reversed"),
v.release_month_reversed_padded: _date_metadata(v.release_date, "month_reversed_padded"),
v.release_month_padded: _date_metadata(v.release_date, "month_padded"),
v.release_day_padded: _date_metadata(v.release_date, "day_padded"),
v.release_month: _date_metadata(v.release_date, "month"),
v.release_day: _date_metadata(v.release_date, "day"),
v.release_day_reversed: _date_metadata(v.release_date, "day_reversed"),
v.release_day_reversed_padded: _date_metadata(v.release_date, "day_reversed_padded"),
v.release_day_of_year: _date_metadata(v.release_date, "day_of_year"),
v.release_day_of_year_padded: _date_metadata(v.release_date, "day_of_year_padded"),
v.release_day_of_year_reversed: _date_metadata(v.release_date, "day_of_year_reversed"),
v.release_day_of_year_reversed_padded: _date_metadata(
v.release_date, "day_of_year_reversed_padded"
),
v.release_date_standardized: _date_metadata(v.release_date, "date_standardized"),
}
PLAYLIST_VARIABLES: Dict[Variable, str] = {
v.playlist_webpage_url: _playlist_get_str(v.playlist_webpage_url, v.webpage_url),
v.playlist_description: _playlist_get_str(v.playlist_description, v.description),
v.playlist_uploader: _playlist_get_str(v.playlist_uploader, v.uploader),
v.playlist_uploader_url: _playlist_get_str(v.playlist_uploader_url, v.playlist_webpage_url),
v.source_index: _playlist_get_int(v.source_index, 1),
v.source_count: _playlist_get_int(v.source_count, 1),
}
SOURCE_VARIABLES: Dict[Variable, str] = {
v.source_uid: _source_get_str(v.source_uid, v.playlist_uid),
v.source_title: _source_get_str(v.source_title, v.playlist_title),
v.source_webpage_url: _source_get_str(v.source_webpage_url, v.playlist_webpage_url),
v.source_description: _source_get_str(v.source_description, v.playlist_description),
v.source_uploader_id: _source_get_str(v.source_uploader_id, v.playlist_uploader_id),
v.source_uploader: _source_get_str(v.source_uploader, v.playlist_uploader),
v.source_uploader_url: _source_get_str(v.source_uploader_url, v.source_webpage_url),
}
SOURCE_DERIVED_VARIABLES: Dict[Variable, str] = {
v.source_index_padded: _pad_int(v.source_index, 2),
}
SIBLING_VARIABLES: Dict[Variable, str] = {
v.playlist_max_upload_date: f"""{{
%array_reduce(
%if_passthrough(
%extract_field_from_siblings('{v.upload_date.variable_name}'),
[{v.upload_date.variable_name}]
),
%max
)
}}"""
}
SIBLING_DERIVED_VARIABLES: Dict[Variable, str] = {
v.playlist_max_upload_year: _date_metadata(v.playlist_max_upload_date, "year"),
v.playlist_max_upload_year_truncated: _date_metadata(
v.playlist_max_upload_date, "year_truncated"
),
}
_VARIABLE_SCRIPTS: Dict[Variable, str] = {}
mergedeep.merge(
_VARIABLE_SCRIPTS,
ENTRY_EMPTY_METADATA,
ENTRY_HARDCODED_VARIABLES,
ENTRY_RELATIVE_VARIABLES,
ENTRY_REQUIRED_VARIABLES,
ENTRY_DEFAULT_VARIABLES,
DOWNLOADER_INJECTED_VARIABLES,
ENTRY_DERIVED_VARIABLES,
ENTRY_UPLOAD_DATE_VARIABLES,
ENTRY_RELEASE_DATE_VARIABLES,
SIBLING_VARIABLES,
SIBLING_DERIVED_VARIABLES,
PLAYLIST_VARIABLES,
SOURCE_VARIABLES,
SOURCE_DERIVED_VARIABLES,
)
VARIABLE_SCRIPTS: Dict[str, str] = {
var.variable_name: script for var, script in _VARIABLE_SCRIPTS.items()
}
def _keys(*variables: Dict[Variable, str]) -> Set[str]:
keys: Set[str] = set()
for variable_set in variables:
keys.update(set(var.variable_name for var in variable_set.keys()))
return keys
UNRESOLVED_VARIABLES: Set[str] = _keys(
ENTRY_EMPTY_METADATA,
DOWNLOADER_INJECTED_VARIABLES,
)
CustomFunctions.register()

View file

@ -0,0 +1,321 @@
from abc import ABC
from abc import abstractmethod
from dataclasses import dataclass
from typing import Dict
from typing import List
from typing import Optional
from typing import Type
from typing import TypeVar
from ytdl_sub.script.types.array import Array
from ytdl_sub.script.types.map import Map
from ytdl_sub.script.types.resolvable import Integer
from ytdl_sub.script.types.resolvable import String
ENTRY_METADATA_VARIABLE_NAME = "entry_metadata"
PLAYLIST_METADATA_VARIABLE_NAME = "playlist_metadata"
SOURCE_METADATA_VARIABLE_NAME = "source_metadata"
TMetadataVariable = TypeVar("TMetadataVariable", bound="MetadataVariable")
TVariable = TypeVar("TVariable", bound="Variable")
def _get(
cast: str,
metadata_variable_name: str,
metadata_key: str,
variable_name: Optional[str],
default: Optional[TVariable | str | int | Dict | List],
as_type: Type[TMetadataVariable],
) -> TMetadataVariable:
if default is None:
# TODO: assert with good error message if key DNE
out = f"%map_get({metadata_variable_name}, '{metadata_key}')"
elif isinstance(default, Variable):
args = f"{metadata_variable_name}, '{metadata_key}', {default.variable_name}"
out = f"%map_get_non_empty({args})"
elif isinstance(default, str):
out = f"%map_get_non_empty({metadata_variable_name}, '{metadata_key}', '{default}')"
elif isinstance(default, dict):
out = f"%map_get_non_empty({metadata_variable_name}, '{metadata_key}', {{}})"
elif isinstance(default, list):
out = f"%map_get_non_empty({metadata_variable_name}, '{metadata_key}', [])"
else:
out = f"%map_get_non_empty({metadata_variable_name}, '{metadata_key}', {default})"
return as_type(
variable_name=variable_name or metadata_key,
metadata_key=metadata_key,
definition=f"{{ %legacy_bracket_safety(%{cast}({out})) }}",
)
@dataclass(frozen=True)
class Variable(ABC):
variable_name: str
definition: str
@classmethod
@abstractmethod
def human_readable_type(cls) -> str:
"""
Script type of the variable, for documentation
"""
@dataclass(frozen=True)
class StringVariable(Variable):
@classmethod
def human_readable_type(cls) -> str:
return String.__name__
def to_sanitized_plex(self, variable_name: str) -> "StringVariable":
"""
Converts a String variable to be plex sanitized
"""
return StringVariable(
variable_name=variable_name,
definition=f"{{%sanitize_plex_episode({self.variable_name})}}",
)
def as_date_variable(self) -> "StringDateVariable":
"""
Converts a String variable to a date variable (which has metadata helpers)
"""
return StringDateVariable(
variable_name=self.variable_name,
definition=self.definition,
)
@dataclass(frozen=True)
class StringDateVariable(StringVariable):
def get_string_date_metadata(
self, date_metadata_key: str, variable_name: Optional[str] = None
) -> StringVariable:
"""
Gets a string-based date metadata variable
"""
return StringVariable(
variable_name=variable_name or date_metadata_key,
definition=f"""{{
%string(
%map_get(
%to_date_metadata({self.variable_name}),
'{date_metadata_key}'
)
)
}}""",
)
def get_integer_date_metadata(
self, date_metadata_key: str, variable_name: str
) -> "IntegerVariable":
"""
Gets an int-based date metadata variable
"""
return IntegerVariable(
variable_name=variable_name,
definition=f"""{{
%int(
%map_get(
%to_date_metadata({self.variable_name}),
'{date_metadata_key}'
)
)
}}""",
)
@dataclass(frozen=True)
class IntegerVariable(Variable):
@classmethod
def human_readable_type(cls) -> str:
return Integer.__name__
def to_padded_int(self, variable_name: str, pad: int) -> StringVariable:
"""
Pads an integer
"""
return StringVariable(
variable_name=variable_name, definition=f"{{%pad_zero({self.variable_name}, {pad})}}"
)
@dataclass(frozen=True)
class ArrayVariable(Variable):
@classmethod
def human_readable_type(cls) -> str:
return Array.__name__
@dataclass(frozen=True)
class MapVariable(Variable):
@classmethod
def human_readable_type(cls) -> str:
return Map.__name__
@dataclass(frozen=True)
class MetadataVariable(Variable, ABC):
metadata_key: str
@dataclass(frozen=True)
class MapMetadataVariable(MetadataVariable, MapVariable):
@classmethod
def from_entry(
cls,
metadata_key: str,
variable_name: Optional[str] = None,
default: Optional["MapMetadataVariable" | Dict] = None,
) -> "MapMetadataVariable":
"""
Creates a map variable from entry metadata
"""
return _get(
"map",
metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME,
metadata_key=metadata_key,
variable_name=variable_name,
default=default,
as_type=MapMetadataVariable,
)
@dataclass(frozen=True)
class ArrayMetadataVariable(MetadataVariable, ArrayVariable):
@classmethod
def from_entry(
cls,
metadata_key: str,
variable_name: Optional[str] = None,
default: Optional["ArrayMetadataVariable" | List] = None,
) -> "ArrayMetadataVariable":
"""
Creates an array variable from entry metadata
"""
return _get(
"array",
metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME,
metadata_key=metadata_key,
variable_name=variable_name,
default=default,
as_type=ArrayMetadataVariable,
)
@dataclass(frozen=True)
class StringMetadataVariable(MetadataVariable, StringVariable):
@classmethod
def from_entry(
cls,
metadata_key: str,
variable_name: Optional[str] = None,
default: Optional[StringVariable | str] = None,
) -> "StringMetadataVariable":
"""
Creates a string variable from entry metadata
"""
return _get(
"string",
metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME,
metadata_key=metadata_key,
variable_name=variable_name,
default=default,
as_type=StringMetadataVariable,
)
@classmethod
def from_playlist(
cls,
metadata_key: str,
variable_name: Optional[str] = None,
default: Optional[StringVariable | str] = None,
) -> "StringMetadataVariable":
"""
Creates a string variable from playlist metadata
"""
return _get(
"string",
metadata_variable_name=PLAYLIST_METADATA_VARIABLE_NAME,
metadata_key=metadata_key,
variable_name=variable_name,
default=default,
as_type=StringMetadataVariable,
)
@classmethod
def from_source(
cls,
metadata_key: str,
variable_name: Optional[str] = None,
default: Optional[StringVariable | str] = None,
) -> "StringMetadataVariable":
"""
Creates a string variable from source metadata
"""
return _get(
"string",
metadata_variable_name=SOURCE_METADATA_VARIABLE_NAME,
metadata_key=metadata_key,
variable_name=variable_name,
default=default,
as_type=StringMetadataVariable,
)
def as_date_variable(self) -> "StringDateMetadataVariable":
"""
Converts a String variable to a date variable (which has metadata helpers)
"""
return StringDateMetadataVariable(
metadata_key=self.metadata_key,
variable_name=self.variable_name,
definition=self.definition,
)
@dataclass(frozen=True)
class StringDateMetadataVariable(StringMetadataVariable, StringDateVariable):
pass
@dataclass(frozen=True)
class IntegerMetadataVariable(MetadataVariable, IntegerVariable):
@classmethod
def from_entry(
cls,
metadata_key: str,
variable_name: Optional[str] = None,
default: Optional[IntegerVariable | int] = None,
) -> "IntegerMetadataVariable":
"""
Creates an int variable from entry metadata
"""
return _get(
"int",
metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME,
metadata_key=metadata_key,
variable_name=variable_name,
default=default,
as_type=IntegerMetadataVariable,
)
@classmethod
def from_playlist(
cls,
metadata_key: str,
variable_name: Optional[str] = None,
default: Optional[IntegerVariable | int] = None,
) -> "IntegerMetadataVariable":
"""
Creates an int variable from playlist metadata
"""
return _get(
"int",
metadata_variable_name=PLAYLIST_METADATA_VARIABLE_NAME,
metadata_key=metadata_key,
variable_name=variable_name,
default=default,
as_type=IntegerMetadataVariable,
)

View file

@ -1,24 +1,22 @@
from ytdl_sub.entries.script.function_scripts import CUSTOM_FUNCTION_SCRIPTS from ytdl_sub.entries.script.function_scripts import CUSTOM_FUNCTION_SCRIPTS
from ytdl_sub.entries.script.variable_scripts import VARIABLE_SCRIPTS from ytdl_sub.entries.script.variable_definitions import VARIABLE_SCRIPTS
from ytdl_sub.script.functions import Functions from ytdl_sub.script.functions import Functions
from ytdl_sub.script.utils.name_validation import is_valid_name from ytdl_sub.script.utils.name_validation import is_valid_name
SUBSCRIPTION_NAME = "subscription_name" # TODO: use this
SUBSCRIPTION_VALUE = "subscription_value"
SUBSCRIPTION_MAP = "subscription_map"
SUBSCRIPTION_ARRAY = "subscription_array" SUBSCRIPTION_ARRAY = "subscription_array"
class OverrideVariables: class OverrideVariables:
@classmethod @staticmethod
def subscription_name(cls) -> str: def subscription_name() -> str:
""" """
Name of the subscription Name of the subscription
""" """
return SUBSCRIPTION_NAME return "subscription_name"
@classmethod @staticmethod
def subscription_value(cls) -> str: def subscription_value() -> str:
""" """
For subscriptions in the form of For subscriptions in the form of
@ -28,10 +26,10 @@ class OverrideVariables:
``subscription_value`` gets set to ``https://...``. ``subscription_value`` gets set to ``https://...``.
""" """
return SUBSCRIPTION_VALUE return "subscription_value"
@classmethod @staticmethod
def subscription_indent_i(cls, index: int) -> str: def subscription_indent_i(index: int) -> str:
""" """
For subscriptions in the form of For subscriptions in the form of
@ -46,8 +44,8 @@ class OverrideVariables:
""" """
return f"subscription_indent_{index + 1}" return f"subscription_indent_{index + 1}"
@classmethod @staticmethod
def subscription_value_i(cls, index: int) -> str: def subscription_value_i(index: int) -> str:
""" """
For subscriptions in the form of For subscriptions in the form of
@ -63,6 +61,37 @@ class OverrideVariables:
""" """
return f"subscription_value_{index + 1}" return f"subscription_value_{index + 1}"
@staticmethod
def subscription_map() -> str:
"""
For subscriptions in the form of
.. code-block:: yaml
+ Subscription Name:
Music Videos:
- "https://url1.com/..."
Concerts:
- "https://url2.com/..."
Stores all the contents under the subscription name into the override variable
``subscription_map`` as a Map value. The above example is stored as:
.. code-block:: python
{
"Music Videos": [
"https://url1.com/..."
],
"Concerts: [
"https://url2.com/..."
]
}
"""
return "subscription_map"
class OverrideHelpers:
@classmethod @classmethod
def is_entry_variable_name(cls, name: str) -> bool: def is_entry_variable_name(cls, name: str) -> bool:
""" """

View file

@ -25,12 +25,10 @@ class AudioExtractOptions(OptionsDictValidator):
""" """
Extracts audio from a video file. Extracts audio from a video file.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
audio_extract: audio_extract:
codec: "mp3" codec: "mp3"
quality: 128 quality: 128
@ -56,6 +54,8 @@ class AudioExtractOptions(OptionsDictValidator):
@property @property
def codec(self) -> str: def codec(self) -> str:
""" """
:expected type: String
:description:
The codec to output after extracting the audio. Supported codecs are aac, flac, mp3, m4a, The codec to output after extracting the audio. Supported codecs are aac, flac, mp3, m4a,
opus, vorbis, wav, and best to grab the best possible format at runtime. opus, vorbis, wav, and best to grab the best possible format at runtime.
""" """
@ -64,6 +64,8 @@ class AudioExtractOptions(OptionsDictValidator):
@property @property
def quality(self) -> Optional[float]: def quality(self) -> Optional[float]:
""" """
:expected type: Float
:description:
Optional. Specify ffmpeg audio quality. Insert a value between ``0`` (better) and ``9`` Optional. Specify ffmpeg audio quality. Insert a value between ``0`` (better) and ``9``
(worse) for variable bitrate, or a specific bitrate like ``128`` for 128k. (worse) for variable bitrate, or a specific bitrate like ``128`` for 128k.
""" """

View file

@ -64,12 +64,10 @@ class ChaptersOptions(OptionsDictValidator):
Embeds chapters to video files if they are present. Additional options to add SponsorBlock Embeds chapters to video files if they are present. Additional options to add SponsorBlock
chapters and remove specific ones. Can also remove chapters using regex. chapters and remove specific ones. Can also remove chapters using regex.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
chapters: chapters:
# Embedded Chapter Fields # Embedded Chapter Fields
embed_chapters: True embed_chapters: True
@ -89,7 +87,6 @@ class ChaptersOptions(OptionsDictValidator):
- "intro" - "intro"
remove_sponsorblock_categories: "all" remove_sponsorblock_categories: "all"
force_key_frames: False force_key_frames: False
""" """
_optional_keys = { _optional_keys = {
@ -135,22 +132,28 @@ class ChaptersOptions(OptionsDictValidator):
@property @property
def embed_chapters(self) -> Optional[bool]: def embed_chapters(self) -> Optional[bool]:
""" """
Optional. Embed chapters into the file. Defaults to True. :expected type: Optional[Boolean]
:description:
Defaults to True. Embed chapters into the file.
""" """
return self._embed_chapters return self._embed_chapters
@property @property
def allow_chapters_from_comments(self) -> bool: def allow_chapters_from_comments(self) -> bool:
""" """
Optional. If chapters do not exist in the video/description itself, attempt to scrape :expected type: Optional[Boolean]
comments to find the chapters. Defaults to False. :description:
Defaults to False. If chapters do not exist in the video/description itself, attempt to
scrape comments to find the chapters.
""" """
return self._allow_chapters_from_comments return self._allow_chapters_from_comments
@property @property
def remove_chapters_regex(self) -> Optional[List[re.Pattern]]: def remove_chapters_regex(self) -> Optional[List[re.Pattern]]:
""" """
Optional. List of regex patterns to match chapter titles against and remove them from the :expected type: Optional[List[RegexString]
:description:
List of regex patterns to match chapter titles against and remove them from the
entry. entry.
""" """
if self._remove_chapters_regex: if self._remove_chapters_regex:
@ -160,7 +163,9 @@ class ChaptersOptions(OptionsDictValidator):
@property @property
def sponsorblock_categories(self) -> Optional[List[str]]: def sponsorblock_categories(self) -> Optional[List[str]]:
""" """
Optional. List of SponsorBlock categories to embed as chapters. Supports "sponsor", :expected type: Optional[List[String]]
:description:
List of SponsorBlock categories to embed as chapters. Supports "sponsor",
"intro", "outro", "selfpromo", "preview", "filler", "interaction", "music_offtopic", "intro", "outro", "selfpromo", "preview", "filler", "interaction", "music_offtopic",
"poi_highlight", or "all" to include all categories. "poi_highlight", or "all" to include all categories.
""" """
@ -174,7 +179,9 @@ class ChaptersOptions(OptionsDictValidator):
@property @property
def remove_sponsorblock_categories(self) -> Optional[List[str]]: def remove_sponsorblock_categories(self) -> Optional[List[str]]:
""" """
Optional. List of SponsorBlock categories to remove from the output file. Can only remove :expected type: Optional[List[String]]
:description:
List of SponsorBlock categories to remove from the output file. Can only remove
categories that are specified in ``sponsorblock_categories`` or "all", which removes categories that are specified in ``sponsorblock_categories`` or "all", which removes
everything specified in ``sponsorblock_categories``. everything specified in ``sponsorblock_categories``.
""" """
@ -190,9 +197,10 @@ class ChaptersOptions(OptionsDictValidator):
@property @property
def force_key_frames(self) -> bool: def force_key_frames(self) -> bool:
""" """
Optional. Force keyframes at cuts when removing sections. This is slow due to needing a :expected type: Optional[Boolean]
re-encode, but the resulting video may have fewer artifacts around the cuts. Defaults to :description:
False. Defaults to False. Force keyframes at cuts when removing sections. This is slow due to
needing a re-encode, but the resulting video may have fewer artifacts around the cuts.
""" """
return self._force_key_frames return self._force_key_frames

View file

@ -11,13 +11,21 @@ from ytdl_sub.validators.string_datetime import StringDatetimeValidator
class DateRangeOptions(OptionsDictValidator): class DateRangeOptions(OptionsDictValidator):
""" """
Only download files uploaded within the specified date range. Only download files uploaded within the specified date range.
Dates must adhere to a yt-dlp datetime. From their docs:
Usage: .. 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``. Can use override variables in this.
Note that yt-dlp will round times to the closest day, meaning that `day` is the lowest
granularity possible.
:Usage:
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
date_range: date_range:
before: "now" before: "now"
after: "today-2weeks" after: "today-2weeks"
@ -33,14 +41,18 @@ class DateRangeOptions(OptionsDictValidator):
@property @property
def before(self) -> Optional[StringDatetimeValidator]: def before(self) -> Optional[StringDatetimeValidator]:
""" """
Optional. Only download videos before this datetime. :expected type: Optional[OverridesFormatter]
:description:
Only download videos before this datetime.
""" """
return self._before return self._before
@property @property
def after(self) -> Optional[StringDatetimeValidator]: def after(self) -> Optional[StringDatetimeValidator]:
""" """
Optional. Only download videos after this datetime. :expected type: Optional[OverridesFormatter]
:description:
Only download videos before this datetime.
""" """
return self._after return self._after

View file

@ -20,12 +20,10 @@ class EmbedThumbnailOptions(BoolValidator, OptionsValidator):
""" """
Whether to embed thumbnails to the audio/video file or not. Whether to embed thumbnails to the audio/video file or not.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
embed_thumbnail: True embed_thumbnail: True
""" """

View file

@ -32,21 +32,19 @@ class FileConvertOptions(OptionsDictValidator):
""" """
Converts video files from one extension to another. Converts video files from one extension to another.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
file_convert: file_convert:
convert_to: "mp4" convert_to: "mp4"
Supports custom ffmpeg conversions: Also supports custom ffmpeg conversions:
:Usage:
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
file_convert: file_convert:
convert_to: "mkv" convert_to: "mkv"
convert_with: "ffmpeg" convert_with: "ffmpeg"
@ -89,32 +87,35 @@ class FileConvertOptions(OptionsDictValidator):
@property @property
def convert_to(self) -> str: def convert_to(self) -> str:
""" """
Convert to a desired file type. Supports: :expected type: String
:description:
Convert to a desired file type. Supports
* Video: avi, flv, mkv, mov, mp4, webm - Video: avi, flv, mkv, mov, mp4, webm
* Audio: aac, flac, mp3, m4a, opus, vorbis, wav - Audio: aac, flac, mp3, m4a, opus, vorbis, wav
""" """
return self._convert_to return self._convert_to
@property @property
def convert_with(self) -> Optional[str]: def convert_with(self) -> Optional[str]:
""" """
Optional. Supports ``yt-dlp`` and ``ffmpeg``. ``yt-dlp`` will convert files within :expected type: Optional[String]
:description:
Supports ``yt-dlp`` and ``ffmpeg``. ``yt-dlp`` will convert files within
yt-dlp whereas ``ffmpeg`` specifies it will be converted using a custom command specified yt-dlp whereas ``ffmpeg`` specifies it will be converted using a custom command specified
with ``ffmpeg_post_process_args``. Defaults to ``yt-dlp``. with ``ffmpeg_post_process_args``. Defaults to ``yt-dlp``.
""" """
return self._convert_with return self._convert_with
@property @property
def ffmpeg_post_process_args(self) -> Optional[OverridesStringFormatterValidator]: def ffmpeg_post_process_args(self) -> Optional[OverridesStringFormatterValidator]:
""" """
Optional. ffmpeg args to post-process an entry file with. The args will be inserted in the :expected type: Optional[OverridesFormatter]
form of: :description:
ffmpeg args to post-process an entry file with. The args will be inserted in the
form of
.. code-block:: bash ``ffmpeg -i input_file.ext {ffmpeg_post_process_args) output_file.output_ext``.
ffmpeg -i input_file.ext {ffmpeg_post_process_args) output_file.output_ext
The output file will use the extension specified in ``convert_to``. Post-processing args The output file will use the extension specified in ``convert_to``. Post-processing args
can still be set with ``convert_with`` set to ``yt-dlp``. can still be set with ``convert_with`` set to ``yt-dlp``.

View file

@ -19,15 +19,15 @@ class FilterExcludeOptions(ListFormatterValidator, OptionsValidator):
Applies a conditional OR on any number of filters comprised of either variables or scripts. Applies a conditional OR on any number of filters comprised of either variables or scripts.
If any filter evaluates to True, the entry will be excluded. If any filter evaluates to True, the entry will be excluded.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
filter_exclude: filter_exclude:
- { %contains( %lower(title), '#short' ) } - >-
- { %contains( %lower(description), '#short' ) } { %contains( %lower(title), '#short' ) }
- >-
{ %contains( %lower(description), '#short' ) }
""" """

View file

@ -19,14 +19,13 @@ class FilterIncludeOptions(ListFormatterValidator, OptionsValidator):
Applies a conditional AND on any number of filters comprised of either variables or scripts. Applies a conditional AND on any number of filters comprised of either variables or scripts.
If all filters evaluate to True, the entry will be included. If all filters evaluate to True, the entry will be included.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
filter_include: filter_include:
- {description} - >-
{description}
- >- - >-
{ {
%regex_search_any( %regex_search_any(

View file

@ -15,8 +15,6 @@ class FormatOptions(OptionsValidator):
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
format: "(bv*[height<=1080]+bestaudio/best[height<=1080])" format: "(bv*[height<=1080]+bestaudio/best[height<=1080])"
""" """

View file

@ -58,31 +58,19 @@ def combine_filters(filters: List[str], to_combine: List[str]) -> List[str]:
class MatchFiltersOptions(OptionsDictValidator): class MatchFiltersOptions(OptionsDictValidator):
""" """
Set ``--match-filters``` to pass into yt-dlp to filter entries from being downloaded. Set ``--match-filters`` to pass into yt-dlp to filter entries from being downloaded.
Uses the same syntax as yt-dlp. Uses the same syntax as yt-dlp. An entry will be downloaded if any one of the filters are met.
For logical AND's between match filters, use the ``&`` operator in a single match filter.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
match_filters:
filters: "original_url!*=/shorts/"
Supports one or multiple filters:
.. code-block:: yaml
presets:
my_example_preset:
match_filters: match_filters:
filters: filters:
- "age_limit<?18" - "age_limit<?18 & like_count>?100"
- "like_count>?100"
# Other common match-filters # Other common match-filters
# - "original_url!*=/shorts/ & !is_live" # - "original_url!*=/shorts/ & !is_live"
# - "age_limit<?18"
# - "availability=?public" # - "availability=?public"
""" """

View file

@ -75,14 +75,13 @@ class MusicTagsOptions(OptionsDictValidator):
a full list of tags for various file types in MediaFile's 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>`_. `source code <https://github.com/beetbox/mediafile/blob/v0.9.0/mediafile.py#L1770>`_.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
presets: presets:
my_example_preset: my_example_preset:
music_tags: music_tags:
tags:
artist: "{artist}" artist: "{artist}"
album: "{album}" album: "{album}"
# Supports id3v2.4 multi-tags # Supports id3v2.4 multi-tags

View file

@ -60,6 +60,8 @@ class SharedNfoTagsOptions(OptionsDictValidator):
@property @property
def nfo_name(self) -> StringFormatterFileNameValidator: def nfo_name(self) -> StringFormatterFileNameValidator:
""" """
:expected type: EntryFormatter
:description:
The NFO file name. The NFO file name.
""" """
return self._nfo_name return self._nfo_name
@ -81,9 +83,11 @@ class SharedNfoTagsOptions(OptionsDictValidator):
@property @property
def kodi_safe(self) -> Optional[bool]: def kodi_safe(self) -> Optional[bool]:
""" """
Optional. Kodi does not support > 3-byte unicode characters, which include emojis and some :expected type: Optional[Boolean]
foreign language characters. Setting this to True will replace those characters with ''. :description:
Defaults to False. Defaults to False. Kodi does not support > 3-byte unicode characters, which include
emojis and some foreign language characters. Setting this to True will replace those
characters with ''.
""" """
return self._kodi_safe return self._kodi_safe
@ -190,21 +194,17 @@ class NfoTagsOptions(SharedNfoTagsOptions):
Adds an NFO file for every download file. An NFO file is simply an XML file 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. with a ``.nfo`` extension. You can add any values into the NFO.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
nfo_tags: nfo_tags:
# required
nfo_name: "{title_sanitized}.nfo" nfo_name: "{title_sanitized}.nfo"
nfo_root: "episodedetails" nfo_root: "episodedetails"
tags: tags:
title: "{title}" title: "{title}"
season: "{upload_year}" season: "{upload_year}"
episode: "{upload_month}{upload_day_padded}" episode: "{upload_month}{upload_day_padded}"
# optional
kodi_safe: False kodi_safe: False
""" """
@ -215,6 +215,8 @@ class NfoTagsOptions(SharedNfoTagsOptions):
@property @property
def nfo_root(self) -> StringFormatterValidator: def nfo_root(self) -> StringFormatterValidator:
""" """
:expected type: EntryFormatter
:description:
The root tag of the NFO's XML. In the usage above, it would look like The root tag of the NFO's XML. In the usage above, it would look like
.. code-block:: xml .. code-block:: xml
@ -228,6 +230,8 @@ class NfoTagsOptions(SharedNfoTagsOptions):
@property @property
def tags(self) -> NfoTagsValidator: def tags(self) -> NfoTagsValidator:
""" """
:expected type: NfoTags
:description:
Tags within the nfo_root tag. In the usage above, it would look like Tags within the nfo_root tag. In the usage above, it would look like
.. code-block:: xml .. code-block:: xml

View file

@ -37,6 +37,8 @@ class OutputDirectoryNfoTagsOptions(SharedNfoTagsOptions):
@property @property
def nfo_root(self) -> StringFormatterValidator: def nfo_root(self) -> StringFormatterValidator:
""" """
:expected type: EntryFormatter
:description:
The root tag of the NFO's XML. In the usage above, it would look like The root tag of the NFO's XML. In the usage above, it would look like
.. code-block:: xml .. code-block:: xml
@ -50,6 +52,8 @@ class OutputDirectoryNfoTagsOptions(SharedNfoTagsOptions):
@property @property
def tags(self) -> NfoTagsValidator: def tags(self) -> NfoTagsValidator:
""" """
:expected type: NfoTags
:description:
Tags within the nfo_root tag. In the usage above, it would look like Tags within the nfo_root tag. In the usage above, it would look like
.. code-block:: xml .. code-block:: xml

View file

@ -122,6 +122,41 @@ class FromSourceVariablesRegex(DictValidator):
class RegexOptions(OptionsDictValidator): class RegexOptions(OptionsDictValidator):
r""" r"""
.. attention::
This plugin will eventually be deprecated and replaced by scripting functions.
You can replicate the example below using the following.
.. code-block:: yaml
# Only includes videos with 'Official Video'
filter_include:
- >-
{ %contains( %lower(title), "official video" ) }
# Excludes videos with '#short' in its description
filter_exclude:
- >-
{ %contains( %lower(description), '#short' ) }
# Creates a capture array with defaults, and assigns
# each capture group to its own variable
overrides:
description_date_capture: >-
{
%regex_capture_many_with_defaults(
description,
[ "([0-9]{4})-([0-9]{2})-([0-9]{2})" ],
[ upload_year, upload_month, upload_day ]
)
}
captured_upload_year: >-
{ %array_at(description_date_capture, 1) }
captured_upload_month: >-
{ %array_at(description_date_capture, 2) }
captured_upload_day: >-
{ %array_at(description_date_capture, 3) }
Performs regex matching on an entry's source or override variables. Regex can be used to filter Performs regex matching on an entry's source or override variables. Regex can be used to filter
entries from proceeding with download or capture groups to create new source variables. entries from proceeding with download or capture groups to create new source variables.
@ -137,12 +172,10 @@ class RegexOptions(OptionsDictValidator):
and using ``title_and_description`` can regex match/exclude from either ``title`` or and using ``title_and_description`` can regex match/exclude from either ``title`` or
``description``. ``description``.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
regex: regex:
# By default, if any match fails and has no defaults, the entry will # By default, if any match fails and has no defaults, the entry will
# be skipped. If False, ytdl-sub will error and stop all downloads # be skipped. If False, ytdl-sub will error and stop all downloads
@ -209,6 +242,8 @@ class RegexOptions(OptionsDictValidator):
@property @property
def skip_if_match_fails(self) -> Optional[bool]: def skip_if_match_fails(self) -> Optional[bool]:
""" """
:expected type: Optional[Boolean]
:description:
Defaults to True. If True, when any match fails and has no defaults, the entry will be Defaults to True. If True, when any match fails and has no defaults, the entry will be
skipped. If False, ytdl-sub will error and all downloads will not proceed. skipped. If False, ytdl-sub will error and all downloads will not proceed.
""" """

View file

@ -48,21 +48,20 @@ class WhenNoChaptersValidator(StringSelectValidator):
class SplitByChaptersOptions(OptionsDictValidator): class SplitByChaptersOptions(OptionsDictValidator):
""" """
Splits a file by chapters into multiple files. Each file becomes its own entry with the Splits a file by chapters into multiple files. Each file becomes its own entry with the
new source variables ``chapter_title``, ``chapter_title_sanitized``, ``chapter_index``, new variables
``chapter_index_padded``, ``chapter_count``.
If a file has no chapters, and ``when_no_chapters`` is set to "pass", then ``chapter_title`` is - ``chapter_title``
set to the entry's title and ``chapter_index``, ``chapter_count`` are both set to 1. - ``chapter_index``
- ``chapter_index_padded``
- ``chapter_count``
Note that when using this plugin and performing dry-run, it assumes embedded chapters are being Note that when using this plugin and performing dry-run, it assumes embedded chapters are being
used with no modifications. used with no modifications.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
split_by_chapters: split_by_chapters:
when_no_chapters: "pass" when_no_chapters: "pass"
""" """
@ -100,8 +99,16 @@ class SplitByChaptersOptions(OptionsDictValidator):
@property @property
def when_no_chapters(self) -> str: def when_no_chapters(self) -> str:
""" """
Behavior to perform when no chapters are present. Supports "pass" (continue processing), :expected type: String
"drop" (exclude it from output), and "error" (stop processing for everything). :description:
Behavior to perform when no chapters are present. Supports
- "pass" (continue processing),
- "drop" (exclude it from output)
- "error" (stop processing for everything).
If a file has no chapters and is set to "pass", then ``chapter_title`` is
set to the entry's title and ``chapter_index``, ``chapter_count`` are both set to 1.
""" """
return self._when_no_chapters return self._when_no_chapters

View file

@ -37,17 +37,17 @@ class SubtitleOptions(OptionsDictValidator):
``lang`` and ``subtitles_ext``. ``lang`` is dynamic since you can download multiple subtitles. ``lang`` and ``subtitles_ext``. ``lang`` is dynamic since you can download multiple subtitles.
It will set the respective language to the correct subtitle file. It will set the respective language to the correct subtitle file.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
subtitles: subtitles:
subtitles_name: "{title_sanitized}.{lang}.{subtitles_ext}" subtitles_name: "{title_sanitized}.{lang}.{subtitles_ext}"
subtitles_type: "srt" subtitles_type: "srt"
embed_subtitles: False embed_subtitles: False
languages: "en" # supports list of multiple languages languages:
- "en" # supports multiple languages
- "de"
allow_auto_generated_subtitles: False allow_auto_generated_subtitles: False
""" """
@ -82,40 +82,50 @@ class SubtitleOptions(OptionsDictValidator):
@property @property
def subtitles_name(self) -> Optional[StringFormatterValidator]: def subtitles_name(self) -> Optional[StringFormatterValidator]:
""" """
Optional. The file name for the media's subtitles if they are present. This can include :expected type: Optional[EntryFormatter]
directories such as ``"Season {upload_year}/{title_sanitized}.{lang}.{subtitles_ext}"``, and :description:
will be placed in the output directory. ``lang`` is dynamic since you can download multiple The file name for the media's subtitles if they are present. This can include
subtitles. It will set the respective language to the correct subtitle file. directories such as ``"Season {upload_year}/{title_sanitized}.{lang}.{subtitles_ext}"``,
and will be placed in the output directory. ``lang`` is dynamic since you can download
multiple subtitles. It will set the respective language to the correct subtitle file.
""" """
return self._subtitles_name return self._subtitles_name
@property @property
def subtitles_type(self) -> Optional[str]: def subtitles_type(self) -> Optional[str]:
""" """
Optional. One of the subtitle file types "srt", "vtt", "ass", "lrc". Defaults to "srt" :expected type: Optional[String]
:description:
Defaults to "srt". One of the subtitle file types "srt", "vtt", "ass", "lrc".
""" """
return self._subtitles_type return self._subtitles_type
@property @property
def embed_subtitles(self) -> Optional[bool]: def embed_subtitles(self) -> Optional[bool]:
""" """
Optional. Whether to embed the subtitles into the video file. Defaults to False. :expected type: Optional[Boolean]
NOTE: webm files can only embed "vtt" subtitle types. :description:
Defaults to False. Whether to embed the subtitles into the video file. Note that
webm files can only embed "vtt" subtitle types.
""" """
return self._embed_subtitles return self._embed_subtitles
@property @property
def languages(self) -> Optional[List[str]]: def languages(self) -> Optional[List[str]]:
""" """
Optional. Language code(s) to download for subtitles. Supports a single or list of multiple :expected type: Optional[List[String]]
language codes. Defaults to "en". :description:
Language code(s) to download for subtitles. Supports a single or list of multiple
language codes. Defaults to only "en".
""" """
return [lang.value for lang in self._languages] return [lang.value for lang in self._languages]
@property @property
def allow_auto_generated_subtitles(self) -> Optional[bool]: def allow_auto_generated_subtitles(self) -> Optional[bool]:
""" """
Optional. Whether to allow auto generated subtitles. Defaults to False. :expected type: Optional[Boolean]
:description:
Defaults to False. Whether to allow auto generated subtitles.
""" """
return self._allow_auto_generated_subtitles return self._allow_auto_generated_subtitles

View file

@ -65,7 +65,7 @@ class ThrottleProtectionOptions(OptionsDictValidator):
range-based values, a random number will be chosen within the range to avoid sleeps looking range-based values, a random number will be chosen within the range to avoid sleeps looking
scripted. scripted.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
@ -110,6 +110,8 @@ class ThrottleProtectionOptions(OptionsDictValidator):
@property @property
def sleep_per_download_s(self) -> Optional[RandomizedRangeValidator]: def sleep_per_download_s(self) -> Optional[RandomizedRangeValidator]:
""" """
:expected type: Optional[Range]
:description:
Number in seconds to sleep between each download. Does not include time it takes for Number in seconds to sleep between each download. Does not include time it takes for
ytdl-sub to perform post-processing. ytdl-sub to perform post-processing.
""" """
@ -118,6 +120,8 @@ class ThrottleProtectionOptions(OptionsDictValidator):
@property @property
def sleep_per_subscription_s(self) -> Optional[RandomizedRangeValidator]: def sleep_per_subscription_s(self) -> Optional[RandomizedRangeValidator]:
""" """
:expected type: Optional[Range]
:description:
Number in seconds to sleep between each subscription. Number in seconds to sleep between each subscription.
""" """
return self._sleep_per_subscription_s return self._sleep_per_subscription_s
@ -125,6 +129,8 @@ class ThrottleProtectionOptions(OptionsDictValidator):
@property @property
def max_downloads_per_subscription(self) -> Optional[RandomizedRangeValidator]: def max_downloads_per_subscription(self) -> Optional[RandomizedRangeValidator]:
""" """
:expected type: Optional[Range]
:description:
Number of downloads to perform per subscription. Number of downloads to perform per subscription.
""" """
return self._max_downloads_per_subscription return self._max_downloads_per_subscription
@ -132,6 +138,8 @@ class ThrottleProtectionOptions(OptionsDictValidator):
@property @property
def subscription_download_probability(self) -> Optional[ProbabilityValidator]: def subscription_download_probability(self) -> Optional[ProbabilityValidator]:
""" """
:expected type: Optional[Float]
:description:
Probability to perform any downloads, recomputed for each subscription. This is only Probability to perform any downloads, recomputed for each subscription. This is only
recommended to set if you run ytdl-sub in a cron-job, that way you are statistically recommended to set if you run ytdl-sub in a cron-job, that way you are statistically
guaranteed over time to eventually download the subscription. guaranteed over time to eventually download the subscription.

View file

@ -17,12 +17,10 @@ class VideoTagsOptions(OptionsDictValidator):
""" """
Adds tags to every downloaded video file using ffmpeg ``-metadata key=value`` args. Adds tags to every downloaded video file using ffmpeg ``-metadata key=value`` args.
Usage: :Usage:
.. code-block:: yaml .. code-block:: yaml
presets:
my_example_preset:
video_tags: video_tags:
title: "{title}" title: "{title}"
date: "{upload_date}" date: "{upload_date}"

View file

@ -19,12 +19,8 @@ class ArrayFunctions:
@staticmethod @staticmethod
def array(maybe_array: AnyArgument) -> Array: def array(maybe_array: AnyArgument) -> Array:
""" """
:description:
Tries to cast an unknown variable type to an Array. Tries to cast an unknown variable type to an Array.
Raises
------
FunctionRuntimeException
If the input type is not actually an Array.
""" """
if not isinstance(maybe_array, Array): if not isinstance(maybe_array, Array):
raise FunctionRuntimeException( raise FunctionRuntimeException(
@ -35,6 +31,7 @@ class ArrayFunctions:
@staticmethod @staticmethod
def array_size(array: Array) -> Integer: def array_size(array: Array) -> Integer:
""" """
:description:
Returns the size of an Array. Returns the size of an Array.
""" """
return Integer(len(array.value)) return Integer(len(array.value))
@ -42,6 +39,7 @@ class ArrayFunctions:
@staticmethod @staticmethod
def array_extend(*arrays: Array) -> Array: def array_extend(*arrays: Array) -> Array:
""" """
:description:
Combine multiple Arrays into a single Array. Combine multiple Arrays into a single Array.
""" """
output: List[Resolvable] = [] output: List[Resolvable] = []
@ -55,6 +53,7 @@ class ArrayFunctions:
array: Array, overlap: Array, only_missing: Optional[Boolean] = None array: Array, overlap: Array, only_missing: Optional[Boolean] = None
) -> Array: ) -> Array:
""" """
:description:
Overlaps ``overlap`` onto ``array``. Can optionally only overlay missing indices. Overlaps ``overlap`` onto ``array``. Can optionally only overlay missing indices.
""" """
output: List[Resolvable] = [] output: List[Resolvable] = []
@ -76,6 +75,7 @@ class ArrayFunctions:
@staticmethod @staticmethod
def array_at(array: Array, idx: Integer) -> AnyArgument: def array_at(array: Array, idx: Integer) -> AnyArgument:
""" """
:description:
Return the element in the Array at index ``idx``. Return the element in the Array at index ``idx``.
""" """
return array.value[idx.value] return array.value[idx.value]
@ -83,6 +83,7 @@ class ArrayFunctions:
@staticmethod @staticmethod
def array_first(array: Array, fallback: AnyArgument) -> AnyArgument: def array_first(array: Array, fallback: AnyArgument) -> AnyArgument:
""" """
:description:
Returns the first element whose boolean conversion is True. Returns fallback Returns the first element whose boolean conversion is True. Returns fallback
if all elements evaluate to False. if all elements evaluate to False.
""" """
@ -95,6 +96,7 @@ class ArrayFunctions:
@staticmethod @staticmethod
def array_contains(array: Array, value: AnyArgument) -> Boolean: def array_contains(array: Array, value: AnyArgument) -> Boolean:
""" """
:description:
Return True if the value exists in the Array. False otherwise. Return True if the value exists in the Array. False otherwise.
""" """
return Boolean(value in array.value) return Boolean(value in array.value)
@ -102,6 +104,7 @@ class ArrayFunctions:
@staticmethod @staticmethod
def array_index(array: Array, value: AnyArgument) -> Integer: def array_index(array: Array, value: AnyArgument) -> Integer:
""" """
:description:
Return the index of the value within the Array if it exists. If it does not, it will Return the index of the value within the Array if it exists. If it does not, it will
throw an error. throw an error.
""" """
@ -118,6 +121,7 @@ class ArrayFunctions:
@staticmethod @staticmethod
def array_slice(array: Array, start: Integer, end: Optional[Integer] = None) -> Array: def array_slice(array: Array, start: Integer, end: Optional[Integer] = None) -> Array:
""" """
:description:
Returns the slice of the Array. Returns the slice of the Array.
""" """
if end is not None: if end is not None:
@ -127,6 +131,7 @@ class ArrayFunctions:
@staticmethod @staticmethod
def array_flatten(array: Array) -> Array: def array_flatten(array: Array) -> Array:
""" """
:description:
Flatten any nested Arrays into a single-dimensional Array. Flatten any nested Arrays into a single-dimensional Array.
""" """
output: List[Resolvable] = [] output: List[Resolvable] = []
@ -141,6 +146,7 @@ class ArrayFunctions:
@staticmethod @staticmethod
def array_reverse(array: Array) -> Array: def array_reverse(array: Array) -> Array:
""" """
:description:
Reverse an Array. Reverse an Array.
""" """
return Array(list(reversed(array.value))) return Array(list(reversed(array.value)))
@ -148,6 +154,7 @@ class ArrayFunctions:
@staticmethod @staticmethod
def array_product(*arrays: Array) -> Array: def array_product(*arrays: Array) -> Array:
""" """
:description:
Returns the Cartesian product of elements from different arrays Returns the Cartesian product of elements from different arrays
""" """
out: List[Resolvable] = [] out: List[Resolvable] = []
@ -161,7 +168,17 @@ class ArrayFunctions:
@staticmethod @staticmethod
def array_apply(array: Array, lambda_function: Lambda) -> Array: def array_apply(array: Array, lambda_function: Lambda) -> Array:
""" """
:description:
Apply a lambda function on every element in the Array. Apply a lambda function on every element in the Array.
:usage:
.. code-block:: python
{
%array_apply( [1, 2, 3] , %string )
}
# ["1", "2", "3"]
""" """
return Array([Array([val]) for val in array.value]) return Array([Array([val]) for val in array.value])
@ -173,6 +190,7 @@ class ArrayFunctions:
reverse_args: Optional[Boolean] = None, reverse_args: Optional[Boolean] = None,
) -> Array: ) -> Array:
""" """
:description:
Apply a lambda function on every element in the Array, with ``fixed_argument`` Apply a lambda function on every element in the Array, with ``fixed_argument``
passed as a second argument to every invocation. passed as a second argument to every invocation.
""" """
@ -184,6 +202,7 @@ class ArrayFunctions:
@staticmethod @staticmethod
def array_enumerate(array: Array, lambda_function: LambdaTwo) -> Array: def array_enumerate(array: Array, lambda_function: LambdaTwo) -> Array:
""" """
:description:
Apply a lambda function on every element in the Array, where each arg Apply a lambda function on every element in the Array, where each arg
passed to the lambda function is ``idx, element`` as two separate args. passed to the lambda function is ``idx, element`` as two separate args.
""" """
@ -192,6 +211,7 @@ class ArrayFunctions:
@staticmethod @staticmethod
def array_reduce(array: Array, lambda_reduce_function: LambdaReduce) -> AnyArgument: def array_reduce(array: Array, lambda_reduce_function: LambdaReduce) -> AnyArgument:
""" """
:description:
Apply a reduce function on pairs of elements in the Array, until one element remains. Apply a reduce function on pairs of elements in the Array, until one element remains.
Executes using the left-most and reduces in the right direction. Executes using the left-most and reduces in the right direction.
""" """

View file

@ -13,6 +13,7 @@ class BooleanFunctions:
@staticmethod @staticmethod
def bool(value: AnyArgument) -> Boolean: def bool(value: AnyArgument) -> Boolean:
""" """
:description:
Cast any type to a Boolean. Cast any type to a Boolean.
""" """
return Boolean(bool(value.value)) return Boolean(bool(value.value))
@ -20,6 +21,7 @@ class BooleanFunctions:
@staticmethod @staticmethod
def eq(left: AnyArgument, right: AnyArgument) -> Boolean: def eq(left: AnyArgument, right: AnyArgument) -> Boolean:
""" """
:description:
``==`` operator. Returns True if left == right. False otherwise. ``==`` operator. Returns True if left == right. False otherwise.
""" """
return Boolean(left.value == right.value) return Boolean(left.value == right.value)
@ -27,6 +29,7 @@ class BooleanFunctions:
@staticmethod @staticmethod
def ne(left: AnyArgument, right: AnyArgument) -> Boolean: def ne(left: AnyArgument, right: AnyArgument) -> Boolean:
""" """
:description:
``!=`` operator. Returns True if left != right. False otherwise. ``!=`` operator. Returns True if left != right. False otherwise.
""" """
return Boolean(left.value != right.value) return Boolean(left.value != right.value)
@ -34,6 +37,7 @@ class BooleanFunctions:
@staticmethod @staticmethod
def lt(left: AnyArgument, right: AnyArgument) -> Boolean: def lt(left: AnyArgument, right: AnyArgument) -> Boolean:
""" """
:description:
``<`` operator. Returns True if left < right. False otherwise. ``<`` operator. Returns True if left < right. False otherwise.
""" """
return Boolean(left.value < right.value) return Boolean(left.value < right.value)
@ -41,6 +45,7 @@ class BooleanFunctions:
@staticmethod @staticmethod
def lte(left: AnyArgument, right: AnyArgument) -> Boolean: def lte(left: AnyArgument, right: AnyArgument) -> Boolean:
""" """
:description:
``<=`` operator. Returns True if left <= right. False otherwise. ``<=`` operator. Returns True if left <= right. False otherwise.
""" """
return Boolean(left.value <= right.value) return Boolean(left.value <= right.value)
@ -48,6 +53,7 @@ class BooleanFunctions:
@staticmethod @staticmethod
def gt(left: AnyArgument, right: AnyArgument) -> Boolean: def gt(left: AnyArgument, right: AnyArgument) -> Boolean:
""" """
:description:
``>`` operator. Returns True if left > right. False otherwise. ``>`` operator. Returns True if left > right. False otherwise.
""" """
return Boolean(left.value > right.value) return Boolean(left.value > right.value)
@ -55,6 +61,7 @@ class BooleanFunctions:
@staticmethod @staticmethod
def gte(left: AnyArgument, right: AnyArgument) -> Boolean: def gte(left: AnyArgument, right: AnyArgument) -> Boolean:
""" """
:description:
``>=`` operator. Returns True if left >= right. False otherwise. ``>=`` operator. Returns True if left >= right. False otherwise.
""" """
return Boolean(left.value >= right.value) return Boolean(left.value >= right.value)
@ -62,6 +69,7 @@ class BooleanFunctions:
@staticmethod @staticmethod
def and_(*values: AnyArgument) -> Boolean: def and_(*values: AnyArgument) -> Boolean:
""" """
:description:
``and`` operator. Returns True if all values evaluate to True. False otherwise. ``and`` operator. Returns True if all values evaluate to True. False otherwise.
""" """
return Boolean(all(bool(val.value) for val in values)) return Boolean(all(bool(val.value) for val in values))
@ -69,6 +77,7 @@ class BooleanFunctions:
@staticmethod @staticmethod
def or_(*values: AnyArgument) -> Boolean: def or_(*values: AnyArgument) -> Boolean:
""" """
:description:
``or`` operator. Returns True if any value evaluates to True. False otherwise. ``or`` operator. Returns True if any value evaluates to True. False otherwise.
""" """
return Boolean(any(bool(val.value) for val in values)) return Boolean(any(bool(val.value) for val in values))
@ -76,6 +85,7 @@ class BooleanFunctions:
@staticmethod @staticmethod
def xor(*values: AnyArgument) -> Boolean: def xor(*values: AnyArgument) -> Boolean:
""" """
:description:
``^`` operator. Returns True if exactly one value is set to True. False otherwise. ``^`` operator. Returns True if exactly one value is set to True. False otherwise.
""" """
bit_array = [bool(val.value) for val in values] bit_array = [bool(val.value) for val in values]
@ -85,6 +95,7 @@ class BooleanFunctions:
@staticmethod @staticmethod
def not_(value: Boolean) -> Boolean: def not_(value: Boolean) -> Boolean:
""" """
:description:
``not`` operator. Returns the opposite of value. ``not`` operator. Returns the opposite of value.
""" """
return Boolean(not value.value) return Boolean(not value.value)
@ -92,6 +103,7 @@ class BooleanFunctions:
@staticmethod @staticmethod
def is_null(value: AnyArgument) -> Boolean: def is_null(value: AnyArgument) -> Boolean:
""" """
:description:
Returns True if a value is null (i.e. an empty string). False otherwise. Returns True if a value is null (i.e. an empty string). False otherwise.
""" """
return Boolean(isinstance(value, String) and value.value == "") return Boolean(isinstance(value, String) and value.value == "")

View file

@ -11,6 +11,7 @@ class ConditionalFunctions:
condition: Boolean, true: ReturnableArgumentA, false: ReturnableArgumentB condition: Boolean, true: ReturnableArgumentA, false: ReturnableArgumentB
) -> Union[ReturnableArgumentA, ReturnableArgumentB]: ) -> Union[ReturnableArgumentA, ReturnableArgumentB]:
""" """
:description:
Conditional ``if`` statement that returns the ``true`` or ``false`` parameter Conditional ``if`` statement that returns the ``true`` or ``false`` parameter
depending on the ``condition`` value. depending on the ``condition`` value.
""" """
@ -23,6 +24,7 @@ class ConditionalFunctions:
maybe_true_arg: ReturnableArgumentA, else_arg: ReturnableArgumentB maybe_true_arg: ReturnableArgumentA, else_arg: ReturnableArgumentB
) -> Union[ReturnableArgumentA, ReturnableArgumentB]: ) -> Union[ReturnableArgumentA, ReturnableArgumentB]:
""" """
:description:
Conditional ``if`` statement that returns the ``maybe_true_arg`` if it evaluates to True, Conditional ``if`` statement that returns the ``maybe_true_arg`` if it evaluates to True,
otherwise returns ``else_arg``. otherwise returns ``else_arg``.
""" """

View file

@ -8,6 +8,7 @@ class DateFunctions:
@staticmethod @staticmethod
def datetime_strftime(posix_timestamp: Integer, date_format: String) -> String: def datetime_strftime(posix_timestamp: Integer, date_format: String) -> String:
""" """
:description:
Converts a posix timestamp to a date using strftime formatting. Converts a posix timestamp to a date using strftime formatting.
""" """
return String(datetime.utcfromtimestamp(posix_timestamp.value).strftime(date_format.value)) return String(datetime.utcfromtimestamp(posix_timestamp.value).strftime(date_format.value))

View file

@ -8,6 +8,7 @@ class ErrorFunctions:
@staticmethod @staticmethod
def throw(error_message: String) -> AnyArgument: def throw(error_message: String) -> AnyArgument:
""" """
:description:
Explicitly throw an error with the provided error message. Explicitly throw an error with the provided error message.
""" """
raise UserThrownRuntimeError(error_message) raise UserThrownRuntimeError(error_message)
@ -15,8 +16,9 @@ class ErrorFunctions:
@staticmethod @staticmethod
def assert_(value: ReturnableArgument, assert_message: String) -> ReturnableArgument: def assert_(value: ReturnableArgument, assert_message: String) -> ReturnableArgument:
""" """
Explicitly throw an error with the provided assert message if ``value`` evaluates to False. :description:
If it evaluates to True, it will return ``value``. Explicitly throw an error with the provided assert message if ``value`` evaluates to
False. If it evaluates to True, it will return ``value``.
""" """
if not bool(value.value): if not bool(value.value):
raise UserThrownRuntimeError(assert_message) raise UserThrownRuntimeError(assert_message)
@ -27,8 +29,9 @@ class ErrorFunctions:
value: AnyArgument, ret: ReturnableArgument, assert_message: String value: AnyArgument, ret: ReturnableArgument, assert_message: String
) -> ReturnableArgument: ) -> ReturnableArgument:
""" """
Explicitly throw an error with the provided assert message if ``value`` evaluates to False. :description:
If it evaluates to True, it will return ``ret``. Explicitly throw an error with the provided assert message if ``value`` evaluates to
False. If it evaluates to True, it will return ``ret``.
""" """
if not bool(value.value): if not bool(value.value):
raise UserThrownRuntimeError(assert_message) raise UserThrownRuntimeError(assert_message)
@ -39,6 +42,7 @@ class ErrorFunctions:
value: ReturnableArgument, equals: AnyArgument, assert_message: String value: ReturnableArgument, equals: AnyArgument, assert_message: String
) -> ReturnableArgument: ) -> ReturnableArgument:
""" """
:description:
Explicitly throw an error with the provided assert message if ``value`` does not equal Explicitly throw an error with the provided assert message if ``value`` does not equal
``equals``. If they do equal, then return ``value``. ``equals``. If they do equal, then return ``value``.
""" """
@ -51,6 +55,7 @@ class ErrorFunctions:
value: ReturnableArgument, equals: AnyArgument, assert_message: String value: ReturnableArgument, equals: AnyArgument, assert_message: String
) -> ReturnableArgument: ) -> ReturnableArgument:
""" """
:description:
Explicitly throw an error with the provided assert message if ``value`` equals Explicitly throw an error with the provided assert message if ``value`` equals
``equals``. If they do equal, then return ``value``. ``equals``. If they do equal, then return ``value``.
""" """

View file

@ -35,6 +35,7 @@ class JsonFunctions:
@staticmethod @staticmethod
def from_json(argument: String) -> AnyArgument: def from_json(argument: String) -> AnyArgument:
""" """
:description:
Converts a JSON string into an actual type. Converts a JSON string into an actual type.
""" """
return _from_json(json.loads(argument.value)) return _from_json(json.loads(argument.value))

View file

@ -18,12 +18,8 @@ class MapFunctions:
@staticmethod @staticmethod
def map(maybe_mapping: AnyArgument) -> Map: def map(maybe_mapping: AnyArgument) -> Map:
""" """
:description:
Tries to cast an unknown variable type to a Map. Tries to cast an unknown variable type to a Map.
Raises
------
FunctionRuntimeException
If the input type is not actually a Map.
""" """
if not isinstance(maybe_mapping, Map): if not isinstance(maybe_mapping, Map):
raise FunctionRuntimeException( raise FunctionRuntimeException(
@ -34,6 +30,7 @@ class MapFunctions:
@staticmethod @staticmethod
def map_size(mapping: Map) -> Integer: def map_size(mapping: Map) -> Integer:
""" """
:description:
Returns the size of a Map. Returns the size of a Map.
""" """
return Integer(len(mapping.value)) return Integer(len(mapping.value))
@ -41,6 +38,7 @@ class MapFunctions:
@staticmethod @staticmethod
def map_contains(mapping: Map, key: AnyArgument) -> Boolean: def map_contains(mapping: Map, key: AnyArgument) -> Boolean:
""" """
:description:
Returns True if the key is in the Map. False otherwise. Returns True if the key is in the Map. False otherwise.
""" """
if not isinstance(key, Hashable): if not isinstance(key, Hashable):
@ -55,6 +53,7 @@ class MapFunctions:
mapping: Map, key: AnyArgument, default: Optional[AnyArgument] = None mapping: Map, key: AnyArgument, default: Optional[AnyArgument] = None
) -> AnyArgument: ) -> AnyArgument:
""" """
:description:
Return ``key``'s value within the Map. If ``key`` does not exist, and ``default`` is Return ``key``'s value within the Map. If ``key`` does not exist, and ``default`` is
provided, it will return ``default``. Otherwise, will error. provided, it will return ``default``. Otherwise, will error.
""" """
@ -70,6 +69,7 @@ class MapFunctions:
@staticmethod @staticmethod
def map_get_non_empty(mapping: Map, key: AnyArgument, default: AnyArgument) -> AnyArgument: def map_get_non_empty(mapping: Map, key: AnyArgument, default: AnyArgument) -> AnyArgument:
""" """
:description:
Return ``key``'s value within the Map. If ``key`` does not exist or is an empty string, Return ``key``'s value within the Map. If ``key`` does not exist or is an empty string,
return ``default``. Otherwise, will error. return ``default``. Otherwise, will error.
""" """
@ -83,6 +83,7 @@ class MapFunctions:
@staticmethod @staticmethod
def map_apply(mapping: Map, lambda_function: LambdaTwo) -> Array: def map_apply(mapping: Map, lambda_function: LambdaTwo) -> Array:
""" """
:description:
Apply a lambda function on the Map, where each arg Apply a lambda function on the Map, where each arg
passed to the lambda function is ``key, value`` as two separate args. passed to the lambda function is ``key, value`` as two separate args.
""" """
@ -91,6 +92,7 @@ class MapFunctions:
@staticmethod @staticmethod
def map_enumerate(mapping: Map, lambda_function: LambdaThree) -> Array: def map_enumerate(mapping: Map, lambda_function: LambdaThree) -> Array:
""" """
:description:
Apply a lambda function on the Map, where each arg Apply a lambda function on the Map, where each arg
passed to the lambda function is ``idx, key, value`` as three separate args. passed to the lambda function is ``idx, key, value`` as three separate args.
""" """

View file

@ -16,6 +16,7 @@ class NumericFunctions:
@staticmethod @staticmethod
def float(value: AnyArgument) -> Float: def float(value: AnyArgument) -> Float:
""" """
:description:
Cast to Float. Cast to Float.
""" """
return Float(value=float(value.value)) return Float(value=float(value.value))
@ -23,6 +24,7 @@ class NumericFunctions:
@staticmethod @staticmethod
def int(value: AnyArgument) -> Integer: def int(value: AnyArgument) -> Integer:
""" """
:description:
Cast to Integer. Cast to Integer.
""" """
return Integer(value=int(value.value)) return Integer(value=int(value.value))
@ -30,6 +32,7 @@ class NumericFunctions:
@staticmethod @staticmethod
def add(*values: Numeric) -> Numeric: def add(*values: Numeric) -> Numeric:
""" """
:description:
``+`` operator. Returns the sum of all values. ``+`` operator. Returns the sum of all values.
""" """
return _to_numeric(sum(val.value for val in values)) return _to_numeric(sum(val.value for val in values))
@ -37,6 +40,7 @@ class NumericFunctions:
@staticmethod @staticmethod
def sub(*values: Numeric) -> Numeric: def sub(*values: Numeric) -> Numeric:
""" """
:description:
``-`` operator. Subtracts all values from left to right. ``-`` operator. Subtracts all values from left to right.
""" """
output = values[0].value output = values[0].value
@ -48,6 +52,7 @@ class NumericFunctions:
@staticmethod @staticmethod
def mul(*values: Numeric) -> Numeric: def mul(*values: Numeric) -> Numeric:
""" """
:description:
``*`` operator. Returns the product of all values. ``*`` operator. Returns the product of all values.
""" """
return _to_numeric(math.prod([val.value for val in values])) return _to_numeric(math.prod([val.value for val in values]))
@ -55,6 +60,7 @@ class NumericFunctions:
@staticmethod @staticmethod
def pow(base: Numeric, exponent: Numeric) -> Numeric: def pow(base: Numeric, exponent: Numeric) -> Numeric:
""" """
:description:
``**`` operator. Returns the exponential of the base and exponent value. ``**`` operator. Returns the exponential of the base and exponent value.
""" """
return _to_numeric(math.pow(base.value, exponent.value)) return _to_numeric(math.pow(base.value, exponent.value))
@ -62,6 +68,7 @@ class NumericFunctions:
@staticmethod @staticmethod
def div(left: Numeric, right: Numeric) -> Numeric: def div(left: Numeric, right: Numeric) -> Numeric:
""" """
:description:
``/`` operator. Returns ``left / right``. ``/`` operator. Returns ``left / right``.
""" """
return _to_numeric(left.value / right.value) return _to_numeric(left.value / right.value)
@ -69,6 +76,7 @@ class NumericFunctions:
@staticmethod @staticmethod
def mod(left: Numeric, right: Numeric) -> Numeric: def mod(left: Numeric, right: Numeric) -> Numeric:
""" """
:description:
``%`` operator. Returns ``left % right``. ``%`` operator. Returns ``left % right``.
""" """
return _to_numeric(value=left.value % right.value) return _to_numeric(value=left.value % right.value)
@ -76,6 +84,7 @@ class NumericFunctions:
@staticmethod @staticmethod
def max(*values: Numeric) -> Numeric: def max(*values: Numeric) -> Numeric:
""" """
:description:
Returns max of all values. Returns max of all values.
""" """
return _to_numeric(max(val.value for val in values)) return _to_numeric(max(val.value for val in values))
@ -83,6 +92,7 @@ class NumericFunctions:
@staticmethod @staticmethod
def min(*values: Numeric) -> Numeric: def min(*values: Numeric) -> Numeric:
""" """
:description:
Returns min of all values. Returns min of all values.
""" """
return _to_numeric(min(val.value for val in values)) return _to_numeric(min(val.value for val in values))

View file

@ -18,6 +18,7 @@ class RegexFunctions:
@staticmethod @staticmethod
def regex_match(regex: String, string: String) -> Array: def regex_match(regex: String, string: String) -> Array:
""" """
:description:
Checks for a match only at the beginning of the string. If a match exists, returns Checks for a match only at the beginning of the string. If a match exists, returns
the string as the first element of the Array. If there are capture groups, returns each the string as the first element of the Array. If there are capture groups, returns each
group as a subsequent element in the Array. group as a subsequent element in the Array.
@ -27,6 +28,7 @@ class RegexFunctions:
@staticmethod @staticmethod
def regex_search(regex: String, string: String) -> Array: def regex_search(regex: String, string: String) -> Array:
""" """
:description:
Checks for a match anywhere in the string. If a match exists, returns Checks for a match anywhere in the string. If a match exists, returns
the string as the first element of the Array. If there are capture groups, returns each the string as the first element of the Array. If there are capture groups, returns each
group as a subsequent element in the Array. group as a subsequent element in the Array.
@ -36,6 +38,7 @@ class RegexFunctions:
@staticmethod @staticmethod
def regex_fullmatch(regex: String, string: String) -> Array: def regex_fullmatch(regex: String, string: String) -> Array:
""" """
:description:
Checks for entire string to be a match. If a match exists, returns Checks for entire string to be a match. If a match exists, returns
the string as the first element of the Array. If there are capture groups, returns each the string as the first element of the Array. If there are capture groups, returns each
group as a subsequent element in the Array. group as a subsequent element in the Array.
@ -45,6 +48,7 @@ class RegexFunctions:
@staticmethod @staticmethod
def regex_capture_groups(regex: String) -> Integer: def regex_capture_groups(regex: String) -> Integer:
""" """
:description:
Returns number of capture groups in regex Returns number of capture groups in regex
""" """
return Integer(re.compile(regex.value).groups) return Integer(re.compile(regex.value).groups)

View file

@ -11,6 +11,7 @@ class StringFunctions:
@staticmethod @staticmethod
def string(value: AnyArgument) -> String: def string(value: AnyArgument) -> String:
""" """
:description:
Cast to String. Cast to String.
""" """
return String(value=str(value.value)) return String(value=str(value.value))
@ -18,6 +19,7 @@ class StringFunctions:
@staticmethod @staticmethod
def contains(string: String, contains: String) -> Boolean: def contains(string: String, contains: String) -> Boolean:
""" """
:description:
Returns True if ``contains`` is in ``string``. False otherwise. Returns True if ``contains`` is in ``string``. False otherwise.
""" """
return Boolean(contains.value in string.value) return Boolean(contains.value in string.value)
@ -25,6 +27,7 @@ class StringFunctions:
@staticmethod @staticmethod
def slice(string: String, start: Integer, end: Optional[Integer] = None) -> String: def slice(string: String, start: Integer, end: Optional[Integer] = None) -> String:
""" """
:description:
Returns the slice of the Array. Returns the slice of the Array.
""" """
if end is not None: if end is not None:
@ -34,6 +37,7 @@ class StringFunctions:
@staticmethod @staticmethod
def lower(string: String) -> String: def lower(string: String) -> String:
""" """
:description:
Lower-case the entire String. Lower-case the entire String.
""" """
return String(string.value.lower()) return String(string.value.lower())
@ -41,6 +45,7 @@ class StringFunctions:
@staticmethod @staticmethod
def upper(string: String) -> String: def upper(string: String) -> String:
""" """
:description:
Upper-case the entire String. Upper-case the entire String.
""" """
return String(string.value.upper()) return String(string.value.upper())
@ -48,6 +53,7 @@ class StringFunctions:
@staticmethod @staticmethod
def capitalize(string: String) -> String: def capitalize(string: String) -> String:
""" """
:description:
Capitalize the first character in the string. Capitalize the first character in the string.
""" """
return String(string.value.capitalize()) return String(string.value.capitalize())
@ -55,6 +61,7 @@ class StringFunctions:
@staticmethod @staticmethod
def titlecase(string: String) -> String: def titlecase(string: String) -> String:
""" """
:description:
Capitalize each word in the string. Capitalize each word in the string.
""" """
return String(string.value.title()) return String(string.value.title())
@ -64,6 +71,7 @@ class StringFunctions:
string: String, old: String, new: String, count: Optional[Integer] = None string: String, old: String, new: String, count: Optional[Integer] = None
) -> String: ) -> String:
""" """
:description:
Replace the ``old`` part of the String with the ``new``. Optionally only replace it Replace the ``old`` part of the String with the ``new``. Optionally only replace it
``count`` number of times. ``count`` number of times.
""" """
@ -75,6 +83,7 @@ class StringFunctions:
@staticmethod @staticmethod
def concat(*values: String) -> String: def concat(*values: String) -> String:
""" """
:description:
Concatenate multiple Strings into a single String. Concatenate multiple Strings into a single String.
""" """
return String("".join(val.value for val in values)) return String("".join(val.value for val in values))
@ -82,6 +91,7 @@ class StringFunctions:
@staticmethod @staticmethod
def pad(string: String, length: Integer, char: String) -> String: def pad(string: String, length: Integer, char: String) -> String:
""" """
:description:
Pads the string to the given length Pads the string to the given length
""" """
output = string.value output = string.value
@ -93,6 +103,7 @@ class StringFunctions:
@staticmethod @staticmethod
def pad_zero(numeric: Numeric, length: Integer) -> String: def pad_zero(numeric: Numeric, length: Integer) -> String:
""" """
:description:
Pads a numeric with zeros to the given length Pads a numeric with zeros to the given length
""" """
return StringFunctions.pad( return StringFunctions.pad(

View file

@ -1,16 +1,11 @@
import sys import sys
from typing import List from typing import List
from typing import Type
from typing import TypeVar from typing import TypeVar
from typing import Union
from ytdl_sub.script.types.resolvable import BuiltInFunctionType from ytdl_sub.script.types.resolvable import BuiltInFunctionType
from ytdl_sub.script.types.resolvable import NamedType
from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments
from ytdl_sub.script.utils.exceptions import UserException from ytdl_sub.script.utils.exceptions import UserException
from ytdl_sub.script.utils.type_checking import FunctionSpec from ytdl_sub.script.utils.type_checking import FunctionSpec
from ytdl_sub.script.utils.type_checking import get_optional_type
from ytdl_sub.script.utils.type_checking import is_optional
from ytdl_sub.script.utils.type_checking import is_union from ytdl_sub.script.utils.type_checking import is_union
TUserException = TypeVar("TUserException", bound=UserException) TUserException = TypeVar("TUserException", bound=UserException)
@ -103,28 +98,10 @@ class FunctionArgumentsExceptionFormatter:
input_spec: FunctionSpec, input_spec: FunctionSpec,
function_instance: BuiltInFunctionType, function_instance: BuiltInFunctionType,
): ):
self._args = input_spec.args self._input_spec = input_spec
self._varargs = input_spec.varargs
self._name = function_instance.name self._name = function_instance.name
self._input_args = function_instance.args self._input_args = function_instance.args
@classmethod
def _to_human_readable_name(cls, python_type: Type[NamedType] | Type[Union[NamedType]]) -> str:
if is_optional(python_type):
return f"Optional[{cls._to_human_readable_name(get_optional_type(python_type))}]"
if is_union(python_type):
return ", ".join(
sorted(cls._to_human_readable_name(arg) for arg in python_type.__args__)
)
return python_type.type_name()
def _expected_args_str(self) -> str:
if self._args is not None:
return f"({', '.join([self._to_human_readable_name(type_) for type_ in self._args])})"
if self._varargs is not None:
return f"({self._to_human_readable_name(self._varargs)}, ...)"
return "()"
def _received_args_str(self) -> str: def _received_args_str(self) -> str:
received_type_names: List[str] = [] received_type_names: List[str] = []
for arg in self._input_args: for arg in self._input_args:
@ -149,5 +126,6 @@ class FunctionArgumentsExceptionFormatter:
""" """
return IncompatibleFunctionArguments( return IncompatibleFunctionArguments(
f"Incompatible arguments passed to function {self._name}.\n" f"Incompatible arguments passed to function {self._name}.\n"
f"Expected {self._expected_args_str()}\nReceived {self._received_args_str()}" f"Expected {self._input_spec.human_readable_input_args()}\n"
f"Received {self._received_args_str()}"
) )

View file

@ -119,6 +119,7 @@ def is_type_compatible(
@dataclass(frozen=True) @dataclass(frozen=True)
class FunctionSpec: class FunctionSpec:
return_type: Type[Resolvable] return_type: Type[Resolvable]
arg_names: List[str]
args: Optional[List[Type[Resolvable | Optional[Resolvable]]]] = None args: Optional[List[Type[Resolvable | Optional[Resolvable]]]] = None
varargs: Optional[Type[Resolvable]] = None varargs: Optional[Type[Resolvable]] = None
@ -223,6 +224,42 @@ class FunctionSpec:
return l_type return l_type
return None return None
@classmethod
def _to_human_readable_name(cls, python_type: Type[NamedType] | Type[Union[NamedType]]) -> str:
if is_optional(python_type):
return f"Optional[{cls._to_human_readable_name(get_optional_type(python_type))}]"
if is_union(python_type):
args = ", ".join(
sorted(cls._to_human_readable_name(arg) for arg in python_type.__args__)
)
return f"Union[{args}]"
return python_type.type_name()
def human_readable_input_args(self) -> str:
"""
Returns
-------
input arg string in human-readable format
"""
if self.args is not None:
args = ", ".join(
f"{name}: {self._to_human_readable_name(type_)}"
for name, type_ in zip(self.arg_names, self.args)
)
elif self.varargs is not None:
args = f"{self.arg_names[0]}: {self._to_human_readable_name(self.varargs)}, ..."
else:
args = ""
return f"({args})"
def human_readable_output_type(self) -> str:
"""
Returns
-------
output type string in human-readable format
"""
return self._to_human_readable_name(self.return_type)
@classmethod @classmethod
def from_callable(cls, callable_ref: Callable[..., Resolvable]) -> "FunctionSpec": def from_callable(cls, callable_ref: Callable[..., Resolvable]) -> "FunctionSpec":
""" """
@ -234,10 +271,12 @@ class FunctionSpec:
if arg_spec.varargs: if arg_spec.varargs:
return FunctionSpec( return FunctionSpec(
return_type=arg_spec.annotations["return"], return_type=arg_spec.annotations["return"],
arg_names=[arg_spec.varargs],
varargs=arg_spec.annotations[arg_spec.varargs], varargs=arg_spec.annotations[arg_spec.varargs],
) )
return FunctionSpec( return FunctionSpec(
return_type=arg_spec.annotations["return"], return_type=arg_spec.annotations["return"],
arg_names=arg_spec.args,
args=[arg_spec.annotations[arg_name] for arg_name in arg_spec.args], args=[arg_spec.annotations[arg_name] for arg_name in arg_spec.args],
) )

View file

@ -9,11 +9,11 @@ from typing import final
from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.config.overrides import Overrides from ytdl_sub.config.overrides import Overrides
from ytdl_sub.entries.variables.override_variables import SUBSCRIPTION_NAME
from ytdl_sub.entries.variables.override_variables import SUBSCRIPTION_VALUE
from ytdl_sub.entries.variables.override_variables import OverrideVariables from ytdl_sub.entries.variables.override_variables import OverrideVariables
from ytdl_sub.utils.script import ScriptUtils
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
from ytdl_sub.validators.validators import DictValidator from ytdl_sub.validators.validators import DictValidator
from ytdl_sub.validators.validators import LiteralDictValidator
from ytdl_sub.validators.validators import StringListValidator from ytdl_sub.validators.validators import StringListValidator
from ytdl_sub.validators.validators import StringValidator from ytdl_sub.validators.validators import StringValidator
from ytdl_sub.validators.validators import Validator from ytdl_sub.validators.validators import Validator
@ -82,7 +82,6 @@ class SubscriptionPresetDictValidator(NamedSubscriptionValidator, DictValidator)
output_dict["overrides"] = dict( output_dict["overrides"] = dict(
output_dict.get("overrides", {}), output_dict.get("overrides", {}),
**self._indent_overrides_dict(), **self._indent_overrides_dict(),
**{SUBSCRIPTION_NAME: self.subscription_name},
) )
return {self.subscription_name: output_dict} return {self.subscription_name: output_dict}
@ -111,7 +110,7 @@ class SubscriptionLeafValidator(NamedSubscriptionValidator, ABC):
f"used as a subscription name" f"used as a subscription name"
) )
self._overrides_to_add: Dict[str, str] = {SUBSCRIPTION_NAME: self.subscription_name} self._overrides_to_add: Dict[str, str] = {}
@final @final
def subscription_dicts(self, global_presets_to_apply: List[str]) -> Dict[str, Dict]: def subscription_dicts(self, global_presets_to_apply: List[str]) -> Dict[str, Dict]:
@ -144,7 +143,7 @@ class SubscriptionValueValidator(SubscriptionLeafValidator, StringValidator):
presets=presets, presets=presets,
indent_overrides=indent_overrides, indent_overrides=indent_overrides,
) )
self._overrides_to_add[SUBSCRIPTION_VALUE] = self.value self._overrides_to_add[OverrideVariables.subscription_value()] = self.value
class SubscriptionListValuesValidator(SubscriptionLeafValidator, StringListValidator): class SubscriptionListValuesValidator(SubscriptionLeafValidator, StringListValidator):
@ -169,7 +168,7 @@ class SubscriptionListValuesValidator(SubscriptionLeafValidator, StringListValid
for idx, list_value in enumerate(self.list): for idx, list_value in enumerate(self.list):
# Write the first list value into subscription_value as well # Write the first list value into subscription_value as well
if idx == 0: if idx == 0:
self._overrides_to_add[SUBSCRIPTION_VALUE] = list_value.value self._overrides_to_add[OverrideVariables.subscription_value()] = list_value.value
self._overrides_to_add[ self._overrides_to_add[
OverrideVariables.subscription_value_i(index=idx) OverrideVariables.subscription_value_i(index=idx)
@ -198,6 +197,29 @@ class SubscriptionWithOverridesValidator(SubscriptionLeafValidator, DictFormatte
self._overrides_to_add = dict(self.dict_with_format_strings, **self._overrides_to_add) self._overrides_to_add = dict(self.dict_with_format_strings, **self._overrides_to_add)
class SubscriptionMapValidator(SubscriptionLeafValidator, LiteralDictValidator):
def __init__(
self,
name,
value,
subscription_name: str,
config: ConfigFile,
presets: List[str],
indent_overrides: List[str],
):
super().__init__(
name=name,
value=value,
subscription_name=subscription_name,
config=config,
presets=presets,
indent_overrides=indent_overrides,
)
self._overrides_to_add[OverrideVariables.subscription_map()] = ScriptUtils.to_script(
self.dict
)
class SubscriptionValidator(SubscriptionOutput): class SubscriptionValidator(SubscriptionOutput):
""" """
Top-level subscription validator Top-level subscription validator
@ -284,7 +306,21 @@ class SubscriptionValidator(SubscriptionOutput):
SubscriptionWithOverridesValidator( SubscriptionWithOverridesValidator(
name=obj_name, name=obj_name,
value=obj, value=obj,
subscription_name=key[1:], subscription_name=key[1:].lstrip(),
config=config,
presets=presets,
indent_overrides=indent_overrides,
)
)
# Subscription defined as
# "\Sub Name":
# custom_key: "value"
elif key.startswith("+"):
self._children.append(
SubscriptionMapValidator(
name=obj_name,
value=obj,
subscription_name=key[1:].lstrip(),
config=config, config=config,
presets=presets, presets=presets,
indent_overrides=indent_overrides, indent_overrides=indent_overrides,

View file

@ -33,6 +33,13 @@ def get_file_extension(file_name: Path | str) -> str:
return file_name.rsplit(".", maxsplit=1)[-1] return file_name.rsplit(".", maxsplit=1)[-1]
def get_md5_hash(contents: str) -> str:
"""
Helper function to compute md5 hash
"""
return hashlib.md5(contents.encode()).hexdigest()
def get_file_md5_hash(full_file_path: Path | str) -> str: def get_file_md5_hash(full_file_path: Path | str) -> str:
""" """
Parameters Parameters

View file

@ -5,9 +5,9 @@ from typing import Dict
from typing import Set from typing import Set
from ytdl_sub.entries.script.function_scripts import CUSTOM_FUNCTION_SCRIPTS from ytdl_sub.entries.script.function_scripts import CUSTOM_FUNCTION_SCRIPTS
from ytdl_sub.entries.script.variable_definitions import Variable from ytdl_sub.entries.script.variable_definitions import UNRESOLVED_VARIABLES
from ytdl_sub.entries.script.variable_scripts import UNRESOLVED_VARIABLES from ytdl_sub.entries.script.variable_definitions import VARIABLE_SCRIPTS
from ytdl_sub.entries.script.variable_scripts import VARIABLE_SCRIPTS from ytdl_sub.entries.script.variable_types import Variable
from ytdl_sub.script.script import Script from ytdl_sub.script.script import Script
from ytdl_sub.script.utils.exceptions import RuntimeException from ytdl_sub.script.utils.exceptions import RuntimeException
from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.exceptions import StringFormattingException

View file

@ -1,5 +1,5 @@
{ {
".ytdl-sub-split_by_chapters_with_regex_video_no_chapters-download-archive.json": "4008e43668447f1a3a6a55520a6ff475", ".ytdl-sub-split_by_chapters_with_regex_video_no_chapters-download-archive.json": "4008e43668447f1a3a6a55520a6ff475",
"Project Zombie/[2010] Oblivion Mod Falcor p.1/01 - Oblivion Mod Falcor p.1.mp3": "a3c01f164eeca4541aeed49264d2fc8c", "Project Zombie/[2010] Oblivion Mod Falcor p.1/01 - Oblivion Mod Falcor p.1.mp3": "d53121df33ac8c4a4699ec8919196552",
"Project Zombie/[2010] Oblivion Mod Falcor p.1/folder.jpg": "fb95b510681676e81c321171fc23143e" "Project Zombie/[2010] Oblivion Mod Falcor p.1/folder.jpg": "fb95b510681676e81c321171fc23143e"
} }

View file

@ -2,6 +2,6 @@
".ytdl-sub-multiple_songs_test-download-archive.json": "54237df5e00d1598dfd39f341ee03d75", ".ytdl-sub-multiple_songs_test-download-archive.json": "54237df5e00d1598dfd39f341ee03d75",
"Project Zombie/[2011] Jesse's Minecraft Server/01 - Jesse's Minecraft Server [Trailer - Mar.21].ogg": "5657c5b92f8980b20d8bbee0fdc7e5d8", "Project Zombie/[2011] Jesse's Minecraft Server/01 - Jesse's Minecraft Server [Trailer - Mar.21].ogg": "5657c5b92f8980b20d8bbee0fdc7e5d8",
"Project Zombie/[2011] Jesse's Minecraft Server/02 - Jesse's Minecraft Server [Trailer - Feb.27].ogg": "a2a3a34e02e26a6c0265530d4499473b", "Project Zombie/[2011] Jesse's Minecraft Server/02 - Jesse's Minecraft Server [Trailer - Feb.27].ogg": "a2a3a34e02e26a6c0265530d4499473b",
"Project Zombie/[2011] Jesse's Minecraft Server/03 - Jesse's Minecraft Server [Trailer - Feb.1].ogg": "b2d6388b4ddf8e3fbca042cb123672c3", "Project Zombie/[2011] Jesse's Minecraft Server/03 - Jesse's Minecraft Server [Trailer - Feb.1].ogg": "0a385da3aa06b994a69b8ab812b44975",
"Project Zombie/[2011] Jesse's Minecraft Server/folder.jpg": "e7830aa8a64b0cde65ba3f7e5fc56530" "Project Zombie/[2011] Jesse's Minecraft Server/folder.jpg": "e7830aa8a64b0cde65ba3f7e5fc56530"
} }

View file

@ -2,7 +2,7 @@
"Project Zombie/.ytdl-sub-pz-download-archive.json": "aadb59c92dcf14ee6617c77423a14584", "Project Zombie/.ytdl-sub-pz-download-archive.json": "aadb59c92dcf14ee6617c77423a14584",
"Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1-thumb.jpg": "fb95b510681676e81c321171fc23143e", "Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1-thumb.jpg": "fb95b510681676e81c321171fc23143e",
"Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1.info.json": "INFO_JSON", "Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1.info.json": "INFO_JSON",
"Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1.mp4": "6fb0ce965b75035079f82c84ad341e85", "Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1.mp4": "246fa05b6443337785575987904848df",
"Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1.nfo": "a1970f06fbc4743fca6db0627de779f3", "Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1.nfo": "a1970f06fbc4743fca6db0627de779f3",
"Project Zombie/Season 2010/s2010.e120201 - Oblivion Mod Falcor p.2-thumb.jpg": "8b32ee9c037fa669e444a0ac181525a1", "Project Zombie/Season 2010/s2010.e120201 - Oblivion Mod Falcor p.2-thumb.jpg": "8b32ee9c037fa669e444a0ac181525a1",
"Project Zombie/Season 2010/s2010.e120201 - Oblivion Mod Falcor p.2.info.json": "INFO_JSON", "Project Zombie/Season 2010/s2010.e120201 - Oblivion Mod Falcor p.2.info.json": "INFO_JSON",
@ -10,7 +10,7 @@
"Project Zombie/Season 2010/s2010.e120201 - Oblivion Mod Falcor p.2.nfo": "4ad498ce223454a4baa7d64bb4a837d6", "Project Zombie/Season 2010/s2010.e120201 - Oblivion Mod Falcor p.2.nfo": "4ad498ce223454a4baa7d64bb4a837d6",
"Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg": "b232d253df621aa770b780c1301d364d", "Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg": "b232d253df621aa770b780c1301d364d",
"Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].info.json": "INFO_JSON", "Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].info.json": "INFO_JSON",
"Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].mp4": "43f271ef8d3a19877f0dc9bc5040b42a", "Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].mp4": "ae1d5e2e3979cea3c96e6a4cfcae8073",
"Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].nfo": "073eefa6e5c6d76edde80258ddf452ee", "Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].nfo": "073eefa6e5c6d76edde80258ddf452ee",
"Project Zombie/Season 2011/s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg": "d17c379ea8b362f5b97c6b213b0342cb", "Project Zombie/Season 2011/s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg": "d17c379ea8b362f5b97c6b213b0342cb",
"Project Zombie/Season 2011/s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27].info.json": "INFO_JSON", "Project Zombie/Season 2011/s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27].info.json": "INFO_JSON",

View file

@ -2,7 +2,7 @@
"JMC/.ytdl-sub-music_video_playlist_test-download-archive.json": "3fdab8d103e51aa70430b6da0ceb07e2", "JMC/.ytdl-sub-music_video_playlist_test-download-archive.json": "3fdab8d103e51aa70430b6da0ceb07e2",
"JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg": "b232d253df621aa770b780c1301d364d", "JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg": "b232d253df621aa770b780c1301d364d",
"JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].info.json": "INFO_JSON", "JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].info.json": "INFO_JSON",
"JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].mp4": "2b9b7968e0db88c53d820868e542a31c", "JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].mp4": "82bcd97a13f2ba361e66ad631aeac32f",
"JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].nfo": "2a2997cbf16fb6b943d9933ad267331e", "JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].nfo": "2a2997cbf16fb6b943d9933ad267331e",
"JMC/Season 01/s01.e11022701 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg": "d17c379ea8b362f5b97c6b213b0342cb", "JMC/Season 01/s01.e11022701 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg": "d17c379ea8b362f5b97c6b213b0342cb",
"JMC/Season 01/s01.e11022701 - Jesse's Minecraft Server [Trailer - Feb.27].info.json": "INFO_JSON", "JMC/Season 01/s01.e11022701 - Jesse's Minecraft Server [Trailer - Feb.27].info.json": "INFO_JSON",

View file

@ -2,7 +2,7 @@
"JMC/.ytdl-sub-JMC-download-archive.json": "3fdab8d103e51aa70430b6da0ceb07e2", "JMC/.ytdl-sub-JMC-download-archive.json": "3fdab8d103e51aa70430b6da0ceb07e2",
"JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg": "b232d253df621aa770b780c1301d364d", "JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg": "b232d253df621aa770b780c1301d364d",
"JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].info.json": "INFO_JSON", "JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].info.json": "INFO_JSON",
"JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].mp4": "2b9b7968e0db88c53d820868e542a31c", "JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].mp4": "82bcd97a13f2ba361e66ad631aeac32f",
"JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].nfo": "2a2997cbf16fb6b943d9933ad267331e", "JMC/Season 01/s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].nfo": "2a2997cbf16fb6b943d9933ad267331e",
"JMC/Season 01/s01.e11022701 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg": "d17c379ea8b362f5b97c6b213b0342cb", "JMC/Season 01/s01.e11022701 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg": "d17c379ea8b362f5b97c6b213b0342cb",
"JMC/Season 01/s01.e11022701 - Jesse's Minecraft Server [Trailer - Feb.27].info.json": "INFO_JSON", "JMC/Season 01/s01.e11022701 - Jesse's Minecraft Server [Trailer - Feb.27].info.json": "INFO_JSON",

View file

@ -1,5 +1,5 @@
{ {
"JMC/Oblivion Mod Falcor p.1.jpg": "fb95b510681676e81c321171fc23143e", "JMC/Oblivion Mod Falcor p.1.jpg": "fb95b510681676e81c321171fc23143e",
"JMC/Oblivion Mod Falcor p.1.mp4": "0448c9fd3eeaba4eca7f650fb93fe21b", "JMC/Oblivion Mod Falcor p.1.mp4": "f2be699684854bdb6e09c02d24bdd5b6",
"JMC/Oblivion Mod Falcor p.1.nfo": "58c2be339869b5d071c1758d55c72ddb" "JMC/Oblivion Mod Falcor p.1.nfo": "58c2be339869b5d071c1758d55c72ddb"
} }

View file

@ -1,5 +1,5 @@
{ {
"JMC/Oblivion Mod Falcor p.1.jpg": "fb95b510681676e81c321171fc23143e", "JMC/Oblivion Mod Falcor p.1.jpg": "fb95b510681676e81c321171fc23143e",
"JMC/Oblivion Mod Falcor p.1.mp4": "0448c9fd3eeaba4eca7f650fb93fe21b", "JMC/Oblivion Mod Falcor p.1.mp4": "f2be699684854bdb6e09c02d24bdd5b6",
"JMC/Oblivion Mod Falcor p.1.nfo": "58c2be339869b5d071c1758d55c72ddb" "JMC/Oblivion Mod Falcor p.1.nfo": "58c2be339869b5d071c1758d55c72ddb"
} }

View file

@ -1,4 +1,4 @@
{ {
"JMC/Oblivion Mod Falcor p.1.mp4": "718c187e6196c85eea73d16ebd489c91", "JMC/Oblivion Mod Falcor p.1.mp4": "d9d2d12feee44ee97729b39ba981c542",
"JMC/Oblivion Mod Falcor p.1.nfo": "58c2be339869b5d071c1758d55c72ddb" "JMC/Oblivion Mod Falcor p.1.nfo": "58c2be339869b5d071c1758d55c72ddb"
} }

View file

@ -164,6 +164,27 @@ def preset_with_subscription_overrides_tilda(
) )
@pytest.fixture
def preset_with_subscription_overrides_map(
preset_with_subscription_value: Dict,
):
return dict(
preset_with_subscription_value,
**{
"parent_preset_2 | parent_preset_1": {
"+ test_2_1": {
"custom_key": "custom_value",
"custom_list": [
"elem1",
"elem2",
"elem3",
],
}
},
},
)
@pytest.fixture @pytest.fixture
def preset_with_subscription_value_nested_presets_and_indent_variables_same_line_old_format_errors( def preset_with_subscription_value_nested_presets_and_indent_variables_same_line_old_format_errors(
preset_with_subscription_value: Dict, preset_with_subscription_value: Dict,
@ -204,9 +225,9 @@ def test_subscription_file_preset_applies(config_file: ConfigFile, preset_with_f
"key-4": "test_preset", "key-4": "test_preset",
} }
overrides = preset_sub.overrides.dict_with_format_strings overrides = preset_sub.overrides.script
# preset overrides take precedence over __preset__ # preset overrides take precedence over __preset__
assert overrides.get("current_override") == "test_preset" assert overrides.get("current_override").native == "test_preset"
def test_subscription_list( def test_subscription_list(
@ -217,13 +238,15 @@ def test_subscription_list(
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked") subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
assert len(subs) == 3 assert len(subs) == 3
sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.dict_with_format_strings sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.script
assert sub_2_1.get("subscription_name") == "test_2_1" assert sub_2_1.get("subscription_name").native == "test_2_1"
assert sub_2_1.get("subscription_value") == "is_2_1_overwritten" assert sub_2_1.get("subscription_value").native == "is_2_1_overwritten"
assert sub_2_1.get("subscription_value_1") == "is_2_1_overwritten" assert sub_2_1.get("subscription_value_1").native == "is_2_1_overwritten"
assert sub_2_1.get("subscription_value_2") == "is_2_1_list_2" assert sub_2_1.get("subscription_value_2").native == "is_2_1_list_2"
assert sub_2_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence assert (
sub_2_1.get("current_override").native == "__preset__"
) # ensure __preset__ takes precedence
def test_subscription_overrides_tilda( def test_subscription_overrides_tilda(
@ -234,10 +257,31 @@ def test_subscription_overrides_tilda(
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked") subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
assert len(subs) == 3 assert len(subs) == 3
sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.dict_with_format_strings sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.script
assert sub_2_1.get("subscription_name") == "test_2_1" assert sub_2_1.get("subscription_name").native == "test_2_1"
assert sub_2_1.get("current_override") == "test_2_1" # tilda sub takes precedence assert sub_2_1.get("current_override").native == "test_2_1" # tilda sub takes precedence
def test_subscription_overrides_map(
config_file: ConfigFile,
preset_with_subscription_overrides_map: Dict,
):
with mock_load_yaml(preset_dict=preset_with_subscription_overrides_map):
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
assert len(subs) == 3
sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.script
assert sub_2_1.get("subscription_name").native == "test_2_1"
assert sub_2_1.get("subscription_map").native == {
"custom_key": "custom_value",
"custom_list": [
"elem1",
"elem2",
"elem3",
],
}
def test_subscription_with_period_in_name( def test_subscription_with_period_in_name(
@ -249,7 +293,7 @@ def test_subscription_with_period_in_name(
assert len(subs) == 2 assert len(subs) == 2
assert subs[1].name == "Mr. Beast" assert subs[1].name == "Mr. Beast"
assert subs[1].overrides.dict_with_format_strings["subscription_name"] == "Mr. Beast" assert subs[1].overrides.script.get("subscription_name").native == "Mr. Beast"
def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables( def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables(
@ -262,26 +306,28 @@ def test_subscription_file_value_applies_from_config_and_nested_and_indent_varia
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked") subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
assert len(subs) == 4 assert len(subs) == 4
sub_test_value = [sub for sub in subs if sub.name == "test_value"][ sub_test_value = [sub for sub in subs if sub.name == "test_value"][0].overrides.script
0 sub_1 = [sub for sub in subs if sub.name == "test_1"][0].overrides.script
].overrides.dict_with_format_strings sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.script
sub_1 = [sub for sub in subs if sub.name == "test_1"][0].overrides.dict_with_format_strings
sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.dict_with_format_strings
assert sub_test_value.get("subscription_indent_1") == "original_1" assert sub_test_value.get("subscription_indent_1").native == "original_1"
assert sub_test_value.get("subscription_indent_2") == "original_2" assert sub_test_value.get("subscription_indent_2").native == "original_2"
assert sub_1.get("subscription_name") == "test_1" assert sub_1.get("subscription_name").native == "test_1"
assert sub_1.get("subscription_value") == "is_1_overwritten" assert sub_1.get("subscription_value").native == "is_1_overwritten"
assert sub_1.get("subscription_indent_1") == "INDENT_1" assert sub_1.get("subscription_indent_1").native == "INDENT_1"
assert sub_1.get("subscription_indent_2") == "INDENT_2" assert sub_1.get("subscription_indent_2").native == "INDENT_2"
assert sub_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence assert (
sub_1.get("current_override").native == "__preset__"
) # ensure __preset__ takes precedence
assert sub_2_1.get("subscription_name") == "test_2_1" assert sub_2_1.get("subscription_name").native == "test_2_1"
assert sub_2_1.get("subscription_value") == "is_2_1_overwritten" assert sub_2_1.get("subscription_value").native == "is_2_1_overwritten"
assert sub_2_1.get("subscription_indent_1") == "INDENT_1" assert sub_2_1.get("subscription_indent_1").native == "INDENT_1"
assert sub_2_1.get("subscription_indent_2") == "original_2" assert sub_2_1.get("subscription_indent_2").native == "original_2"
assert sub_2_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence assert (
sub_2_1.get("current_override").native == "__preset__"
) # ensure __preset__ takes precedence
@pytest.mark.parametrize("all_same_line", [True, False]) @pytest.mark.parametrize("all_same_line", [True, False])
@ -301,28 +347,28 @@ def test_subscription_file_value_applies_from_config_and_nested_and_indent_varia
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked") subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
assert len(subs) == 4 assert len(subs) == 4
sub_test_value = [sub for sub in subs if sub.name == "test_value"][ sub_test_value = [sub for sub in subs if sub.name == "test_value"][0].overrides.script
0 sub_1 = [sub for sub in subs if sub.name == "test_1"][0].overrides.script
].overrides.dict_with_format_strings sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.script
sub_1 = [sub for sub in subs if sub.name == "test_1"][0].overrides.dict_with_format_strings
sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.dict_with_format_strings
assert sub_test_value.get("subscription_indent_1") == "original_1" assert sub_test_value.get("subscription_indent_1").native == "original_1"
assert sub_test_value.get("subscription_indent_2") == "original_2" assert sub_test_value.get("subscription_indent_2").native == "original_2"
assert sub_1.get("subscription_name") == "test_1" assert sub_1.get("subscription_name").native == "test_1"
assert sub_1.get("subscription_value") == "is_1_overwritten" assert sub_1.get("subscription_value").native == "is_1_overwritten"
assert sub_1.get("subscription_indent_1") == "INDENT_1" assert sub_1.get("subscription_indent_1").native == "INDENT_1"
assert sub_1.get("subscription_indent_2") == "INDENT_2" assert sub_1.get("subscription_indent_2").native == "INDENT_2"
assert sub_1.get("subscription_indent_3") == "INDENT_3" assert sub_1.get("subscription_indent_3").native == "INDENT_3"
assert sub_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence # ensure __preset__ takes precedence
assert sub_1.get("current_override").native == "__preset__"
assert sub_2_1.get("subscription_name") == "test_2_1" assert sub_2_1.get("subscription_name").native == "test_2_1"
assert sub_2_1.get("subscription_value") == "is_2_1_overwritten" assert sub_2_1.get("subscription_value").native == "is_2_1_overwritten"
assert sub_2_1.get("subscription_indent_1") == "INDENT_1" assert sub_2_1.get("subscription_indent_1").native == "INDENT_1"
assert sub_2_1.get("subscription_indent_2") == "original_2" assert sub_2_1.get("subscription_indent_2").native == "original_2"
assert sub_2_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence # ensure __preset__ takes precedence
assert "subscription_indent_3" not in sub_2_1 assert sub_2_1.get("current_override").native == "__preset__"
assert "subscription_indent_3" not in sub_2_1.variable_names
def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables_same_line_old_format_errors( def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables_same_line_old_format_errors(
@ -372,12 +418,15 @@ def test_tv_show_subscriptions(config_file: ConfigFile, tv_show_subscriptions_pa
assert len(subs) == 7 assert len(subs) == 7
assert subs[3].name == "Jake Trains" assert subs[3].name == "Jake Trains"
jake_train_overrides = subs[3].overrides.dict_with_format_strings jake_train_overrides = subs[3].overrides.script
assert jake_train_overrides["subscription_name"] == "Jake Trains" assert jake_train_overrides.get("subscription_name").native == "Jake Trains"
assert jake_train_overrides["subscription_value"] == "https://www.youtube.com/@JakeTrains" assert (
assert jake_train_overrides["subscription_indent_1"] == "Kids" jake_train_overrides.get("subscription_value").native
assert jake_train_overrides["subscription_indent_2"] == "TV-Y" == "https://www.youtube.com/@JakeTrains"
)
assert jake_train_overrides.get("subscription_indent_1").native == "Kids"
assert jake_train_overrides.get("subscription_indent_2").native == "TV-Y"
def test_advanced_tv_show_subscriptions( def test_advanced_tv_show_subscriptions(
@ -389,17 +438,20 @@ def test_advanced_tv_show_subscriptions(
assert len(subs) == 9 assert len(subs) == 9
assert subs[3].name == "Jake Trains" assert subs[3].name == "Jake Trains"
jake_train_overrides = subs[3].overrides.dict_with_format_strings jake_train_overrides = subs[3].overrides.script
assert jake_train_overrides["subscription_name"] == "Jake Trains" assert jake_train_overrides.get("subscription_name").native == "Jake Trains"
assert jake_train_overrides["subscription_value"] == "https://www.youtube.com/@JakeTrains" assert (
assert jake_train_overrides["subscription_indent_1"] == "Kids" jake_train_overrides.get("subscription_value").native
assert jake_train_overrides["subscription_indent_2"] == "TV-Y" == "https://www.youtube.com/@JakeTrains"
)
assert jake_train_overrides.get("subscription_indent_1").native == "Kids"
assert jake_train_overrides.get("subscription_indent_2").native == "TV-Y"
assert subs[5].name == "Gardening with Ciscoe" assert subs[5].name == "Gardening with Ciscoe"
overrides = subs[5].overrides overrides = subs[5].overrides
assert overrides.apply_formatter(overrides.dict["subscription_name"]) == "Gardening with Ciscoe" assert overrides.script.get("subscription_name").native == "Gardening with Ciscoe"
assert ( assert (
overrides.apply_formatter(overrides.dict["url"]) overrides.apply_formatter(overrides.dict["url"])
== "https://www.youtube.com/@gardeningwithciscoe4430" == "https://www.youtube.com/@gardeningwithciscoe4430"
@ -417,11 +469,14 @@ def test_music_subscriptions(default_config: ConfigFile, music_subscriptions_pat
assert len(subs) == 14 assert len(subs) == 14
assert subs[2].name == "Stan Getz" assert subs[2].name == "Stan Getz"
monk = subs[2].overrides.dict_with_format_strings monk = subs[2].overrides.script
assert monk["subscription_name"] == "Stan Getz" assert monk.get("subscription_name").native == "Stan Getz"
assert monk["subscription_value"] == "https://www.youtube.com/@stangetzofficial/releases" assert (
assert monk["subscription_indent_1"] == "Jazz" monk.get("subscription_value").native
== "https://www.youtube.com/@stangetzofficial/releases"
)
assert monk.get("subscription_indent_1").native == "Jazz"
def test_music_video_subscriptions(default_config: ConfigFile, music_video_subscription_path: Path): def test_music_video_subscriptions(default_config: ConfigFile, music_video_subscription_path: Path):
@ -431,14 +486,14 @@ def test_music_video_subscriptions(default_config: ConfigFile, music_video_subsc
assert len(subs) == 3 assert len(subs) == 3
assert subs[1].name == "Michael Jackson" assert subs[1].name == "Michael Jackson"
monk = subs[1].overrides.dict_with_format_strings monk = subs[1].overrides.script
assert monk["subscription_name"] == "Michael Jackson" assert monk.get("subscription_name").native == "Michael Jackson"
assert ( assert (
monk["subscription_value"] monk.get("subscription_value").native
== "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E" == "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E"
) )
assert monk["subscription_indent_1"] == "Pop" assert monk.get("subscription_indent_1").native == "Pop"
def test_default_docker_config_and_subscriptions(): def test_default_docker_config_and_subscriptions():

View file

View file

@ -0,0 +1,30 @@
from typing import Type
from tools.docgen.docgen import DocGen
from tools.docgen.entry_variables import EntryVariablesDocGen
from tools.docgen.override_variables import OverrideVariablesDocGen
from tools.docgen.plugins import PluginsDocGen
from tools.docgen.scripting_functions import ScriptingFunctionsDocGen
from ytdl_sub.utils.file_handler import get_md5_hash
def _test_doc_gen(doc_gen: Type[DocGen]) -> None:
expected_md5_hash = get_md5_hash(doc_gen.generate_and_maybe_write_to_file())
with open(doc_gen.LOCATION, "r", encoding="utf-8") as file_doc:
md5_hash = get_md5_hash(file_doc.read())
assert md5_hash == expected_md5_hash
class TestDocGen:
def test_entry_variables_generated(self):
_test_doc_gen(EntryVariablesDocGen)
def test_override_variables_generated(self):
_test_doc_gen(OverrideVariablesDocGen)
def test_scripting_functions_generated(self):
_test_doc_gen(ScriptingFunctionsDocGen)
def test_plugins_generated(self):
_test_doc_gen(PluginsDocGen)

View file

@ -0,0 +1,5 @@
from tools.docgen.docgen import REGENERATE_DOCS
def test_docgen_regenerate_disabled():
assert REGENERATE_DOCS is False

View file

@ -40,7 +40,7 @@ class TestFunction:
with pytest.raises( with pytest.raises(
IncompatibleFunctionArguments, IncompatibleFunctionArguments,
match=_incompatible_arguments_match( match=_incompatible_arguments_match(
expected="Map, AnyArgument, Optional[AnyArgument]", expected="mapping: Map, key: AnyArgument, default: Optional[AnyArgument]",
recieved="%if(...)->Union[Array, Map], String", recieved="%if(...)->Union[Array, Map], String",
), ),
): ):
@ -49,11 +49,11 @@ class TestFunction:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"function_str, expected_types, received_types", "function_str, expected_types, received_types",
[ [
("{%array_at({'a': 'dict?'}, 1)}", "Array, Integer", "Map, Integer"), ("{%array_at({'a': 'dict?'}, 1)}", "array: Array, idx: Integer", "Map, Integer"),
("{%array_extend('not', 'array')}", "Array, ...", "String, String"), ("{%array_extend('not', 'array')}", "arrays: Array, ...", "String, String"),
( (
"{%replace('hi mom', 'mom', 'dad', 1, 0)}", "{%replace('hi mom', 'mom', 'dad', 1, 0)}",
"String, String, String, Optional[Integer]", "string: String, old: String, new: String, count: Optional[Integer]",
"String, String, String, Integer, Integer", "String, String, String, Integer, Integer",
), ),
], ],

32
tools/docgen/docgen.py Normal file
View file

@ -0,0 +1,32 @@
from abc import abstractmethod
from pathlib import Path
REGENERATE_DOCS: bool = False
class DocGen:
"""
Home-made auto doc generation
"""
LOCATION: Path
@classmethod
@abstractmethod
def generate(cls) -> str:
"""
Generate the docs as a single string
"""
@classmethod
def generate_and_maybe_write_to_file(cls) -> str:
"""
Maybe writes the docs to their file if the global is set to True, and returns
the generated docs
"""
contents = cls.generate()
if REGENERATE_DOCS:
with open(cls.LOCATION, "w", encoding="utf-8") as out:
out.write(contents)
return contents

View file

@ -0,0 +1,49 @@
from pathlib import Path
from typing import Any
from typing import Dict
from typing import Type
from tools.docgen.docgen import DocGen
from tools.docgen.utils import cached_properties
from tools.docgen.utils import camel_case_to_human
from tools.docgen.utils import get_function_docs
from tools.docgen.utils import line_section
from tools.docgen.utils import section
from ytdl_sub.entries.script.variable_definitions import VARIABLES
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
def _variable_class_to_name(obj: Type[Any]) -> str:
assert "VariableDefinitions" in obj.__name__, f"{obj.__name__} doesnt have VariableDefinitions"
return (
camel_case_to_human(obj.__name__)
.replace("Variable Definitions", "Variables")
.replace("Ytdl Sub", "Ytdl-Sub")
)
class EntryVariablesDocGen(DocGen):
LOCATION = Path("docs/source/config_reference/scripting/entry_variables.rst")
@classmethod
def generate(cls) -> str:
docs = section("Entry Variables", level=0)
parent_objs: Dict[str, Type[Any]] = {
_variable_class_to_name(obj): obj for obj in VariableDefinitions.__bases__
}
for idx, name in enumerate(sorted(parent_objs.keys())):
docs += line_section(section_idx=idx)
docs += section(name, level=1)
for variable_function_name in cached_properties(parent_objs[name]):
docs += get_function_docs(
function_name=variable_function_name,
obj=parent_objs[name],
pre_docstring=f":type: ``{getattr(VARIABLES, variable_function_name).human_readable_type()}``\n",
level=2,
)
return docs

View file

@ -0,0 +1,25 @@
from pathlib import Path
from tools.docgen.docgen import DocGen
from tools.docgen.utils import get_function_docs
from tools.docgen.utils import section
from tools.docgen.utils import static_methods
from ytdl_sub.entries.variables.override_variables import OverrideVariables
class OverrideVariablesDocGen(DocGen):
LOCATION = Path("docs/source/config_reference/scripting/override_variables.rst")
@classmethod
def generate(cls) -> str:
docs = section("Override Variables", level=0)
for name in static_methods(OverrideVariables):
docs += get_function_docs(
function_name=name,
obj=OverrideVariables,
level=1,
)
return docs

91
tools/docgen/plugins.py Normal file
View file

@ -0,0 +1,91 @@
import inspect
from pathlib import Path
from typing import Any
from typing import Dict
from typing import Optional
from typing import Type
from tools.docgen.docgen import DocGen
from tools.docgen.utils import line_section
from tools.docgen.utils import properties
from tools.docgen.utils import section
from ytdl_sub.config.overrides import Overrides
from ytdl_sub.config.plugin.plugin_mapping import PluginMapping
from ytdl_sub.config.preset_options import OutputOptions
from ytdl_sub.config.preset_options import YTDLOptions
from ytdl_sub.config.validators.options import OptionsValidator
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
def should_filter_all_properties(plugin_name: str) -> bool:
return plugin_name in (
"format",
"match_filters",
"music_tags",
"filter_include",
"filter_exclude",
"embed_thumbnail",
"video_tags",
"download",
)
def should_filter_property(property_name: str) -> bool:
return property_name.startswith("_") or property_name in (
"value",
"source_variable_capture_dict",
"dict",
"keys",
"dict_with_format_strings",
"subscription_name",
"list",
)
def get_function_docs(function_name: str, obj: Any, level: int) -> str:
docs = f"\n``{function_name}``\n\n"
docs += inspect.cleandoc(getattr(obj, function_name).__doc__)
docs += "\n\n"
return docs
def generate_plugin_docs(name: str, options: Type[OptionsValidator], offset: int) -> str:
docs = ""
docs += section(name, level=offset + 0)
docs += inspect.cleandoc(options.__doc__)
docs += "\n"
if should_filter_all_properties(name):
return docs
property_names = [prop for prop in properties(options) if not should_filter_property(prop)]
for property_name in sorted(property_names):
docs += get_function_docs(function_name=property_name, obj=options, level=offset + 1)
return docs
class PluginsDocGen(DocGen):
LOCATION = Path("docs/source/config_reference/plugins.rst")
@classmethod
def generate(cls):
options_dict: Dict[str, Type[OptionsValidator]] = {
"output_options": OutputOptions,
"ytdl_options": YTDLOptions,
"overrides": Overrides,
"download": MultiUrlValidator,
}
for plugin_name, plugin_type in PluginMapping._MAPPING.items():
if plugin_name.startswith("_"):
continue
options_dict[plugin_name] = plugin_type.plugin_options_type
docs = section("Plugins", level=0)
for idx, name in enumerate(sorted(options_dict.keys())):
docs += line_section(section_idx=idx)
docs += generate_plugin_docs(name, options_dict[name], offset=1)
return docs

View file

@ -0,0 +1,82 @@
import inspect
from pathlib import Path
from typing import Any
from typing import Dict
from typing import Optional
from typing import Type
from tools.docgen.docgen import DocGen
from tools.docgen.utils import camel_case_to_human
from tools.docgen.utils import line_section
from tools.docgen.utils import section
from tools.docgen.utils import static_methods
from ytdl_sub.entries.script.custom_functions import CustomFunctions
from ytdl_sub.script.functions import Functions
from ytdl_sub.script.utils.type_checking import FunctionSpec
def maybe_get_function_name(function_name: str) -> Optional[str]:
if function_name in ["register"]:
return None
if function_name.endswith("_"):
return function_name[:-1]
return function_name
def function_class_to_name(obj: Type[Any]) -> str:
assert "Functions" in obj.__name__
return camel_case_to_human(obj.__name__)
def function_type_hinting(display_function_name: str, function: Any) -> str:
spec = FunctionSpec.from_callable(function)
out = ":spec: ``"
out += display_function_name
out += spec.human_readable_input_args()
out += " -> "
out += spec.human_readable_output_type()
out += "``\n\n"
return out
def get_function_docstring(
function_name: str, function: Any, level: int, display_function_name: Optional[str] = None
) -> str:
display_function_name = display_function_name if display_function_name else function_name
docs = section(display_function_name, level=level)
docs += function_type_hinting(display_function_name=display_function_name, function=function)
docs += inspect.cleandoc(function.__doc__)
docs += "\n"
return docs
class ScriptingFunctionsDocGen(DocGen):
LOCATION = Path("docs/source/config_reference/scripting/scripting_functions.rst")
@classmethod
def generate(cls) -> str:
docs = section("Scripting Functions", level=0)
parent_objs: Dict[str, Type[Any]] = {
function_class_to_name(obj): obj for obj in Functions.__bases__
}
parent_objs["Ytdl-Sub Functions"] = CustomFunctions
for idx, name in enumerate(sorted(parent_objs.keys())):
docs += line_section(section_idx=idx)
docs += section(name, level=1)
for function_name in static_methods(parent_objs[name]):
if display_function_name := maybe_get_function_name(function_name):
docs += get_function_docstring(
function_name=function_name,
display_function_name=display_function_name,
function=getattr(parent_objs[name], function_name),
level=2,
)
return docs

66
tools/docgen/utils.py Normal file
View file

@ -0,0 +1,66 @@
import inspect
from functools import cached_property
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Type
LEVEL_CHARS: Dict[int, str] = {0: "=", 1: "-", 2: "~", 3: "^"}
def section(name: str, level: int, as_code: bool = False) -> str:
if as_code:
name = f"``{name}``"
return f"\n{name}\n{len(name) * LEVEL_CHARS[level]}\n"
def properties(obj: Type[Any]) -> List[str]:
return sorted(prop for prop in dir(obj) if isinstance(getattr(obj, prop), property))
def cached_properties(obj: Type[Any]) -> List[str]:
return sorted(prop for prop in dir(obj) if isinstance(getattr(obj, prop), cached_property))
def static_methods(obj: Type[Any]) -> List[str]:
return sorted(
name for name in dir(obj) if isinstance(inspect.getattr_static(obj, name), staticmethod)
)
def camel_case_to_human(string: str) -> str:
output_str = string[0]
for char in string[1:]:
if char.islower():
output_str += char
else:
output_str += f" {char}"
return output_str
def get_function_docs(
function_name: str,
obj: Any,
level: int,
display_function_name: Optional[str] = None,
pre_docstring: Optional[str] = None,
) -> str:
display_function_name = display_function_name if display_function_name else function_name
docs = section(display_function_name, level=level)
docs += pre_docstring or ""
docs += inspect.cleandoc(getattr(obj, function_name).__doc__)
docs += "\n"
return docs
def line() -> str:
return "\n" + ("-" * 100) + "\n"
def line_section(section_idx: int) -> str:
if section_idx > 0:
return line()
return ""