ytdl-sub/src/ytdl_sub/utils/scriptable.py
Jesse Bannon 017db953bf
[FEATURE] Automatically handle playlists ordered in reverse (#948)
Playlists have always been a pain-point with ytdl-sub. If an author adds new videos to the end of a playlist, as opposed to the front, it breaks ytdl-sub's intuition of incremental scraping by breaking on the first (oldest) video. This update now makes it possible to handle this in the prebuilt TV Show presets:
- Add each URL variable (`url`, `url2`, ...) into the `download` portion of the subscription twice
- First definition is what we all know and use, simply scrapes first-to-last, then downloads last-to-first
- Second definition does the following:
  - Check to see if a URL is a YouTube playlist URL, if so...
    - Set the field to download, but with modifications to scrape last-to-first, then download first-to-last
  - Otherwise...
    - Set the field to an empty string, which means ytdl-sub will skip it
2024-04-01 04:24:41 -07:00

96 lines
3.3 KiB
Python

import copy
from abc import ABC
from typing import Any
from typing import Dict
from typing import Optional
from typing import Set
from ytdl_sub.entries.script.function_scripts import CUSTOM_FUNCTION_SCRIPTS
from ytdl_sub.entries.script.variable_definitions import UNRESOLVED_VARIABLES
from ytdl_sub.entries.script.variable_definitions import VARIABLE_SCRIPTS
from ytdl_sub.entries.script.variable_types import Variable
from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_DEFINITIONS
from ytdl_sub.script.script import Script
from ytdl_sub.script.utils.exceptions import RuntimeException
from ytdl_sub.utils.exceptions import StringFormattingException
from ytdl_sub.utils.script import ScriptUtils
BASE_SCRIPT: Script = Script(
ScriptUtils.add_sanitized_variables(VARIABLE_SCRIPTS)
| ScriptUtils.add_sanitized_variables(REQUIRED_OVERRIDE_VARIABLE_DEFINITIONS)
| CUSTOM_FUNCTION_SCRIPTS
)
class Scriptable(ABC):
"""
Shared class between Entry and Overrides to manage their underlying Script.
"""
def __init__(self, initialize_base_script: bool = False):
self._script: Optional[Script] = None
self._unresolvable: Optional[Set[str]] = None
if initialize_base_script:
self.initialize_base_script()
def initialize_base_script(self):
"""
Initializes with base values
"""
self._script = copy.deepcopy(BASE_SCRIPT)
self._unresolvable = copy.deepcopy(UNRESOLVED_VARIABLES)
@property
def script(self) -> Script:
"""
Initialized script
"""
assert self._script is not None, "Not initialized"
return self._script
@property
def unresolvable(self) -> Set[str]:
"""
Initialized unresolvable variables
"""
assert self._unresolvable is not None, "Not initialized"
return self._unresolvable
def update_script(self) -> None:
"""
Updates any potential variables to a resolvable. This is done
to avoid re-resolving the same variables over-and-over.
"""
self.script.resolve(unresolvable=self.unresolvable, update=True)
def add(self, values: Dict[str | Variable, Any]) -> None:
"""
Add new values to the script
"""
values_as_str: Dict[str, str] = {
(var.variable_name if isinstance(var, Variable) else var): definition
for var, definition in values.items()
}
self._unresolvable -= set(list(values_as_str.keys()))
self.script.add(
ScriptUtils.add_sanitized_variables(
{
name: ScriptUtils.to_script(definition)
for name, definition in values_as_str.items()
}
),
unresolvable=self.unresolvable,
)
self.update_script()
for name, definition in values_as_str.items():
try:
_ = self.script.get(variable_name=name)
except RuntimeException as exc:
raise StringFormattingException(
f"Tried to create the variable with name {name} and definition:\n"
f"{definition}\n"
f"But could not because it has variable dependencies that are not resolved yet"
) from exc