from typing import Dict from typing import List from typing import Optional from ytdl_sub.config.preset_options import AddsVariablesMixin from ytdl_sub.validators.strict_dict_validator import StrictDictValidator from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator from ytdl_sub.validators.validators import ListValidator from ytdl_sub.validators.validators import StringValidator class CollectionThumbnailValidator(StrictDictValidator): _required_keys = {"name", "uid"} def __init__(self, name, value): super().__init__(name, value) self._name = self._validate_key(key="name", validator=StringFormatterValidator) self._uid = self._validate_key(key="uid", validator=OverridesStringFormatterValidator) @property def name(self) -> StringFormatterValidator: """ File name for the thumbnail """ return self._name @property def uid(self) -> OverridesStringFormatterValidator: """ yt-dlp's unique ID of the thumbnail """ return self._uid class CollectionThumbnailListValidator(ListValidator[CollectionThumbnailValidator]): _inner_list_type = CollectionThumbnailValidator class CollectionUrlValidator(StrictDictValidator): _required_keys = {"url"} _optional_keys = {"variables", "source_thumbnails", "playlist_thumbnails"} def __init__(self, name, value): super().__init__(name, value) # TODO: url validate using yt-dlp IE self._url = self._validate_key(key="url", validator=StringValidator) self._variables = self._validate_key_if_present( key="variables", validator=DictFormatterValidator, default={} ) self._source_thumbnails = self._validate_key_if_present( key="source_thumbnails", validator=CollectionThumbnailListValidator, default=[] ) self._playlist_thumbnails = self._validate_key_if_present( key="playlist_thumbnails", validator=CollectionThumbnailListValidator, default=[] ) @property def url(self) -> str: """ URL to download from, listed in priority from lowest (top) to highest (bottom). If a download exists in more than one URL, it will resolve to the bottom-most one and inherit those variables. """ return self._url.value @property def variables(self) -> DictFormatterValidator: """ Source variables to add to each entry. The top-most collection must define all possible variables. Collections below can redefine all of them or a subset of the top-most variables. """ return self._variables @property def source_thumbnails(self) -> Optional[CollectionThumbnailListValidator]: """ Thumbnails to download from the source, if any exist. The hierarchy is defined as source -> playlist -> entry. Usage: .. code-block:: yaml source_thumbnails: - name: "poster.jpg" uid: "avatar_uncropped" UID is the yt-dlp thumbnail ID or can specify ``latest_entry`` to use the last entry's thumbnail that was downloaded. """ return self._source_thumbnails @property def playlist_thumbnails(self) -> Optional[CollectionThumbnailListValidator]: """ Thumbnails to download from the source, if any exist. The hierarchy is defined as source -> playlist -> entry. Usage: .. code-block:: yaml playlist_thumbnails: - name: "poster.jpg" uid: "avatar_uncropped" UID is the yt-dlp thumbnail ID or can specify ``latest_entry`` to use the last entry's thumbnail that was downloaded. """ return self._playlist_thumbnails class CollectionUrlListValidator(ListValidator[CollectionUrlValidator]): _inner_list_type = CollectionUrlValidator _expected_value_type_name = "collection url list" def __init__(self, name, value): super().__init__(name, value) added_variables: Dict[str, str] = self.list[0].variables.dict_with_format_strings for idx, collection_url_validator in enumerate(self.list[1:]): collection_variables = collection_url_validator.variables.dict_with_format_strings # see if this collection contains new added vars (it should not) for var in collection_variables.keys(): if var not in added_variables: raise self._validation_exception( f"Collection url {idx} contains the variable '{var}' that the first " f"collection url does not. The first collection url must define all added " f"variables." ) # see if this collection is missing any added vars (if so, inherit from the top) for var in added_variables.keys(): if var not in collection_variables.keys(): collection_variables[var] = added_variables[var] class CollectionValidator(StrictDictValidator, AddsVariablesMixin): """ Downloads from multiple URLs. If an entry is returned from more than one URL, it will resolve to the bottom-most URL settings. """ _required_keys = {"urls"} def __init__(self, name, value): super().__init__(name, value) self._urls = self._validate_key(key="urls", validator=CollectionUrlListValidator) @property def collection_urls(self) -> CollectionUrlListValidator: """ Required. The Soundcloud user's url, i.e. ``soundcloud.com/the_username`` """ return self._urls def added_source_variables(self) -> List[str]: """ Returns ------- List of variables added. The first collection url always contains all the variables. """ return list(self._urls.list[0].variables.keys) def validate_with_variables( self, source_variables: List[str], override_variables: Dict[str, str] ) -> None: """ Ensures new variables added are not existing variables """ for source_var_name in self.added_source_variables(): if source_var_name in source_variables: raise self._validation_exception( f"'{source_var_name}' cannot be used as a variable name because it " f"is an existing source variable" ) base_variables = dict( override_variables, **{source_var: "dummy_string" for source_var in source_variables} ) # Apply formatting to each new source variable, ensure it resolves for collection_url in self.collection_urls.list: for ( source_var_name, source_var_formatter_str, ) in collection_url.variables.dict_with_format_strings.items(): _ = StringFormatterValidator( name=f"{self._name}.{source_var_name}", value=source_var_formatter_str ).apply_formatter(base_variables)