Skip to content

Commit

Permalink
[FEATURE] Subscription nesting (#747)
Browse files Browse the repository at this point in the history
Closes Feature Request: #743

Subscriptions support using presets as keys, and using keys to set override variables as values.
For example:

```
tv_show:
  only_recent:
    [News]:
      "Breaking News": "https://www.youtube.com/@SomeBreakingNews"

  [Tech]:
    "Two Minute Papers": "https://www.youtube.com/@TwoMinutePapers"
```
Will create two subscriptions named "Breaking News" and "Two Minute Papers", equivalent to:

```
"Breaking News":
  preset:
    - "tv_show"
    - "only_recent"

  overrides:
    subscription_indent_1: "News"
    subscription_name: "Breaking News"
    subscription_value: "https://www.youtube.com/@SomeBreakingNews"

"Two Minute Papers":
  preset:
    - "tv_show"

  overrides:
    subscription_indent_1: "Tech"
    subscription_name: "Two Minute Papers"     subscription_value: "https://www.youtube.com/@TwoMinutePapers"
```

You can provide as many parent presets in the form of keys, and subscription indents as ``[keys]``.
This can drastically simplify subscription definitions by setting things you want configurable like so in your
parent preset:

```
presets:
  tv_show_name:
    overrides:
      tv_show_name: "{subscription_name}"
      url: "{subscription_value}"
      genre: "{subscription_indent_1}"
```

NOTE!!!
Changing your subscription name from 'legacy' subscriptions will need their download archive file migrated to a new name. A future update will assist in this migration process. Either wait until then or, if you consider yourself a ytdl-sub expert, keep a backup of your subscription file before migrating.
  • Loading branch information
jmbannon authored Oct 2, 2023
1 parent 3e41e0f commit c255f40
Show file tree
Hide file tree
Showing 6 changed files with 455 additions and 77 deletions.
63 changes: 61 additions & 2 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ and subscriptions.
.. autoclass:: ytdl_sub.config.config_validator.ConfigOptions()
:members:
:member-order: bysource
:exclude-members: persist_logs, experimental
:exclude-members: subscription_value, persist_logs, experimental

persist_logs
""""""""""""
Expand Down Expand Up @@ -299,8 +299,66 @@ custom variables: ``{output_directory}``, ``{playlist_name}``, and ``{url}``. Th
the `parent preset`_ to ``playlist_preset_ex``, and must define the variables ``{playlist_name}``
and ``{url}`` since the preset did not.

.. _beautifying subscriptions:

Beautifying Subscriptions
^^^^^^^^^^^^^^^^^^^^^^^^^
Subscriptions support using presets as keys, and using keys to set override variables as values.
For example:

.. code-block:: yaml
:caption: subscription.yaml
tv_show:
only_recent:
[News]:
"Breaking News": "https://www.youtube.com/@SomeBreakingNews"
[Tech]:
"Two Minute Papers": "https://www.youtube.com/@TwoMinutePapers"
Will create two subscriptions named "Breaking News" and "Two Minute Papers", equivalent to:

.. code-block:: yaml
"Breaking News":
preset:
- "tv_show"
- "only_recent"
overrides:
subscription_indent_1: "News"
subscription_name: "Breaking News"
subscription_value: "https://www.youtube.com/@SomeBreakingNews"
"Two Minute Papers":
preset:
- "tv_show"
overrides:
subscription_indent_1: "Tech"
subscription_name: "Two Minute Papers"
subscription_value: "https://www.youtube.com/@TwoMinutePapers"
You can provide as many parent presets in the form of keys, and subscription indents as ``[keys]``.
This can drastically simplify subscription definitions by setting things like so in your
parent preset:

.. code-block:: yaml
presets:
tv_show_name:
overrides:
tv_show_name: "{subscription_name}"
url: "{subscription_value}"
genre: "{subscription_indent_1}"
.. _subscription value:

File Preset
^^^^^^^^^^^
NOTE: This is deprecated in favor of using the method in :ref:`beautifying subscriptions`.

You can apply a preset to all subscriptions in the ``subscription.yaml`` file
by using the file-wide ``__preset__``:

Expand All @@ -318,10 +376,11 @@ by using the file-wide ``__preset__``:
This ``subscription.yaml`` is equivalent to the one above it because all
subscriptions automatically set ``__preset__`` as a `parent preset`_.

.. _subscription value:

Subscription Value
^^^^^^^^^^^^^^^^^^^
NOTE: This is deprecated in favor of using the method in :ref:`beautifying subscriptions`.

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.
Expand Down
8 changes: 8 additions & 0 deletions docs/deprecation_notices.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Deprecation Notices
===================

Oct 2023
--------

subscription preset and value
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The use of ``__value__`` will go away in Dec 2023 in favor of the method found in
:ref:`beautifying subscriptions`. ``__preset__`` will still be supported for the time being.

July 2023
---------

Expand Down
62 changes: 29 additions & 33 deletions src/ytdl_sub/subscriptions/subscription.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import copy
from typing import Any
from typing import Dict
from typing import List
from typing import Optional

from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.config.preset import Preset
from ytdl_sub.subscriptions.subscription_download import SubscriptionDownload
from ytdl_sub.subscriptions.subscription_validators import SubscriptionValidator
from ytdl_sub.utils.exceptions import ValidationException
from ytdl_sub.utils.logger import Logger
from ytdl_sub.utils.yaml import load_yaml
from ytdl_sub.validators.validators import LiteralDictValidator

FILE_PRESET_APPLY_KEY = "__preset__"
FILE_SUBSCRIPTION_VALUE_KEY = "__value__"

logger = Logger.get("subscription")


class Subscription(SubscriptionDownload):
@classmethod
Expand Down Expand Up @@ -68,15 +73,23 @@ def from_dict(cls, config: ConfigFile, preset_name: str, preset_dict: Dict) -> "
def _maybe_get_subscription_value(
cls, config: ConfigFile, subscription_dict: Dict
) -> Optional[str]:
subscription_value_key: Optional[str] = config.config_options.subscription_value
if FILE_SUBSCRIPTION_VALUE_KEY in subscription_dict:
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"
)
return subscription_dict[FILE_SUBSCRIPTION_VALUE_KEY]

return config.config_options.subscription_value # can be None
subscription_value_key = subscription_dict[FILE_SUBSCRIPTION_VALUE_KEY]

if subscription_value_key is not None:
logger.warning(
"Using %s in a subscription will eventually be deprecated in favor of writing "
"to the override variable `subscription_value`. Please update by Dec 2023.",
FILE_SUBSCRIPTION_VALUE_KEY,
)
return subscription_value_key

@classmethod
def from_file_path(cls, config: ConfigFile, subscription_path: str) -> List["Subscription"]:
Expand Down Expand Up @@ -121,39 +134,22 @@ def from_file_path(cls, config: ConfigFile, subscription_path: str) -> List["Sub
config = copy.deepcopy(config)
config.presets.dict[FILE_PRESET_APPLY_KEY] = file_preset.dict

for subscription_key, subscription_object in subscription_dict.items():
subscriptions_dict: Dict[str, Any] = {
key: obj
for key, obj in subscription_dict.items()
if key not in [FILE_PRESET_APPLY_KEY, FILE_SUBSCRIPTION_VALUE_KEY]
}

# Skip file preset or value
if subscription_key in [FILE_PRESET_APPLY_KEY, FILE_SUBSCRIPTION_VALUE_KEY]:
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 file_subscription_value:
subscription_object = {"overrides": {file_subscription_value: subscription_object}}
elif isinstance(subscription_object, dict):
pass
elif isinstance(subscription_object, str) and not file_subscription_value:
raise ValidationException(
f"Subscription {subscription_key} is a string, but the subscription value "
f"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 has_file_preset:
parent_preset = subscription_object.get("preset", [])
# Preset can be a single string
if isinstance(parent_preset, str):
parent_preset = [parent_preset]

# If it's not a string or list, it will fail downstream
if isinstance(parent_preset, list):
subscription_object["preset"] = parent_preset + [FILE_PRESET_APPLY_KEY]
subscriptions_dicts = SubscriptionValidator(
name="",
value=subscriptions_dict,
config=config,
presets=[FILE_PRESET_APPLY_KEY] if has_file_preset else [],
indent_overrides=[],
subscription_value=file_subscription_value,
).subscription_dicts()

for subscription_key, subscription_object in subscriptions_dicts.items():
subscriptions.append(
cls.from_dict(
config=config,
Expand Down
Loading

0 comments on commit c255f40

Please sign in to comment.