refactored, tests passing

This commit is contained in:
Jesse Bannon 2023-11-01 15:50:04 -07:00
parent 056b111be6
commit f2d60b3ade
4 changed files with 242 additions and 90 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

@ -120,45 +120,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 +152,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)

View file

@ -1,12 +1,15 @@
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.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
@ -36,18 +39,6 @@ def subscription_value_variable_name() -> str:
return "subscription_value" 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 +66,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 +93,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 +142,72 @@ 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[f"subscription_value_{idx + 1}"] = 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 +215,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 +259,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 +272,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 +323,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

@ -116,6 +116,25 @@ 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_same_line_old_format_errors(
preset_with_subscription_value: Dict,
): ):
return dict( return dict(
preset_with_subscription_value, preset_with_subscription_value,
@ -191,6 +210,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 +229,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,11 +250,13 @@ 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
@ -262,6 +285,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,6 +293,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"
@ -299,6 +324,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 +333,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 +341,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 +391,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 +408,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 +424,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 +439,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"