[FEATURE] Extend subscription syntax (#790)

Extends the subscription.yaml syntax at the cost of introducing a breaking change.

# New Syntax
## Mix-n-match presets and indent variables
```
# Can mix/match presets and indent override variables.
# Uses presets TV Show, Only Recent and assigns Kids, TV-Y to subscription_indent_1 and _2
TV Show | = Kids | = TV-Y | Only Recent:
  "Jake Trains": "https://..."
```

## Subscriptions with list-value support
```
TV Show | = Kids | = TV-Y | Only Recent:
  "Jake Trains":
    - "https://url.1..."  # Assigns to subscription_value and subscription_value_1
    - "https://url.2..."  # Assigns to subscription_value_2
```

## Subscriptions with override-keys support
```
TV Show | = Kids | = TV-Y | Only Recent:
  "~Jake Trains":  # the ~ means "all keys underneath get assigned as override variables"
    url: "https://url.1..."  # Assigns to url
    url2: "https://url.2..."  # Assigns to url2
```

# Breaking Changes
In the TV show subscriptions example (https://github.com/jmbannon/ytdl-sub/blob/master/examples/tv_show_subscriptions.yaml), it had
```
TV Show Full Archive:
  # Sets "Kids" for genre, "TV-Y" for content rating
  = Kids | TV-Y:
    "Jake Trains": "https://www.youtube.com/@JakeTrains"
    "Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel"
```
This must be changed to
```
TV Show Full Archive:
  # Sets "Kids" for genre, "TV-Y" for content rating
  = Kids | = TV-Y:  # Each indent variable assignment must have an = before it
    "Jake Trains": "https://www.youtube.com/@JakeTrains"
    "Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel"
```
This commit is contained in:
Jesse Bannon 2023-11-01 20:24:06 -07:00 committed by GitHub
parent 056b111be6
commit 9f408d2196
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 397 additions and 118 deletions

View file

@ -13,12 +13,12 @@ TV Show Full Archive:
"Opeth": "https://www.youtube.com/channel/UCmQSJTFZaXN85gYk6W3XbdQ" "Opeth": "https://www.youtube.com/channel/UCmQSJTFZaXN85gYk6W3XbdQ"
# Sets "Kids" for genre, "TV-Y" for content rating # Sets "Kids" for genre, "TV-Y" for content rating
= Kids | TV-Y: = Kids | = TV-Y:
"Jake Trains": "https://www.youtube.com/@JakeTrains" "Jake Trains": "https://www.youtube.com/@JakeTrains"
"Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel" "Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel"
# All subscriptions under this will use the `TV Show Only Recent` preset # All subscriptions under this will use the `TV Show Only Recent` preset
TV Show Only Recent: TV Show Only Recent:
= News | TV-14: = News | = TV-14:
"BBC": "https://www.youtube.com/@BBCNews" "BBC": "https://www.youtube.com/@BBCNews"

View file

@ -10,6 +10,7 @@ from ytdl_sub.config.defaults import DEFAULT_FFPROBE_PATH
from ytdl_sub.config.defaults import DEFAULT_LOCK_DIRECTORY from ytdl_sub.config.defaults import DEFAULT_LOCK_DIRECTORY
from ytdl_sub.config.defaults import MAX_FILE_NAME_BYTES from ytdl_sub.config.defaults import MAX_FILE_NAME_BYTES
from ytdl_sub.prebuilt_presets import PREBUILT_PRESETS from ytdl_sub.prebuilt_presets import PREBUILT_PRESETS
from ytdl_sub.subscriptions.utils import SUBSCRIPTION_VALUE_CONFIG_KEY
from ytdl_sub.validators.file_path_validators import FFmpegFileValidator from ytdl_sub.validators.file_path_validators import FFmpegFileValidator
from ytdl_sub.validators.file_path_validators import FFprobeFileValidator from ytdl_sub.validators.file_path_validators import FFprobeFileValidator
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
@ -106,7 +107,7 @@ class ConfigOptions(StrictDictValidator):
"ffprobe_path", "ffprobe_path",
"file_name_max_bytes", "file_name_max_bytes",
"experimental", "experimental",
"subscription_value", SUBSCRIPTION_VALUE_CONFIG_KEY,
} }
def __init__(self, name: str, value: Any): def __init__(self, name: str, value: Any):
@ -142,7 +143,7 @@ class ConfigOptions(StrictDictValidator):
key="file_name_max_bytes", validator=IntValidator, default=MAX_FILE_NAME_BYTES key="file_name_max_bytes", validator=IntValidator, default=MAX_FILE_NAME_BYTES
) )
self._subscription_value = self._validate_key_if_present( self._subscription_value = self._validate_key_if_present(
key="subscription_value", validator=StringValidator key=SUBSCRIPTION_VALUE_CONFIG_KEY, validator=StringValidator
) )
@property @property

View file

@ -9,6 +9,7 @@ from yt_dlp.utils import sanitize_filename
from ytdl_sub.config.defaults import DEFAULT_DOWNLOAD_ARCHIVE_NAME from ytdl_sub.config.defaults import DEFAULT_DOWNLOAD_ARCHIVE_NAME
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.subscriptions.utils import SUBSCRIPTION_NAME
from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.exceptions import ValidationException
from ytdl_sub.validators.file_path_validators import OverridesStringFormatterFilePathValidator from ytdl_sub.validators.file_path_validators import OverridesStringFormatterFilePathValidator
from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator
@ -120,45 +121,9 @@ class YTDLOptions(LiteralDictValidator):
""" """
class OverridesVariables(DictFormatterValidator):
"""
Override variables that are automatically added to every subscription.
"""
def _add_override_variable(self, key_name: str, format_string: str, sanitize: bool = False):
if sanitize:
key_name = f"{key_name}_sanitized"
format_string = sanitize_filename(format_string)
self._value[key_name] = StringFormatterValidator(
name="__should_never_fail__",
value=format_string,
)
def __init__(self, name, value):
super().__init__(name, value)
# Add sanitized and non-sanitized override variables
for sanitize in [True, False]:
self._add_override_variable(
key_name="subscription_name",
format_string=self.subscription_name,
sanitize=sanitize,
)
@property
def subscription_name(self) -> str:
"""
Returns
-------
Name of the subscription
"""
return self._root_name
# Disable for proper docstring formatting # Disable for proper docstring formatting
# pylint: disable=line-too-long # pylint: disable=line-too-long
class Overrides(OverridesVariables): class Overrides(DictFormatterValidator):
""" """
Optional. This section allows you to define variables that can be used in any string formatter. Optional. This section allows you to define variables that can be used in any string formatter.
For example, if you want your file and thumbnail files to match without copy-pasting a large For example, if you want your file and thumbnail files to match without copy-pasting a large
@ -188,6 +153,16 @@ class Overrides(OverridesVariables):
# pylint: enable=line-too-long # pylint: enable=line-too-long
def _add_override_variable(self, key_name: str, format_string: str, sanitize: bool = False):
if sanitize:
key_name = f"{key_name}_sanitized"
format_string = sanitize_filename(format_string)
self._value[key_name] = StringFormatterValidator(
name="__should_never_fail__",
value=format_string,
)
def __init__(self, name, value): def __init__(self, name, value):
super().__init__(name, value) super().__init__(name, value)
@ -199,6 +174,23 @@ class Overrides(OverridesVariables):
sanitize=True, sanitize=True,
) )
if SUBSCRIPTION_NAME not in self._value:
for sanitized in [True, False]:
self._add_override_variable(
key_name=SUBSCRIPTION_NAME,
format_string=self.subscription_name,
sanitize=sanitized,
)
@property
def subscription_name(self) -> str:
"""
Returns
-------
Name of the subscription
"""
return self._root_name
def apply_formatter( def apply_formatter(
self, self,
formatter: StringFormatterValidator, formatter: StringFormatterValidator,

View file

@ -1,53 +1,25 @@
import copy import copy
import dataclasses
from abc import ABC from abc import ABC
from abc import abstractmethod from abc import abstractmethod
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 typing import final
from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.config.preset_options import Overrides from ytdl_sub.config.preset_options import Overrides
from ytdl_sub.subscriptions.utils import SUBSCRIPTION_NAME
from ytdl_sub.subscriptions.utils import SUBSCRIPTION_VALUE
from ytdl_sub.subscriptions.utils import subscription_indent_variable_name
from ytdl_sub.subscriptions.utils import subscription_list_variable_name
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
from ytdl_sub.validators.validators import DictValidator from ytdl_sub.validators.validators import DictValidator
from ytdl_sub.validators.validators import StringListValidator from ytdl_sub.validators.validators import StringListValidator
from ytdl_sub.validators.validators import StringValidator from ytdl_sub.validators.validators import StringValidator
from ytdl_sub.validators.validators import Validator from ytdl_sub.validators.validators import Validator
def subscription_indent_variable_name(index: int) -> str:
"""
Parameters
----------
index
0th-based index
Returns
-------
subscription_index_i, where i is 1-based index
"""
return f"subscription_indent_{index + 1}"
def subscription_value_variable_name() -> str:
"""
Returns
-------
The override variable name containing the subscription value if present
"""
return "subscription_value"
def maybe_indent_override_values(value: str) -> List[str]:
"""
Returns
-------
Value if it is an overide [Value]. None otherwise.
"""
if value.startswith("="):
# Drop the =, split on |, and strip each indent_value (both left + right)
return [indent_value.strip() for indent_value in value[1:].split("|")]
return []
class SubscriptionOutput(Validator, ABC): class SubscriptionOutput(Validator, ABC):
def __init__(self, name, value, presets: List[str], indent_overrides: List[str]): def __init__(self, name, value, presets: List[str], indent_overrides: List[str]):
super().__init__(name, value) super().__init__(name, value)
@ -75,6 +47,15 @@ class SubscriptionOutput(Validator, ABC):
Subscriptions in the form of ``{ subscription_name: preset_dict }`` Subscriptions in the form of ``{ subscription_name: preset_dict }``
""" """
@property
def subscription_name(self) -> str:
"""
Returns
-------
The name of the subscription
"""
return self._leaf_name
class SubscriptionPresetDictValidator(SubscriptionOutput, DictValidator): class SubscriptionPresetDictValidator(SubscriptionOutput, DictValidator):
def __init__(self, name, value, presets: List[str], indent_overrides: List[str]): def __init__(self, name, value, presets: List[str], indent_overrides: List[str]):
@ -93,12 +74,46 @@ class SubscriptionPresetDictValidator(SubscriptionOutput, DictValidator):
output_dict["preset"] = parent_presets + self._presets + global_presets_to_apply output_dict["preset"] = parent_presets + self._presets + global_presets_to_apply
output_dict["overrides"] = dict( output_dict["overrides"] = dict(
output_dict.get("overrides", {}), **self._indent_overrides_dict() output_dict.get("overrides", {}),
**self._indent_overrides_dict(),
**{SUBSCRIPTION_NAME: self.subscription_name},
) )
return {self._leaf_name: output_dict} return {self.subscription_name: output_dict}
class SubscriptionValueValidator(SubscriptionOutput, StringValidator): class SubscriptionLeafValidator(SubscriptionOutput, ABC):
def __init__(
self,
name,
value,
config: ConfigFile,
presets: List[str],
indent_overrides: List[str],
):
super().__init__(name=name, value=value, presets=presets, indent_overrides=indent_overrides)
if self.subscription_name in config.presets.keys:
raise self._validation_exception(
f"{self.subscription_name} conflicts with an existing preset name and cannot be "
f"used as a subscription name"
)
self._overrides_to_add: Dict[str, str] = {SUBSCRIPTION_NAME: self.subscription_name}
@final
def subscription_dicts(self, global_presets_to_apply: List[str]) -> Dict[str, Dict]:
return {
self.subscription_name: {
"preset": self._presets + global_presets_to_apply,
"overrides": dict(
self._indent_overrides_dict(),
**self._overrides_to_add,
),
}
}
class SubscriptionValueValidator(SubscriptionLeafValidator, StringValidator):
def __init__( def __init__(
self, self,
name, name,
@ -108,30 +123,73 @@ class SubscriptionValueValidator(SubscriptionOutput, StringValidator):
indent_overrides: List[str], indent_overrides: List[str],
subscription_value: Optional[str], subscription_value: Optional[str],
): ):
super().__init__(name=name, value=value, presets=presets, indent_overrides=indent_overrides) super().__init__(
name=name,
value=value,
config=config,
presets=presets,
indent_overrides=indent_overrides,
)
if self._leaf_name in config.presets.keys:
raise self._validation_exception(
f"{self._leaf_name} conflicts with an existing preset name and cannot be "
f"used as a subscription name"
)
self._subscription_value: Optional[str] = subscription_value
def subscription_dicts(self, global_presets_to_apply: List[str]) -> Dict[str, Dict]:
subscription_value_dict: Dict[str, str] = {"subscription_value": self.value}
# TODO: Eventually delete in favor of {subscription_value} # TODO: Eventually delete in favor of {subscription_value}
if self._subscription_value: if subscription_value:
subscription_value_dict[self._subscription_value] = self.value self._overrides_to_add[subscription_value] = self.value
self._overrides_to_add[SUBSCRIPTION_VALUE] = self.value
return {
self._leaf_name: { class SubscriptionListValuesValidator(SubscriptionLeafValidator, StringListValidator):
"preset": self._presets + global_presets_to_apply, def __init__(
"overrides": dict( self,
subscription_value_dict, name,
**self._indent_overrides_dict(), value,
), config: ConfigFile,
} presets: List[str],
} indent_overrides: List[str],
):
super().__init__(
name=name,
value=value,
config=config,
presets=presets,
indent_overrides=indent_overrides,
)
for idx, list_value in enumerate(self.list):
# Write the first list value into subscription_value as well
if idx == 0:
self._overrides_to_add[SUBSCRIPTION_VALUE] = list_value.value
self._overrides_to_add[subscription_list_variable_name(index=idx)] = list_value.value
class SubscriptionWithOverridesValidator(SubscriptionLeafValidator, DictFormatterValidator):
def __init__(
self,
name,
value,
config: ConfigFile,
presets: List[str],
indent_overrides: List[str],
):
super().__init__(
name=name,
value=value,
config=config,
presets=presets,
indent_overrides=indent_overrides,
)
self._overrides_to_add = dict(self.dict_with_format_strings, **self._overrides_to_add)
@property
def subscription_name(self) -> str:
"""
Returns
-------
Name of the subscription
"""
# drop the ~ in "~Subscription Name":
return super().subscription_name[1:]
class SubscriptionValidator(SubscriptionOutput): class SubscriptionValidator(SubscriptionOutput):
@ -139,6 +197,35 @@ class SubscriptionValidator(SubscriptionOutput):
Top-level subscription validator Top-level subscription validator
""" """
@dataclasses.dataclass
class PresetIndentKey:
presets: List[str] = dataclasses.field(default_factory=list)
indent_overrides: List[str] = dataclasses.field(default_factory=list)
def _preset_indent_key(self, key: str, config: ConfigFile) -> Optional[PresetIndentKey]:
presets: List[str] = []
indent_overrides: List[str] = []
stripped_split_keys = [sub_key.strip() for sub_key in key.split("|")]
for sub_key in stripped_split_keys:
if sub_key.startswith("="):
indent_overrides.append(sub_key[1:].strip())
elif sub_key in config.presets.keys:
presets.append(sub_key)
else:
if presets or indent_overrides:
raise self._validation_exception(
f"'{sub_key.strip()}' in '{key.strip()}' is not a preset name. "
f"To use as a subscription indent value, define it as '= {sub_key.strip()}'"
)
if not presets and not indent_overrides:
return None
return SubscriptionValidator.PresetIndentKey(
presets=presets, indent_overrides=indent_overrides
)
def __init__( def __init__(
self, self,
name, name,
@ -154,6 +241,8 @@ class SubscriptionValidator(SubscriptionOutput):
for key, obj in value.items(): for key, obj in value.items():
obj_name = f"{name}.{key}" if name else key obj_name = f"{name}.{key}" if name else key
# Subscription defined as
# "Sub Name": "value"
if isinstance(obj, str): if isinstance(obj, str):
self._children.append( self._children.append(
SubscriptionValueValidator( SubscriptionValueValidator(
@ -165,26 +254,45 @@ class SubscriptionValidator(SubscriptionOutput):
subscription_value=subscription_value, subscription_value=subscription_value,
) )
) )
elif isinstance(value, dict): # Subscription defined as
if key in config.presets.keys: # "Sub Name":
self._children.append( # - "value1"
SubscriptionValidator( # - "value2"
name=obj_name, elif isinstance(obj, list):
value=obj, self._children.append(
config=config, SubscriptionListValuesValidator(
presets=presets + [key], name=obj_name,
indent_overrides=indent_overrides, value=obj,
subscription_value=subscription_value, config=config,
) presets=presets,
indent_overrides=indent_overrides,
) )
elif override_values := maybe_indent_override_values(key): )
elif isinstance(obj, dict):
# Subscription defined as
# "~Sub Name":
# override_1: "abc"
# override_2: "123"
if key.startswith("~"):
self._children.append( self._children.append(
SubscriptionValidator( SubscriptionWithOverridesValidator(
name=obj_name, name=obj_name,
value=obj, value=obj,
config=config, config=config,
presets=presets, presets=presets,
indent_overrides=indent_overrides + override_values, indent_overrides=indent_overrides,
)
)
elif (
preset_indent_key := self._preset_indent_key(key=key, config=config)
) is not None:
self._children.append(
SubscriptionValidator(
name=obj_name,
value=obj,
config=config,
presets=presets + preset_indent_key.presets,
indent_overrides=indent_overrides + preset_indent_key.indent_overrides,
subscription_value=subscription_value, subscription_value=subscription_value,
) )
) )
@ -197,6 +305,10 @@ class SubscriptionValidator(SubscriptionOutput):
indent_overrides=indent_overrides, indent_overrides=indent_overrides,
) )
) )
else:
raise self._validation_exception(
"Subscription value should either be a string, list, or object"
)
def subscription_dicts(self, global_presets_to_apply: List[str]) -> Dict[str, Dict]: def subscription_dicts(self, global_presets_to_apply: List[str]) -> Dict[str, Dict]:
subscription_dicts: Dict[str, Dict] = {} subscription_dicts: Dict[str, Dict] = {}

View file

@ -0,0 +1,33 @@
SUBSCRIPTION_NAME = "subscription_name"
SUBSCRIPTION_VALUE = "subscription_value"
# Key used in configs, should delete at some point
SUBSCRIPTION_VALUE_CONFIG_KEY = "subscription_value"
def subscription_indent_variable_name(index: int) -> str:
"""
Parameters
----------
index
0th-based index
Returns
-------
subscription_index_i, where i is 1-based index
"""
return f"subscription_indent_{index + 1}"
def subscription_list_variable_name(index: int) -> str:
"""
Parameters
----------
index
0th-based index
Returns
-------
subscription_value_i, where i is 1-based index
"""
return f"subscription_value_{index + 1}"

View file

@ -116,6 +116,72 @@ def preset_with_subscription_value_nested_presets_and_indent_variables(
@pytest.fixture @pytest.fixture
def preset_with_subscription_value_nested_presets_and_indent_variables_same_line( def preset_with_subscription_value_nested_presets_and_indent_variables_same_line(
preset_with_subscription_value: Dict, preset_with_subscription_value: Dict,
):
return dict(
preset_with_subscription_value,
**{
"parent_preset_2": {
"=INDENT_1": {
"parent_preset_1": {"test_2_1": "is_2_1_overwritten"},
"= INDENT_2 | = INDENT_3 ": {
"test_1": "is_1_overwritten",
},
}
}
},
)
@pytest.fixture
def preset_with_subscription_value_nested_presets_and_indent_variables_all_same_line(
preset_with_subscription_value: Dict,
):
return dict(
preset_with_subscription_value,
**{
"parent_preset_2 | =INDENT_1 | parent_preset_1": {
"test_2_1": "is_2_1_overwritten",
},
"parent_preset_2 | =INDENT_1 |= INDENT_2 | = INDENT_3 ": {
"test_1": "is_1_overwritten",
},
},
)
@pytest.fixture
def preset_with_subscription_list(
preset_with_subscription_value: Dict,
):
return dict(
preset_with_subscription_value,
**{
"parent_preset_2 | parent_preset_1": {
"test_2_1": ["is_2_1_overwritten", "is_2_1_list_2"]
},
},
)
@pytest.fixture
def preset_with_subscription_overrides_tilda(
preset_with_subscription_value: Dict,
):
return dict(
preset_with_subscription_value,
**{
"parent_preset_2 | parent_preset_1": {
"~test_2_1": {
"current_override": "test_2_1",
}
},
},
)
@pytest.fixture
def preset_with_subscription_value_nested_presets_and_indent_variables_same_line_old_format_errors(
preset_with_subscription_value: Dict,
): ):
return dict( return dict(
preset_with_subscription_value, preset_with_subscription_value,
@ -191,6 +257,7 @@ def test_subscription_file_value_applies_sub_file_takes_precedence(
assert value_sub.get("test_file_subscription_value") == "is_overwritten" assert value_sub.get("test_file_subscription_value") == "is_overwritten"
assert value_sub.get("test_config_subscription_value") == "original" assert value_sub.get("test_config_subscription_value") == "original"
assert value_sub.get("subscription_name") == "test_value" assert value_sub.get("subscription_name") == "test_value"
assert value_sub.get("subscription_name_sanitized") == "test_value"
assert value_sub.get("subscription_value") == "is_overwritten" assert value_sub.get("subscription_value") == "is_overwritten"
assert value_sub.get("current_override") == "__preset__" # ensure __preset__ takes precedence assert value_sub.get("current_override") == "__preset__" # ensure __preset__ takes precedence
@ -209,6 +276,7 @@ def test_subscription_file_value_applies_from_config(
assert value_sub.get("test_file_subscription_value") == "original" assert value_sub.get("test_file_subscription_value") == "original"
assert value_sub.get("test_config_subscription_value") == "is_overwritten" assert value_sub.get("test_config_subscription_value") == "is_overwritten"
assert value_sub.get("subscription_name") == "test_value" assert value_sub.get("subscription_name") == "test_value"
assert value_sub.get("subscription_name_sanitized") == "test_value"
assert value_sub.get("subscription_value") == "is_overwritten" assert value_sub.get("subscription_value") == "is_overwritten"
assert value_sub.get("current_override") == "__preset__" # ensure __preset__ takes precedence assert value_sub.get("current_override") == "__preset__" # ensure __preset__ takes precedence
@ -229,15 +297,56 @@ def test_subscription_file_value_applies_from_config_and_nested(
assert sub_1.get("test_config_subscription_value") == "is_1_overwritten" assert sub_1.get("test_config_subscription_value") == "is_1_overwritten"
assert sub_1.get("subscription_name") == "test_1" assert sub_1.get("subscription_name") == "test_1"
assert sub_1.get("subscription_name_sanitized") == "test_1"
assert sub_1.get("subscription_value") == "is_1_overwritten" assert sub_1.get("subscription_value") == "is_1_overwritten"
assert sub_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence assert sub_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence
assert sub_2_1.get("test_config_subscription_value") == "is_2_1_overwritten" assert sub_2_1.get("test_config_subscription_value") == "is_2_1_overwritten"
assert sub_2_1.get("subscription_name") == "test_2_1" assert sub_2_1.get("subscription_name") == "test_2_1"
assert sub_2_1.get("subscription_name_sanitized") == "test_2_1"
assert sub_2_1.get("subscription_value") == "is_2_1_overwritten" assert sub_2_1.get("subscription_value") == "is_2_1_overwritten"
assert sub_2_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence assert sub_2_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence
def test_subscription_list(
config_file_with_subscription_value: ConfigFile,
preset_with_subscription_list: Dict,
):
with mock_load_yaml(preset_dict=preset_with_subscription_list):
subs = Subscription.from_file_path(
config=config_file_with_subscription_value, subscription_path="mocked"
)
assert len(subs) == 3
# Test __value__ worked correctly from the config
sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.dict_with_format_strings
assert sub_2_1.get("subscription_name") == "test_2_1"
assert sub_2_1.get("subscription_name_sanitized") == "test_2_1"
assert sub_2_1.get("subscription_value") == "is_2_1_overwritten"
assert sub_2_1.get("subscription_value_1") == "is_2_1_overwritten"
assert sub_2_1.get("subscription_value_2") == "is_2_1_list_2"
assert sub_2_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence
def test_subscription_overrides_tilda(
config_file_with_subscription_value: ConfigFile,
preset_with_subscription_overrides_tilda: Dict,
):
with mock_load_yaml(preset_dict=preset_with_subscription_overrides_tilda):
subs = Subscription.from_file_path(
config=config_file_with_subscription_value, subscription_path="mocked"
)
assert len(subs) == 3
# Test __value__ worked correctly from the config
sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.dict_with_format_strings
assert sub_2_1.get("subscription_name") == "test_2_1"
assert sub_2_1.get("subscription_name_sanitized") == "test_2_1"
assert sub_2_1.get("current_override") == "test_2_1" # tilda sub takes precedence
def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables( def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables(
config_file_with_subscription_value: ConfigFile, config_file_with_subscription_value: ConfigFile,
preset_with_subscription_value_nested_presets_and_indent_variables: Dict, preset_with_subscription_value_nested_presets_and_indent_variables: Dict,
@ -262,6 +371,7 @@ def test_subscription_file_value_applies_from_config_and_nested_and_indent_varia
assert sub_1.get("test_config_subscription_value") == "is_1_overwritten" assert sub_1.get("test_config_subscription_value") == "is_1_overwritten"
assert sub_1.get("subscription_name") == "test_1" assert sub_1.get("subscription_name") == "test_1"
assert sub_1.get("subscription_name_sanitized") == "test_1"
assert sub_1.get("subscription_value") == "is_1_overwritten" assert sub_1.get("subscription_value") == "is_1_overwritten"
assert sub_1.get("subscription_indent_1") == "INDENT_1" assert sub_1.get("subscription_indent_1") == "INDENT_1"
assert sub_1.get("subscription_indent_2") == "INDENT_2" assert sub_1.get("subscription_indent_2") == "INDENT_2"
@ -269,19 +379,27 @@ def test_subscription_file_value_applies_from_config_and_nested_and_indent_varia
assert sub_2_1.get("test_config_subscription_value") == "is_2_1_overwritten" assert sub_2_1.get("test_config_subscription_value") == "is_2_1_overwritten"
assert sub_2_1.get("subscription_name") == "test_2_1" assert sub_2_1.get("subscription_name") == "test_2_1"
assert sub_2_1.get("subscription_name_sanitized") == "test_2_1"
assert sub_2_1.get("subscription_value") == "is_2_1_overwritten" assert sub_2_1.get("subscription_value") == "is_2_1_overwritten"
assert sub_2_1.get("subscription_indent_1") == "INDENT_1" assert sub_2_1.get("subscription_indent_1") == "INDENT_1"
assert sub_2_1.get("subscription_indent_2") == "original_2" assert sub_2_1.get("subscription_indent_2") == "original_2"
assert sub_2_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence assert sub_2_1.get("current_override") == "__preset__" # ensure __preset__ takes precedence
@pytest.mark.parametrize("all_same_line", [True, False])
def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables_same_line( def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables_same_line(
config_file_with_subscription_value: ConfigFile, config_file_with_subscription_value: ConfigFile,
preset_with_subscription_value_nested_presets_and_indent_variables_same_line: Dict, preset_with_subscription_value_nested_presets_and_indent_variables_same_line: Dict,
preset_with_subscription_value_nested_presets_and_indent_variables_all_same_line: Dict,
all_same_line: bool,
): ):
with mock_load_yaml( preset_dict = preset_with_subscription_value_nested_presets_and_indent_variables_same_line
preset_dict=preset_with_subscription_value_nested_presets_and_indent_variables_same_line if all_same_line:
): preset_dict = (
preset_with_subscription_value_nested_presets_and_indent_variables_all_same_line
)
with mock_load_yaml(preset_dict=preset_dict):
subs = Subscription.from_file_path( subs = Subscription.from_file_path(
config=config_file_with_subscription_value, subscription_path="mocked" config=config_file_with_subscription_value, subscription_path="mocked"
) )
@ -299,6 +417,7 @@ def test_subscription_file_value_applies_from_config_and_nested_and_indent_varia
assert sub_1.get("test_config_subscription_value") == "is_1_overwritten" assert sub_1.get("test_config_subscription_value") == "is_1_overwritten"
assert sub_1.get("subscription_name") == "test_1" assert sub_1.get("subscription_name") == "test_1"
assert sub_1.get("subscription_name_sanitized") == "test_1"
assert sub_1.get("subscription_value") == "is_1_overwritten" assert sub_1.get("subscription_value") == "is_1_overwritten"
assert sub_1.get("subscription_indent_1") == "INDENT_1" assert sub_1.get("subscription_indent_1") == "INDENT_1"
assert sub_1.get("subscription_indent_2") == "INDENT_2" assert sub_1.get("subscription_indent_2") == "INDENT_2"
@ -307,6 +426,7 @@ def test_subscription_file_value_applies_from_config_and_nested_and_indent_varia
assert sub_2_1.get("test_config_subscription_value") == "is_2_1_overwritten" assert sub_2_1.get("test_config_subscription_value") == "is_2_1_overwritten"
assert sub_2_1.get("subscription_name") == "test_2_1" assert sub_2_1.get("subscription_name") == "test_2_1"
assert sub_2_1.get("subscription_name_sanitized") == "test_2_1"
assert sub_2_1.get("subscription_value") == "is_2_1_overwritten" assert sub_2_1.get("subscription_value") == "is_2_1_overwritten"
assert sub_2_1.get("subscription_indent_1") == "INDENT_1" assert sub_2_1.get("subscription_indent_1") == "INDENT_1"
assert sub_2_1.get("subscription_indent_2") == "original_2" assert sub_2_1.get("subscription_indent_2") == "original_2"
@ -314,6 +434,24 @@ def test_subscription_file_value_applies_from_config_and_nested_and_indent_varia
assert "subscription_indent_3" not in sub_2_1 assert "subscription_indent_3" not in sub_2_1
def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables_same_line_old_format_errors(
config_file_with_subscription_value: ConfigFile,
preset_with_subscription_value_nested_presets_and_indent_variables_same_line_old_format_errors: Dict,
):
with mock_load_yaml(
preset_dict=preset_with_subscription_value_nested_presets_and_indent_variables_same_line_old_format_errors
), pytest.raises(
ValidationException,
match=re.escape(
"Validation error in parent_preset_2.=INDENT_1: 'INDENT_3' in '= INDENT_2 | INDENT_3' is not a preset name. "
"To use as a subscription indent value, define it as '= INDENT_3'"
),
):
Subscription.from_file_path(
config=config_file_with_subscription_value, subscription_path="mocked"
)
def test_subscription_file_bad_value(config_file: ConfigFile): def test_subscription_file_bad_value(config_file: ConfigFile):
with mock_load_yaml(preset_dict={"__value__": {"should be": "string"}}), pytest.raises( with mock_load_yaml(preset_dict={"__value__": {"should be": "string"}}), pytest.raises(
ValidationException, ValidationException,
@ -346,7 +484,7 @@ def test_subscription_file_using_conflicting_preset_name(config_file: ConfigFile
def test_subscription_file_invalid_form(config_file: ConfigFile): def test_subscription_file_invalid_form(config_file: ConfigFile):
with mock_load_yaml(preset_dict={"sub_name": 4332}), pytest.raises( with mock_load_yaml(preset_dict={"sub_name": 4332}), pytest.raises(
ValidationException, ValidationException,
match=re.escape(f"Validation error in sub_name: should be of type object."), match=re.escape(f"Subscription value should either be a string, list, or object"),
): ):
_ = Subscription.from_file_path(config=config_file, subscription_path="mocked") _ = Subscription.from_file_path(config=config_file, subscription_path="mocked")
@ -363,6 +501,7 @@ def test_tv_show_subscriptions(
jake_train_overrides = subs[2].overrides.dict_with_format_strings jake_train_overrides = subs[2].overrides.dict_with_format_strings
assert jake_train_overrides["subscription_name"] == "Jake Trains" assert jake_train_overrides["subscription_name"] == "Jake Trains"
assert jake_train_overrides["subscription_name_sanitized"] == "Jake Trains"
assert jake_train_overrides["subscription_value"] == "https://www.youtube.com/@JakeTrains" assert jake_train_overrides["subscription_value"] == "https://www.youtube.com/@JakeTrains"
assert jake_train_overrides["subscription_indent_1"] == "Kids" assert jake_train_overrides["subscription_indent_1"] == "Kids"
assert jake_train_overrides["subscription_indent_2"] == "TV-Y" assert jake_train_overrides["subscription_indent_2"] == "TV-Y"
@ -378,6 +517,7 @@ def test_music_subscriptions(default_config: ConfigFile, music_subscriptions_pat
monk = subs[2].overrides.dict_with_format_strings monk = subs[2].overrides.dict_with_format_strings
assert monk["subscription_name"] == "Stan Getz" assert monk["subscription_name"] == "Stan Getz"
assert monk["subscription_name_sanitized"] == "Stan Getz"
assert monk["subscription_value"] == "https://www.youtube.com/@stangetzofficial/releases" assert monk["subscription_value"] == "https://www.youtube.com/@stangetzofficial/releases"
assert monk["subscription_indent_1"] == "Jazz" assert monk["subscription_indent_1"] == "Jazz"
@ -392,6 +532,7 @@ def test_music_video_subscriptions(default_config: ConfigFile, music_video_subsc
monk = subs[1].overrides.dict_with_format_strings monk = subs[1].overrides.dict_with_format_strings
assert monk["subscription_name"] == "Michael Jackson" assert monk["subscription_name"] == "Michael Jackson"
assert monk["subscription_name_sanitized"] == "Michael Jackson"
assert ( assert (
monk["subscription_value"] monk["subscription_value"]
== "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E" == "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E"