From fb7993cf70062cce849974df51cb1676d56df1fb Mon Sep 17 00:00:00 2001 From: jbannon Date: Sat, 9 Apr 2022 22:09:10 +0000 Subject: [PATCH] override format string, thumbnail hotfix --- ytdl_subscribe/subscriptions/subscription.py | 14 +++++- .../base/string_formatter_validators.py | 9 +++- ytdl_subscribe/validators/base/validators.py | 21 ++++++++- .../config/metadata_options/nfo_validator.py | 11 +++++ .../output_options_validator.py | 12 +++-- .../validators/config/preset_validator.py | 47 +++++++++++++++++++ ytdl_subscribe/validators/exceptions.py | 4 ++ 7 files changed, 111 insertions(+), 7 deletions(-) diff --git a/ytdl_subscribe/subscriptions/subscription.py b/ytdl_subscribe/subscriptions/subscription.py index d40ea49b..894d27e6 100644 --- a/ytdl_subscribe/subscriptions/subscription.py +++ b/ytdl_subscribe/subscriptions/subscription.py @@ -7,7 +7,6 @@ from typing import Generic from typing import Optional from typing import Type from typing import TypeVar -from typing import overload import dicttoxml import music_tag @@ -169,6 +168,19 @@ class Subscription(Generic[S], ABC): if self.output_options.thumbnail_name: source_thumbnail_path = entry.thumbnail_path(relative_directory=self.working_directory) + # Bug that mismatches webp and jpg extensions. Try to hotfix here + if not os.path.isfile(source_thumbnail_path): + actual_thumbnail_ext = ".webp" + if entry.thumbnail_ext == "webp": + actual_thumbnail_ext = ".jpg" + + source_thumbnail_path = source_thumbnail_path.replace( + f".{entry.thumbnail_ext}", actual_thumbnail_ext + ) + if not os.path.isfile(source_thumbnail_path): + # TODO: make more formal + raise ValueError("Youtube thumbnails are a lie") + output_thumbnail_name = self._apply_formatter( formatter=self.output_options.thumbnail_name, entry=entry ) diff --git a/ytdl_subscribe/validators/base/string_formatter_validators.py b/ytdl_subscribe/validators/base/string_formatter_validators.py index 39e6b240..49349a8e 100644 --- a/ytdl_subscribe/validators/base/string_formatter_validators.py +++ b/ytdl_subscribe/validators/base/string_formatter_validators.py @@ -8,6 +8,7 @@ from typing import final from ytdl_subscribe.validators.base.validators import LiteralDictValidator from ytdl_subscribe.validators.base.validators import Validator from ytdl_subscribe.validators.exceptions import StringFormattingException +from ytdl_subscribe.validators.exceptions import StringFormattingVariableNotFoundException class StringFormatterValidator(Validator): @@ -89,7 +90,7 @@ class StringFormatterValidator(Validator): raise self._validation_exception( f"Format variable '{variable_name}' does not exist. " f"Available variables: {available_fields}", - exception_class=StringFormattingException, + exception_class=StringFormattingVariableNotFoundException, ) return StringFormatterValidator( @@ -119,6 +120,12 @@ class StringFormatterValidator(Validator): return formatter.format_string +class OverridesStringFormatterValidator(StringFormatterValidator): + """ + A string formatter that should strictly use overrides that resolve without any entry variables. + """ + + class DictFormatterValidator(LiteralDictValidator): """ Validates a dictionary made up of key: string_formatters diff --git a/ytdl_subscribe/validators/base/validators.py b/ytdl_subscribe/validators/base/validators.py index 057556ed..a7142467 100644 --- a/ytdl_subscribe/validators/base/validators.py +++ b/ytdl_subscribe/validators/base/validators.py @@ -103,6 +103,10 @@ class DictValidator(Validator): _expected_value_type = dict _expected_value_type_name = "object" + def __init__(self, name, value): + super().__init__(name, value) + self.__validator_dict: Dict[str, Validator] = {} + @final @property def _dict(self) -> dict: @@ -113,6 +117,15 @@ class DictValidator(Validator): """ return self._value + @final + @property + def _validator_dict(self) -> Dict[str, Validator]: + """ + Returns dict containing names and validators of any keys that were validated. + This allows top-level validators to recursively search a dict validator. + """ + return self.__validator_dict + @final @property def _keys(self) -> List[str]: @@ -147,11 +160,15 @@ class DictValidator(Validator): if key not in self._dict and default is None: raise self._validation_exception(f"{key} is missing when it should be present.") - return validator( - name=f"{self._name}.{key}", + validator_name = f"{self._name}.{key}" + validator_instance = validator( + name=validator_name, value=self._dict.get(key, default), ) + self.__validator_dict[validator_name] = validator_instance + return validator_instance + @final def _validate_key_if_present( self, diff --git a/ytdl_subscribe/validators/config/metadata_options/nfo_validator.py b/ytdl_subscribe/validators/config/metadata_options/nfo_validator.py index 6cda1995..e4706569 100644 --- a/ytdl_subscribe/validators/config/metadata_options/nfo_validator.py +++ b/ytdl_subscribe/validators/config/metadata_options/nfo_validator.py @@ -1,5 +1,8 @@ from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator from ytdl_subscribe.validators.base.string_formatter_validators import DictFormatterValidator +from ytdl_subscribe.validators.base.string_formatter_validators import ( + OverridesStringFormatterValidator, +) from ytdl_subscribe.validators.base.string_formatter_validators import StringFormatterValidator @@ -12,3 +15,11 @@ class NFOValidator(StrictDictValidator): self.nfo_name = self._validate_key(key="nfo_name", validator=StringFormatterValidator) self.nfo_root = self._validate_key(key="nfo_root", validator=StringFormatterValidator) self.tags = self._validate_key(key="tags", validator=DictFormatterValidator) + + +class OutputDirectoryNFOValidator(NFOValidator): + def __init__(self, name, value): + super().__init__(name, value) + self.nfo_name = self._validate_key( + key="nfo_name", validator=OverridesStringFormatterValidator + ) diff --git a/ytdl_subscribe/validators/config/output_options/output_options_validator.py b/ytdl_subscribe/validators/config/output_options/output_options_validator.py index 56d474f9..8e356f6e 100644 --- a/ytdl_subscribe/validators/config/output_options/output_options_validator.py +++ b/ytdl_subscribe/validators/config/output_options/output_options_validator.py @@ -1,4 +1,7 @@ from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator +from ytdl_subscribe.validators.base.string_formatter_validators import ( + OverridesStringFormatterValidator, +) from ytdl_subscribe.validators.base.string_formatter_validators import StringFormatterValidator from ytdl_subscribe.validators.base.string_select_validator import StringSelectValidator @@ -18,13 +21,16 @@ class OutputOptionsValidator(StrictDictValidator): def __init__(self, name, value): super().__init__(name, value) - self.output_directory: StringFormatterValidator = self._validate_key( - key="output_directory", validator=StringFormatterValidator + # Output directory should resolve without any entry variables. + # This is to check the directory for any download-archives before any downloads begin + self.output_directory: OverridesStringFormatterValidator = self._validate_key( + key="output_directory", validator=OverridesStringFormatterValidator ) + + # file name and thumbnails however can use entry variables self.file_name: StringFormatterValidator = self._validate_key( key="file_name", validator=StringFormatterValidator ) - self.convert_thumbnail = self._validate_key_if_present( key="convert_thumbnail", validator=ConvertThumbnailValidator ) diff --git a/ytdl_subscribe/validators/config/preset_validator.py b/ytdl_subscribe/validators/config/preset_validator.py index 13ce9e23..3267b87a 100644 --- a/ytdl_subscribe/validators/config/preset_validator.py +++ b/ytdl_subscribe/validators/config/preset_validator.py @@ -5,6 +5,11 @@ from typing import Optional from typing import Type from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator +from ytdl_subscribe.validators.base.string_formatter_validators import ( + OverridesStringFormatterValidator, +) +from ytdl_subscribe.validators.base.validators import DictValidator +from ytdl_subscribe.validators.base.validators import Validator from ytdl_subscribe.validators.config.metadata_options.metadata_options_validator import ( MetadataOptionsValidator, ) @@ -25,6 +30,7 @@ from ytdl_subscribe.validators.config.source_options.source_validators import So from ytdl_subscribe.validators.config.ytdl_options.ytdl_options_validator import ( YTDLOptionsValidator, ) +from ytdl_subscribe.validators.exceptions import StringFormattingVariableNotFoundException from ytdl_subscribe.validators.exceptions import ValidationException PRESET_SOURCE_VALIDATOR_MAPPING: Dict[str, Type[DownloadStrategyValidator]] = { @@ -74,6 +80,43 @@ class PresetValidator(StrictDictValidator): return download_strategy_validator.source_validator + def __validate_override_string_formatter_validator( + self, formatter_validator: OverridesStringFormatterValidator + ): + # Gather all resolvable override variables + resolvable_override_variables: List[str] = [] + for name, override_variable in self.overrides.dict.items(): + try: + _ = override_variable.apply_formatter(self.overrides.dict_with_format_strings) + except StringFormattingVariableNotFoundException: + continue + resolvable_override_variables.append(name) + + for variable_name in formatter_validator.format_variables: + if variable_name not in resolvable_override_variables: + raise StringFormattingVariableNotFoundException( + f"This variable can only use override variables that resolve without needing " + f"variables from a downloaded file. The only override variables defined that " + f"meet this condition are: {', '.join(sorted(resolvable_override_variables))}" + ) + + def __recursive_preset_validate( + self, validator_dict: Optional[Dict[str, Validator]] = None + ) -> None: + """ + Ensure all OverridesStringFormatterValidator's only contain variables from the overrides + and resolve. + """ + if validator_dict is None: + validator_dict = self._validator_dict + + for name, validator in validator_dict.items(): + if isinstance(validator, DictValidator): + self.__recursive_preset_validate(validator._validator_dict) + + if isinstance(validator, OverridesStringFormatterValidator): + self.__validate_override_string_formatter_validator(validator) + def __init__(self, name: str, value: Any): super().__init__(name=name, value=value) @@ -94,3 +137,7 @@ class PresetValidator(StrictDictValidator): self.overrides = self._validate_key( key="overrides", validator=OverridesValidator, default={} ) + + # After all options are initialized, perform a recursive post-validate that requires + # values from multiple validators + self.__recursive_preset_validate() diff --git a/ytdl_subscribe/validators/exceptions.py b/ytdl_subscribe/validators/exceptions.py index 97a228a5..f787481d 100644 --- a/ytdl_subscribe/validators/exceptions.py +++ b/ytdl_subscribe/validators/exceptions.py @@ -4,3 +4,7 @@ class ValidationException(ValueError): class StringFormattingException(ValidationException): """Tried to format a string but failed due to user misconfigured variables""" + + +class StringFormattingVariableNotFoundException(StringFormattingException): + """Tried to format a string but the variable was not found"""