diff --git a/src/ytdl_sub/config/preset.py b/src/ytdl_sub/config/preset.py index e88e4fc0..e1b4c6c3 100644 --- a/src/ytdl_sub/config/preset.py +++ b/src/ytdl_sub/config/preset.py @@ -20,6 +20,7 @@ from ytdl_sub.config.preset_options import TOptionsValidator from ytdl_sub.config.preset_options import YTDLOptions from ytdl_sub.downloaders.url.validators import MultiUrlValidator from ytdl_sub.entries.entry import Entry +from ytdl_sub.entries.script.variable_scripts import VARIABLE_SCRIPTS from ytdl_sub.prebuilt_presets import PREBUILT_PRESET_NAMES from ytdl_sub.prebuilt_presets import PUBLISHED_PRESET_NAMES from ytdl_sub.utils.exceptions import ValidationException @@ -156,7 +157,7 @@ class Preset(_PresetShell): @property def _source_variables(self) -> List[str]: - return Entry.source_variables() + return list(VARIABLE_SCRIPTS.keys()) def __validate_and_get_plugins(self) -> PresetPlugins: preset_plugins = PresetPlugins() @@ -201,29 +202,24 @@ class Preset(_PresetShell): # Set the formatter variables to be the overrides variable_dict = copy.deepcopy(self.overrides.dict_with_format_strings) - # If the formatter supports source variables, set the formatter variables to include - # both source and override variables - if not isinstance(formatter_validator, OverridesStringFormatterValidator): - source_variables = { - source_var: "dummy_string" - for source_var in self._source_variables - + self.downloader_options.added_source_variables() + source_variables = { + source_var: "dummy_string" + for source_var in self._source_variables + + self.downloader_options.added_source_variables() + } + variable_dict = dict(source_variables, **variable_dict) + + # For all plugins, add in any extra added source variables + # TODO: Check in order variables are added + for plugin_options in self.plugins.plugin_options: + added_plugin_variables = { + source_var: "dummy_string" for source_var in plugin_options.added_source_variables() } - variable_dict = dict(source_variables, **variable_dict) + # sanity check plugin variables do not override source variables + expected_len = len(variable_dict) + len(added_plugin_variables) + variable_dict = dict(variable_dict, **added_plugin_variables) - # For all plugins, add in any extra added source variables - for plugin_options in self.plugins.plugin_options: - added_plugin_variables = { - source_var: "dummy_string" - for source_var in plugin_options.added_source_variables() - } - # sanity check plugin variables do not override source variables - expected_len = len(variable_dict) + len(added_plugin_variables) - variable_dict = dict(variable_dict, **added_plugin_variables) - - assert ( - len(variable_dict) == expected_len - ), "plugin variables overwrote source variables" + assert len(variable_dict) == expected_len, "plugin variables overwrote source variables" _ = formatter_validator.apply_formatter(variable_dict=variable_dict) diff --git a/src/ytdl_sub/config/preset_options.py b/src/ytdl_sub/config/preset_options.py index b4738a8c..188eb6a3 100644 --- a/src/ytdl_sub/config/preset_options.py +++ b/src/ytdl_sub/config/preset_options.py @@ -1,3 +1,5 @@ +import copy +import json from abc import ABC from typing import Any from typing import Dict @@ -5,10 +7,13 @@ from typing import List from typing import Optional from typing import TypeVar +import mergedeep from yt_dlp.utils import sanitize_filename from ytdl_sub.config.defaults import DEFAULT_DOWNLOAD_ARCHIVE_NAME from ytdl_sub.entries.entry import Entry +from ytdl_sub.entries.script.variable_definitions import VARIABLES +from ytdl_sub.entries.script.variable_scripts import VARIABLE_SCRIPTS from ytdl_sub.entries.variables.override_variables import SUBSCRIPTION_NAME from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.validators.file_path_validators import OverridesStringFormatterFilePathValidator @@ -212,11 +217,13 @@ class Overrides(DictFormatterValidator): ------- The format_string after .format has been called """ - variable_dict = self.dict_with_format_strings + variable_dict = copy.deepcopy(VARIABLE_SCRIPTS) + mergedeep.merge(variable_dict, self.dict_with_format_strings) + if entry: - variable_dict = dict(entry.to_dict(), **variable_dict) + mergedeep.merge(variable_dict, {VARIABLES.entry_metadata.variable_name: json.dumps(entry._kwargs)}) if function_overrides: - variable_dict = dict(variable_dict, **function_overrides) + mergedeep.merge(variable_dict, function_overrides) return formatter.apply_formatter(variable_dict) diff --git a/src/ytdl_sub/downloaders/url/validators.py b/src/ytdl_sub/downloaders/url/validators.py index 2f799873..cfca4dad 100644 --- a/src/ytdl_sub/downloaders/url/validators.py +++ b/src/ytdl_sub/downloaders/url/validators.py @@ -281,7 +281,7 @@ class MultiUrlValidator(OptionsValidator): # Ensure at least URL is non-empty has_non_empty_url = False for url_validator in self.urls.list: - has_non_empty_url |= bool(url_validator.url.apply_formatter(override_variables)) + has_non_empty_url |= bool(url_validator.url.apply_formatter(base_variables)) if not has_non_empty_url: raise self._validation_exception("Must contain at least one url that is non-empty") diff --git a/src/ytdl_sub/entries/entry.py b/src/ytdl_sub/entries/entry.py index 0538c66c..cd7e5aad 100644 --- a/src/ytdl_sub/entries/entry.py +++ b/src/ytdl_sub/entries/entry.py @@ -6,12 +6,11 @@ from typing import Optional from typing import final from ytdl_sub.entries.base_entry import BaseEntry -from ytdl_sub.entries.variables.entry_variables import EntryVariables from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS from ytdl_sub.validators.audo_codec_validator import VIDEO_CODEC_EXTS -class Entry(EntryVariables, BaseEntry): +class Entry(BaseEntry): """ Entry object to represent a single media object returned from yt-dlp. """ diff --git a/src/ytdl_sub/entries/script/function_scripts.py b/src/ytdl_sub/entries/script/function_scripts.py index 40f680a3..939b642d 100644 --- a/src/ytdl_sub/entries/script/function_scripts.py +++ b/src/ytdl_sub/entries/script/function_scripts.py @@ -110,7 +110,8 @@ class CustomFunctions: @staticmethod def register(): - Functions.register_function(CustomFunctions.legacy_bracket_safety) - Functions.register_function(CustomFunctions.sanitize) - Functions.register_function(CustomFunctions.sanitize_plex_episode) - Functions.register_function(CustomFunctions.to_date_metadata) + if not Functions.is_built_in("sanitize"): + Functions.register_function(CustomFunctions.legacy_bracket_safety) + Functions.register_function(CustomFunctions.sanitize) + Functions.register_function(CustomFunctions.sanitize_plex_episode) + Functions.register_function(CustomFunctions.to_date_metadata) diff --git a/src/ytdl_sub/entries/script/variable_scripts.py b/src/ytdl_sub/entries/script/variable_scripts.py index 8c81b1f2..f273606e 100644 --- a/src/ytdl_sub/entries/script/variable_scripts.py +++ b/src/ytdl_sub/entries/script/variable_scripts.py @@ -98,6 +98,9 @@ def source_get_int(key: MetadataVariable, default: Optional[Variable | int] = No ############################################################################################### # Scripts +ENTRY_EMPTY_METADATA: Dict[Variable, str] = { + v.entry_metadata: "{ {} }" +} ENTRY_HARDCODED_VARIABLES: Dict[Variable, str] = { v.info_json_ext: "info.json", @@ -236,6 +239,7 @@ SOURCE_DERIVED_VARIABLES: Dict[Variable, str] = { _VARIABLE_SCRIPTS: Dict[Variable, str] = {} mergedeep.merge( _VARIABLE_SCRIPTS, + ENTRY_EMPTY_METADATA, ENTRY_HARDCODED_VARIABLES, ENTRY_REQUIRED_VARIABLES, ENTRY_DEFAULT_VARIABLES, diff --git a/src/ytdl_sub/entries/variables/entry_variables.py b/src/ytdl_sub/entries/variables/entry_variables.py deleted file mode 100644 index d907f5de..00000000 --- a/src/ytdl_sub/entries/variables/entry_variables.py +++ /dev/null @@ -1,857 +0,0 @@ -from datetime import datetime -from typing import Union - -from yt_dlp.utils import sanitize_filename - -from ytdl_sub.entries.base_entry import BaseEntry -from ytdl_sub.entries.base_entry import BaseEntryVariables -from ytdl_sub.entries.variables.kwargs import CHANNEL -from ytdl_sub.entries.variables.kwargs import CHANNEL_ID -from ytdl_sub.entries.variables.kwargs import CREATOR -from ytdl_sub.entries.variables.kwargs import DOWNLOAD_INDEX -from ytdl_sub.entries.variables.kwargs import EXT -from ytdl_sub.entries.variables.kwargs import PLAYLIST_COUNT -from ytdl_sub.entries.variables.kwargs import PLAYLIST_DESCRIPTION -from ytdl_sub.entries.variables.kwargs import PLAYLIST_INDEX -from ytdl_sub.entries.variables.kwargs import PLAYLIST_MAX_UPLOAD_YEAR -from ytdl_sub.entries.variables.kwargs import PLAYLIST_MAX_UPLOAD_YEAR_TRUNCATED -from ytdl_sub.entries.variables.kwargs import PLAYLIST_TITLE -from ytdl_sub.entries.variables.kwargs import PLAYLIST_UID -from ytdl_sub.entries.variables.kwargs import PLAYLIST_UPLOADER -from ytdl_sub.entries.variables.kwargs import PLAYLIST_UPLOADER_ID -from ytdl_sub.entries.variables.kwargs import PLAYLIST_UPLOADER_URL -from ytdl_sub.entries.variables.kwargs import PLAYLIST_WEBPAGE_URL -from ytdl_sub.entries.variables.kwargs import RELEASE_DATE -from ytdl_sub.entries.variables.kwargs import SOURCE_COUNT -from ytdl_sub.entries.variables.kwargs import SOURCE_DESCRIPTION -from ytdl_sub.entries.variables.kwargs import SOURCE_INDEX -from ytdl_sub.entries.variables.kwargs import SOURCE_TITLE -from ytdl_sub.entries.variables.kwargs import SOURCE_UID -from ytdl_sub.entries.variables.kwargs import SOURCE_UPLOADER -from ytdl_sub.entries.variables.kwargs import SOURCE_UPLOADER_ID -from ytdl_sub.entries.variables.kwargs import SOURCE_UPLOADER_URL -from ytdl_sub.entries.variables.kwargs import SOURCE_WEBPAGE_URL -from ytdl_sub.entries.variables.kwargs import UPLOAD_DATE -from ytdl_sub.entries.variables.kwargs import UPLOAD_DATE_INDEX - -# 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 - - -def pad(num: int, width: int = 2): - """Pad integers""" - return str(num).zfill(width) - - -_days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] - -Self = Union[BaseEntry, "EntryVariables"] - - -class EntryVariables(BaseEntryVariables): - @property - def source_title(self: Self) -> str: - """ - Returns - ------- - str - Name of the source (i.e. channel with multiple playlists) if it exists, otherwise - returns its playlist_title. - """ - return self.kwargs_get(SOURCE_TITLE, self.playlist_title) - - @property - def source_title_sanitized(self: Self) -> str: - """ - Returns - ------- - str - The source title, sanitized - """ - return sanitize_filename(self.source_title) - - @property - def source_uid(self: Self) -> str: - """ - Returns - ------- - str - The source unique id if it exists, otherwise returns the playlist unique ID. - """ - return self.kwargs_get(SOURCE_UID, self.playlist_uid) - - @property - def source_index(self: Self) -> int: - """ - Returns - ------- - int - 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). - """ - return self.kwargs_get(SOURCE_INDEX, self.playlist_index) - - @property - def source_index_padded(self: Self) -> str: - """ - Returns - ------- - int - The source index, padded. - """ - return pad(self.source_index, 2) - - @property - def source_count(self: Self) -> int: - """ - Returns - ------- - int - The source count if it exists, otherwise returns 1. - """ - return self.kwargs_get(SOURCE_COUNT, self.playlist_count) - - @property - def source_webpage_url(self: Self) -> str: - """ - Returns - ------- - str - The source webpage url if it exists, otherwise returns the playlist webpage url. - """ - return self.kwargs_get(SOURCE_WEBPAGE_URL, self.playlist_webpage_url) - - @property - def source_description(self: Self) -> str: - """ - Returns - ------- - str - The source description if it exists, otherwise returns the playlist description. - """ - return self.kwargs_get(SOURCE_DESCRIPTION, self.playlist_description) - - @property - def playlist_uid(self: Self) -> str: - """ - Returns - ------- - str - The playlist unique ID if it exists, otherwise return the entry unique ID. - """ - return self.kwargs_get(PLAYLIST_UID, self.uid) - - @property - def playlist_title(self: Self) -> str: - """ - Returns - ------- - str - Name of its parent playlist/channel if it exists, otherwise returns its title. - """ - return self.kwargs_get(PLAYLIST_TITLE, self.title) - - @property - def playlist_title_sanitized(self: Self) -> str: - """ - Returns - ------- - str - The playlist name, sanitized - """ - return sanitize_filename(self.playlist_title) - - @property - def playlist_index(self: Self) -> int: - """ - Returns - ------- - int - 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 self.kwargs_get(PLAYLIST_INDEX, 1) - - @property - def playlist_index_reversed(self: Self) -> int: - """ - Returns - ------- - int - Playlist index reversed via ``playlist_count - playlist_index + 1`` - """ - return self.playlist_count - self.playlist_index + 1 - - @property - def playlist_index_padded(self: Self) -> str: - """ - Returns - ------- - str - playlist_index padded two digits - """ - return pad(self.playlist_index, width=2) - - @property - def playlist_index_reversed_padded(self: Self) -> str: - """ - Returns - ------- - str - playlist_index_reversed padded two digits - """ - return pad(self.playlist_index_reversed, width=2) - - @property - def playlist_index_padded6(self: Self) -> str: - """ - Returns - ------- - str - playlist_index padded six digits. - """ - return pad(self.playlist_index, width=6) - - @property - def playlist_index_reversed_padded6(self: Self) -> str: - """ - Returns - ------- - str - playlist_index_reversed padded six digits. - """ - return pad(self.playlist_index_reversed, width=6) - - @property - def playlist_count(self: Self) -> int: - """ - Returns - ------- - int - 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 self.kwargs_get(PLAYLIST_COUNT, 1) - - @property - def playlist_description(self: Self) -> str: - """ - Returns - ------- - str - The playlist description if it exists, otherwise returns the entry's description. - """ - return self.kwargs_get(PLAYLIST_DESCRIPTION, self.description) - - @property - def playlist_webpage_url(self: Self) -> str: - """ - Returns - ------- - str - The playlist webpage url if it exists. Otherwise, returns the entry webpage url. - """ - return self.kwargs_get(PLAYLIST_WEBPAGE_URL, self.webpage_url) - - @property - def playlist_max_upload_year(self: Self) -> int: - """ - Returns - ------- - int - Max upload_year for all entries in this entry's playlist if it exists, otherwise returns - ``upload_year`` - """ - # override in EntryParent - return self.kwargs_get(PLAYLIST_MAX_UPLOAD_YEAR, self.upload_year) - - @property - def playlist_max_upload_year_truncated(self: Self) -> int: - """ - Returns - ------- - int - The max playlist truncated upload year for all entries in this entry's playlist if it - exists, otherwise returns ``upload_year_truncated``. - """ - return self.kwargs_get(PLAYLIST_MAX_UPLOAD_YEAR_TRUNCATED, self.upload_year_truncated) - - @property - def playlist_uploader_id(self: Self) -> str: - """ - Returns - ------- - str - The playlist uploader id if it exists, otherwise returns the entry uploader ID. - """ - return self.kwargs_get(PLAYLIST_UPLOADER_ID, self.uploader_id) - - @property - def playlist_uploader(self: Self) -> str: - """ - Returns - ------- - str - The playlist uploader if it exists, otherwise return the entry uploader. - """ - return self.kwargs_get(PLAYLIST_UPLOADER, self.uploader) - - @property - def playlist_uploader_sanitized(self: Self) -> str: - """ - Returns - ------- - str - The playlist uploader, sanitized. - """ - return sanitize_filename(self.playlist_uploader) - - @property - def playlist_uploader_url(self: Self) -> str: - """ - Returns - ------- - str - The playlist uploader url if it exists, otherwise returns the playlist webpage_url. - """ - return self.kwargs_get(PLAYLIST_UPLOADER_URL, self.playlist_webpage_url) - - @property - def source_uploader_id(self: Self) -> str: - """ - Returns - ------- - str - The source uploader id if it exists, otherwise returns the playlist_uploader_id - """ - return self.kwargs_get(SOURCE_UPLOADER_ID, self.playlist_uploader_id) - - @property - def source_uploader(self: Self) -> str: - """ - Returns - ------- - str - The source uploader if it exists, otherwise return the playlist_uploader - """ - return self.kwargs_get(SOURCE_UPLOADER, self.playlist_uploader) - - @property - def source_uploader_url(self: Self) -> str: - """ - Returns - ------- - str - The source uploader url if it exists, otherwise returns the source webpage_url. - """ - return self.kwargs_get(SOURCE_UPLOADER_URL, self.source_webpage_url) - - @property - def creator(self: Self) -> str: - """ - Returns - ------- - str - The creator name if it exists, otherwise returns the channel. - """ - return self.kwargs_get(CREATOR, self.channel) - - @property - def creator_sanitized(self: Self) -> str: - """ - Returns - ------- - str - The creator name, sanitized - """ - return sanitize_filename(self.creator) - - @property - def channel(self: Self) -> str: - """ - Returns - ------- - str - The channel name if it exists, otherwise returns the uploader. - """ - return self.kwargs_get(CHANNEL, self.uploader) - - @property - def channel_sanitized(self: Self) -> str: - """ - Returns - ------- - str - The channel name, sanitized. - """ - return sanitize_filename(self.channel) - - @property - def channel_id(self: Self) -> str: - """ - Returns - ------- - str - The channel id if it exists, otherwise returns the entry uploader ID. - """ - return self.kwargs_get(CHANNEL_ID, self.uploader_id) - - @property - def ext(self: Self) -> str: - """ - Returns - ------- - str - The downloaded entry's file extension - """ - return self.kwargs(EXT) - - @property - def thumbnail_ext(self: Self) -> str: - """ - Returns - ------- - str - 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 "jpg" - - @property - def download_index(self: Self) -> int: - """ - Returns - ------- - int - The i'th entry downloaded. NOTE that this is fetched dynamically from the download - archive. - """ - return self.kwargs_get(DOWNLOAD_INDEX, 0) + 1 - - @property - def download_index_padded6(self: Self) -> str: - """ - Returns - ------- - str - The download_index padded six digits - """ - return pad(self.download_index, 6) - - @property - def upload_date_index(self: Self) -> int: - """ - Returns - ------- - int - The i'th entry downloaded with this upload date. - """ - return self.kwargs_get(UPLOAD_DATE_INDEX, 0) + 1 - - @property - def upload_date_index_padded(self: Self) -> str: - """ - Returns - ------- - int - The upload_date_index padded two digits - """ - return pad(self.upload_date_index, 2) - - @property - def upload_date_index_reversed(self: Self) -> int: - """ - Returns - ------- - int - 100 - upload_date_index - """ - return 100 - self.upload_date_index - - @property - def upload_date_index_reversed_padded(self: Self) -> str: - """ - Returns - ------- - int - The upload_date_index padded two digits - """ - return pad(self.upload_date_index_reversed, 2) - - @property - def upload_date(self: Self) -> str: - """ - Returns - ------- - str - The entry's uploaded date, in YYYYMMDD format. If not present, return today's date. - """ - return self.kwargs_get(UPLOAD_DATE, datetime.now().strftime("%Y%m%d")) - - @property - def upload_year(self: Self) -> int: - """ - Returns - ------- - int - The entry's upload year - """ - return int(self.upload_date[:4]) - - @property - def upload_year_truncated(self: Self) -> int: - """ - Returns - ------- - int - The last two digits of the upload year, i.e. 22 in 2022 - """ - return int(str(self.upload_year)[-2:]) - - @property - def upload_year_truncated_reversed(self: Self) -> int: - """ - Returns - ------- - int - The upload year truncated, but reversed using ``100 - {upload_year_truncated}``, i.e. - 2022 returns ``100 - 22`` = ``78`` - """ - return 100 - self.upload_year_truncated - - @property - def upload_month_reversed(self: Self) -> int: - """ - Returns - ------- - int - The upload month, but reversed using ``13 - {upload_month}``, i.e. March returns ``10`` - """ - return 13 - self.upload_month - - @property - def upload_month_reversed_padded(self: Self) -> str: - """ - Returns - ------- - str - The reversed upload month, but padded. i.e. November returns "02" - """ - return pad(self.upload_month_reversed) - - @property - def upload_month_padded(self: Self) -> str: - """ - Returns - ------- - str - The entry's upload month padded to two digits, i.e. March returns "03" - """ - return self.upload_date[4:6] - - @property - def upload_day_padded(self: Self) -> str: - """ - Returns - ------- - str - The entry's upload day padded to two digits, i.e. the fifth returns "05" - """ - return self.upload_date[6:8] - - @property - def upload_month(self: Self) -> int: - """ - Returns - ------- - int - The upload month as an integer (no padding). - """ - return int(self.upload_month_padded.lstrip("0")) - - @property - def upload_day(self: Self) -> int: - """ - Returns - ------- - int - The upload day as an integer (no padding). - """ - return int(self.upload_day_padded.lstrip("0")) - - @property - def upload_day_reversed(self: Self) -> int: - """ - Returns - ------- - int - 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`` - """ - total_days_in_month = _days_in_month[self.upload_month] - if self.upload_month == 2 and self.upload_year % 4 == 0: # leap year - total_days_in_month += 1 - - return total_days_in_month + 1 - self.upload_day - - @property - def upload_day_reversed_padded(self: Self) -> str: - """ - Returns - ------- - str - The reversed upload day, but padded. i.e. August 30th returns "02". - """ - return pad(self.upload_day_reversed) - - @property - def upload_day_of_year(self: Self) -> int: - """ - Returns - ------- - int - The day of the year, i.e. February 1st returns ``32`` - """ - output = sum(_days_in_month[: self.upload_month]) + self.upload_day - if self.upload_month > 2 and self.upload_year % 4 == 0: - output += 1 - - return output - - @property - def upload_day_of_year_padded(self: Self) -> str: - """ - Returns - ------- - str - The upload day of year, but padded i.e. February 1st returns "032" - """ - return pad(self.upload_day_of_year, width=3) - - @property - def upload_day_of_year_reversed(self: Self) -> int: - """ - Returns - ------- - int - 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`` - """ - total_days_in_year = 365 - if self.upload_year % 4 == 0: - total_days_in_year += 1 - - return total_days_in_year + 1 - self.upload_day_of_year - - @property - def upload_day_of_year_reversed_padded(self: Self) -> str: - """ - Returns - ------- - str - The reversed upload day of year, but padded i.e. December 31st returns "001" - """ - return pad(self.upload_day_of_year_reversed, width=3) - - @property - def upload_date_standardized(self: Self) -> str: - """ - Returns - ------- - str - The uploaded date formatted as YYYY-MM-DD - """ - return f"{self.upload_year}-{self.upload_month_padded}-{self.upload_day_padded}" - - @property - def release_date(self: Self) -> str: - """ - Returns - ------- - str - The entry's release date, in YYYYMMDD format. If not present, return the upload date. - """ - return self.kwargs_get(RELEASE_DATE, self.upload_date) - - @property - def release_year(self: Self) -> int: - """ - Returns - ------- - int - The entry's release year - """ - return int(self.release_date[:4]) - - @property - def release_year_truncated(self: Self) -> int: - """ - Returns - ------- - int - The last two digits of the release year, i.e. 22 in 2022 - """ - return int(str(self.release_year)[-2:]) - - @property - def release_year_truncated_reversed(self: Self) -> int: - """ - Returns - ------- - int - The release year truncated, but reversed using ``100 - {release_year_truncated}``, i.e. - 2022 returns ``100 - 22`` = ``78`` - """ - return 100 - self.release_year_truncated - - @property - def release_month_reversed(self: Self) -> int: - """ - Returns - ------- - int - The release month, but reversed - using ``13 - {release_month}``, i.e. March returns ``10`` - """ - return 13 - self.release_month - - @property - def release_month_reversed_padded(self: Self) -> str: - """ - Returns - ------- - str - The reversed release month, but padded. i.e. November returns "02" - """ - return pad(self.release_month_reversed) - - @property - def release_month_padded(self: Self) -> str: - """ - Returns - ------- - str - The entry's release month padded to two digits, i.e. March returns "03" - """ - return self.release_date[4:6] - - @property - def release_day_padded(self: Self) -> str: - """ - Returns - ------- - str - The entry's release day padded to two digits, i.e. the fifth returns "05" - """ - return self.release_date[6:8] - - @property - def release_month(self: Self) -> int: - """ - Returns - ------- - int - The release month as an integer (no padding). - """ - return int(self.release_month_padded.lstrip("0")) - - @property - def release_day(self: Self) -> int: - """ - Returns - ------- - int - The release day as an integer (no padding). - """ - return int(self.release_day_padded.lstrip("0")) - - @property - def release_day_reversed(self: Self) -> int: - """ - Returns - ------- - int - 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`` - """ - total_days_in_month = _days_in_month[self.release_month] - if self.release_month == 2 and self.release_year % 4 == 0: # leap year - total_days_in_month += 1 - - return total_days_in_month + 1 - self.release_day - - @property - def release_day_reversed_padded(self: Self) -> str: - """ - Returns - ------- - str - The reversed release day, but padded. i.e. August 30th returns "02". - """ - return pad(self.release_day_reversed) - - @property - def release_day_of_year(self: Self) -> int: - """ - Returns - ------- - int - The day of the year, i.e. February 1st returns ``32`` - """ - output = sum(_days_in_month[: self.release_month]) + self.release_day - if self.release_month > 2 and self.release_year % 4 == 0: - output += 1 - - return output - - @property - def release_day_of_year_padded(self: Self) -> str: - """ - Returns - ------- - str - The release day of year, but padded i.e. February 1st returns "032" - """ - return pad(self.release_day_of_year, width=3) - - @property - def release_day_of_year_reversed(self: Self) -> int: - """ - Returns - ------- - int - 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`` - """ - total_days_in_year = 365 - if self.release_year % 4 == 0: - total_days_in_year += 1 - - return total_days_in_year + 1 - self.release_day_of_year - - @property - def release_day_of_year_reversed_padded(self: Self) -> str: - """ - Returns - ------- - str - The reversed release day of year, but padded i.e. December 31st returns "001" - """ - return pad(self.release_day_of_year_reversed, width=3) - - @property - def release_date_standardized(self: Self) -> str: - """ - Returns - ------- - str - The release date formatted as YYYY-MM-DD - """ - return f"{self.release_year}-{self.release_month_padded}-{self.release_day_padded}" diff --git a/src/ytdl_sub/script/parser.py b/src/ytdl_sub/script/parser.py index 27103693..4e7356ab 100644 --- a/src/ytdl_sub/script/parser.py +++ b/src/ytdl_sub/script/parser.py @@ -1,3 +1,4 @@ +import json from enum import Enum from typing import Dict from typing import List @@ -159,6 +160,8 @@ class _Parser: self._error_highlight_pos = pos if pos is not None else self._pos def _read(self, increment_pos: bool = True, length: int = 1) -> Optional[str]: + if isinstance(self._text, int): + pass if self._pos >= len(self._text): return None @@ -562,7 +565,7 @@ def parse( Entrypoint for parsing ytdl-sub code into a Syntax Tree """ return _Parser( - text=text, + text=json.dumps(text) if not isinstance(text, str) else text, name=name, custom_function_names=custom_function_names, variable_names=variable_names, diff --git a/src/ytdl_sub/validators/string_formatter_validators.py b/src/ytdl_sub/validators/string_formatter_validators.py index 8891604d..e0f114f1 100644 --- a/src/ytdl_sub/validators/string_formatter_validators.py +++ b/src/ytdl_sub/validators/string_formatter_validators.py @@ -7,6 +7,10 @@ from typing import final from yt_dlp.utils import sanitize_filename +from ytdl_sub.entries.script.variable_definitions import VARIABLES +from ytdl_sub.script.parser import parse +from ytdl_sub.script.script import Script +from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.utils.exceptions import InvalidVariableNameException from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException @@ -74,54 +78,10 @@ class StringFormatterValidator(StringValidator): """ _expected_value_type_name = "format string" - _variable_not_found_error_msg_formatter = ( - "Format variable '{variable_name}' does not exist. Available variables: {available_fields}" - ) - - _max_format_recursion = 8 - - def __validate_and_get_format_variables(self) -> List[str]: - """ - Returns - ------- - list[str] - List of format variables in the format string - - Raises - ------ - ValidationException - If the format string contains invalid variable formatting - """ - open_bracket_count = self.format_string.count("{") - close_bracket_count = self.format_string.count("}") - - if open_bracket_count != close_bracket_count: - raise self._validation_exception( - "Brackets are reserved for {variable_names} and should contain " - "a single open and close bracket.", - exception_class=StringFormattingException, - ) - - format_variables: List[str] = list(re.findall(_fields_validator, self.format_string)) - - if len(format_variables) != open_bracket_count: - raise self._validation_exception( - error_message=_fields_validator_exception_message, - exception_class=StringFormattingException, - ) - - for variable in format_variables: - if iskeyword(variable): - raise self._validation_exception( - f"'{variable}' is a Python keyword and cannot be used as a variable.", - exception_class=StringFormattingException, - ) - - return format_variables def __init__(self, name, value: str): super().__init__(name=name, value=value) - self.format_variables = self.__validate_and_get_format_variables() + _ = parse(str(value)) @final @property @@ -133,36 +93,12 @@ class StringFormatterValidator(StringValidator): """ return self._value - def _apply_formatter( - self, formatter: "StringFormatterValidator", variable_dict: Dict[str, str] - ) -> "StringFormatterValidator": - # Ensure the variable names exist within the entry and overrides - for variable_name in formatter.format_variables: - # If the variable exists, but is sanitized... - if ( - variable_name.endswith("_sanitized") - and variable_name.removesuffix("_sanitized") in variable_dict - ): - # Resolve just the non-sanitized version, then sanitize it - variable_dict[variable_name] = sanitize_filename( - StringFormatterValidator( - name=self._name, value=f"{{{variable_name.removesuffix('_sanitized')}}}" - ).apply_formatter(variable_dict) - ) - # If the variable doesn't exist, error - elif variable_name not in variable_dict: - available_fields = ", ".join(sorted(variable_dict.keys())) - raise self._validation_exception( - self._variable_not_found_error_msg_formatter.format( - variable_name=variable_name, available_fields=available_fields - ), - exception_class=StringFormattingVariableNotFoundException, - ) - - return StringFormatterValidator( - name=self._name, - value=formatter.format_string.format(**OrderedDict(variable_dict)), - ) + def _variable_dict(self, variable_dict: Dict[str, str]) -> Dict[str, str]: + sanitized_variables = { + f"{var_name}_sanitized": f"{{%sanitize({var_name})}}" + for var_name in variable_dict.keys() + } + return dict(variable_dict, **sanitized_variables) def apply_formatter(self, variable_dict: Dict[str, str]) -> str: """ @@ -177,23 +113,12 @@ class StringFormatterValidator(StringValidator): ------- Format string formatted """ - formatter = self - recursion_depth = 0 - max_depth = self._max_format_recursion - - while formatter.format_variables and recursion_depth < max_depth: - formatter = self._apply_formatter(formatter=formatter, variable_dict=variable_dict) - recursion_depth += 1 - - if formatter.format_variables: - raise self._validation_exception( - f"Attempted to format but failed after reaching max recursion depth of " - f"{max_depth}. Try to keep variables dependent on only one other variable at max. " - f"Unresolved variables: {', '.join(sorted(formatter.format_variables))}", - exception_class=StringFormattingException, - ) - - return formatter.format_string + out = ( + Script(self._variable_dict(variable_dict)) + .add({"tmp_var": self.format_string}) + .resolve()["tmp_var"] + ) + return str(out) # pylint: disable=line-too-long @@ -214,6 +139,31 @@ class OverridesStringFormatterValidator(StringFormatterValidator): "static string. Available override variables: {available_fields}" ) + def apply_formatter(self, variable_dict: Dict[str, str]) -> str: + """ + Calls `format` on the format string using the variable_dict as input kwargs + + Parameters + ---------- + variable_dict + kwargs to pass to the format string + + Returns + ------- + Format string formatted + """ + output = ( + Script(self._variable_dict(variable_dict)) + .add({"tmp_var": self.format_string}) + .resolve(unresolvable={VARIABLES.entry_metadata.variable_name}) + ) + if "tmp_var" not in output: + raise self._validation_exception( + "Has a dependency on entry variables when it is not allowed", + exception_class=StringFormattingVariableNotFoundException, + ) + return str(output["tmp_var"]) + # pylint: enable=line-too-long diff --git a/tests/conftest.py b/tests/conftest.py index aa35b6ed..442eda31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,7 @@ from resources import file_fixture_path from yt_dlp.utils import sanitize_filename from ytdl_sub.config.config_file import ConfigFile +from ytdl_sub.entries.script.function_scripts import CustomFunctions from ytdl_sub.subscriptions.subscription_download import SubscriptionDownload from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.logger import Logger @@ -26,6 +27,14 @@ from ytdl_sub.utils.logger import LoggerLevels from ytdl_sub.utils.yaml import load_yaml +@pytest.fixture(autouse=True) +def register_custom_functions(): + """ + Clean logs after every test + """ + CustomFunctions.register() + + @pytest.fixture(autouse=True) def cleanup_debug_file(): """