Merge branch 'master' into prebuilt_presets
This commit is contained in:
commit
1b2fe5c7ab
78 changed files with 4567 additions and 2632 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -149,3 +149,5 @@ docker/testing/volumes
|
||||||
|
|
||||||
ffmpeg.exe
|
ffmpeg.exe
|
||||||
ffprobe.exe
|
ffprobe.exe
|
||||||
|
|
||||||
|
tools/docgen/out
|
||||||
0
docs/source/_static/custom.css
Normal file
0
docs/source/_static/custom.css
Normal 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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()
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
Entry Variables
|
Entry Variables
|
||||||
===============
|
===============
|
||||||
|
|
||||||
|
|
@ -6,434 +7,643 @@ Entry Variables
|
||||||
|
|
||||||
channel
|
channel
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
The channel name if it exists, otherwise returns the uploader.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The channel name if it exists, otherwise returns the uploader.
|
||||||
|
|
||||||
channel_id
|
channel_id
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
The channel id if it exists, otherwise returns the entry uploader ID.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The channel id if it exists, otherwise returns the entry uploader ID.
|
||||||
|
|
||||||
chapters
|
chapters
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
Chapters if they exist
|
:type: ``Array``
|
||||||
|
:description:
|
||||||
|
Chapters if they exist
|
||||||
|
|
||||||
comments
|
comments
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
Comments if they are requested
|
:type: ``Array``
|
||||||
|
:description:
|
||||||
|
Comments if they are requested
|
||||||
|
|
||||||
creator
|
creator
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
The creator name if it exists, otherwise returns the channel.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The creator name if it exists, otherwise returns the channel.
|
||||||
|
|
||||||
description
|
description
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
The description if it exists. Otherwise, returns an emtpy string.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
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
|
||||||
~~~~~
|
~~~~~
|
||||||
The unix epoch of when the metadata was scraped by yt-dlp.
|
:type: ``Integer``
|
||||||
|
:description:
|
||||||
|
The unix epoch of when the metadata was scraped by yt-dlp.
|
||||||
|
|
||||||
epoch_date
|
epoch_date
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
The epoch's date, in YYYYMMDD format.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The epoch's date, in YYYYMMDD format.
|
||||||
|
|
||||||
epoch_hour
|
epoch_hour
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
The epoch's hour
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The epoch's hour
|
||||||
|
|
||||||
ext
|
ext
|
||||||
~~~
|
~~~
|
||||||
The downloaded entry's file extension
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The downloaded entry's file extension
|
||||||
|
|
||||||
extractor
|
extractor
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
The yt-dlp extractor name
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The yt-dlp extractor name
|
||||||
|
|
||||||
extractor_key
|
extractor_key
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
The yt-dlp extractor key
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
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
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
The "info.json" extension
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The "info.json" extension
|
||||||
|
|
||||||
requested_subtitles
|
requested_subtitles
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
Subtitles if they are requested and exist
|
:type: ``Map``
|
||||||
|
:description:
|
||||||
|
Subtitles if they are requested and exist
|
||||||
|
|
||||||
sponsorblock_chapters
|
sponsorblock_chapters
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
Sponsorblock Chapters if they are requested and exist
|
:type: ``Array``
|
||||||
|
:description:
|
||||||
|
Sponsorblock Chapters if they are requested and exist
|
||||||
|
|
||||||
thumbnail_ext
|
thumbnail_ext
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
The download entry's thumbnail extension. Will always return 'jpg'. Until there is a
|
:type: ``String``
|
||||||
need to support other image types, we always convert to jpg.
|
:description:
|
||||||
|
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.
|
||||||
|
|
||||||
title
|
title
|
||||||
~~~~~
|
~~~~~
|
||||||
The title of the entry. If a title does not exist, returns its unique ID.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The title of the entry. If a title does not exist, returns its unique ID.
|
||||||
|
|
||||||
title_sanitized_plex
|
title_sanitized_plex
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
The sanitized title with additional sanitizing for Plex. It replaces numbers with
|
:type: ``String``
|
||||||
fixed-width numbers so Plex does not recognize them as season or episode numbers.
|
:description:
|
||||||
|
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.
|
||||||
|
|
||||||
uid
|
uid
|
||||||
~~~
|
~~~
|
||||||
The entry's unique ID
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The entry's unique ID
|
||||||
|
|
||||||
uid_sanitized_plex
|
uid_sanitized_plex
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
The sanitized uid with additional sanitizing for Plex. Replaces numbers with
|
:type: ``String``
|
||||||
fixed-width numbers so Plex does not recognize them as season or episode numbers.
|
:description:
|
||||||
|
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.
|
||||||
|
|
||||||
uploader
|
uploader
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
The uploader if it exists, otherwise return the uploader ID.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The uploader if it exists, otherwise return the uploader ID.
|
||||||
|
|
||||||
uploader_id
|
uploader_id
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
The uploader id if it exists, otherwise return the unique ID.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The uploader id if it exists, otherwise return the unique ID.
|
||||||
|
|
||||||
uploader_url
|
uploader_url
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
The uploader url if it exists, otherwise returns the webpage_url.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The uploader url if it exists, otherwise returns the webpage_url.
|
||||||
|
|
||||||
webpage_url
|
webpage_url
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
The url to the webpage.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The url to the webpage.
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
Metadata Variables
|
Metadata Variables
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
entry_metadata
|
entry_metadata
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
The entry's info.json
|
:type: ``Map``
|
||||||
|
:description:
|
||||||
|
The entry's info.json
|
||||||
|
|
||||||
playlist_metadata
|
playlist_metadata
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
Metadata from the playlist (i.e. the parent metadata, like playlist -> entry)
|
:type: ``Map``
|
||||||
|
:description:
|
||||||
|
Metadata from the playlist (i.e. the parent metadata, like playlist -> entry)
|
||||||
|
|
||||||
sibling_metadata
|
sibling_metadata
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
Metadata from any sibling entries that reside in the same playlist as this entry.
|
:type: ``Array``
|
||||||
|
:description:
|
||||||
|
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
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
Playlist count if it exists, otherwise returns ``1``.
|
:type: ``Integer``
|
||||||
|
:description:
|
||||||
|
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
|
||||||
this value change. Use with caution.
|
this value change. Use with caution.
|
||||||
|
|
||||||
playlist_description
|
playlist_description
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
The playlist description if it exists, otherwise returns the entry's description.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The playlist description if it exists, otherwise returns the entry's description.
|
||||||
|
|
||||||
playlist_index
|
playlist_index
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
Playlist index if it exists, otherwise returns ``1``.
|
:type: ``Integer``
|
||||||
|
:description:
|
||||||
|
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
|
||||||
this value change. Use with caution.
|
this value change. Use with caution.
|
||||||
|
|
||||||
playlist_index_padded
|
playlist_index_padded
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
playlist_index padded two digits
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
playlist_index padded two digits
|
||||||
|
|
||||||
playlist_index_padded6
|
playlist_index_padded6
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
playlist_index padded six digits.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
playlist_index padded six digits.
|
||||||
|
|
||||||
playlist_index_reversed
|
playlist_index_reversed
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Playlist index reversed via ``playlist_count - playlist_index + 1``
|
:type: ``Integer``
|
||||||
|
:description:
|
||||||
|
Playlist index reversed via ``playlist_count - playlist_index + 1``
|
||||||
|
|
||||||
playlist_index_reversed_padded
|
playlist_index_reversed_padded
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
playlist_index_reversed padded two digits
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
playlist_index_reversed padded two digits
|
||||||
|
|
||||||
playlist_index_reversed_padded6
|
playlist_index_reversed_padded6
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
playlist_index_reversed padded six digits.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
playlist_index_reversed padded six digits.
|
||||||
|
|
||||||
playlist_max_upload_date
|
playlist_max_upload_date
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Max upload_date for all entries in this entry's playlist if it exists, otherwise returns
|
:type: ``String``
|
||||||
``upload_date``
|
:description:
|
||||||
|
Max upload_date for all entries in this entry's playlist if it exists, otherwise returns
|
||||||
|
``upload_date``
|
||||||
|
|
||||||
playlist_max_upload_year
|
playlist_max_upload_year
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Max upload_year for all entries in this entry's playlist if it exists, otherwise returns
|
:type: ``Integer``
|
||||||
``upload_year``
|
:description:
|
||||||
|
Max upload_year for all entries in this entry's playlist if it exists, otherwise returns
|
||||||
|
``upload_year``
|
||||||
|
|
||||||
playlist_max_upload_year_truncated
|
playlist_max_upload_year_truncated
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
The max playlist truncated upload year for all entries in this entry's playlist if it
|
:type: ``Integer``
|
||||||
exists, otherwise returns ``upload_year_truncated``.
|
:description:
|
||||||
|
The max playlist truncated upload year for all entries in this entry's playlist if it
|
||||||
|
exists, otherwise returns ``upload_year_truncated``.
|
||||||
|
|
||||||
playlist_title
|
playlist_title
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
Name of its parent playlist/channel if it exists, otherwise returns its title.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
Name of its parent playlist/channel if it exists, otherwise returns its title.
|
||||||
|
|
||||||
playlist_uid
|
playlist_uid
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
The playlist unique ID if it exists, otherwise return the entry unique ID.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The playlist unique ID if it exists, otherwise return the entry unique ID.
|
||||||
|
|
||||||
playlist_uploader
|
playlist_uploader
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
The playlist uploader if it exists, otherwise return the entry uploader.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The playlist uploader if it exists, otherwise return the entry uploader.
|
||||||
|
|
||||||
playlist_uploader_id
|
playlist_uploader_id
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
The playlist uploader id if it exists, otherwise returns the entry uploader ID.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The playlist uploader id if it exists, otherwise returns the entry uploader ID.
|
||||||
|
|
||||||
playlist_uploader_url
|
playlist_uploader_url
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
The playlist uploader url if it exists, otherwise returns the playlist webpage_url.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The playlist uploader url if it exists, otherwise returns the playlist webpage_url.
|
||||||
|
|
||||||
playlist_webpage_url
|
playlist_webpage_url
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
The playlist webpage url if it exists. Otherwise, returns the entry webpage url.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The playlist webpage url if it exists. Otherwise, returns the entry webpage url.
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
Release Date Variables
|
Release Date Variables
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
release_date
|
release_date
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
The entry’s release date, in YYYYMMDD format. If not present, return the upload date.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The entry’s 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
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
The day of the year, i.e. February 1st returns ``32``
|
:type: ``Integer``
|
||||||
|
:description:
|
||||||
|
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``
|
||||||
i.e. February 2nd would have release_day_of_year_reversed of ``365 + 1 - 32`` = ``334``
|
: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``
|
||||||
|
|
||||||
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``
|
||||||
i.e. August 8th would have release_day_reversed of ``31 + 1 - 8`` = ``24``
|
: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``
|
||||||
|
|
||||||
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``
|
||||||
2022 returns ``100 - 22`` = ``78``
|
:description:
|
||||||
|
The upload year truncated, but reversed using ``100 - {release_year_truncated}``, i.e.
|
||||||
|
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
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
The source description if it exists, otherwise returns the playlist description.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The source description if it exists, otherwise returns the playlist description.
|
||||||
|
|
||||||
source_index
|
source_index
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
Source index if it exists, otherwise returns ``1``.
|
:type: ``Integer``
|
||||||
|
:description:
|
||||||
|
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
|
||||||
(it is easy for this value to change).
|
(it is easy for this value to change).
|
||||||
|
|
||||||
source_index_padded
|
source_index_padded
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
The source index, padded.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The source index, padded two digits.
|
||||||
|
|
||||||
source_title
|
source_title
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
Name of the source (i.e. channel with multiple playlists) if it exists, otherwise
|
:type: ``String``
|
||||||
returns its playlist_title.
|
:description:
|
||||||
|
Name of the source (i.e. channel with multiple playlists) if it exists, otherwise
|
||||||
|
returns its playlist_title.
|
||||||
|
|
||||||
source_uid
|
source_uid
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
The source unique id if it exists, otherwise returns the playlist unique ID.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The source unique id if it exists, otherwise returns the playlist unique ID.
|
||||||
|
|
||||||
source_uploader
|
source_uploader
|
||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
The source uploader if it exists, otherwise return the playlist_uploader
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The source uploader if it exists, otherwise return the playlist_uploader
|
||||||
|
|
||||||
source_uploader_id
|
source_uploader_id
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
The source uploader id if it exists, otherwise returns the playlist_uploader_id
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The source uploader id if it exists, otherwise returns the playlist_uploader_id
|
||||||
|
|
||||||
source_uploader_url
|
source_uploader_url
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
The source uploader url if it exists, otherwise returns the source webpage_url.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The source uploader url if it exists, otherwise returns the source webpage_url.
|
||||||
|
|
||||||
source_webpage_url
|
source_webpage_url
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
The source webpage url if it exists, otherwise returns the playlist webpage url.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The source webpage url if it exists, otherwise returns the playlist webpage url.
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
Upload Date Variables
|
Upload Date Variables
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
upload_date
|
upload_date
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
The entry’s uploaded date, in YYYYMMDD format. If not present, return today’s date.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The entry’s uploaded date, in YYYYMMDD format. If not present, return today’s date.
|
||||||
|
|
||||||
upload_date_standardized
|
upload_date_standardized
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
The uploaded date formatted as YYYY-MM-DD
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The uploaded date formatted as YYYY-MM-DD
|
||||||
|
|
||||||
upload_day
|
upload_day
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
The upload day as an integer (no padding).
|
:type: ``Integer``
|
||||||
|
:description:
|
||||||
|
The upload day as an integer (no padding).
|
||||||
|
|
||||||
upload_day_of_year
|
upload_day_of_year
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
The day of the year, i.e. February 1st returns ``32``
|
:type: ``Integer``
|
||||||
|
:description:
|
||||||
|
The day of the year, i.e. February 1st returns ``32``
|
||||||
|
|
||||||
upload_day_of_year_padded
|
upload_day_of_year_padded
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
The upload 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"
|
||||||
|
|
||||||
upload_day_of_year_reversed
|
upload_day_of_year_reversed
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
The upload day, but reversed using ``{total_days_in_year} + 1 - {upload_day}``,
|
:type: ``Integer``
|
||||||
i.e. February 2nd would have upload_day_of_year_reversed of ``365 + 1 - 32`` = ``334``
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
upload_day_of_year_reversed_padded
|
upload_day_of_year_reversed_padded
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
The reversed upload 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"
|
||||||
|
|
||||||
upload_day_padded
|
upload_day_padded
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
The entry's upload 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"
|
||||||
|
|
||||||
upload_day_reversed
|
upload_day_reversed
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
The upload day, but reversed using ``{total_days_in_month} + 1 - {upload_day}``,
|
:type: ``Integer``
|
||||||
i.e. August 8th would have upload_day_reversed of ``31 + 1 - 8`` = ``24``
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
upload_day_reversed_padded
|
upload_day_reversed_padded
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
The reversed upload day, but padded. i.e. August 30th returns "02".
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The reversed upload day, but padded. i.e. August 30th returns "02".
|
||||||
|
|
||||||
upload_month
|
upload_month
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
The upload month as an integer (no padding).
|
:type: ``Integer``
|
||||||
|
:description:
|
||||||
|
The upload month as an integer (no padding).
|
||||||
|
|
||||||
upload_month_padded
|
upload_month_padded
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
The entry's upload 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"
|
||||||
|
|
||||||
upload_month_reversed
|
upload_month_reversed
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
The upload month, but reversed using ``13 - {upload_month}``, i.e. March returns ``10``
|
:type: ``Integer``
|
||||||
|
:description:
|
||||||
|
The upload month, but reversed using ``13 - {upload_month}``, i.e. March returns ``10``
|
||||||
|
|
||||||
upload_month_reversed_padded
|
upload_month_reversed_padded
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
The reversed upload month, but padded. i.e. November returns "02"
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The reversed upload month, but padded. i.e. November returns "02"
|
||||||
|
|
||||||
upload_year
|
upload_year
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
The entry's upload year
|
:type: ``Integer``
|
||||||
|
:description:
|
||||||
|
The entry's upload year
|
||||||
|
|
||||||
upload_year_truncated
|
upload_year_truncated
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
The last two digits of the upload year, i.e. 22 in 2022
|
:type: ``Integer``
|
||||||
|
:description:
|
||||||
|
The last two digits of the upload year, i.e. 22 in 2022
|
||||||
|
|
||||||
upload_year_truncated_reversed
|
upload_year_truncated_reversed
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
The upload year truncated, but reversed using ``100 - {upload_year_truncated}``, i.e.
|
:type: ``Integer``
|
||||||
2022 returns ``100 - 22`` = ``78``
|
:description:
|
||||||
|
The upload year truncated, but reversed using ``100 - {upload_year_truncated}``, i.e.
|
||||||
|
2022 returns ``100 - 22`` = ``78``
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
Ytdl-Sub Variables
|
Ytdl-Sub Variables
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
download_index
|
download_index
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
The i'th entry downloaded. NOTE that this is fetched dynamically from the download
|
:type: ``Integer``
|
||||||
archive.
|
:description:
|
||||||
|
The i'th entry downloaded. NOTE that this is fetched dynamically from the download
|
||||||
|
archive.
|
||||||
|
|
||||||
download_index_padded6
|
download_index_padded6
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
The download_index padded six digits
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The download_index padded six digits
|
||||||
|
|
||||||
upload_date_index
|
upload_date_index
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
The i'th entry downloaded with this upload date.
|
:type: ``Integer``
|
||||||
|
:description:
|
||||||
|
The i'th entry downloaded with this upload date.
|
||||||
|
|
||||||
upload_date_index_padded
|
upload_date_index_padded
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
The upload_date_index padded two digits
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The upload_date_index padded two digits
|
||||||
|
|
||||||
upload_date_index_reversed
|
upload_date_index_reversed
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
100 - upload_date_index
|
:type: ``Integer``
|
||||||
|
:description:
|
||||||
|
100 - upload_date_index
|
||||||
|
|
||||||
upload_date_index_reversed_padded
|
upload_date_index_reversed_padded
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
The upload_date_index padded two digits
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The upload_date_index padded two digits
|
||||||
|
|
||||||
ytdl_sub_input_url
|
ytdl_sub_input_url
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
The input URL used in ytdl-sub to create this entry.
|
:type: ``String``
|
||||||
|
:description:
|
||||||
|
The input URL used in ytdl-sub to create this entry.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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``.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,414 +7,571 @@ Array Functions
|
||||||
|
|
||||||
array
|
array
|
||||||
~~~~~
|
~~~~~
|
||||||
``array(maybe_array: AnyArgument) -> Array``
|
:spec: ``array(maybe_array: AnyArgument) -> Array``
|
||||||
|
|
||||||
Tries to cast an unknown variable type to an Array.
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Apply a lambda function on every element in the Array.
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Return the element in the Array at index ``idx``.
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Return True if the value exists in the Array. False otherwise.
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Apply a lambda function on every element in the Array, where each arg
|
:description:
|
||||||
passed to the lambda function is ``idx, element`` as two separate args.
|
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.
|
||||||
|
|
||||||
array_extend
|
array_extend
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
``array_extend(arrays: Array, ...) -> Array``
|
:spec: ``array_extend(arrays: Array, ...) -> Array``
|
||||||
|
|
||||||
Combine multiple Arrays into a single Array.
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Flatten any nested Arrays into a single-dimensional Array.
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Return the index of the value within the Array if it exists. If it does not, it will
|
:description:
|
||||||
throw an error.
|
Return the index of the value within the Array if it exists. If it does not, it will
|
||||||
|
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``
|
||||||
|
|
||||||
Returns the Cartesian product of elements from different arrays
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Apply a reduce function on pairs of elements in the Array, until one element remains.
|
:description:
|
||||||
Executes using the left-most and reduces in the right direction.
|
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.
|
||||||
|
|
||||||
array_reverse
|
array_reverse
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
``array_reverse(array: Array) -> Array``
|
:spec: ``array_reverse(array: Array) -> Array``
|
||||||
|
|
||||||
Reverse an Array.
|
:description:
|
||||||
|
Reverse an Array.
|
||||||
|
|
||||||
array_size
|
array_size
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
``array_size(array: Array) -> Integer``
|
:spec: ``array_size(array: Array) -> Integer``
|
||||||
|
|
||||||
Returns the size of an Array.
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Returns the slice of the Array.
|
:description:
|
||||||
|
Returns the slice of the Array.
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
Boolean Functions
|
Boolean Functions
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
and
|
and
|
||||||
~~~
|
~~~
|
||||||
``and(values: AnyArgument, ...) -> Boolean``
|
:spec: ``and(values: AnyArgument, ...) -> Boolean``
|
||||||
|
|
||||||
``and`` operator. Returns True if all values evaluate to True. False otherwise.
|
:description:
|
||||||
|
``and`` operator. Returns True if all values evaluate to True. False otherwise.
|
||||||
|
|
||||||
bool
|
bool
|
||||||
~~~~
|
~~~~
|
||||||
``bool(value: AnyArgument) -> Boolean``
|
:spec: ``bool(value: AnyArgument) -> Boolean``
|
||||||
|
|
||||||
Cast any type to a Boolean.
|
:description:
|
||||||
|
Cast any type to a Boolean.
|
||||||
|
|
||||||
eq
|
eq
|
||||||
~~
|
~~
|
||||||
``eq(left: AnyArgument, right: AnyArgument) -> Boolean``
|
:spec: ``eq(left: AnyArgument, right: AnyArgument) -> Boolean``
|
||||||
|
|
||||||
``==`` operator. Returns True if left == right. False otherwise.
|
:description:
|
||||||
|
``==`` operator. Returns True if left == right. False otherwise.
|
||||||
|
|
||||||
gt
|
gt
|
||||||
~~
|
~~
|
||||||
``gt(left: AnyArgument, right: AnyArgument) -> Boolean``
|
:spec: ``gt(left: AnyArgument, right: AnyArgument) -> Boolean``
|
||||||
|
|
||||||
``>`` operator. Returns True if left > right. False otherwise.
|
:description:
|
||||||
|
``>`` operator. Returns True if left > right. False otherwise.
|
||||||
|
|
||||||
gte
|
gte
|
||||||
~~~
|
~~~
|
||||||
``gte(left: AnyArgument, right: AnyArgument) -> Boolean``
|
:spec: ``gte(left: AnyArgument, right: AnyArgument) -> Boolean``
|
||||||
|
|
||||||
``>=`` operator. Returns True if left >= right. False otherwise.
|
:description:
|
||||||
|
``>=`` 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``
|
||||||
|
|
||||||
``<`` operator. Returns True if left < right. False otherwise.
|
:description:
|
||||||
|
``<`` operator. Returns True if left < right. False otherwise.
|
||||||
|
|
||||||
lte
|
lte
|
||||||
~~~
|
~~~
|
||||||
``lte(left: AnyArgument, right: AnyArgument) -> Boolean``
|
:spec: ``lte(left: AnyArgument, right: AnyArgument) -> Boolean``
|
||||||
|
|
||||||
``<=`` operator. Returns True if left <= right. False otherwise.
|
:description:
|
||||||
|
``<=`` operator. Returns True if left <= right. False otherwise.
|
||||||
|
|
||||||
ne
|
ne
|
||||||
~~
|
~~
|
||||||
``ne(left: AnyArgument, right: AnyArgument) -> Boolean``
|
:spec: ``ne(left: AnyArgument, right: AnyArgument) -> Boolean``
|
||||||
|
|
||||||
``!=`` operator. Returns True if left != right. False otherwise.
|
:description:
|
||||||
|
``!=`` operator. Returns True if left != right. False otherwise.
|
||||||
|
|
||||||
not
|
not
|
||||||
~~~
|
~~~
|
||||||
``not(value: Boolean) -> Boolean``
|
:spec: ``not(value: Boolean) -> Boolean``
|
||||||
|
|
||||||
``not`` operator. Returns the opposite of value.
|
:description:
|
||||||
|
``not`` operator. Returns the opposite of value.
|
||||||
|
|
||||||
or
|
or
|
||||||
~~
|
~~
|
||||||
``or(values: AnyArgument, ...) -> Boolean``
|
:spec: ``or(values: AnyArgument, ...) -> Boolean``
|
||||||
|
|
||||||
``or`` operator. Returns True if any value evaluates to True. False otherwise.
|
:description:
|
||||||
|
``or`` operator. Returns True if any value evaluates to True. False otherwise.
|
||||||
|
|
||||||
xor
|
xor
|
||||||
~~~
|
~~~
|
||||||
``xor(values: AnyArgument, ...) -> Boolean``
|
:spec: ``xor(values: AnyArgument, ...) -> Boolean``
|
||||||
|
|
||||||
``^`` operator. Returns True if exactly one value is set to True. False otherwise.
|
:description:
|
||||||
|
``^`` 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]``
|
||||||
|
|
||||||
Conditional ``if`` statement that returns the ``true`` or ``false`` parameter
|
:description:
|
||||||
depending on the ``condition`` value.
|
Conditional ``if`` statement that returns the ``true`` or ``false`` parameter
|
||||||
|
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]``
|
||||||
|
|
||||||
Conditional ``if`` statement that returns the ``maybe_true_arg`` if it evaluates to True,
|
:description:
|
||||||
otherwise returns ``else_arg``.
|
Conditional ``if`` statement that returns the ``maybe_true_arg`` if it evaluates to True,
|
||||||
|
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``
|
||||||
|
|
||||||
Converts a posix timestamp to a date using strftime formatting.
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Explicitly throw an error with the provided error message.
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Converts a JSON string into an actual type.
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Tries to cast an unknown variable type to a Map.
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Apply a lambda function on the Map, where each arg
|
:description:
|
||||||
passed to the lambda function is ``key, value`` as two separate args.
|
Apply a lambda function on the Map, where each arg
|
||||||
|
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``
|
||||||
|
|
||||||
Returns True if the key is in the Map. False otherwise.
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Apply a lambda function on the Map, where each arg
|
:description:
|
||||||
passed to the lambda function is ``idx, key, value`` as three separate args.
|
Apply a lambda function on the Map, where each arg
|
||||||
|
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``
|
||||||
|
|
||||||
Return ``key``'s value within the Map. If ``key`` does not exist, and ``default`` is
|
:description:
|
||||||
provided, it will return ``default``. Otherwise, will error.
|
Return ``key``'s value within the Map. If ``key`` does not exist, and ``default`` is
|
||||||
|
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``
|
||||||
|
|
||||||
Return ``key``'s value within the Map. If ``key`` does not exist or is an empty string,
|
:description:
|
||||||
return ``default``. Otherwise, will error.
|
Return ``key``'s value within the Map. If ``key`` does not exist or is an empty string,
|
||||||
|
return ``default``. Otherwise, will error.
|
||||||
|
|
||||||
map_size
|
map_size
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
``map_size(mapping: Map) -> Integer``
|
:spec: ``map_size(mapping: Map) -> Integer``
|
||||||
|
|
||||||
Returns the size of a Map.
|
:description:
|
||||||
|
Returns the size of a Map.
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
Numeric Functions
|
Numeric Functions
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
add
|
add
|
||||||
~~~
|
~~~
|
||||||
``add(values: Numeric, ...) -> Numeric``
|
:spec: ``add(values: Numeric, ...) -> Numeric``
|
||||||
|
|
||||||
``+`` operator. Returns the sum of all values.
|
:description:
|
||||||
|
``+`` operator. Returns the sum of all values.
|
||||||
|
|
||||||
div
|
div
|
||||||
~~~
|
~~~
|
||||||
``div(left: Numeric, right: Numeric) -> Numeric``
|
:spec: ``div(left: Numeric, right: Numeric) -> Numeric``
|
||||||
|
|
||||||
``/`` operator. Returns ``left / right``.
|
:description:
|
||||||
|
``/`` operator. Returns ``left / right``.
|
||||||
|
|
||||||
float
|
float
|
||||||
~~~~~
|
~~~~~
|
||||||
``float(value: AnyArgument) -> Float``
|
:spec: ``float(value: AnyArgument) -> Float``
|
||||||
|
|
||||||
Cast to Float.
|
:description:
|
||||||
|
Cast to Float.
|
||||||
|
|
||||||
int
|
int
|
||||||
~~~
|
~~~
|
||||||
``int(value: AnyArgument) -> Integer``
|
:spec: ``int(value: AnyArgument) -> Integer``
|
||||||
|
|
||||||
Cast to Integer.
|
:description:
|
||||||
|
Cast to Integer.
|
||||||
|
|
||||||
max
|
max
|
||||||
~~~
|
~~~
|
||||||
``max(values: Numeric, ...) -> Numeric``
|
:spec: ``max(values: Numeric, ...) -> Numeric``
|
||||||
|
|
||||||
Returns max of all values.
|
:description:
|
||||||
|
Returns max of all values.
|
||||||
|
|
||||||
min
|
min
|
||||||
~~~
|
~~~
|
||||||
``min(values: Numeric, ...) -> Numeric``
|
:spec: ``min(values: Numeric, ...) -> Numeric``
|
||||||
|
|
||||||
Returns min of all values.
|
:description:
|
||||||
|
Returns min of all values.
|
||||||
|
|
||||||
mod
|
mod
|
||||||
~~~
|
~~~
|
||||||
``mod(left: Numeric, right: Numeric) -> Numeric``
|
:spec: ``mod(left: Numeric, right: Numeric) -> Numeric``
|
||||||
|
|
||||||
``%`` operator. Returns ``left % right``.
|
:description:
|
||||||
|
``%`` operator. Returns ``left % right``.
|
||||||
|
|
||||||
mul
|
mul
|
||||||
~~~
|
~~~
|
||||||
``mul(values: Numeric, ...) -> Numeric``
|
:spec: ``mul(values: Numeric, ...) -> Numeric``
|
||||||
|
|
||||||
``*`` operator. Returns the product of all values.
|
:description:
|
||||||
|
``*`` operator. Returns the product of all values.
|
||||||
|
|
||||||
pow
|
pow
|
||||||
~~~
|
~~~
|
||||||
``pow(base: Numeric, exponent: Numeric) -> Numeric``
|
:spec: ``pow(base: Numeric, exponent: Numeric) -> Numeric``
|
||||||
|
|
||||||
``**`` operator. Returns the exponential of the base and exponent value.
|
:description:
|
||||||
|
``**`` operator. Returns the exponential of the base and exponent value.
|
||||||
|
|
||||||
sub
|
sub
|
||||||
~~~
|
~~~
|
||||||
``sub(values: Numeric, ...) -> Numeric``
|
:spec: ``sub(values: Numeric, ...) -> Numeric``
|
||||||
|
|
||||||
``-`` operator. Subtracts all values from left to right.
|
:description:
|
||||||
|
``-`` 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``
|
||||||
|
|
||||||
Checks for entire string to be a match. If a match exists, returns
|
:description:
|
||||||
the string as the first element of the Array. If there are capture groups, returns each
|
Checks for entire string to be a match. If a match exists, returns
|
||||||
group as a subsequent element in the Array.
|
the string as the first element of the Array. If there are capture groups, returns each
|
||||||
|
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``
|
||||||
|
|
||||||
Checks for a match only at the beginning of the string. If a match exists, returns
|
:description:
|
||||||
the string as the first element of the Array. If there are capture groups, returns each
|
Checks for a match only at the beginning of the string. If a match exists, returns
|
||||||
group as a subsequent element in the Array.
|
the string as the first element of the Array. If there are capture groups, returns each
|
||||||
|
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``
|
||||||
|
|
||||||
Checks for a match anywhere in the string. If a match exists, returns
|
:description:
|
||||||
the string as the first element of the Array. If there are capture groups, returns each
|
Checks for a match anywhere in the string. If a match exists, returns
|
||||||
group as a subsequent element in the Array.
|
the string as the first element of the Array. If there are capture groups, returns each
|
||||||
|
group as a subsequent element in the Array.
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
String Functions
|
String Functions
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
capitalize
|
capitalize
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
``capitalize(string: String) -> String``
|
:spec: ``capitalize(string: String) -> String``
|
||||||
|
|
||||||
Capitalize the first character in the string.
|
:description:
|
||||||
|
Capitalize the first character in the string.
|
||||||
|
|
||||||
concat
|
concat
|
||||||
~~~~~~
|
~~~~~~
|
||||||
``concat(values: String, ...) -> String``
|
:spec: ``concat(values: String, ...) -> String``
|
||||||
|
|
||||||
Concatenate multiple Strings into a single String.
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Lower-case the entire String.
|
:description:
|
||||||
|
Lower-case the entire String.
|
||||||
|
|
||||||
pad
|
pad
|
||||||
~~~
|
~~~
|
||||||
``pad(string: String, length: Integer, char: String) -> String``
|
:spec: ``pad(string: String, length: Integer, char: String) -> String``
|
||||||
|
|
||||||
Pads the string to the given length
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Pads a numeric with zeros to the given length
|
:description:
|
||||||
|
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``
|
||||||
|
|
||||||
Replace the ``old`` part of the String with the ``new``. Optionally only replace it
|
:description:
|
||||||
``count`` number of times.
|
Replace the ``old`` part of the String with the ``new``. Optionally only replace it
|
||||||
|
``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``
|
||||||
|
|
||||||
Returns the slice of the Array.
|
:description:
|
||||||
|
Returns the slice of the Array.
|
||||||
|
|
||||||
string
|
string
|
||||||
~~~~~~
|
~~~~~~
|
||||||
``string(value: AnyArgument) -> String``
|
:spec: ``string(value: AnyArgument) -> String``
|
||||||
|
|
||||||
Cast to String.
|
:description:
|
||||||
|
Cast to String.
|
||||||
|
|
||||||
titlecase
|
titlecase
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
``titlecase(string: String) -> String``
|
:spec: ``titlecase(string: String) -> String``
|
||||||
|
|
||||||
Capitalize each word in the string.
|
:description:
|
||||||
|
Capitalize each word in the string.
|
||||||
|
|
||||||
upper
|
upper
|
||||||
~~~~~
|
~~~~~
|
||||||
``upper(string: String) -> String``
|
:spec: ``upper(string: String) -> String``
|
||||||
|
|
||||||
Upper-case the entire String.
|
:description:
|
||||||
|
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.
|
||||||
|
|
|
||||||
319
docs/source/config_reference/scripting/scripting_types.rst
Normal file
319
docs/source/config_reference/scripting/scripting_types.rst
Normal 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``.
|
||||||
|
|
@ -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
|
||||||
~~~~
|
~~~~
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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``.
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
"""
|
|
||||||
|
|
@ -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
|
|
||||||
"""
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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()
|
|
||||||
321
src/ytdl_sub/entries/script/variable_types.py
Normal file
321
src/ytdl_sub/entries/script/variable_types.py
Normal 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,
|
||||||
|
)
|
||||||
|
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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``.
|
||||||
|
|
|
||||||
|
|
@ -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' ) }
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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])"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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}"
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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 == "")
|
||||||
|
|
|
||||||
|
|
@ -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``.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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``.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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()}"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -156,7 +156,7 @@ def preset_with_subscription_overrides_tilda(
|
||||||
preset_with_subscription_value,
|
preset_with_subscription_value,
|
||||||
**{
|
**{
|
||||||
"parent_preset_2 | parent_preset_1": {
|
"parent_preset_2 | parent_preset_1": {
|
||||||
"~test_2_1": {
|
"~ test_2_1": {
|
||||||
"current_override": "test_2_1",
|
"current_override": "test_2_1",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -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():
|
||||||
|
|
|
||||||
0
tests/unit/docgen/__init__.py
Normal file
0
tests/unit/docgen/__init__.py
Normal file
30
tests/unit/docgen/test_docgen.py
Normal file
30
tests/unit/docgen/test_docgen.py
Normal 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)
|
||||||
5
tests/unit/docgen/test_docgen_regenerate_disabled.py
Normal file
5
tests/unit/docgen/test_docgen_regenerate_disabled.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from tools.docgen.docgen import REGENERATE_DOCS
|
||||||
|
|
||||||
|
|
||||||
|
def test_docgen_regenerate_disabled():
|
||||||
|
assert REGENERATE_DOCS is False
|
||||||
|
|
@ -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
32
tools/docgen/docgen.py
Normal 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
|
||||||
49
tools/docgen/entry_variables.py
Normal file
49
tools/docgen/entry_variables.py
Normal 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
|
||||||
25
tools/docgen/override_variables.py
Normal file
25
tools/docgen/override_variables.py
Normal 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
91
tools/docgen/plugins.py
Normal 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
|
||||||
82
tools/docgen/scripting_functions.py
Normal file
82
tools/docgen/scripting_functions.py
Normal 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
66
tools/docgen/utils.py
Normal 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 ""
|
||||||
Loading…
Reference in a new issue