[REFACTOR] Single download options (#668)

* [REFACTOR] Single download options

* update

* fix unit test msg
This commit is contained in:
Jesse Bannon 2023-07-25 14:41:33 -07:00 committed by GitHub
parent bf3c61213b
commit 3c0d05a383
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 48 additions and 125 deletions

View file

@ -4,8 +4,7 @@ from typing import Type
from ytdl_sub.config.plugin import Plugin from ytdl_sub.config.plugin import Plugin
from ytdl_sub.downloaders.source_plugin import SourcePlugin from ytdl_sub.downloaders.source_plugin import SourcePlugin
from ytdl_sub.downloaders.url.multi_url import MultiUrlDownloader from ytdl_sub.downloaders.url.downloader import MultiUrlDownloader
from ytdl_sub.downloaders.url.url import UrlDownloader
from ytdl_sub.plugins.audio_extract import AudioExtractPlugin from ytdl_sub.plugins.audio_extract import AudioExtractPlugin
from ytdl_sub.plugins.chapters import ChaptersPlugin from ytdl_sub.plugins.chapters import ChaptersPlugin
from ytdl_sub.plugins.date_range import DateRangePlugin from ytdl_sub.plugins.date_range import DateRangePlugin
@ -30,7 +29,7 @@ class DownloadStrategyMapping:
_MAPPING: Dict[str, Dict[str, Type[SourcePlugin]]] = { _MAPPING: Dict[str, Dict[str, Type[SourcePlugin]]] = {
"download": { "download": {
"multi_url": MultiUrlDownloader, "multi_url": MultiUrlDownloader,
"url": UrlDownloader, "url": MultiUrlDownloader,
}, },
} }

View file

@ -16,12 +16,6 @@ from ytdl_sub.config.plugin import PluginPriority
from ytdl_sub.config.preset_options import Overrides from ytdl_sub.config.preset_options import Overrides
from ytdl_sub.downloaders.source_plugin import SourcePlugin from ytdl_sub.downloaders.source_plugin import SourcePlugin
from ytdl_sub.downloaders.source_plugin import SourcePluginExtension from ytdl_sub.downloaders.source_plugin import SourcePluginExtension
from ytdl_sub.downloaders.url.multi_url_source_options_validator import (
MultiUrlSourceOptionsValidator,
)
from ytdl_sub.downloaders.url.multi_url_source_options_validator import (
TMultiURLSourceOptionsValidator,
)
from ytdl_sub.downloaders.url.validators import MultiUrlValidator from ytdl_sub.downloaders.url.validators import MultiUrlValidator
from ytdl_sub.downloaders.url.validators import UrlThumbnailListValidator from ytdl_sub.downloaders.url.validators import UrlThumbnailListValidator
from ytdl_sub.downloaders.url.validators import UrlValidator from ytdl_sub.downloaders.url.validators import UrlValidator
@ -59,7 +53,7 @@ class UrlDownloaderThumbnailPlugin(SourcePluginExtension):
def __init__( def __init__(
self, self,
options: MultiUrlSourceOptionsValidator, options: MultiUrlValidator,
overrides: Overrides, overrides: Overrides,
enhanced_download_archive: EnhancedDownloadArchive, enhanced_download_archive: EnhancedDownloadArchive,
): ):
@ -71,7 +65,7 @@ class UrlDownloaderThumbnailPlugin(SourcePluginExtension):
self._thumbnails_downloaded: Set[str] = set() self._thumbnails_downloaded: Set[str] = set()
self._collection_url_mapping: Dict[str, UrlValidator] = { self._collection_url_mapping: Dict[str, UrlValidator] = {
self.overrides.apply_formatter(collection_url.url): collection_url self.overrides.apply_formatter(collection_url.url): collection_url
for collection_url in options.collection_validator.urls.list for collection_url in options.urls.list
} }
def _download_parent_thumbnails( def _download_parent_thumbnails(
@ -160,7 +154,7 @@ class UrlDownloaderCollectionVariablePlugin(SourcePluginExtension):
def __init__( def __init__(
self, self,
options: MultiUrlSourceOptionsValidator, options: MultiUrlValidator,
overrides: Overrides, overrides: Overrides,
enhanced_download_archive: EnhancedDownloadArchive, enhanced_download_archive: EnhancedDownloadArchive,
): ):
@ -172,7 +166,7 @@ class UrlDownloaderCollectionVariablePlugin(SourcePluginExtension):
self._thumbnails_downloaded: Set[str] = set() self._thumbnails_downloaded: Set[str] = set()
self._collection_url_mapping: Dict[str, UrlValidator] = { self._collection_url_mapping: Dict[str, UrlValidator] = {
self.overrides.apply_formatter(collection_url.url): collection_url self.overrides.apply_formatter(collection_url.url): collection_url
for collection_url in options.collection_validator.urls.list for collection_url in options.urls.list
} }
def modify_entry_metadata(self, entry: Entry) -> Optional[Entry]: def modify_entry_metadata(self, entry: Entry) -> Optional[Entry]:
@ -194,12 +188,13 @@ class UrlDownloaderCollectionVariablePlugin(SourcePluginExtension):
return entry return entry
class BaseUrlDownloader(SourcePlugin[TMultiURLSourceOptionsValidator], ABC): class MultiUrlDownloader(SourcePlugin[MultiUrlValidator], ABC):
""" """
Class that interacts with ytdl to perform the download of metadata and content, Class that interacts with ytdl to perform the download of metadata and content,
and should translate that to list of Entry objects. and should translate that to list of Entry objects.
""" """
plugin_options_type = MultiUrlValidator
plugin_extensions = [UrlDownloaderThumbnailPlugin, UrlDownloaderCollectionVariablePlugin] plugin_extensions = [UrlDownloaderThumbnailPlugin, UrlDownloaderCollectionVariablePlugin]
@classmethod @classmethod
@ -214,7 +209,7 @@ class BaseUrlDownloader(SourcePlugin[TMultiURLSourceOptionsValidator], ABC):
def __init__( def __init__(
self, self,
options: TMultiURLSourceOptionsValidator, options: MultiUrlValidator,
enhanced_download_archive: EnhancedDownloadArchive, enhanced_download_archive: EnhancedDownloadArchive,
download_ytdl_options: YTDLOptionsBuilder, download_ytdl_options: YTDLOptionsBuilder,
metadata_ytdl_options: YTDLOptionsBuilder, metadata_ytdl_options: YTDLOptionsBuilder,
@ -300,7 +295,7 @@ class BaseUrlDownloader(SourcePlugin[TMultiURLSourceOptionsValidator], ABC):
@property @property
def collection(self) -> MultiUrlValidator: def collection(self) -> MultiUrlValidator:
"""Return the download options collection""" """Return the download options collection"""
return self.plugin_options.collection_validator return self.plugin_options
@contextlib.contextmanager @contextlib.contextmanager
def _separate_download_archives(self, clear_info_json_files: bool = False): def _separate_download_archives(self, clear_info_json_files: bool = False):

View file

@ -1,14 +1,8 @@
from typing import Dict
from typing import List
from ytdl_sub.downloaders.url.downloader import BaseUrlDownloader
from ytdl_sub.downloaders.url.multi_url_source_options_validator import (
MultiUrlSourceOptionsValidator,
)
from ytdl_sub.downloaders.url.validators import MultiUrlValidator from ytdl_sub.downloaders.url.validators import MultiUrlValidator
class MultiUrlDownloadOptions(MultiUrlValidator, MultiUrlSourceOptionsValidator): # TODO: Remove later - keep for docstring
class MultiUrlDownloadOptions(MultiUrlValidator):
""" """
Downloads from multiple URLs. If an entry is returned from more than one URL, it will Downloads from multiple URLs. If an entry is returned from more than one URL, it will
resolve to the bottom-most URL settings. resolve to the bottom-most URL settings.
@ -42,29 +36,3 @@ class MultiUrlDownloadOptions(MultiUrlValidator, MultiUrlSourceOptionsValidator)
- name: "season{season_index}-poster.jpg" - name: "season{season_index}-poster.jpg"
uid: "latest_entry" uid: "latest_entry"
""" """
@property
def collection_validator(self) -> MultiUrlValidator:
"""Returns itself!"""
return self
def validate_with_variables(
self, source_variables: List[str], override_variables: Dict[str, str]
) -> None:
"""
Validates any source variables added by the collection
"""
super().validate_with_variables(
source_variables=source_variables, override_variables=override_variables
)
has_non_empty_url = False
for url_validator in self.urls.list:
has_non_empty_url |= bool(url_validator.url.apply_formatter(override_variables))
if not has_non_empty_url:
raise self._validation_exception("Must contain at least one url that is non-empty")
class MultiUrlDownloader(BaseUrlDownloader[MultiUrlDownloadOptions]):
plugin_options_type = MultiUrlDownloadOptions

View file

@ -1,47 +0,0 @@
import abc
from abc import ABC
from typing import Dict
from typing import List
from typing import TypeVar
from ytdl_sub.config.preset_options import OptionsValidator
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
class MultiUrlSourceOptionsValidator(OptionsValidator, ABC):
"""
Placeholder class to define downloader options
"""
@property
@abc.abstractmethod
def collection_validator(self) -> MultiUrlValidator:
"""
Returns
-------
MultiUrlValidator
To determine how the entries are downloaded
"""
def added_source_variables(self) -> List[str]:
"""
Returns
-------
Added source variables on the collection
"""
return self.collection_validator.added_source_variables()
def validate_with_variables(
self, source_variables: List[str], override_variables: Dict[str, str]
) -> None:
"""
Validates any source variables added by the collection
"""
self.collection_validator.validate_with_variables(
source_variables=source_variables, override_variables=override_variables
)
TMultiURLSourceOptionsValidator = TypeVar(
"TMultiURLSourceOptionsValidator", bound=MultiUrlSourceOptionsValidator
)

View file

@ -1,12 +1,8 @@
from ytdl_sub.downloaders.url.downloader import BaseUrlDownloader
from ytdl_sub.downloaders.url.multi_url_source_options_validator import (
MultiUrlSourceOptionsValidator,
)
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
from ytdl_sub.downloaders.url.validators import UrlValidator from ytdl_sub.downloaders.url.validators import UrlValidator
class UrlDownloadOptions(UrlValidator, MultiUrlSourceOptionsValidator): # TODO: Remove later - keep for docstring
class UrlDownloadOptions(UrlValidator):
""" """
Downloads from a single URL supported by yt-dlp. Downloads from a single URL supported by yt-dlp.
@ -28,15 +24,3 @@ class UrlDownloadOptions(UrlValidator, MultiUrlSourceOptionsValidator):
uid: "banner_uncropped" uid: "banner_uncropped"
download_reverse: True download_reverse: True
""" """
@property
def collection_validator(self) -> MultiUrlValidator:
"""Returns itself!"""
return MultiUrlValidator(
name=self._name,
value={"urls": [self._value]},
)
class UrlDownloader(BaseUrlDownloader[UrlDownloadOptions]):
plugin_options_type = UrlDownloadOptions

View file

@ -1,9 +1,10 @@
import copy
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import List from typing import List
from typing import Optional from typing import Optional
from ytdl_sub.config.preset_options import OptionsDictValidator from ytdl_sub.config.preset_options import OptionsValidator
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator 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 DictFormatterValidator
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
@ -172,26 +173,41 @@ class UrlListValidator(ListValidator[UrlValidator]):
collection_variables[var] = added_variables[var] collection_variables[var] = added_variables[var]
class MultiUrlValidator(OptionsDictValidator): class MultiUrlValidator(OptionsValidator):
""" """
Downloads from multiple URLs. If an entry is returned from more than one URL, it will Downloads from multiple URLs. If an entry is returned from more than one URL, it will
resolve to the bottom-most URL settings. resolve to the bottom-most URL settings.
""" """
_required_keys = {"urls"}
@classmethod @classmethod
def partial_validate(cls, name: str, value: Any) -> None: def partial_validate(cls, name: str, value: Any) -> None:
""" """
Partially validate a collection Partially validate a collection
""" """
if isinstance(value, dict): if isinstance(value, dict):
value["urls"] = value.get("urls", [{"url": "placeholder"}]) value["url"] = value.get("url", "sadfasdf")
_ = cls(name, value) _ = cls(name, value)
def __init__(self, name, value): def __init__(self, name, value):
super().__init__(name, value) super().__init__(name, value)
self._urls = self._validate_key(key="urls", validator=UrlListValidator)
# Copy since we're popping things
value_copy = copy.deepcopy(value)
if isinstance(value, dict):
# Pop old required field in case it's still there
value_copy.pop("download_strategy", None)
if "urls" in value_copy:
self._urls = UrlListValidator(name=name, value=value_copy["urls"])
else:
# Validate using a single URL validator first
_ = UrlValidator(name=name, value=value_copy)
self._urls = UrlListValidator(name=name, value=[value_copy])
else:
# Should error here. TODO: Add simplifications download here (string, list)
self._urls = UrlListValidator(
name=name, value=UrlValidator(name=name, value=value_copy)
)
@property @property
def urls(self) -> UrlListValidator: def urls(self) -> UrlListValidator:
@ -248,3 +264,11 @@ class MultiUrlValidator(OptionsDictValidator):
_ = StringFormatterValidator( _ = StringFormatterValidator(
name=f"{self._name}.{source_var_name}", value=source_var_formatter_str name=f"{self._name}.{source_var_name}", value=source_var_formatter_str
).apply_formatter(base_variables) ).apply_formatter(base_variables)
# 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))
if not has_non_empty_url:
raise self._validation_exception("Must contain at least one url that is non-empty")

View file

@ -98,7 +98,8 @@ class TestConfigFilePartiallyValidatesPresets:
preset_dict={"download": {"download_strategy": "multi_url", "bad_key": "nope"}}, preset_dict={"download": {"download_strategy": "multi_url", "bad_key": "nope"}},
expected_error_message="Validation error in partial_preset.download: " expected_error_message="Validation error in partial_preset.download: "
"'partial_preset.download' contains the field 'bad_key' which is not allowed. " "'partial_preset.download' contains the field 'bad_key' which is not allowed. "
"Allowed fields: urls", "Allowed fields: download_reverse, playlist_thumbnails, source_thumbnails, url, "
"variables",
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(

View file

@ -3,7 +3,6 @@ import re
import pytest import pytest
from ytdl_sub.config.preset import Preset from ytdl_sub.config.preset import Preset
from ytdl_sub.downloaders.url.multi_url import MultiUrlDownloadOptions
from ytdl_sub.plugins.nfo_tags import NfoTagsOptions from ytdl_sub.plugins.nfo_tags import NfoTagsOptions
from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException
from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.exceptions import ValidationException

View file

@ -10,7 +10,7 @@ import pytest
from resources import copy_file_fixture from resources import copy_file_fixture
from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.downloaders.url.downloader import BaseUrlDownloader from ytdl_sub.downloaders.url.downloader import MultiUrlDownloader
from ytdl_sub.downloaders.ytdlp import YTDLP from ytdl_sub.downloaders.ytdlp import YTDLP
from ytdl_sub.entries.variables.kwargs import DESCRIPTION from ytdl_sub.entries.variables.kwargs import DESCRIPTION
from ytdl_sub.entries.variables.kwargs import EPOCH from ytdl_sub.entries.variables.kwargs import EPOCH
@ -201,7 +201,7 @@ def mock_download_collection_entries(
with patch.object( with patch.object(
YTDLP, "extract_info_via_info_json", new=_write_entries_to_working_dir YTDLP, "extract_info_via_info_json", new=_write_entries_to_working_dir
), patch.object( ), patch.object(
BaseUrlDownloader, "_extract_entry_info_with_retry", new=lambda _, entry: entry MultiUrlDownloader, "_extract_entry_info_with_retry", new=lambda _, entry: entry
): ):
# Stub out metadata. TODO: update this if we do metadata plugins # Stub out metadata. TODO: update this if we do metadata plugins
yield yield