[FEATURE] Add __value__ to subscription files to be able to create one-liner subs (#641)
* [FEATURE] Add __value__ to subscription files to create one-liner subs * docs
This commit is contained in:
parent
557854fc83
commit
7a43b0a90b
3 changed files with 186 additions and 32 deletions
|
|
@ -306,6 +306,32 @@ by using the file-wide ``__preset__``:
|
||||||
This ``subscription.yaml`` is equivalent to the one above it because all
|
This ``subscription.yaml`` is equivalent to the one above it because all
|
||||||
subscriptions automatically set ``__preset__`` as a `parent preset`_.
|
subscriptions automatically set ``__preset__`` as a `parent preset`_.
|
||||||
|
|
||||||
|
File Subscription Value
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
With a clever config and use of ``__preset__``, your subscriptions can typically boil
|
||||||
|
down to a name and url. You can set ``__value__`` to the name of an override variable,
|
||||||
|
and use the override variable ``subscription_name`` to achieve one-liner subscriptions.
|
||||||
|
Using the example above, we can do:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
:caption: subscription.yaml
|
||||||
|
|
||||||
|
__preset__:
|
||||||
|
preset: "playlist_preset_ex"
|
||||||
|
overrides:
|
||||||
|
playlist_name: "{subscription_name}"
|
||||||
|
|
||||||
|
__value__: "url"
|
||||||
|
|
||||||
|
# single-line subscription
|
||||||
|
"diy-playlist": "https://youtube.com/playlist?list=UCsvn_Po0SmunchJYtttWpOxMg"
|
||||||
|
|
||||||
|
``"diy-playlist"`` gets assigned to the ``playlist_name`` override variable by setting
|
||||||
|
it with ``subscription_name`` , and the url gets assigned to ``url`` by setting ``__value__``
|
||||||
|
to write values to it.
|
||||||
|
|
||||||
|
Traditional subscriptions that can override presets will still work when using ``__value__``.
|
||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
.. _source-variables:
|
.. _source-variables:
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ from typing import List
|
||||||
from ytdl_sub.config.config_file import ConfigFile
|
from ytdl_sub.config.config_file import ConfigFile
|
||||||
from ytdl_sub.config.preset import Preset
|
from ytdl_sub.config.preset import Preset
|
||||||
from ytdl_sub.subscriptions.subscription_download import SubscriptionDownload
|
from ytdl_sub.subscriptions.subscription_download import SubscriptionDownload
|
||||||
|
from ytdl_sub.utils.exceptions import ValidationException
|
||||||
from ytdl_sub.utils.yaml import load_yaml
|
from ytdl_sub.utils.yaml import load_yaml
|
||||||
from ytdl_sub.validators.validators import LiteralDictValidator
|
from ytdl_sub.validators.validators import LiteralDictValidator
|
||||||
|
|
||||||
FILE_PRESET_APPLY_KEY = "__preset__"
|
FILE_PRESET_APPLY_KEY = "__preset__"
|
||||||
|
FILE_SUBSCRIPTION_VALUE_KEY = "__value__"
|
||||||
|
|
||||||
|
|
||||||
class Subscription(SubscriptionDownload):
|
class Subscription(SubscriptionDownload):
|
||||||
|
|
@ -64,7 +66,9 @@ class Subscription(SubscriptionDownload):
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_file_path(cls, config: ConfigFile, subscription_path: str) -> List["Subscription"]:
|
def from_file_path(cls, config: ConfigFile, subscription_path: str) -> List["Subscription"]:
|
||||||
"""
|
"""
|
||||||
Loads subscriptions from a file and applies __preset__ to all of them if present.
|
Loads subscriptions from a file and applies ``__preset__`` to all of them if present.
|
||||||
|
If a subscription is in the form of key: value, it will set value to the override
|
||||||
|
variable defined in ``__value__``.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
|
@ -76,11 +80,17 @@ class Subscription(SubscriptionDownload):
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
List of subscriptions, for each one in the subscription yaml
|
List of subscriptions, for each one in the subscription yaml
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValidationException
|
||||||
|
If subscription file is misconfigured
|
||||||
"""
|
"""
|
||||||
subscriptions: List["Subscription"] = []
|
subscriptions: List["Subscription"] = []
|
||||||
subscription_dict = load_yaml(file_path=subscription_path)
|
subscription_dict = load_yaml(file_path=subscription_path)
|
||||||
|
|
||||||
has_file_preset = FILE_PRESET_APPLY_KEY in subscription_dict
|
has_file_preset = FILE_PRESET_APPLY_KEY in subscription_dict
|
||||||
|
has_file_subscription_value = FILE_SUBSCRIPTION_VALUE_KEY in subscription_dict
|
||||||
|
|
||||||
# If a file preset is present...
|
# If a file preset is present...
|
||||||
if has_file_preset:
|
if has_file_preset:
|
||||||
|
|
@ -94,12 +104,39 @@ class Subscription(SubscriptionDownload):
|
||||||
config = copy.deepcopy(config)
|
config = copy.deepcopy(config)
|
||||||
config.presets.dict[FILE_PRESET_APPLY_KEY] = file_preset.dict
|
config.presets.dict[FILE_PRESET_APPLY_KEY] = file_preset.dict
|
||||||
|
|
||||||
|
if has_file_subscription_value:
|
||||||
|
if not isinstance(subscription_dict[FILE_SUBSCRIPTION_VALUE_KEY], str):
|
||||||
|
raise ValidationException(
|
||||||
|
f"Using {FILE_SUBSCRIPTION_VALUE_KEY} in a subscription"
|
||||||
|
f"must be a string that corresponds to an override variable"
|
||||||
|
)
|
||||||
|
|
||||||
for subscription_key, subscription_object in subscription_dict.items():
|
for subscription_key, subscription_object in subscription_dict.items():
|
||||||
|
|
||||||
# Skip file preset
|
# Skip file preset or value
|
||||||
if subscription_key == FILE_PRESET_APPLY_KEY:
|
if subscription_key in [FILE_PRESET_APPLY_KEY, FILE_SUBSCRIPTION_VALUE_KEY]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# If the subscription obj is just a string, set it to the override variable
|
||||||
|
# defined in FILE_SUBSCRIPTION_VALUE_KEY
|
||||||
|
if isinstance(subscription_object, str) and has_file_subscription_value:
|
||||||
|
subscription_object = {
|
||||||
|
"overrides": {
|
||||||
|
subscription_dict[FILE_SUBSCRIPTION_VALUE_KEY]: subscription_object
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elif isinstance(subscription_object, dict):
|
||||||
|
pass
|
||||||
|
elif isinstance(subscription_object, str) and not has_file_subscription_value:
|
||||||
|
raise ValidationException(
|
||||||
|
f"Subscription {subscription_key} is a string, but "
|
||||||
|
f"{FILE_SUBSCRIPTION_VALUE_KEY} is not set to an override variable"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValidationException(
|
||||||
|
f"Subscription {subscription_key} should be in the form of a preset"
|
||||||
|
)
|
||||||
|
|
||||||
# If it has file_preset, inject it as a parent preset
|
# If it has file_preset, inject it as a parent preset
|
||||||
if has_file_preset:
|
if has_file_preset:
|
||||||
parent_preset = subscription_object.get("preset", [])
|
parent_preset = subscription_object.get("preset", [])
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import re
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
@ -5,44 +6,134 @@ import pytest
|
||||||
|
|
||||||
from ytdl_sub.config.config_file import ConfigFile
|
from ytdl_sub.config.config_file import ConfigFile
|
||||||
from ytdl_sub.plugins.nfo_tags import NfoTagsOptions
|
from ytdl_sub.plugins.nfo_tags import NfoTagsOptions
|
||||||
|
from ytdl_sub.subscriptions.subscription import FILE_SUBSCRIPTION_VALUE_KEY
|
||||||
from ytdl_sub.subscriptions.subscription import Subscription
|
from ytdl_sub.subscriptions.subscription import Subscription
|
||||||
|
from ytdl_sub.utils.exceptions import ValidationException
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def preset_file(youtube_video: Dict, output_options: Dict) -> Dict:
|
def preset_file(youtube_video: Dict, output_options: Dict):
|
||||||
return {
|
with patch("ytdl_sub.subscriptions.subscription.load_yaml") as mock_load_yaml:
|
||||||
"__preset__": {
|
mock_load_yaml.return_value = {
|
||||||
"download": youtube_video,
|
"__preset__": {
|
||||||
"output_options": output_options,
|
"download": youtube_video,
|
||||||
"nfo_tags": {
|
"output_options": output_options,
|
||||||
"tags": {"key-3": "file_preset"},
|
"nfo_tags": {
|
||||||
|
"tags": {"key-3": "file_preset"},
|
||||||
|
},
|
||||||
|
"overrides": {"test_override": "original"},
|
||||||
},
|
},
|
||||||
},
|
"__value__": "test_override",
|
||||||
"test_preset": {
|
"test_preset": {
|
||||||
"preset": "parent_preset_3",
|
"preset": "parent_preset_3",
|
||||||
"nfo_tags": {
|
"nfo_tags": {
|
||||||
"tags": {"key-4": "test_preset"},
|
"tags": {"key-4": "test_preset"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def preset_file_with_value(youtube_video: Dict, output_options: Dict):
|
||||||
|
with patch("ytdl_sub.subscriptions.subscription.load_yaml") as mock_load_yaml:
|
||||||
|
mock_load_yaml.return_value = {
|
||||||
|
"__preset__": {
|
||||||
|
"preset": "parent_preset_3",
|
||||||
|
"download": youtube_video,
|
||||||
|
"output_options": output_options,
|
||||||
|
"nfo_tags": {
|
||||||
|
"tags": {"key-3": "file_preset"},
|
||||||
|
},
|
||||||
|
"overrides": {"test_override": "original"},
|
||||||
|
},
|
||||||
|
"__value__": "test_override",
|
||||||
|
"test_value": "is_overwritten",
|
||||||
|
}
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def preset_file_invalid_value():
|
||||||
|
with patch("ytdl_sub.subscriptions.subscription.load_yaml") as mock_load_yaml:
|
||||||
|
mock_load_yaml.return_value = {
|
||||||
|
"__value__": {"should be": "string"},
|
||||||
|
}
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def preset_file_subscription_using_value_when_not_defined():
|
||||||
|
with patch("ytdl_sub.subscriptions.subscription.load_yaml") as mock_load_yaml:
|
||||||
|
mock_load_yaml.return_value = {"sub_name": "single value, __value__ not defined"}
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def preset_file_subscription_with_invalid_form():
|
||||||
|
with patch("ytdl_sub.subscriptions.subscription.load_yaml") as mock_load_yaml:
|
||||||
|
mock_load_yaml.return_value = {"sub_name": 4332}
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(preset_file.__name__)
|
||||||
|
def test_subscription_file_preset_applies(config_file: ConfigFile):
|
||||||
|
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
||||||
|
assert len(subs) == 1
|
||||||
|
|
||||||
|
# Test __preset__ worked correctly
|
||||||
|
preset_sub = subs[0]
|
||||||
|
nfo_options: NfoTagsOptions = preset_sub.plugins.get(NfoTagsOptions)
|
||||||
|
tags_string_dict = {
|
||||||
|
key: formatter[0].format_string for key, formatter in nfo_options.tags.string_tags.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
assert tags_string_dict == {
|
||||||
|
"key-1": "preset_0",
|
||||||
|
"key-2": "preset_2",
|
||||||
|
"key-3": "file_preset",
|
||||||
|
"key-4": "test_preset",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_subscription_file_preset_applies(config_file: ConfigFile, preset_file: Dict):
|
@pytest.mark.usefixtures(preset_file_with_value.__name__)
|
||||||
with patch("ytdl_sub.subscriptions.subscription.load_yaml") as mock_load_yaml:
|
def test_subscription_file_value_applies(config_file: ConfigFile):
|
||||||
mock_load_yaml.return_value = preset_file
|
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
||||||
|
assert len(subs) == 1
|
||||||
|
|
||||||
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
# Test __value__ worked correctly
|
||||||
assert len(subs) == 1
|
value_sub = subs[0]
|
||||||
|
assert value_sub.overrides.dict_with_format_strings.get("test_override") == "is_overwritten"
|
||||||
|
|
||||||
nfo_options: NfoTagsOptions = subs[0].plugins.get(NfoTagsOptions)
|
|
||||||
tags_string_dict = {
|
|
||||||
key: formatter[0].format_string
|
|
||||||
for key, formatter in nfo_options.tags.string_tags.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
assert tags_string_dict == {
|
@pytest.mark.usefixtures(preset_file_invalid_value.__name__)
|
||||||
"key-1": "preset_0",
|
def test_subscription_file_bad_value(config_file: ConfigFile):
|
||||||
"key-2": "preset_2",
|
with pytest.raises(
|
||||||
"key-3": "file_preset",
|
ValidationException,
|
||||||
"key-4": "test_preset",
|
match=re.escape(
|
||||||
}
|
f"Using {FILE_SUBSCRIPTION_VALUE_KEY} in a subscription"
|
||||||
|
f"must be a string that corresponds to an override variable"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
_ = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(preset_file_subscription_using_value_when_not_defined.__name__)
|
||||||
|
def test_subscription_file_using_value_when_not_defined(config_file: ConfigFile):
|
||||||
|
with pytest.raises(
|
||||||
|
ValidationException,
|
||||||
|
match=re.escape(
|
||||||
|
f"Subscription sub_name is a string, but "
|
||||||
|
f"{FILE_SUBSCRIPTION_VALUE_KEY} is not set to an override variable"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
_ = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(preset_file_subscription_with_invalid_form.__name__)
|
||||||
|
def test_subscription_file_invalid_form(config_file: ConfigFile):
|
||||||
|
with pytest.raises(
|
||||||
|
ValidationException,
|
||||||
|
match=re.escape(f"Subscription sub_name should be in the form of a preset"),
|
||||||
|
):
|
||||||
|
_ = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue