From f2d55fdb23425b39600811731892049fdc0c703f Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Fri, 29 Dec 2023 14:39:19 -0800 Subject: [PATCH] [REFACTOR] Organize variable definitions (#860) --- .../scripting/entry_variables.rst | 435 ++++-- .../scripting/scripting_functions.rst | 2 +- .../config/validators/variable_validation.py | 2 +- .../info_json/info_json_downloader.py | 5 +- src/ytdl_sub/entries/entry.py | 12 +- src/ytdl_sub/entries/entry_parent.py | 9 +- .../entries/script/custom_functions.py | 4 +- .../entries/script/variable_definitions.py | 1337 +++++++++++------ .../entries/script/variable_scripts.py | 296 ---- src/ytdl_sub/entries/script/variable_types.py | 321 ++++ .../entries/variables/override_variables.py | 2 +- src/ytdl_sub/utils/scriptable.py | 6 +- tools/docgen/entry_variables.py | 6 +- tools/docgen/utils.py | 12 +- 14 files changed, 1532 insertions(+), 917 deletions(-) delete mode 100644 src/ytdl_sub/entries/script/variable_scripts.py create mode 100644 src/ytdl_sub/entries/script/variable_types.py diff --git a/docs/source/config_reference/scripting/entry_variables.rst b/docs/source/config_reference/scripting/entry_variables.rst index c4467330..8aa85ca6 100644 --- a/docs/source/config_reference/scripting/entry_variables.rst +++ b/docs/source/config_reference/scripting/entry_variables.rst @@ -7,110 +7,163 @@ Entry Variables 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 ~~~~~~~~~~ -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 if they exist +:type: ``Array`` +:description: + Chapters if they exist comments ~~~~~~~~ -Comments if they are requested +:type: ``Array`` +:description: + Comments if they are requested 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 ~~~~~~~~~~~ -The description if it exists. Otherwise, returns an emtpy string. +:type: ``String`` +:description: + The description if it exists. Otherwise, returns an emtpy string. 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 ~~~~~ -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 ~~~~~~~~~~ -The epoch's date, in YYYYMMDD format. +:type: ``String`` +:description: + The epoch's date, in YYYYMMDD format. epoch_hour ~~~~~~~~~~ -The epoch's hour +:type: ``String`` +:description: + The epoch's hour ext ~~~ -The downloaded entry's file extension +:type: ``String`` +:description: + The downloaded entry's file extension extractor ~~~~~~~~~ -The yt-dlp extractor name +:type: ``String`` +:description: + The yt-dlp extractor name extractor_key ~~~~~~~~~~~~~ -The yt-dlp extractor key +:type: ``String`` +:description: + The yt-dlp extractor 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 ~~~~~~~~~~~~~ -The "info.json" extension +:type: ``String`` +:description: + The "info.json" extension requested_subtitles ~~~~~~~~~~~~~~~~~~~ -Subtitles if they are requested and exist +:type: ``Map`` +:description: + Subtitles if they are requested and exist sponsorblock_chapters ~~~~~~~~~~~~~~~~~~~~~ -Sponsorblock Chapters if they are requested and exist +:type: ``Array`` +:description: + Sponsorblock Chapters if they are requested and exist thumbnail_ext ~~~~~~~~~~~~~ -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. +:type: ``String`` +: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 ~~~~~ -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 ~~~~~~~~~~~~~~~~~~~~ -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. +:type: ``String`` +: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 ~~~ -The entry's unique ID +:type: ``String`` +:description: + The entry's unique ID uid_sanitized_plex ~~~~~~~~~~~~~~~~~~ -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. +:type: ``String`` +: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 ~~~~~~~~ -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 ~~~~~~~~~~~ -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 ~~~~~~~~~~~~ -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 ~~~~~~~~~~~ -The url to the webpage. +:type: ``String`` +:description: + The url to the webpage. ---------------------------------------------------------------------------------------------------- @@ -119,19 +172,28 @@ Metadata Variables entry_metadata ~~~~~~~~~~~~~~ -The entry's info.json +:type: ``Map`` +:description: + The entry's info.json 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 ~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~ -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) ---------------------------------------------------------------------------------------------------- @@ -140,80 +202,114 @@ Playlist Variables 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 -this value change. Use with caution. + Note that for channels/playlists, any change (i.e. adding or removing a video) will make + this value change. Use with caution. 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 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 -this value change. Use with caution. + Note that for channels/playlists, any change (i.e. adding or removing a video) will make + this value change. Use with caution. playlist_index_padded ~~~~~~~~~~~~~~~~~~~~~ -playlist_index padded two digits +:type: ``String`` +:description: + playlist_index padded two digits playlist_index_padded6 ~~~~~~~~~~~~~~~~~~~~~~ -playlist_index padded six digits. +:type: ``String`` +:description: + playlist_index padded six digits. 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 two digits +:type: ``String`` +:description: + playlist_index_reversed padded two digits playlist_index_reversed_padded6 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -playlist_index_reversed padded six digits. +:type: ``String`` +:description: + playlist_index_reversed padded six digits. playlist_max_upload_date ~~~~~~~~~~~~~~~~~~~~~~~~ -Max upload_date for all entries in this entry's playlist if it exists, otherwise returns -``upload_date`` +:type: ``String`` +:description: + Max upload_date for all entries in this entry's playlist if it exists, otherwise returns + ``upload_date`` playlist_max_upload_year ~~~~~~~~~~~~~~~~~~~~~~~~ -Max upload_year for all entries in this entry's playlist if it exists, otherwise returns -``upload_year`` +:type: ``Integer`` +:description: + Max upload_year for all entries in this entry's playlist if it exists, otherwise returns + ``upload_year`` playlist_max_upload_year_truncated ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The max playlist truncated upload year for all entries in this entry's playlist if it -exists, otherwise returns ``upload_year_truncated``. +:type: ``Integer`` +: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 ~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~~~~ -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. ---------------------------------------------------------------------------------------------------- @@ -222,75 +318,108 @@ Release Date Variables 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 ~~~~~~~~~~~~~~~~~~~~~~~~~ -The release date formatted as YYYY-MM-DD +:type: ``String`` +:description: + The uploaded date formatted as YYYY-MM-DD 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 ~~~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The release 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`` +:type: ``Integer`` +:description: + The upload day, but reversed using ``{total_days_in_year} + 1 - {release_day}``, + i.e. February 2nd would have release_day_of_year_reversed of ``365 + 1 - 32`` = ``334`` 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 ~~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~~~~ -The release 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`` +:type: ``Integer`` +:description: + The upload day, but reversed using ``{total_days_in_month} + 1 - {release_day}``, + i.e. August 8th would have release_day_reversed of ``31 + 1 - 8`` = ``24`` 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 ~~~~~~~~~~~~~ -The release month as an integer (no padding). +:type: ``Integer`` +:description: + The upload month as an integer (no padding). 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 ~~~~~~~~~~~~~~~~~~~~~~ -The release month, but reversed -using ``13 - {release_month}``, i.e. March returns ``10`` +:type: ``Integer`` +:description: + The upload month, but reversed using ``13 - {release_month}``, i.e. March returns ``10`` 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 ~~~~~~~~~~~~ -The entry's release year +:type: ``Integer`` +:description: + The entry's upload year 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The release year truncated, but reversed using ``100 - {release_year_truncated}``, i.e. -2022 returns ``100 - 22`` = ``78`` +:type: ``Integer`` +:description: + The upload year truncated, but reversed using ``100 - {release_year_truncated}``, i.e. + 2022 returns ``100 - 22`` = ``78`` ---------------------------------------------------------------------------------------------------- @@ -299,47 +428,67 @@ Source Variables 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 ~~~~~~~~~~~~~~~~~~ -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 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 easy for this value to change). + 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). source_index_padded ~~~~~~~~~~~~~~~~~~~ -The source index, padded. +:type: ``String`` +:description: + The source index, padded two digits. source_title ~~~~~~~~~~~~ -Name of the source (i.e. channel with multiple playlists) if it exists, otherwise -returns its playlist_title. +:type: ``String`` +:description: + Name of the source (i.e. channel with multiple playlists) if it exists, otherwise + returns its playlist_title. 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 ~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~~ -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. ---------------------------------------------------------------------------------------------------- @@ -348,74 +497,108 @@ Upload Date Variables 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 ~~~~~~~~~~~~~~~~~~~~~~~~ -The uploaded date formatted as YYYY-MM-DD +:type: ``String`` +:description: + The uploaded date formatted as YYYY-MM-DD 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 ~~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -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`` +:type: ``Integer`` +: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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~~~ -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`` +:type: ``Integer`` +: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 ~~~~~~~~~~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~ -The upload month as an integer (no padding). +:type: ``Integer`` +:description: + The upload month as an integer (no padding). 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 ~~~~~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -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 ~~~~~~~~~~~ -The entry's upload year +:type: ``Integer`` +:description: + The entry's upload year 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The upload year truncated, but reversed using ``100 - {upload_year_truncated}``, i.e. -2022 returns ``100 - 22`` = ``78`` +:type: ``Integer`` +:description: + The upload year truncated, but reversed using ``100 - {upload_year_truncated}``, i.e. + 2022 returns ``100 - 22`` = ``78`` ---------------------------------------------------------------------------------------------------- @@ -424,29 +607,43 @@ Ytdl-Sub Variables download_index ~~~~~~~~~~~~~~ -The i'th entry downloaded. NOTE that this is fetched dynamically from the download -archive. +:type: ``Integer`` +:description: + The i'th entry downloaded. NOTE that this is fetched dynamically from the download + archive. download_index_padded6 ~~~~~~~~~~~~~~~~~~~~~~ -The download_index padded six digits +:type: ``String`` +:description: + The download_index padded six digits 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 ~~~~~~~~~~~~~~~~~~~~~~~~ -The upload_date_index padded two digits +:type: ``String`` +:description: + The upload_date_index padded two digits upload_date_index_reversed ~~~~~~~~~~~~~~~~~~~~~~~~~~ -100 - upload_date_index +:type: ``Integer`` +:description: + 100 - upload_date_index 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 ~~~~~~~~~~~~~~~~~~ -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. diff --git a/docs/source/config_reference/scripting/scripting_functions.rst b/docs/source/config_reference/scripting/scripting_functions.rst index d757f07c..c271669e 100644 --- a/docs/source/config_reference/scripting/scripting_functions.rst +++ b/docs/source/config_reference/scripting/scripting_functions.rst @@ -603,7 +603,7 @@ Takes a date in the form of YYYYMMDD and returns a Map containing: - year (Integer) - month (Integer) - day (Integer) -- year_truncated (String, YY from YY[YY]) +- year_truncated (Integer, YY from YY[YY]) - month_padded (String) - day_padded (String) - year_truncated_reversed (Integer, 100 - year_truncated) diff --git a/src/ytdl_sub/config/validators/variable_validation.py b/src/ytdl_sub/config/validators/variable_validation.py index d52489a7..af8a777f 100644 --- a/src/ytdl_sub/config/validators/variable_validation.py +++ b/src/ytdl_sub/config/validators/variable_validation.py @@ -13,7 +13,7 @@ from ytdl_sub.config.plugin.preset_plugins import PresetPlugins from ytdl_sub.config.preset_options import OutputOptions from ytdl_sub.config.validators.options import OptionsValidator 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.script.script import Script from ytdl_sub.validators.string_formatter_validators import validate_formatters diff --git a/src/ytdl_sub/downloaders/info_json/info_json_downloader.py b/src/ytdl_sub/downloaders/info_json/info_json_downloader.py index f1b29aa4..509a4f8d 100644 --- a/src/ytdl_sub/downloaders/info_json/info_json_downloader.py +++ b/src/ytdl_sub/downloaders/info_json/info_json_downloader.py @@ -11,10 +11,9 @@ from ytdl_sub.config.validators.options import OptionsDictValidator from ytdl_sub.downloaders.source_plugin import SourcePlugin from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder 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 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.file_handler import FileHandler from ytdl_sub.utils.file_handler import get_file_extension @@ -114,7 +113,7 @@ class InfoJsonDownloader(SourcePlugin[InfoJsonDownloaderOptions]): inj.variable_name, VARIABLE_SCRIPTS[inj.variable_name], ) - for inj in DOWNLOADER_INJECTED_VARIABLES + for inj in v.injected_variables() } ) entries.append(entry) diff --git a/src/ytdl_sub/entries/entry.py b/src/ytdl_sub/entries/entry.py index b996c20b..c7442992 100644 --- a/src/ytdl_sub/entries/entry.py +++ b/src/ytdl_sub/entries/entry.py @@ -12,8 +12,10 @@ from typing import final 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 Variable 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.utils.script import ScriptUtils 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 _YTDL_SUB_ENTRY_VARIABLES_KWARG_KEY: str = "ytdl_sub_entry_variables" -ytdl_sub_chapters_from_comments = Variable("ytdl_sub_chapters_from_comments") -ytdl_sub_split_by_chapters_parent_uid = Variable("ytdl_sub_split_by_chapters_parent_uid") +ytdl_sub_chapters_from_comments = ArrayVariable( + "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") diff --git a/src/ytdl_sub/entries/entry_parent.py b/src/ytdl_sub/entries/entry_parent.py index 21ff7d18..6cf7dcb1 100644 --- a/src/ytdl_sub/entries/entry_parent.py +++ b/src/ytdl_sub/entries/entry_parent.py @@ -3,15 +3,14 @@ from typing import Any from typing import Dict from typing import List from typing import Optional +from typing import Set from ytdl_sub.entries.base_entry import BaseEntry from ytdl_sub.entries.base_entry import TBaseEntry from ytdl_sub.entries.entry import Entry 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_scripts import ENTRY_DEFAULT_VARIABLES -from ytdl_sub.entries.script.variable_scripts import ENTRY_REQUIRED_VARIABLES +from ytdl_sub.entries.script.variable_types import MetadataVariable v: VariableDefinitions = VARIABLES @@ -53,8 +52,8 @@ class EntryParent(BaseEntry): def _sibling_entry_metadata(self) -> List[Dict[str, Any]]: sibling_entry_metadata: List[Dict[str, Any]] = [] - variable_filter: List[MetadataVariable] = list(ENTRY_REQUIRED_VARIABLES.keys()) + list( - ENTRY_DEFAULT_VARIABLES.keys() + variable_filter: Set[MetadataVariable] = ( + v.required_entry_variables() | v.default_entry_variables() ) for entry in self.entry_children(): sibling_entry_metadata.append( diff --git a/src/ytdl_sub/entries/script/custom_functions.py b/src/ytdl_sub/entries/script/custom_functions.py index a7440b9d..690095de 100644 --- a/src/ytdl_sub/entries/script/custom_functions.py +++ b/src/ytdl_sub/entries/script/custom_functions.py @@ -101,7 +101,7 @@ class CustomFunctions: - year (Integer) - month (Integer) - day (Integer) - - year_truncated (String, YY from YY[YY]) + - year_truncated (Integer, YY from YY[YY]) - month_padded (String) - day_padded (String) - year_truncated_reversed (Integer, 100 - year_truncated) @@ -149,7 +149,7 @@ class CustomFunctions: String("year"): Integer(year), String("month"): Integer(month), String("day"): Integer(day), - String("year_truncated"): String(year_truncated), + String("year_truncated"): Integer(year_truncated), String("month_padded"): String(month_padded), String("day_padded"): String(day_padded), String("year_truncated_reversed"): Integer(100 - year_truncated), diff --git a/src/ytdl_sub/entries/script/variable_definitions.py b/src/ytdl_sub/entries/script/variable_definitions.py index e711a214..d06b015f 100644 --- a/src/ytdl_sub/entries/script/variable_definitions.py +++ b/src/ytdl_sub/entries/script/variable_definitions.py @@ -1,770 +1,1065 @@ from abc import ABC -from dataclasses import dataclass +from functools import cache +from functools import cached_property +from typing import Dict +from typing import Set + +from ytdl_sub.entries.script.custom_functions import CustomFunctions +from ytdl_sub.entries.script.variable_types import ArrayMetadataVariable +from ytdl_sub.entries.script.variable_types import IntegerMetadataVariable +from ytdl_sub.entries.script.variable_types import IntegerVariable +from ytdl_sub.entries.script.variable_types import MapMetadataVariable +from ytdl_sub.entries.script.variable_types import MapVariable +from ytdl_sub.entries.script.variable_types import MetadataVariable +from ytdl_sub.entries.script.variable_types import StringDateMetadataVariable +from ytdl_sub.entries.script.variable_types import StringDateVariable +from ytdl_sub.entries.script.variable_types import StringMetadataVariable +from ytdl_sub.entries.script.variable_types import StringVariable +from ytdl_sub.entries.script.variable_types import Variable # This file contains mixins to a BaseEntry subclass. Ignore pylint's "no kwargs member" suggestion # pylint: disable=no-member # pylint: disable=too-many-public-methods - - -@dataclass(frozen=True) -class Variable: - variable_name: str - - -@dataclass(frozen=True) -class InternalVariable(Variable): - pass - - -@dataclass(frozen=True) -class Metadata(Variable): - pass - - -@dataclass(frozen=True) -class MetadataVariable(Variable): - metadata_key: str - - -@dataclass(frozen=True) -class RelativeMetadata(MetadataVariable, Metadata): - pass - - -@dataclass(frozen=True) -class SiblingMetadata(MetadataVariable): - pass +# pylint: disable=too-many-lines class MetadataVariableDefinitions(ABC): - @property - def entry_metadata(self: "VariableDefinitions") -> Metadata: + @cached_property + def entry_metadata(self: "VariableDefinitions") -> MapVariable: """ - The entry's info.json + :description: + The entry's info.json """ - return Metadata("entry_metadata") + return MapVariable(variable_name="entry_metadata", definition="{ {} }") - @property - def playlist_metadata(self: "VariableDefinitions") -> RelativeMetadata: + @cached_property + def playlist_metadata(self: "VariableDefinitions") -> MapMetadataVariable: """ - Metadata from the playlist (i.e. the parent metadata, like playlist -> entry) + :description: + Metadata from the playlist (i.e. the parent metadata, like playlist -> entry) """ - return RelativeMetadata("playlist_metadata", metadata_key="playlist_metadata") + return MapMetadataVariable.from_entry( + metadata_key="playlist_metadata", + default={}, + ) - @property - def source_metadata(self: "VariableDefinitions") -> RelativeMetadata: + @cached_property + def source_metadata(self: "VariableDefinitions") -> MapMetadataVariable: """ - Metadata from the source (i.e. the grandparent metadata, like channel -> playlist -> entry) + :description: + Metadata from the source + (i.e. the grandparent metadata, like channel -> playlist -> entry) """ - return RelativeMetadata("source_metadata", metadata_key="source_metadata") + return MapMetadataVariable.from_entry( + metadata_key="source_metadata", + default={}, + ) - @property - def sibling_metadata(self: "VariableDefinitions") -> SiblingMetadata: + @cached_property + def sibling_metadata(self: "VariableDefinitions") -> ArrayMetadataVariable: """ - Metadata from any sibling entries that reside in the same playlist as this entry. + :description: + Metadata from any sibling entries that reside in the same playlist as this entry. """ - return SiblingMetadata("sibling_metadata", metadata_key="sibling_metadata") + return ArrayMetadataVariable.from_entry( + metadata_key="sibling_metadata", + default=[], + ) class PlaylistVariableDefinitions(ABC): - @property - def playlist_uid(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def playlist_uid(self: "VariableDefinitions") -> StringMetadataVariable: """ - The playlist unique ID if it exists, otherwise return the entry unique ID. + :description: + The playlist unique ID if it exists, otherwise return the entry unique ID. """ - return MetadataVariable(variable_name="playlist_uid", metadata_key="playlist_id") - - @property - def playlist_title(self: "VariableDefinitions") -> MetadataVariable: - """ - Name of its parent playlist/channel if it exists, otherwise returns its title. - """ - return MetadataVariable(variable_name="playlist_title", metadata_key="playlist_title") - - @property - def playlist_index(self: "VariableDefinitions") -> MetadataVariable: - """ - Playlist index if it exists, otherwise returns ``1``. - - Note that for channels/playlists, any change (i.e. adding or removing a video) will make - this value change. Use with caution. - """ - return MetadataVariable(metadata_key="playlist_index", variable_name="playlist_index") - - @property - def playlist_index_reversed(self: "VariableDefinitions") -> Variable: - """ - Playlist index reversed via ``playlist_count - playlist_index + 1`` - """ - return Variable("playlist_index_reversed") - - @property - def playlist_index_padded(self: "VariableDefinitions") -> Variable: - """ - playlist_index padded two digits - """ - return Variable("playlist_index_padded") - - @property - def playlist_index_reversed_padded(self: "VariableDefinitions") -> Variable: - """ - playlist_index_reversed padded two digits - """ - return Variable("playlist_index_reversed_padded") - - @property - def playlist_index_padded6(self: "VariableDefinitions") -> Variable: - """ - playlist_index padded six digits. - """ - return Variable("playlist_index_padded6") - - @property - def playlist_index_reversed_padded6(self: "VariableDefinitions") -> Variable: - """ - playlist_index_reversed padded six digits. - """ - return Variable("playlist_index_reversed_padded6") - - @property - def playlist_count(self: "VariableDefinitions") -> MetadataVariable: - """ - Playlist count if it exists, otherwise returns ``1``. - - Note that for channels/playlists, any change (i.e. adding or removing a video) will make - this value change. Use with caution. - """ - return MetadataVariable(variable_name="playlist_count", metadata_key="playlist_count") - - @property - def playlist_description(self: "VariableDefinitions") -> MetadataVariable: - """ - The playlist description if it exists, otherwise returns the entry's description. - """ - return MetadataVariable( - variable_name="playlist_description", metadata_key=self.description.metadata_key + return StringMetadataVariable.from_playlist( + metadata_key="playlist_id", variable_name="playlist_uid", default=self.uid ) - @property - def playlist_webpage_url(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def playlist_title(self: "VariableDefinitions") -> StringMetadataVariable: """ - The playlist webpage url if it exists. Otherwise, returns the entry webpage url. + :description: + Name of its parent playlist/channel if it exists, otherwise returns its title. """ - return MetadataVariable( - variable_name="playlist_webpage_url", metadata_key=self.webpage_url.metadata_key + return StringMetadataVariable.from_entry(metadata_key="playlist_title", default=self.title) + + @cached_property + def playlist_index(self: "VariableDefinitions") -> IntegerMetadataVariable: + """ + :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 + this value change. Use with caution. + """ + return IntegerMetadataVariable.from_entry(metadata_key="playlist_index", default=1) + + @cached_property + def playlist_index_reversed(self: "VariableDefinitions") -> IntegerVariable: + """ + :description: + Playlist index reversed via ``playlist_count - playlist_index + 1`` + """ + return IntegerVariable( + variable_name="playlist_index_reversed", + definition=f"""{{ + %sub( + {self.playlist_count.variable_name}, + {self.playlist_index.variable_name}, + -1 + ) + }}""", ) - @property - def playlist_max_upload_date(self: "VariableDefinitions") -> Variable: + @cached_property + def playlist_index_padded(self: "VariableDefinitions") -> StringVariable: """ - Max upload_date for all entries in this entry's playlist if it exists, otherwise returns - ``upload_date`` + :description: + playlist_index padded two digits """ - return Variable("playlist_max_upload_date") + return self.playlist_index.to_padded_int(variable_name="playlist_index_padded", pad=2) - @property - def playlist_max_upload_year(self: "VariableDefinitions") -> Variable: + @cached_property + def playlist_index_reversed_padded(self: "VariableDefinitions") -> StringVariable: """ - Max upload_year for all entries in this entry's playlist if it exists, otherwise returns - ``upload_year`` + :description: + playlist_index_reversed padded two digits """ - # override in EntryParent - return Variable("playlist_max_upload_year") + return self.playlist_index_reversed.to_padded_int( + variable_name="playlist_index_reversed_padded", + pad=2, + ) - @property - def playlist_max_upload_year_truncated(self: "VariableDefinitions") -> Variable: + @cached_property + def playlist_index_padded6(self: "VariableDefinitions") -> StringVariable: """ - The max playlist truncated upload year for all entries in this entry's playlist if it - exists, otherwise returns ``upload_year_truncated``. + :description: + playlist_index padded six digits. """ - return Variable("playlist_max_upload_year_truncated") + return self.playlist_index.to_padded_int(variable_name="playlist_index_padded6", pad=6) - @property - def playlist_uploader_id(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def playlist_index_reversed_padded6(self: "VariableDefinitions") -> StringVariable: """ - The playlist uploader id if it exists, otherwise returns the entry uploader ID. + :description: + playlist_index_reversed padded six digits. """ - return MetadataVariable("playlist_uploader_id", metadata_key="playlist_uploader_id") + return self.playlist_index_reversed.to_padded_int( + variable_name="playlist_index_reversed_padded6", pad=6 + ) - @property - def playlist_uploader(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def playlist_count(self: "VariableDefinitions") -> IntegerMetadataVariable: """ - The playlist uploader if it exists, otherwise return the entry uploader. - """ - return MetadataVariable("playlist_uploader", metadata_key=self.uploader.metadata_key) + :description: + Playlist count if it exists, otherwise returns ``1``. - @property - def playlist_uploader_url(self: "VariableDefinitions") -> MetadataVariable: + Note that for channels/playlists, any change (i.e. adding or removing a video) will make + this value change. Use with caution. """ - The playlist uploader url if it exists, otherwise returns the playlist webpage_url. + return IntegerMetadataVariable.from_entry(metadata_key="playlist_count", default=1) + + @cached_property + def playlist_description(self: "VariableDefinitions") -> StringMetadataVariable: """ - return MetadataVariable( - "playlist_uploader_url", metadata_key=self.uploader_url.metadata_key + :description: + The playlist description if it exists, otherwise returns the entry's description. + """ + return StringMetadataVariable.from_playlist( + metadata_key=self.description.metadata_key, + variable_name="playlist_description", + default=self.description, + ) + + @cached_property + def playlist_webpage_url(self: "VariableDefinitions") -> StringMetadataVariable: + """ + :description: + The playlist webpage url if it exists. Otherwise, returns the entry webpage url. + """ + return StringMetadataVariable.from_playlist( + metadata_key=self.webpage_url.metadata_key, + variable_name="playlist_webpage_url", + default=self.webpage_url, + ) + + @cached_property + def playlist_max_upload_date(self: "VariableDefinitions") -> StringDateVariable: + """ + :description: + Max upload_date for all entries in this entry's playlist if it exists, otherwise returns + ``upload_date`` + """ + return StringVariable( + variable_name="playlist_max_upload_date", + definition=f"""{{ + %array_reduce( + %if_passthrough( + %extract_field_from_siblings('{self.upload_date.variable_name}'), + [{self.upload_date.variable_name}] + ), + %max + ) + }}""", + ).as_date_variable() + + @cached_property + def playlist_max_upload_year(self: "VariableDefinitions") -> IntegerVariable: + """ + :description: + Max upload_year for all entries in this entry's playlist if it exists, otherwise returns + ``upload_year`` + """ + return self.playlist_max_upload_date.get_integer_date_metadata( + date_metadata_key="year", + variable_name="playlist_max_upload_year", + ) + + @cached_property + def playlist_max_upload_year_truncated(self: "VariableDefinitions") -> IntegerVariable: + """ + :description: + The max playlist truncated upload year for all entries in this entry's playlist if it + exists, otherwise returns ``upload_year_truncated``. + """ + return self.playlist_max_upload_date.get_integer_date_metadata( + date_metadata_key="year_truncated", variable_name="playlist_max_upload_year_truncated" + ) + + @cached_property + def playlist_uploader_id(self: "VariableDefinitions") -> StringMetadataVariable: + """ + :description: + The playlist uploader id if it exists, otherwise returns the entry uploader ID. + """ + return StringMetadataVariable.from_entry( + metadata_key="playlist_uploader_id", + default=self.uploader_id, + ) + + @cached_property + def playlist_uploader(self: "VariableDefinitions") -> StringMetadataVariable: + """ + :description: + The playlist uploader if it exists, otherwise return the entry uploader. + """ + return StringMetadataVariable.from_playlist( + metadata_key=self.uploader.metadata_key, + variable_name="playlist_uploader", + default=self.uploader, + ) + + @cached_property + def playlist_uploader_url(self: "VariableDefinitions") -> StringMetadataVariable: + """ + :description: + The playlist uploader url if it exists, otherwise returns the playlist webpage_url. + """ + return StringMetadataVariable.from_playlist( + metadata_key=self.uploader_url.metadata_key, + variable_name="playlist_uploader_url", + default=self.webpage_url, ) class SourceVariableDefinitions(ABC): - @property - def source_title(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def source_title(self: "VariableDefinitions") -> StringMetadataVariable: """ - Name of the source (i.e. channel with multiple playlists) if it exists, otherwise - returns its playlist_title. + :description: + Name of the source (i.e. channel with multiple playlists) if it exists, otherwise + returns its playlist_title. """ - return MetadataVariable("source_title", metadata_key=self.title.metadata_key) + return StringMetadataVariable.from_source( + metadata_key=self.title.metadata_key, + variable_name="source_title", + default=self.playlist_title, + ) - @property - def source_uid(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def source_uid(self: "VariableDefinitions") -> StringMetadataVariable: """ - The source unique id if it exists, otherwise returns the playlist unique ID. + :description: + The source unique id if it exists, otherwise returns the playlist unique ID. """ - return MetadataVariable("source_uid", metadata_key=self.uid.metadata_key) + return StringMetadataVariable.from_source( + metadata_key=self.uid.metadata_key, + variable_name="source_uid", + default=self.playlist_uid, + ) - @property - def source_index(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def source_index(self: "VariableDefinitions") -> IntegerMetadataVariable: """ - Source index if it exists, otherwise returns ``1``. + :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 easy for this value to change). + 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). """ - return MetadataVariable("source_index", metadata_key=self.playlist_index.metadata_key) + return IntegerMetadataVariable.from_playlist( + metadata_key=self.playlist_index.metadata_key, + variable_name="source_index", + default=1, + ) - @property - def source_index_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def source_index_padded(self: "VariableDefinitions") -> StringVariable: """ - The source index, padded. + :description: + The source index, padded two digits. """ - return Variable("source_index_padded") + return self.source_index.to_padded_int(variable_name="source_index_padded", pad=2) - @property - def source_count(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def source_count(self: "VariableDefinitions") -> IntegerMetadataVariable: """ - The source count if it exists, otherwise returns the playlist count. + :description: + The source count if it exists, otherwise returns ``1``. """ - return MetadataVariable("source_count", metadata_key=self.playlist_count.metadata_key) + return IntegerMetadataVariable.from_playlist( + metadata_key=self.playlist_count.metadata_key, + variable_name="source_count", + default=1, + ) - @property - def source_webpage_url(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def source_webpage_url(self: "VariableDefinitions") -> StringMetadataVariable: """ - The source webpage url if it exists, otherwise returns the playlist webpage url. + :description: + The source webpage url if it exists, otherwise returns the playlist webpage url. """ - return MetadataVariable("source_webpage_url", metadata_key=self.webpage_url.metadata_key) + return StringMetadataVariable.from_source( + metadata_key=self.webpage_url.metadata_key, + variable_name="source_webpage_url", + default=self.playlist_webpage_url, + ) - @property - def source_description(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def source_description(self: "VariableDefinitions") -> StringMetadataVariable: """ - The source description if it exists, otherwise returns the playlist description. + :description: + The source description if it exists, otherwise returns the playlist description. """ - return MetadataVariable("source_description", metadata_key=self.description.metadata_key) + return StringMetadataVariable.from_source( + metadata_key=self.description.metadata_key, + variable_name="source_description", + default=self.playlist_description, + ) - @property - def source_uploader_id(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def source_uploader_id(self: "VariableDefinitions") -> StringMetadataVariable: """ - The source uploader id if it exists, otherwise returns the playlist_uploader_id + :description: + The source uploader id if it exists, otherwise returns the playlist_uploader_id """ - return MetadataVariable("source_uploader_id", metadata_key=self.uploader_id.metadata_key) + return StringMetadataVariable.from_source( + metadata_key=self.uploader_id.metadata_key, + variable_name="source_uploader_id", + default=self.playlist_uploader_id, + ) - @property - def source_uploader(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def source_uploader(self: "VariableDefinitions") -> StringMetadataVariable: """ - The source uploader if it exists, otherwise return the playlist_uploader + :description: + The source uploader if it exists, otherwise return the playlist_uploader """ - return MetadataVariable("source_uploader", metadata_key=self.uploader.metadata_key) + return StringMetadataVariable.from_source( + metadata_key=self.uploader.metadata_key, + variable_name="source_uploader", + default=self.playlist_uploader, + ) - @property - def source_uploader_url(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def source_uploader_url(self: "VariableDefinitions") -> StringMetadataVariable: """ - The source uploader url if it exists, otherwise returns the source webpage_url. + :description: + The source uploader url if it exists, otherwise returns the source webpage_url. """ - return MetadataVariable("source_uploader_url", metadata_key=self.uploader_url.metadata_key) + return StringMetadataVariable.from_source( + metadata_key=self.uploader_url.metadata_key, + variable_name="source_uploader_url", + default=self.source_webpage_url, + ) class UploadDateVariableDefinitions(ABC): - @property - def upload_date(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def upload_date(self: "VariableDefinitions") -> StringDateMetadataVariable: """ - The entry’s uploaded date, in YYYYMMDD format. If not present, return today’s date. + :description: + The entry’s uploaded date, in YYYYMMDD format. If not present, return today’s date. """ - return MetadataVariable(variable_name="upload_date", metadata_key="upload_date") + return StringDateMetadataVariable.from_entry(metadata_key="upload_date").as_date_variable() - @property - def upload_year(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_year(self: "VariableDefinitions") -> IntegerVariable: """ - The entry's upload year + :description: + The entry's upload year """ - return Variable("upload_year") + return self.upload_date.get_integer_date_metadata( + date_metadata_key="year", variable_name="upload_year" + ) - @property - def upload_year_truncated(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_year_truncated(self: "VariableDefinitions") -> IntegerVariable: """ - The last two digits of the upload year, i.e. 22 in 2022 + :description: + The last two digits of the upload year, i.e. 22 in 2022 """ - return Variable("upload_year_truncated") + return self.upload_date.get_integer_date_metadata( + date_metadata_key="year_truncated", variable_name="upload_year_truncated" + ) - @property - def upload_year_truncated_reversed(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_year_truncated_reversed(self: "VariableDefinitions") -> IntegerVariable: """ - The upload year truncated, but reversed using ``100 - {upload_year_truncated}``, i.e. - 2022 returns ``100 - 22`` = ``78`` + :description: + The upload year truncated, but reversed using ``100 - {upload_year_truncated}``, i.e. + 2022 returns ``100 - 22`` = ``78`` """ - return Variable("upload_year_truncated_reversed") + return self.upload_date.get_integer_date_metadata( + date_metadata_key="year_truncated_reversed", + variable_name="upload_year_truncated_reversed", + ) - @property - def upload_month_reversed(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_month_reversed(self: "VariableDefinitions") -> IntegerVariable: """ - The upload month, but reversed using ``13 - {upload_month}``, i.e. March returns ``10`` + :description: + The upload month, but reversed using ``13 - {upload_month}``, i.e. March returns ``10`` """ - return Variable("upload_month_reversed") + return self.upload_date.get_integer_date_metadata( + date_metadata_key="month_reversed", variable_name="upload_month_reversed" + ) - @property - def upload_month_reversed_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_month_reversed_padded(self: "VariableDefinitions") -> StringVariable: """ - The reversed upload month, but padded. i.e. November returns "02" + :description: + The reversed upload month, but padded. i.e. November returns "02" """ - return Variable("upload_month_reversed_padded") + return self.upload_date.get_string_date_metadata( + date_metadata_key="month_reversed_padded", variable_name="upload_month_reversed_padded" + ) - @property - def upload_month_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_month_padded(self: "VariableDefinitions") -> StringVariable: """ - The entry's upload month padded to two digits, i.e. March returns "03" + :description: + The entry's upload month padded to two digits, i.e. March returns "03" """ - return Variable("upload_month_padded") + return self.upload_date.get_string_date_metadata( + date_metadata_key="month_padded", variable_name="upload_month_padded" + ) - @property - def upload_day_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_day_padded(self: "VariableDefinitions") -> StringVariable: """ - The entry's upload day padded to two digits, i.e. the fifth returns "05" + :description: + The entry's upload day padded to two digits, i.e. the fifth returns "05" """ - return Variable("upload_day_padded") + return self.upload_date.get_string_date_metadata( + date_metadata_key="day_padded", variable_name="upload_day_padded" + ) - @property - def upload_month(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_month(self: "VariableDefinitions") -> IntegerVariable: """ - The upload month as an integer (no padding). + :description: + The upload month as an integer (no padding). """ - return Variable("upload_month") + return self.upload_date.get_integer_date_metadata( + date_metadata_key="month", variable_name="upload_month" + ) - @property - def upload_day(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_day(self: "VariableDefinitions") -> IntegerVariable: """ - The upload day as an integer (no padding). + :description: + The upload day as an integer (no padding). """ - return Variable("upload_day") + return self.upload_date.get_integer_date_metadata( + date_metadata_key="day", variable_name="upload_day" + ) - @property - def upload_day_reversed(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_day_reversed(self: "VariableDefinitions") -> IntegerVariable: """ - 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`` + :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`` """ - return Variable("upload_day_reversed") + return self.upload_date.get_integer_date_metadata( + date_metadata_key="day_reversed", variable_name="upload_day_reversed" + ) - @property - def upload_day_reversed_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_day_reversed_padded(self: "VariableDefinitions") -> StringVariable: """ - The reversed upload day, but padded. i.e. August 30th returns "02". + :description: + The reversed upload day, but padded. i.e. August 30th returns "02". """ - return Variable("upload_day_reversed_padded") + return self.upload_date.get_string_date_metadata( + date_metadata_key="day_reversed_padded", variable_name="upload_day_reversed_padded" + ) - @property - def upload_day_of_year(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_day_of_year(self: "VariableDefinitions") -> IntegerVariable: """ - The day of the year, i.e. February 1st returns ``32`` + :description: + The day of the year, i.e. February 1st returns ``32`` """ - return Variable("upload_day_of_year") + return self.upload_date.get_integer_date_metadata( + date_metadata_key="day_of_year", variable_name="upload_day_of_year" + ) - @property - def upload_day_of_year_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_day_of_year_padded(self: "VariableDefinitions") -> StringVariable: """ - The upload day of year, but padded i.e. February 1st returns "032" + :description: + The upload day of year, but padded i.e. February 1st returns "032" """ - return Variable("upload_day_of_year_padded") + return self.upload_date.get_string_date_metadata( + date_metadata_key="day_of_year_padded", variable_name="upload_day_of_year_padded" + ) - @property - def upload_day_of_year_reversed(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_day_of_year_reversed(self: "VariableDefinitions") -> IntegerVariable: """ - 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`` + :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`` """ - return Variable("upload_day_of_year_reversed") + return self.upload_date.get_integer_date_metadata( + date_metadata_key="day_of_year_reversed", variable_name="upload_day_of_year_reversed" + ) - @property - def upload_day_of_year_reversed_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_day_of_year_reversed_padded(self: "VariableDefinitions") -> StringVariable: """ - The reversed upload day of year, but padded i.e. December 31st returns "001" + :description: + The reversed upload day of year, but padded i.e. December 31st returns "001" """ - return Variable("upload_day_of_year_reversed_padded") + return self.upload_date.get_string_date_metadata( + date_metadata_key="day_of_year_reversed_padded", + variable_name="upload_day_of_year_reversed_padded", + ) - @property - def upload_date_standardized(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_date_standardized(self: "VariableDefinitions") -> StringVariable: """ - The uploaded date formatted as YYYY-MM-DD + :description: + The uploaded date formatted as YYYY-MM-DD """ - return Variable("upload_date_standardized") + return self.upload_date.get_string_date_metadata( + date_metadata_key="date_standardized", variable_name="upload_date_standardized" + ) class ReleaseDateVariableDefinitions(ABC): - @property - def release_date(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def release_date(self: "VariableDefinitions") -> StringDateMetadataVariable: """ - The entry’s release date, in YYYYMMDD format. If not present, return the upload date. + :description: + The entry’s release date, in YYYYMMDD format. If not present, return the upload date. """ - return MetadataVariable(variable_name="release_date", metadata_key="release_date") + return StringMetadataVariable.from_entry( + metadata_key="release_date", default=self.upload_date + ).as_date_variable() - @property - def release_year(self: "VariableDefinitions") -> Variable: + @cached_property + def release_year(self: "VariableDefinitions") -> IntegerVariable: """ - The entry's release year + :description: + The entry's upload year """ - return Variable("release_year") + return self.release_date.get_integer_date_metadata( + date_metadata_key="year", variable_name="release_year" + ) - @property - def release_year_truncated(self: "VariableDefinitions") -> Variable: + @cached_property + def release_year_truncated(self: "VariableDefinitions") -> IntegerVariable: """ - The last two digits of the release year, i.e. 22 in 2022 + :description: + The last two digits of the upload year, i.e. 22 in 2022 """ - return Variable("release_year_truncated") + return self.release_date.get_integer_date_metadata( + date_metadata_key="year_truncated", variable_name="release_year_truncated" + ) - @property - def release_year_truncated_reversed(self: "VariableDefinitions") -> Variable: + @cached_property + def release_year_truncated_reversed(self: "VariableDefinitions") -> IntegerVariable: """ - The release year truncated, but reversed using ``100 - {release_year_truncated}``, i.e. - 2022 returns ``100 - 22`` = ``78`` + :description: + The upload year truncated, but reversed using ``100 - {release_year_truncated}``, i.e. + 2022 returns ``100 - 22`` = ``78`` """ - return Variable("release_year_truncated_reversed") + return self.release_date.get_integer_date_metadata( + date_metadata_key="year_truncated_reversed", + variable_name="release_year_truncated_reversed", + ) - @property - def release_month_reversed(self: "VariableDefinitions") -> Variable: + @cached_property + def release_month_reversed(self: "VariableDefinitions") -> IntegerVariable: """ - The release month, but reversed - using ``13 - {release_month}``, i.e. March returns ``10`` + :description: + The upload month, but reversed using ``13 - {release_month}``, i.e. March returns ``10`` """ - return Variable("release_month_reversed") + return self.release_date.get_integer_date_metadata( + date_metadata_key="month_reversed", variable_name="release_month_reversed" + ) - @property - def release_month_reversed_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def release_month_reversed_padded(self: "VariableDefinitions") -> StringVariable: """ - The reversed release month, but padded. i.e. November returns "02" + :description: + The reversed upload month, but padded. i.e. November returns "02" """ - return Variable("release_month_reversed_padded") + return self.release_date.get_string_date_metadata( + date_metadata_key="month_reversed_padded", variable_name="release_month_reversed_padded" + ) - @property - def release_month_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def release_month_padded(self: "VariableDefinitions") -> StringVariable: """ - The entry's release month padded to two digits, i.e. March returns "03" + :description: + The entry's upload month padded to two digits, i.e. March returns "03" """ - return Variable("release_month_padded") + return self.release_date.get_string_date_metadata( + date_metadata_key="month_padded", variable_name="release_month_padded" + ) - @property - def release_day_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def release_day_padded(self: "VariableDefinitions") -> StringVariable: """ - The entry's release day padded to two digits, i.e. the fifth returns "05" + :description: + The entry's upload day padded to two digits, i.e. the fifth returns "05" """ - return Variable("release_day_padded") + return self.release_date.get_string_date_metadata( + date_metadata_key="day_padded", variable_name="release_day_padded" + ) - @property - def release_month(self: "VariableDefinitions") -> Variable: + @cached_property + def release_month(self: "VariableDefinitions") -> IntegerVariable: """ - The release month as an integer (no padding). + :description: + The upload month as an integer (no padding). """ - return Variable("release_month") + return self.release_date.get_integer_date_metadata( + date_metadata_key="month", variable_name="release_month" + ) - @property - def release_day(self: "VariableDefinitions") -> Variable: + @cached_property + def release_day(self: "VariableDefinitions") -> IntegerVariable: """ - The release day as an integer (no padding). + :description: + The upload day as an integer (no padding). """ - return Variable("release_day") + return self.release_date.get_integer_date_metadata( + date_metadata_key="day", variable_name="release_day" + ) - @property - def release_day_reversed(self: "VariableDefinitions") -> Variable: + @cached_property + def release_day_reversed(self: "VariableDefinitions") -> IntegerVariable: """ - The release 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`` + :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`` """ - return Variable("release_day_reversed") + return self.release_date.get_integer_date_metadata( + date_metadata_key="day_reversed", variable_name="release_day_reversed" + ) - @property - def release_day_reversed_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def release_day_reversed_padded(self: "VariableDefinitions") -> StringVariable: """ - The reversed release day, but padded. i.e. August 30th returns "02". + :description: + The reversed upload day, but padded. i.e. August 30th returns "02". """ - return Variable("release_day_reversed_padded") + return self.release_date.get_string_date_metadata( + date_metadata_key="day_reversed_padded", variable_name="release_day_reversed_padded" + ) - @property - def release_day_of_year(self: "VariableDefinitions") -> Variable: + @cached_property + def release_day_of_year(self: "VariableDefinitions") -> IntegerVariable: """ - The day of the year, i.e. February 1st returns ``32`` + :description: + The day of the year, i.e. February 1st returns ``32`` """ - return Variable("release_day_of_year") + return self.release_date.get_integer_date_metadata( + date_metadata_key="day_of_year", variable_name="release_day_of_year" + ) - @property - def release_day_of_year_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def release_day_of_year_padded(self: "VariableDefinitions") -> StringVariable: """ - The release day of year, but padded i.e. February 1st returns "032" + :description: + The upload day of year, but padded i.e. February 1st returns "032" """ - return Variable("release_day_of_year_padded") + return self.release_date.get_string_date_metadata( + date_metadata_key="day_of_year_padded", variable_name="release_day_of_year_padded" + ) - @property - def release_day_of_year_reversed(self: "VariableDefinitions") -> Variable: + @cached_property + def release_day_of_year_reversed(self: "VariableDefinitions") -> IntegerVariable: """ - The release 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`` + :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`` """ - return Variable("release_day_of_year_reversed") + return self.release_date.get_integer_date_metadata( + date_metadata_key="day_of_year_reversed", variable_name="release_day_of_year_reversed" + ) - @property - def release_day_of_year_reversed_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def release_day_of_year_reversed_padded(self: "VariableDefinitions") -> StringVariable: """ - The reversed release day of year, but padded i.e. December 31st returns "001" + :description: + The reversed upload day of year, but padded i.e. December 31st returns "001" """ - return Variable("release_day_of_year_reversed_padded") + return self.release_date.get_string_date_metadata( + date_metadata_key="day_of_year_reversed_padded", + variable_name="release_day_of_year_reversed_padded", + ) - @property - def release_date_standardized(self: "VariableDefinitions") -> Variable: + @cached_property + def release_date_standardized(self: "VariableDefinitions") -> StringVariable: """ - The release date formatted as YYYY-MM-DD + :description: + The uploaded date formatted as YYYY-MM-DD """ - return Variable("release_date_standardized") + return self.release_date.get_string_date_metadata( + date_metadata_key="date_standardized", variable_name="release_date_standardized" + ) class YtdlSubVariableDefinitions(ABC): - @property - def ytdl_sub_input_url(self: "VariableDefinitions") -> Variable: + @cached_property + def ytdl_sub_input_url(self: "VariableDefinitions") -> StringVariable: """ - The input URL used in ytdl-sub to create this entry. + :description: + The input URL used in ytdl-sub to create this entry. """ - return Variable("ytdl_sub_input_url") + return StringVariable(variable_name="ytdl_sub_input_url", definition="{ %string('') }") - @property - def download_index(self: "VariableDefinitions") -> Variable: + @cached_property + def download_index(self: "VariableDefinitions") -> IntegerVariable: """ - The i'th entry downloaded. NOTE that this is fetched dynamically from the download - archive. + :description: + The i'th entry downloaded. NOTE that this is fetched dynamically from the download + archive. """ - return Variable(variable_name="download_index") + return IntegerVariable(variable_name="download_index", definition="{ %int(1) }") - @property - def download_index_padded6(self: "VariableDefinitions") -> Variable: + @cached_property + def download_index_padded6(self: "VariableDefinitions") -> StringVariable: """ - The download_index padded six digits + :description: + The download_index padded six digits """ - return Variable("download_index_padded6") + return self.download_index.to_padded_int(variable_name="download_index_padded6", pad=6) - @property - def upload_date_index(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_date_index(self: "VariableDefinitions") -> IntegerVariable: """ - The i'th entry downloaded with this upload date. + :description: + The i'th entry downloaded with this upload date. """ - return Variable(variable_name="upload_date_index") + return IntegerVariable(variable_name="upload_date_index", definition="{ %int(1) }") - @property - def upload_date_index_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_date_index_padded(self: "VariableDefinitions") -> StringVariable: """ - The upload_date_index padded two digits + :description: + The upload_date_index padded two digits """ - return Variable("upload_date_index_padded") + return self.upload_date_index.to_padded_int(variable_name="upload_date_index_padded", pad=2) - @property - def upload_date_index_reversed(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_date_index_reversed(self: "VariableDefinitions") -> IntegerVariable: """ - 100 - upload_date_index + :description: + 100 - upload_date_index """ - return Variable("upload_date_index_reversed") + return IntegerVariable( + variable_name="upload_date_index_reversed", + definition=f"{{%sub(100, {self.upload_date_index.variable_name})}}", + ) - @property - def upload_date_index_reversed_padded(self: "VariableDefinitions") -> Variable: + @cached_property + def upload_date_index_reversed_padded(self: "VariableDefinitions") -> StringVariable: """ - The upload_date_index padded two digits + :description: + The upload_date_index padded two digits """ - return Variable("upload_date_index_reversed_padded") + return self.upload_date_index_reversed.to_padded_int( + variable_name="upload_date_index_reversed_padded", pad=2 + ) class EntryVariableDefinitions(ABC): - @property - def uid(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def uid(self: "VariableDefinitions") -> StringMetadataVariable: """ - The entry's unique ID + :description: + The entry's unique ID """ - return MetadataVariable(metadata_key="id", variable_name="uid") + return StringMetadataVariable.from_entry(metadata_key="id", variable_name="uid") - @property - def duration(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def duration(self: "VariableDefinitions") -> IntegerMetadataVariable: """ - The duration of the entry in seconds + :description: + The duration of the entry in seconds if it exists. Defaults to zero otherwise. """ - return MetadataVariable("duration", metadata_key="duration") + return IntegerMetadataVariable.from_entry(metadata_key="duration", default=0) - @property - def uid_sanitized_plex(self: "VariableDefinitions") -> Variable: + @cached_property + def uid_sanitized_plex(self: "VariableDefinitions") -> StringVariable: """ - 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. + :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. """ - return Variable("uid_sanitized_plex") + return self.uid.to_sanitized_plex(variable_name="uid_sanitized_plex") - @property - def ie_key(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def ie_key(self: "VariableDefinitions") -> StringMetadataVariable: """ - The ie_key, used in legacy yt-dlp things as the 'info-extractor key' + :description: + The ie_key, used in legacy yt-dlp things as the 'info-extractor key'. + If it does not exist, return ``extractor_key`` """ - return MetadataVariable(metadata_key="ie_key", variable_name="ie_key") + return StringMetadataVariable.from_entry(metadata_key="ie_key", default=self.extractor_key) - @property - def extractor_key(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def extractor_key(self: "VariableDefinitions") -> StringMetadataVariable: """ - The yt-dlp extractor key + :description: + The yt-dlp extractor key """ - return MetadataVariable(metadata_key="extractor_key", variable_name="extractor_key") + return StringMetadataVariable.from_entry(metadata_key="extractor_key") - @property - def extractor(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def extractor(self: "VariableDefinitions") -> StringMetadataVariable: """ - The yt-dlp extractor name + :description: + The yt-dlp extractor name """ - return MetadataVariable(variable_name="extractor", metadata_key="extractor") + return StringMetadataVariable.from_entry(metadata_key="extractor") - @property - def epoch(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def epoch(self: "VariableDefinitions") -> IntegerMetadataVariable: """ - The unix epoch of when the metadata was scraped by yt-dlp. + :description: + The unix epoch of when the metadata was scraped by yt-dlp. """ - return MetadataVariable(metadata_key="epoch", variable_name="epoch") + return IntegerMetadataVariable.from_entry(metadata_key="epoch") - @property - def epoch_date(self: "VariableDefinitions") -> Variable: + @cached_property + def epoch_date(self: "VariableDefinitions") -> StringVariable: """ - The epoch's date, in YYYYMMDD format. + :description: + The epoch's date, in YYYYMMDD format. """ - return Variable("epoch_date") + return StringVariable( + variable_name="epoch_date", + definition=f"{{%datetime_strftime({self.epoch.variable_name}, '%Y%m%d')}}", + ) - @property - def epoch_hour(self: "VariableDefinitions") -> Variable: + @cached_property + def epoch_hour(self: "VariableDefinitions") -> StringVariable: """ - The epoch's hour + :description: + The epoch's hour """ - return Variable("epoch_hour") + return StringVariable( + variable_name="epoch_hour", + definition=f"{{%datetime_strftime({self.epoch.variable_name}, '%H')}}", + ) - @property - def title(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def title(self: "VariableDefinitions") -> StringMetadataVariable: """ - The title of the entry. If a title does not exist, returns its unique ID. + :description: + The title of the entry. If a title does not exist, returns its unique ID. """ - return MetadataVariable(variable_name="title", metadata_key="title") + return StringMetadataVariable.from_entry(metadata_key="title", default=self.uid) - @property - def title_sanitized_plex(self: "VariableDefinitions") -> Variable: + @cached_property + def title_sanitized_plex(self: "VariableDefinitions") -> StringVariable: """ - 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. + :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. """ - return Variable("title_sanitized_plex") + return self.title.to_sanitized_plex(variable_name="title_sanitized_plex") - @property - def webpage_url(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def webpage_url(self: "VariableDefinitions") -> StringMetadataVariable: """ - The url to the webpage. + :description: + The url to the webpage. """ - return MetadataVariable(metadata_key="webpage_url", variable_name="webpage_url") + return StringMetadataVariable.from_entry(metadata_key="webpage_url") - @property - def info_json_ext(self: "VariableDefinitions") -> Variable: + @cached_property + def info_json_ext(self: "VariableDefinitions") -> StringVariable: """ - The "info.json" extension + :description: + The "info.json" extension """ - return Variable("info_json_ext") + return StringVariable( + variable_name="info_json_ext", + definition="info.json", + ) - @property - def description(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def description(self: "VariableDefinitions") -> StringMetadataVariable: """ - The description if it exists. Otherwise, returns an emtpy string. + :description: + The description if it exists. Otherwise, returns an emtpy string. """ - return MetadataVariable(variable_name="description", metadata_key="description") + return StringMetadataVariable.from_entry( + metadata_key="description", + default="", + ) - @property - def uploader_id(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def uploader_id(self: "VariableDefinitions") -> StringMetadataVariable: """ - The uploader id if it exists, otherwise return the unique ID. + :description: + The uploader id if it exists, otherwise return the unique ID. """ - return MetadataVariable(variable_name="uploader_id", metadata_key="uploader_id") + return StringMetadataVariable.from_entry( + metadata_key="uploader_id", + default=self.uid, + ) - @property - def uploader(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def uploader(self: "VariableDefinitions") -> StringMetadataVariable: """ - The uploader if it exists, otherwise return the uploader ID. + :description: + The uploader if it exists, otherwise return the uploader ID. """ - return MetadataVariable(variable_name="uploader", metadata_key="uploader") + return StringMetadataVariable.from_entry( + metadata_key="uploader", + default=self.uploader_id, + ) - @property - def uploader_url(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def uploader_url(self: "VariableDefinitions") -> StringMetadataVariable: """ - The uploader url if it exists, otherwise returns the webpage_url. + :description: + The uploader url if it exists, otherwise returns the webpage_url. """ - return MetadataVariable("uploader_url", metadata_key="uploader_url") + return StringMetadataVariable.from_entry( + metadata_key="uploader_url", + default=self.webpage_url, + ) - @property - def creator(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def creator(self: "VariableDefinitions") -> StringMetadataVariable: """ - The creator name if it exists, otherwise returns the channel. + :description: + The creator name if it exists, otherwise returns the channel. """ - return MetadataVariable(variable_name="creator", metadata_key="creator") + return StringMetadataVariable.from_entry( + metadata_key="creator", + default=self.channel, + ) - @property - def channel(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def channel(self: "VariableDefinitions") -> StringMetadataVariable: """ - The channel name if it exists, otherwise returns the uploader. + :description: + The channel name if it exists, otherwise returns the uploader. """ - return MetadataVariable(variable_name="channel", metadata_key="channel") + return StringMetadataVariable.from_entry( + metadata_key="channel", + default=self.uploader, + ) - @property - def channel_id(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def channel_id(self: "VariableDefinitions") -> StringMetadataVariable: """ - The channel id if it exists, otherwise returns the entry uploader ID. + :description: + The channel id if it exists, otherwise returns the entry uploader ID. """ - return MetadataVariable(variable_name="channel_id", metadata_key="channel_id") + return StringMetadataVariable.from_entry( + metadata_key="channel_id", + default=self.uploader_id, + ) - @property - def ext(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def ext(self: "VariableDefinitions") -> StringMetadataVariable: """ - The downloaded entry's file extension + :description: + The downloaded entry's file extension """ - return MetadataVariable(variable_name="ext", metadata_key="ext") + return StringMetadataVariable.from_entry(metadata_key="ext") - @property - def thumbnail_ext(self: "VariableDefinitions") -> Variable: + @cached_property + def thumbnail_ext(self: "VariableDefinitions") -> StringVariable: """ - 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. + :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. """ - return Variable("thumbnail_ext") + return StringVariable( + variable_name="thumbnail_ext", + definition="jpg", + ) - @property - def comments(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def comments(self: "VariableDefinitions") -> ArrayMetadataVariable: """ - Comments if they are requested + :description: + Comments if they are requested """ - return MetadataVariable("comments", "comments") + return ArrayMetadataVariable( + metadata_key="comments", variable_name="comments", definition="{ [] }" + ) - @property - def chapters(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def chapters(self: "VariableDefinitions") -> ArrayMetadataVariable: """ - Chapters if they exist + :description: + Chapters if they exist """ - return MetadataVariable("chapters", "chapters") + return ArrayMetadataVariable( + metadata_key="chapters", variable_name="chapters", definition="{ [] }" + ) - @property - def sponsorblock_chapters(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def sponsorblock_chapters(self: "VariableDefinitions") -> ArrayMetadataVariable: """ - Sponsorblock Chapters if they are requested and exist + :description: + Sponsorblock Chapters if they are requested and exist """ - return MetadataVariable("sponsorblock_chapters", "sponsorblock_chapters") + return ArrayMetadataVariable( + metadata_key="sponsorblock_chapters", + variable_name="sponsorblock_chapters", + definition="{ [] }", + ) - @property - def requested_subtitles(self: "VariableDefinitions") -> MetadataVariable: + @cached_property + def requested_subtitles(self: "VariableDefinitions") -> MapMetadataVariable: """ - Subtitles if they are requested and exist + :description: + Subtitles if they are requested and exist """ - return MetadataVariable("requested_subtitles", "requested_subtitles") + return MapMetadataVariable( + metadata_key="requested_subtitles", + variable_name="requested_subtitles", + definition="{ {} }", + ) class VariableDefinitions( @@ -776,8 +1071,90 @@ class VariableDefinitions( ReleaseDateVariableDefinitions, YtdlSubVariableDefinitions, ): - pass + @cache + def scripts(self) -> Dict[str, str]: + """ + Returns all variables and their scripts in dict form + """ + return { + var.variable_name: var.definition + for var in [ + getattr(self, attr) + for attr in dir(self) + if isinstance(getattr(self, attr), Variable) + ] + } + + @cache + def injected_variables(self) -> Set[MetadataVariable]: + """ + Returns variables that get injected in the download-stage + """ + return { + self.download_index, + self.upload_date_index, + self.comments, + self.requested_subtitles, + self.chapters, + self.sponsorblock_chapters, + self.ytdl_sub_input_url, + } + + @cache + def required_entry_variables(self) -> Set[MetadataVariable]: + """ + Returns variables that the entry requires to exist + """ + return { + self.uid, + self.extractor_key, + self.epoch, + self.webpage_url, + self.ext, + } + + @cache + def default_entry_variables(self) -> Set[MetadataVariable]: + """ + Returns variables that reside on the entry that may or may not exist, + but have defaults + """ + return { + self.title, + self.extractor, + self.description, + self.ie_key, + self.uploader_id, + self.uploader, + self.uploader_url, + self.upload_date, + self.release_date, + self.channel, + self.creator, + self.channel_id, + self.duration, + self.playlist_index, + self.playlist_count, + self.playlist_uid, + self.playlist_title, + self.playlist_uploader_id, + } + + @cache + def unresolvable_static_variables(self) -> Set[Variable]: + """ + Returns variables that are not static (i.e. depend on runtime) + """ + return { + VARIABLES.entry_metadata, + } | self.injected_variables() -# Singleton to use externally +# Singletons to use externally VARIABLES: VariableDefinitions = VariableDefinitions() +VARIABLE_SCRIPTS: Dict[str, str] = VARIABLES.scripts() +UNRESOLVED_VARIABLES: Set[str] = { + var.variable_name for var in VARIABLES.unresolvable_static_variables() +} + +CustomFunctions.register() diff --git a/src/ytdl_sub/entries/script/variable_scripts.py b/src/ytdl_sub/entries/script/variable_scripts.py deleted file mode 100644 index 90808c37..00000000 --- a/src/ytdl_sub/entries/script/variable_scripts.py +++ /dev/null @@ -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() diff --git a/src/ytdl_sub/entries/script/variable_types.py b/src/ytdl_sub/entries/script/variable_types.py new file mode 100644 index 00000000..7cca58cd --- /dev/null +++ b/src/ytdl_sub/entries/script/variable_types.py @@ -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, + ) diff --git a/src/ytdl_sub/entries/variables/override_variables.py b/src/ytdl_sub/entries/variables/override_variables.py index 4e06d441..f3ee78b1 100644 --- a/src/ytdl_sub/entries/variables/override_variables.py +++ b/src/ytdl_sub/entries/variables/override_variables.py @@ -1,5 +1,5 @@ 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.utils.name_validation import is_valid_name diff --git a/src/ytdl_sub/utils/scriptable.py b/src/ytdl_sub/utils/scriptable.py index 105bce98..aba9d1fc 100644 --- a/src/ytdl_sub/utils/scriptable.py +++ b/src/ytdl_sub/utils/scriptable.py @@ -5,9 +5,9 @@ from typing import Dict from typing import Set 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_scripts import UNRESOLVED_VARIABLES -from ytdl_sub.entries.script.variable_scripts import VARIABLE_SCRIPTS +from ytdl_sub.entries.script.variable_definitions import UNRESOLVED_VARIABLES +from ytdl_sub.entries.script.variable_definitions import VARIABLE_SCRIPTS +from ytdl_sub.entries.script.variable_types import Variable from ytdl_sub.script.script import Script from ytdl_sub.script.utils.exceptions import RuntimeException from ytdl_sub.utils.exceptions import StringFormattingException diff --git a/tools/docgen/entry_variables.py b/tools/docgen/entry_variables.py index 02cca731..df53b13a 100644 --- a/tools/docgen/entry_variables.py +++ b/tools/docgen/entry_variables.py @@ -4,11 +4,12 @@ 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 properties from tools.docgen.utils import section +from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.entries.script.variable_definitions import VariableDefinitions @@ -37,10 +38,11 @@ class EntryVariablesDocGen(DocGen): docs += line_section(section_idx=idx) docs += section(name, level=1) - for variable_function_name in properties(parent_objs[name]): + 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, ) diff --git a/tools/docgen/utils.py b/tools/docgen/utils.py index 94cab101..81ddc3fa 100644 --- a/tools/docgen/utils.py +++ b/tools/docgen/utils.py @@ -1,4 +1,5 @@ import inspect +from functools import cached_property from typing import Any from typing import Dict from typing import List @@ -18,6 +19,10 @@ 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) @@ -36,11 +41,16 @@ def camel_case_to_human(string: str) -> str: def get_function_docs( - function_name: str, obj: Any, level: int, display_function_name: Optional[str] = None + 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