[FEATURE] Toggleable plugin field enable for all dict-based plugins
This commit is contained in:
parent
2076de3074
commit
13dcfb9c10
11 changed files with 145 additions and 10 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
|
from functools import cached_property
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
@ -8,9 +9,11 @@ from typing import Tuple
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from ytdl_sub.config.overrides import Overrides
|
from ytdl_sub.config.overrides import Overrides
|
||||||
|
from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator
|
||||||
from ytdl_sub.config.validators.options import TOptionsValidator
|
from ytdl_sub.config.validators.options import TOptionsValidator
|
||||||
from ytdl_sub.entries.entry import Entry
|
from ytdl_sub.entries.entry import Entry
|
||||||
from ytdl_sub.utils.file_handler import FileMetadata
|
from ytdl_sub.utils.file_handler import FileMetadata
|
||||||
|
from ytdl_sub.utils.script import ScriptUtils
|
||||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadArchiver
|
from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadArchiver
|
||||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||||
|
|
||||||
|
|
@ -40,6 +43,17 @@ class Plugin(BasePlugin[TOptionsValidator], Generic[TOptionsValidator], ABC):
|
||||||
Class to define the new plugin functionality
|
Class to define the new plugin functionality
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_enabled(self) -> bool:
|
||||||
|
"""
|
||||||
|
Returns True if enabled, False if disabled.
|
||||||
|
"""
|
||||||
|
if isinstance(self.plugin_options, ToggleableOptionsDictValidator):
|
||||||
|
return ScriptUtils.bool_formatter_output(
|
||||||
|
self.overrides.apply_formatter(self.plugin_options.enable)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
def ytdl_options_match_filters(self) -> Tuple[List[str], List[str]]:
|
def ytdl_options_match_filters(self) -> Tuple[List[str], List[str]]:
|
||||||
"""
|
"""
|
||||||
Returns
|
Returns
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from typing import TypeVar
|
||||||
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
|
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
|
||||||
from ytdl_sub.utils.exceptions import ValidationException
|
from ytdl_sub.utils.exceptions import ValidationException
|
||||||
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 OverridesBooleanFormatterValidator
|
||||||
from ytdl_sub.validators.validators import Validator
|
from ytdl_sub.validators.validators import Validator
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
|
|
@ -57,3 +58,25 @@ TOptionsValidator = TypeVar("TOptionsValidator", bound=OptionsValidator)
|
||||||
|
|
||||||
class OptionsDictValidator(StrictDictValidator, OptionsValidator, ABC):
|
class OptionsDictValidator(StrictDictValidator, OptionsValidator, ABC):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ToggleableOptionsDictValidator(OptionsDictValidator):
|
||||||
|
_optional_keys = {"enable"}
|
||||||
|
|
||||||
|
def __init__(self, name, value):
|
||||||
|
assert "enable" in self._optional_keys, ""
|
||||||
|
super().__init__(name, value)
|
||||||
|
|
||||||
|
self._enable = self._validate_key(
|
||||||
|
key="enable", validator=OverridesBooleanFormatterValidator, default="False"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enable(self) -> OverridesBooleanFormatterValidator:
|
||||||
|
"""
|
||||||
|
:expected type: Optional[Boolean]
|
||||||
|
:description:
|
||||||
|
Whether to enable or disable this plugin when it is defined in a config. Defaults
|
||||||
|
to enabled (True).
|
||||||
|
"""
|
||||||
|
return self._enable
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import json
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
@ -8,6 +7,7 @@ from ytdl_sub.config.validators.options import OptionsValidator
|
||||||
from ytdl_sub.entries.entry import Entry
|
from ytdl_sub.entries.entry import Entry
|
||||||
from ytdl_sub.utils.exceptions import StringFormattingException
|
from ytdl_sub.utils.exceptions import StringFormattingException
|
||||||
from ytdl_sub.utils.logger import Logger
|
from ytdl_sub.utils.logger import Logger
|
||||||
|
from ytdl_sub.utils.script import ScriptUtils
|
||||||
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
|
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
|
||||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||||
|
|
||||||
|
|
@ -53,7 +53,9 @@ class FilterExcludePlugin(Plugin[FilterExcludeOptions]):
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
for formatter in self.plugin_options.list:
|
for formatter in self.plugin_options.list:
|
||||||
out = json.loads(self.overrides.apply_formatter(formatter=formatter, entry=entry))
|
out = ScriptUtils.bool_formatter_output(
|
||||||
|
self.overrides.apply_formatter(formatter=formatter, entry=entry)
|
||||||
|
)
|
||||||
if bool(out):
|
if bool(out):
|
||||||
logger.info(
|
logger.info(
|
||||||
"Filtering '%s' from the filter %s evaluating to True",
|
"Filtering '%s' from the filter %s evaluating to True",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import json
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
@ -8,6 +7,7 @@ from ytdl_sub.config.validators.options import OptionsValidator
|
||||||
from ytdl_sub.entries.entry import Entry
|
from ytdl_sub.entries.entry import Entry
|
||||||
from ytdl_sub.utils.exceptions import StringFormattingException
|
from ytdl_sub.utils.exceptions import StringFormattingException
|
||||||
from ytdl_sub.utils.logger import Logger
|
from ytdl_sub.utils.logger import Logger
|
||||||
|
from ytdl_sub.utils.script import ScriptUtils
|
||||||
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
|
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
|
||||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||||
|
|
||||||
|
|
@ -61,7 +61,9 @@ class FilterIncludePlugin(Plugin[FilterIncludeOptions]):
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
for formatter in self.plugin_options.list:
|
for formatter in self.plugin_options.list:
|
||||||
out = json.loads(self.overrides.apply_formatter(formatter=formatter, entry=entry))
|
out = ScriptUtils.bool_formatter_output(
|
||||||
|
self.overrides.apply_formatter(formatter=formatter, entry=entry)
|
||||||
|
)
|
||||||
if not bool(out):
|
if not bool(out):
|
||||||
logger.info(
|
logger.info(
|
||||||
"Filtering '%s' from the filter %s evaluating to False",
|
"Filtering '%s' from the filter %s evaluating to False",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from typing import Tuple
|
||||||
|
|
||||||
from ytdl_sub.config.overrides import Overrides
|
from ytdl_sub.config.overrides import Overrides
|
||||||
from ytdl_sub.config.plugin.plugin import Plugin
|
from ytdl_sub.config.plugin.plugin import Plugin
|
||||||
from ytdl_sub.config.validators.options import OptionsDictValidator
|
from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator
|
||||||
from ytdl_sub.entries.entry import Entry
|
from ytdl_sub.entries.entry import Entry
|
||||||
from ytdl_sub.utils.file_handler import FileMetadata
|
from ytdl_sub.utils.file_handler import FileMetadata
|
||||||
from ytdl_sub.utils.logger import Logger
|
from ytdl_sub.utils.logger import Logger
|
||||||
|
|
@ -59,7 +59,7 @@ class RandomizedRangeValidator(StrictDictValidator):
|
||||||
return int(self.randomized_float())
|
return int(self.randomized_float())
|
||||||
|
|
||||||
|
|
||||||
class ThrottleProtectionOptions(OptionsDictValidator):
|
class ThrottleProtectionOptions(ToggleableOptionsDictValidator):
|
||||||
"""
|
"""
|
||||||
Provides options to make ytdl-sub look more 'human-like' to protect from throttling. For
|
Provides options to make ytdl-sub look more 'human-like' to protect from throttling. For
|
||||||
range-based values, a random number will be chosen within the range to avoid sleeps looking
|
range-based values, a random number will be chosen within the range to avoid sleeps looking
|
||||||
|
|
@ -84,7 +84,7 @@ class ThrottleProtectionOptions(OptionsDictValidator):
|
||||||
subscription_download_probability: 1.0
|
subscription_download_probability: 1.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_optional_keys = {
|
_optional_keys = ToggleableOptionsDictValidator._optional_keys | {
|
||||||
"sleep_per_download_s",
|
"sleep_per_download_s",
|
||||||
"sleep_per_subscription_s",
|
"sleep_per_subscription_s",
|
||||||
"max_downloads_per_subscription",
|
"max_downloads_per_subscription",
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
|
||||||
-------
|
-------
|
||||||
List of plugins defined in the subscription, initialized and ready to use.
|
List of plugins defined in the subscription, initialized and ready to use.
|
||||||
"""
|
"""
|
||||||
return [
|
plugins = [
|
||||||
plugin_type(
|
plugin_type(
|
||||||
options=plugin_options,
|
options=plugin_options,
|
||||||
overrides=self.overrides,
|
overrides=self.overrides,
|
||||||
|
|
@ -198,6 +198,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
|
||||||
)
|
)
|
||||||
for plugin_type, plugin_options in self.plugins.zipped()
|
for plugin_type, plugin_options in self.plugins.zipped()
|
||||||
]
|
]
|
||||||
|
return [plugin for plugin in plugins if plugin.is_enabled]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _cleanup_entry_files(cls, entry: Entry):
|
def _cleanup_entry_files(cls, entry: Entry):
|
||||||
|
|
|
||||||
|
|
@ -38,3 +38,15 @@ class ScriptUtils:
|
||||||
out = f"{{%from_json('''{dumped_json}''')}}"
|
out = f"{{%from_json('''{dumped_json}''')}}"
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def bool_formatter_output(cls, output: str) -> bool:
|
||||||
|
"""
|
||||||
|
Translate formatter output to a boolean
|
||||||
|
"""
|
||||||
|
if not output or output.lower() == "false":
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return bool(json.loads(output))
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
return True
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ class OverridesStringFormatterValidator(StringFormatterValidator):
|
||||||
# pylint: enable=line-too-long
|
# pylint: enable=line-too-long
|
||||||
|
|
||||||
|
|
||||||
class OverridesIntegerFormatterValidator(StringFormatterValidator):
|
class OverridesIntegerFormatterValidator(OverridesStringFormatterValidator):
|
||||||
_expected_value_type_name = "integer"
|
_expected_value_type_name = "integer"
|
||||||
|
|
||||||
def post_process(self, resolved: str) -> str:
|
def post_process(self, resolved: str) -> str:
|
||||||
|
|
@ -106,6 +106,10 @@ class OverridesIntegerFormatterValidator(StringFormatterValidator):
|
||||||
return resolved
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
class OverridesBooleanFormatterValidator(OverridesStringFormatterValidator):
|
||||||
|
_expected_value_type_name = "boolean"
|
||||||
|
|
||||||
|
|
||||||
class ListFormatterValidator(ListValidator[StringFormatterValidator]):
|
class ListFormatterValidator(ListValidator[StringFormatterValidator]):
|
||||||
_inner_list_type = StringFormatterValidator
|
_inner_list_type = StringFormatterValidator
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ def assert_logs(
|
||||||
for call_args in patched_debug.call_args_list:
|
for call_args in patched_debug.call_args_list:
|
||||||
occurrences += int(expected_message in call_args.args[0])
|
occurrences += int(expected_message in call_args.args[0])
|
||||||
|
|
||||||
if expected_occurrences:
|
if expected_occurrences is not None:
|
||||||
assert (
|
assert (
|
||||||
occurrences == expected_occurrences
|
occurrences == expected_occurrences
|
||||||
), f"{expected_message} was expected {expected_occurrences} times, got {occurrences}"
|
), f"{expected_message} was expected {expected_occurrences} times, got {occurrences}"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import pytest
|
||||||
from conftest import assert_logs
|
from conftest import assert_logs
|
||||||
|
|
||||||
from ytdl_sub.plugins.throttle_protection import logger as throttle_protection_logger
|
from ytdl_sub.plugins.throttle_protection import logger as throttle_protection_logger
|
||||||
|
|
@ -57,3 +58,59 @@ class TestThrottleProtectionPlugin:
|
||||||
expected_occurrences=1,
|
expected_occurrences=1,
|
||||||
):
|
):
|
||||||
_ = subscription.download(dry_run=False)
|
_ = subscription.download(dry_run=False)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"disable_value",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
False,
|
||||||
|
"{tp_bool_string}",
|
||||||
|
"{tp_empty_string}",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_disabled(
|
||||||
|
self,
|
||||||
|
config,
|
||||||
|
subscription_name,
|
||||||
|
output_directory,
|
||||||
|
mock_download_collection_entries,
|
||||||
|
disable_value,
|
||||||
|
):
|
||||||
|
preset_dict = {
|
||||||
|
"preset": [
|
||||||
|
"Kodi Music Videos",
|
||||||
|
],
|
||||||
|
"overrides": {
|
||||||
|
"url": "https://your.name.here",
|
||||||
|
"music_video_directory": output_directory,
|
||||||
|
"tp_bool_string": "{ %bool(False) }",
|
||||||
|
"tp_empty_string": "",
|
||||||
|
},
|
||||||
|
"throttle_protection": {
|
||||||
|
"enable": disable_value,
|
||||||
|
"sleep_per_download_s": {
|
||||||
|
"min": 0.01,
|
||||||
|
"max": 0.01,
|
||||||
|
},
|
||||||
|
"sleep_per_subscription_s": {
|
||||||
|
"min": 0.02,
|
||||||
|
"max": 0.02,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription = Subscription.from_dict(
|
||||||
|
config=config,
|
||||||
|
preset_name=subscription_name,
|
||||||
|
preset_dict=preset_dict,
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock_download_collection_entries(
|
||||||
|
is_youtube_channel=False, num_urls=1, is_extracted_audio=False
|
||||||
|
), assert_logs(
|
||||||
|
logger=throttle_protection_logger,
|
||||||
|
expected_message="Sleeping between downloads for %0.2f seconds",
|
||||||
|
log_level="debug",
|
||||||
|
expected_occurrences=0,
|
||||||
|
):
|
||||||
|
_ = subscription.download(dry_run=False)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
|
import pytest
|
||||||
from unit.script.conftest import single_variable_output
|
from unit.script.conftest import single_variable_output
|
||||||
|
|
||||||
from ytdl_sub.utils.script import ScriptUtils
|
from ytdl_sub.utils.script import ScriptUtils
|
||||||
|
|
@ -31,3 +32,22 @@ class TestScriptUtils:
|
||||||
|
|
||||||
output = single_variable_output(ScriptUtils.to_script(json_dict))
|
output = single_variable_output(ScriptUtils.to_script(json_dict))
|
||||||
assert output == expected_output
|
assert output == expected_output
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"input_str, expected_output",
|
||||||
|
[
|
||||||
|
("", False),
|
||||||
|
("true", True),
|
||||||
|
("false", False),
|
||||||
|
("[ ]", False),
|
||||||
|
("{ }", False),
|
||||||
|
("True", True),
|
||||||
|
("False", False),
|
||||||
|
("lol not False", True),
|
||||||
|
("0", False),
|
||||||
|
("-1", True),
|
||||||
|
("1", True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_bool_formatter_output(self, input_str: str, expected_output: bool):
|
||||||
|
assert ScriptUtils.bool_formatter_output(input_str) == expected_output
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue