ytdl-sub 2025.12.30__py3-none-any.whl → 2025.12.31.post1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ytdl_sub/__init__.py CHANGED
@@ -1 +1 @@
1
- __pypi_version__ = "2025.12.30";__local_version__ = "2025.12.30+344753c"
1
+ __pypi_version__ = "2025.12.31.post1";__local_version__ = "2025.12.31+d373935"
@@ -4,15 +4,16 @@ from typing import Iterable
4
4
  from typing import Optional
5
5
  from typing import Set
6
6
 
7
- import mergedeep
8
-
9
7
  from ytdl_sub.entries.entry import Entry
10
8
  from ytdl_sub.entries.script.variable_definitions import VARIABLES
11
9
  from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_NAMES
12
10
  from ytdl_sub.entries.variables.override_variables import OverrideHelpers
13
11
  from ytdl_sub.script.parser import parse
14
12
  from ytdl_sub.script.script import Script
13
+ from ytdl_sub.script.types.function import BuiltInFunction
15
14
  from ytdl_sub.script.types.resolvable import Resolvable
15
+ from ytdl_sub.script.types.resolvable import String
16
+ from ytdl_sub.script.types.syntax_tree import SyntaxTree
16
17
  from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
17
18
  from ytdl_sub.utils.exceptions import InvalidVariableNameException
18
19
  from ytdl_sub.utils.exceptions import StringFormattingException
@@ -134,29 +135,35 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
134
135
  )
135
136
 
136
137
  def initial_variables(
137
- self, unresolved_variables: Optional[Dict[str, str]] = None
138
- ) -> Dict[str, str]:
138
+ self, unresolved_variables: Optional[Dict[str, SyntaxTree]] = None
139
+ ) -> Dict[str, SyntaxTree]:
139
140
  """
140
141
  Returns
141
142
  -------
142
143
  Variables and format strings for all Override variables + additional variables (Optional)
143
144
  """
144
- initial_variables: Dict[str, str] = {}
145
- mergedeep.merge(
146
- initial_variables,
147
- self.dict_with_format_strings,
148
- unresolved_variables if unresolved_variables else {},
149
- )
150
- return ScriptUtils.add_sanitized_variables(initial_variables)
145
+ initial_variables: Dict[str, SyntaxTree] = self.dict_with_parsed_format_strings
146
+ if unresolved_variables:
147
+ initial_variables |= unresolved_variables
148
+ return ScriptUtils.add_sanitized_parsed_variables(initial_variables)
151
149
 
152
150
  def initialize_script(self, unresolved_variables: Set[str]) -> "Overrides":
153
151
  """
154
152
  Initialize the override script with any unresolved variables
155
153
  """
156
- self.script.add(
154
+ self.script.add_parsed(
157
155
  self.initial_variables(
158
156
  unresolved_variables={
159
- var_name: f"{{%throw('Plugin variable {var_name} has not been created yet')}}"
157
+ var_name: SyntaxTree(
158
+ ast=[
159
+ BuiltInFunction(
160
+ name="throw",
161
+ args=[
162
+ String(f"Plugin variable {var_name} has not been created yet")
163
+ ],
164
+ )
165
+ ]
166
+ )
160
167
  for var_name in unresolved_variables
161
168
  }
162
169
  )
@@ -1,5 +1,7 @@
1
+ from typing import Iterable
1
2
  from typing import List
2
3
  from typing import Optional
4
+ from typing import Set
3
5
  from typing import Tuple
4
6
  from typing import Type
5
7
 
@@ -44,3 +46,34 @@ class PresetPlugins:
44
46
  if plugin_type in plugin_option_types:
45
47
  return self.plugin_options[plugin_option_types.index(plugin_type)]
46
48
  return None
49
+
50
+ def get_added_and_modified_variables(
51
+ self, additional_options: List[OptionsValidator]
52
+ ) -> Iterable[Tuple[OptionsValidator, Set[str], Set[str]]]:
53
+ """
54
+ Iterates and returns the plugin options, added variables, modified variables
55
+ """
56
+ for plugin_options in self.plugin_options + additional_options:
57
+ added_variables: Set[str] = set()
58
+ modified_variables: Set[str] = set()
59
+
60
+ for plugin_added_variables in plugin_options.added_variables(
61
+ unresolved_variables=set(),
62
+ ).values():
63
+ added_variables |= set(plugin_added_variables)
64
+
65
+ for plugin_modified_variables in plugin_options.modified_variables().values():
66
+ modified_variables = plugin_modified_variables
67
+
68
+ yield plugin_options, added_variables, modified_variables
69
+
70
+ def get_all_variables(self, additional_options: List[OptionsValidator]) -> Set[str]:
71
+ """
72
+ Returns set of all added and modified variables' names.
73
+ """
74
+ all_variables: Set[str] = set()
75
+ for _, added, modified in self.get_added_and_modified_variables(additional_options):
76
+ all_variables.update(added)
77
+ all_variables.update(modified)
78
+
79
+ return all_variables
ytdl_sub/config/preset.py CHANGED
@@ -2,6 +2,7 @@ import copy
2
2
  from typing import Any
3
3
  from typing import Dict
4
4
  from typing import List
5
+ from typing import Set
5
6
 
6
7
  from mergedeep import mergedeep
7
8
 
@@ -11,7 +12,6 @@ from ytdl_sub.config.plugin.plugin_mapping import PluginMapping
11
12
  from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
12
13
  from ytdl_sub.config.preset_options import OutputOptions
13
14
  from ytdl_sub.config.preset_options import YTDLOptions
14
- from ytdl_sub.config.validators.variable_validation import VariableValidation
15
15
  from ytdl_sub.downloaders.url.validators import MultiUrlValidator
16
16
  from ytdl_sub.prebuilt_presets import PREBUILT_PRESET_NAMES
17
17
  from ytdl_sub.prebuilt_presets import PUBLISHED_PRESET_NAMES
@@ -172,6 +172,37 @@ class Preset(_PresetShell):
172
172
  mergedeep.merge({}, *reversed(presets_to_merge), strategy=mergedeep.Strategy.ADDITIVE)
173
173
  )
174
174
 
175
+ def _initialize_overrides_script(self, overrides: Overrides) -> Overrides:
176
+ """
177
+ Do some gymnastics to initialize the Overrides script.
178
+ """
179
+ unresolved_variables: Set[str] = set()
180
+
181
+ for (
182
+ plugin_options,
183
+ added_variables,
184
+ modified_variables,
185
+ ) in self.plugins.get_added_and_modified_variables(
186
+ additional_options=[self.downloader_options, self.output_options]
187
+ ):
188
+ for added_variable in added_variables:
189
+ if not overrides.ensure_added_plugin_variable_valid(added_variable=added_variable):
190
+ # pylint: disable=protected-access
191
+ raise plugin_options._validation_exception(
192
+ f"Cannot use the variable name {added_variable} because it exists as a"
193
+ " built-in ytdl-sub variable name."
194
+ )
195
+ # pylint: enable=protected-access
196
+
197
+ # Set unresolved as variables that are added but do not exist as
198
+ # entry/override variables since they are created at run-time
199
+ unresolved_variables |= added_variables | modified_variables
200
+
201
+ # Initialize overrides with unresolved variables + modified variables to throw an error.
202
+ # For modified variables, this is to prevent a resolve(update=True) to setting any
203
+ # dependencies until it has been explicitly added
204
+ return overrides.initialize_script(unresolved_variables=unresolved_variables)
205
+
175
206
  def __init__(self, config: ConfigValidator, name: str, value: Any):
176
207
  super().__init__(name=name, value=value)
177
208
 
@@ -192,16 +223,11 @@ class Preset(_PresetShell):
192
223
  )
193
224
 
194
225
  self.plugins: PresetPlugins = self._validate_and_get_plugins()
195
- self.overrides = self._validate_key(key="overrides", validator=Overrides, default={})
196
-
226
+ self.overrides = self._initialize_overrides_script(
227
+ overrides=self._validate_key(key="overrides", validator=Overrides, default={})
228
+ )
197
229
  self.overrides.ensure_variable_names_not_a_plugin(plugin_names=PRESET_KEYS)
198
230
 
199
- VariableValidation(
200
- downloader_options=self.downloader_options,
201
- output_options=self.output_options,
202
- plugins=self.plugins,
203
- ).initialize_preset_overrides(overrides=self.overrides).ensure_proper_usage()
204
-
205
231
  @property
206
232
  def name(self) -> str:
207
233
  """
@@ -1,10 +1,4 @@
1
- import copy
2
1
  from typing import Dict
3
- from typing import Iterable
4
- from typing import List
5
- from typing import Optional
6
- from typing import Set
7
- from typing import Tuple
8
2
 
9
3
  from ytdl_sub.config.overrides import Overrides
10
4
  from ytdl_sub.config.plugin.plugin_mapping import PluginMapping
@@ -13,155 +7,27 @@ from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
13
7
  from ytdl_sub.config.preset_options import OutputOptions
14
8
  from ytdl_sub.config.validators.options import OptionsValidator
15
9
  from ytdl_sub.downloaders.url.validators import MultiUrlValidator
16
- from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_NAMES
17
- from ytdl_sub.script.script import Script
18
- from ytdl_sub.script.script import _is_function
19
- from ytdl_sub.utils.scriptable import BASE_SCRIPT
20
- from ytdl_sub.validators.string_formatter_validators import to_variable_dependency_format_string
21
10
  from ytdl_sub.validators.string_formatter_validators import validate_formatters
22
11
 
23
- # Entry variables to mock during validation
24
- _DUMMY_ENTRY_VARIABLES: Dict[str, str] = {
25
- name: to_variable_dependency_format_string(
26
- # pylint: disable=protected-access
27
- script=BASE_SCRIPT,
28
- parsed_format_string=BASE_SCRIPT._variables[name],
29
- # pylint: enable=protected-access
30
- )
31
- for name in BASE_SCRIPT.variable_names
32
- }
33
-
34
-
35
- def _add_dummy_variables(variables: Iterable[str]) -> Dict[str, str]:
36
- dummy_variables: Dict[str, str] = {}
37
- for var in variables:
38
- dummy_variables[var] = ""
39
- dummy_variables[f"{var}_sanitized"] = ""
40
-
41
- return dummy_variables
42
-
43
-
44
- def _add_dummy_overrides(overrides: Overrides) -> Dict[str, str]:
45
- # Have the dummy override variable contain all variable deps that it uses in the string
46
- dummy_overrides: Dict[str, str] = {}
47
- for override_name in _override_variables(overrides):
48
- if _is_function(override_name):
49
- continue
50
-
51
- # pylint: disable=protected-access
52
- dummy_overrides[override_name] = to_variable_dependency_format_string(
53
- script=overrides.script, parsed_format_string=overrides.script._variables[override_name]
54
- )
55
- # pylint: enable=protected-access
56
- return dummy_overrides
57
-
58
-
59
- def _get_added_and_modified_variables(
60
- plugins: PresetPlugins, downloader_options: MultiUrlValidator, output_options: OutputOptions
61
- ) -> Iterable[Tuple[OptionsValidator, Set[str], Set[str]]]:
62
- """
63
- Iterates and returns the plugin options, added variables, modified variables
64
- """
65
- options: List[OptionsValidator] = plugins.plugin_options
66
- options.append(downloader_options)
67
- options.append(output_options)
68
-
69
- for plugin_options in options:
70
- added_variables: Set[str] = set()
71
- modified_variables: Set[str] = set()
72
-
73
- for plugin_added_variables in plugin_options.added_variables(
74
- unresolved_variables=set(),
75
- ).values():
76
- added_variables |= set(plugin_added_variables)
77
-
78
- for plugin_modified_variables in plugin_options.modified_variables().values():
79
- modified_variables = plugin_modified_variables
80
-
81
- yield plugin_options, added_variables, modified_variables
82
-
83
-
84
- def _override_variables(overrides: Overrides) -> Set[str]:
85
- return set(list(overrides.initial_variables().keys()))
86
-
87
12
 
88
13
  class VariableValidation:
89
14
  def __init__(
90
15
  self,
16
+ overrides: Overrides,
91
17
  downloader_options: MultiUrlValidator,
92
18
  output_options: OutputOptions,
93
19
  plugins: PresetPlugins,
94
20
  ):
21
+ self.overrides = overrides
95
22
  self.downloader_options = downloader_options
96
23
  self.output_options = output_options
97
24
  self.plugins = plugins
98
25
 
99
- self.script: Optional[Script] = None
100
- self.resolved_variables: Set[str] = set()
101
- self.unresolved_variables: Set[str] = set()
102
-
103
- def initialize_preset_overrides(self, overrides: Overrides) -> "VariableValidation":
104
- """
105
- Do some gymnastics to initialize the Overrides script.
106
- """
107
- override_variables = set(list(overrides.initial_variables().keys()))
108
-
109
- # Set resolved variables as all entry + override variables
110
- # at this point to generate every possible added/modified variable
111
- self.resolved_variables = set(_DUMMY_ENTRY_VARIABLES.keys()) | override_variables
112
- plugin_variables: Set[str] = set()
113
-
114
- for (
115
- plugin_options,
116
- added_variables,
117
- modified_variables,
118
- ) in _get_added_and_modified_variables(
119
- plugins=self.plugins,
120
- downloader_options=self.downloader_options,
121
- output_options=self.output_options,
122
- ):
123
-
124
- for added_variable in added_variables:
125
- if not overrides.ensure_added_plugin_variable_valid(added_variable=added_variable):
126
- # pylint: disable=protected-access
127
- raise plugin_options._validation_exception(
128
- f"Cannot use the variable name {added_variable} because it exists as a"
129
- " built-in ytdl-sub variable name."
130
- )
131
- # pylint: enable=protected-access
132
-
133
- # Set unresolved as variables that are added but do not exist as
134
- # entry/override variables since they are created at run-time
135
- self.unresolved_variables |= added_variables | modified_variables
136
- plugin_variables |= added_variables | modified_variables
137
-
138
- # Then update resolved variables to reflect that
139
- self.resolved_variables -= self.unresolved_variables
140
-
141
- # Initialize overrides with unresolved variables + modified variables to throw an error.
142
- # For modified variables, this is to prevent a resolve(update=True) to setting any
143
- # dependencies until it has been explicitly added
144
- overrides = overrides.initialize_script(unresolved_variables=self.unresolved_variables)
145
-
146
- # copy the script and mock entry variables
147
- self.script = copy.deepcopy(overrides.script)
148
- self.script.add(
149
- variables=_add_dummy_overrides(overrides=overrides)
150
- | _add_dummy_variables(variables=plugin_variables)
151
- | _DUMMY_ENTRY_VARIABLES
26
+ self.script = self.overrides.script
27
+ self.unresolved_variables = self.plugins.get_all_variables(
28
+ additional_options=[self.output_options, self.downloader_options]
152
29
  )
153
30
 
154
- return self
155
-
156
- def _update_script(self) -> None:
157
- _ = self.script.resolve(unresolvable=self.unresolved_variables, update=True)
158
-
159
- def _add_subscription_override_variables(self) -> None:
160
- """
161
- Add dummy subscription variables for script validation
162
- """
163
- self.resolved_variables |= REQUIRED_OVERRIDE_VARIABLE_NAMES
164
-
165
31
  def _add_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> None:
166
32
  """
167
33
  Add dummy variables for script validation
@@ -171,19 +37,17 @@ class VariableValidation:
171
37
  ).get(plugin_op, set())
172
38
  modified_variables = options.modified_variables().get(plugin_op, set())
173
39
 
174
- resolved_variables = added_variables | modified_variables
40
+ self.unresolved_variables -= added_variables | modified_variables
175
41
 
176
- self.resolved_variables |= resolved_variables
177
- self.unresolved_variables -= resolved_variables
178
-
179
- def ensure_proper_usage(self) -> None:
42
+ def ensure_proper_usage(self) -> Dict:
180
43
  """
181
44
  Validate variables resolve as plugins are executed, and return
182
45
  a mock script which contains actualized added variables from the plugins
183
46
  """
184
47
 
48
+ resolved_subscription: Dict = {}
49
+
185
50
  self._add_variables(PluginOperation.DOWNLOADER, options=self.downloader_options)
186
- self._add_subscription_override_variables()
187
51
 
188
52
  # Always add output options first
189
53
  self._add_variables(PluginOperation.MODIFY_ENTRY_METADATA, options=self.output_options)
@@ -200,16 +64,28 @@ class VariableValidation:
200
64
  self._add_variables(PluginOperation.MODIFY_ENTRY, options=plugin_options)
201
65
 
202
66
  # Validate that any formatter in the plugin options can resolve
203
- validate_formatters(
67
+ resolved_subscription |= validate_formatters(
204
68
  script=self.script,
205
69
  unresolved_variables=self.unresolved_variables,
206
70
  validator=plugin_options,
207
71
  )
208
72
 
209
- validate_formatters(
73
+ resolved_subscription |= validate_formatters(
210
74
  script=self.script,
211
75
  unresolved_variables=self.unresolved_variables,
212
76
  validator=self.output_options,
213
77
  )
214
78
 
79
+ # TODO: make this a function
80
+ raw_download_output = validate_formatters(
81
+ script=self.script,
82
+ unresolved_variables=self.unresolved_variables,
83
+ validator=self.downloader_options.urls,
84
+ )
85
+ resolved_subscription["download"] = []
86
+ for url_output in raw_download_output["download"]:
87
+ if url_output["url"]:
88
+ resolved_subscription["download"].append(url_output)
89
+
215
90
  assert not self.unresolved_variables
91
+ return resolved_subscription
@@ -21,7 +21,7 @@ class UrlThumbnailValidator(StrictDictValidator):
21
21
  def __init__(self, name, value):
22
22
  super().__init__(name, value)
23
23
 
24
- self._name = self._validate_key(key="name", validator=StringFormatterValidator)
24
+ self._thumb_name = self._validate_key(key="name", validator=StringFormatterValidator)
25
25
  self._uid = self._validate_key(key="uid", validator=OverridesStringFormatterValidator)
26
26
 
27
27
  @property
@@ -29,7 +29,7 @@ class UrlThumbnailValidator(StrictDictValidator):
29
29
  """
30
30
  File name for the thumbnail
31
31
  """
32
- return self._name
32
+ return self._thumb_name
33
33
 
34
34
  @property
35
35
  def uid(self) -> OverridesStringFormatterValidator:
ytdl_sub/script/parser.py CHANGED
@@ -31,6 +31,8 @@ from ytdl_sub.script.utils.exceptions import InvalidSyntaxException
31
31
  from ytdl_sub.script.utils.exceptions import InvalidVariableName
32
32
  from ytdl_sub.script.utils.exceptions import UserException
33
33
  from ytdl_sub.script.utils.exceptions import VariableDoesNotExist
34
+ from ytdl_sub.script.utils.name_validation import is_function
35
+ from ytdl_sub.script.utils.name_validation import to_function_name
34
36
  from ytdl_sub.script.utils.name_validation import validate_variable_name
35
37
 
36
38
  # pylint: disable=invalid-name
@@ -144,6 +146,9 @@ class _Parser:
144
146
  ):
145
147
  self._text = text
146
148
  self._name = name
149
+ if name and is_function(name):
150
+ self._name = to_function_name(name)
151
+
147
152
  self._custom_function_names = custom_function_names
148
153
  self._variable_names = variable_names
149
154
  self._pos = 0
ytdl_sub/script/script.py CHANGED
@@ -20,28 +20,13 @@ from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments
20
20
  from ytdl_sub.script.utils.exceptions import InvalidCustomFunctionArguments
21
21
  from ytdl_sub.script.utils.exceptions import RuntimeException
22
22
  from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
23
+ from ytdl_sub.script.utils.name_validation import is_function
24
+ from ytdl_sub.script.utils.name_validation import to_function_definition_name
25
+ from ytdl_sub.script.utils.name_validation import to_function_name
23
26
  from ytdl_sub.script.utils.name_validation import validate_variable_name
24
27
  from ytdl_sub.script.utils.type_checking import FunctionSpec
25
28
 
26
29
 
27
- def _is_function(override_name: str):
28
- return override_name.startswith("%")
29
-
30
-
31
- def _function_name(function_key: str) -> str:
32
- """
33
- Drop the % in %custom_function
34
- """
35
- return function_key[1:]
36
-
37
-
38
- def _to_function_definition_name(function_key: str) -> str:
39
- """
40
- Add % in %custom_function
41
- """
42
- return f"%{function_key}"
43
-
44
-
45
30
  class Script:
46
31
  """
47
32
  Takes a dictionary of both
@@ -241,23 +226,23 @@ class Script:
241
226
 
242
227
  def __init__(self, script: Dict[str, str]):
243
228
  function_names: Set[str] = {
244
- _function_name(name) for name in script.keys() if _is_function(name)
229
+ to_function_name(name) for name in script.keys() if is_function(name)
245
230
  }
246
231
  variable_names: Set[str] = {
247
- validate_variable_name(name) for name in script.keys() if not _is_function(name)
232
+ validate_variable_name(name) for name in script.keys() if not is_function(name)
248
233
  }
249
234
 
250
235
  self._functions: Dict[str, SyntaxTree] = {
251
236
  # custom_function_name must be passed to properly type custom function
252
237
  # arguments uniquely if they're nested (i.e. $0 to $custom_func___0)
253
- _function_name(function_key): parse(
238
+ to_function_name(function_key): parse(
254
239
  text=function_value,
255
- name=_function_name(function_key),
240
+ name=to_function_name(function_key),
256
241
  custom_function_names=function_names,
257
242
  variable_names=variable_names,
258
243
  )
259
244
  for function_key, function_value in script.items()
260
- if _is_function(function_key)
245
+ if is_function(function_key)
261
246
  }
262
247
 
263
248
  self._variables: Dict[str, SyntaxTree] = {
@@ -268,7 +253,7 @@ class Script:
268
253
  variable_names=variable_names,
269
254
  )
270
255
  for variable_key, variable_value in script.items()
271
- if not _is_function(variable_key)
256
+ if not is_function(variable_key)
272
257
  }
273
258
  self._validate()
274
259
 
@@ -485,12 +470,12 @@ class Script:
485
470
  added_variables_to_validate: Set[str] = set()
486
471
 
487
472
  functions_to_add = {
488
- _function_name(name): definition
473
+ to_function_name(name): definition
489
474
  for name, definition in variables.items()
490
- if _is_function(name)
475
+ if is_function(name)
491
476
  }
492
477
  variables_to_add = {
493
- name: definition for name, definition in variables.items() if not _is_function(name)
478
+ name: definition for name, definition in variables.items() if not is_function(name)
494
479
  }
495
480
 
496
481
  custom_function_names = set(self._functions.keys()) | functions_to_add.keys()
@@ -520,6 +505,46 @@ class Script:
520
505
 
521
506
  return self
522
507
 
508
+ def add_parsed(self, variables: Dict[str, SyntaxTree]) -> "Script":
509
+ """
510
+ Adds already parsed, new variables to the script.
511
+
512
+ Parameters
513
+ ----------
514
+ variables
515
+ Mapping containing variable name to definition.
516
+
517
+ Returns
518
+ -------
519
+ Script
520
+ self
521
+ """
522
+ added_variables_to_validate: Set[str] = set()
523
+
524
+ functions_to_add = {
525
+ to_function_name(name): definition
526
+ for name, definition in variables.items()
527
+ if is_function(name)
528
+ }
529
+ variables_to_add = {
530
+ name: definition for name, definition in variables.items() if not is_function(name)
531
+ }
532
+
533
+ for definitions in [functions_to_add, variables_to_add]:
534
+ for name, parsed in definitions.items():
535
+ if parsed.maybe_resolvable is None:
536
+ added_variables_to_validate.add(name)
537
+
538
+ if name in functions_to_add:
539
+ self._functions[name] = parsed
540
+ else:
541
+ self._variables[name] = parsed
542
+
543
+ if added_variables_to_validate:
544
+ self._validate(added_variables=added_variables_to_validate)
545
+
546
+ return self
547
+
523
548
  def resolve_once(
524
549
  self,
525
550
  variable_definitions: Dict[str, str],
@@ -560,6 +585,46 @@ class Script:
560
585
  for name in variable_definitions.keys():
561
586
  self._variables.pop(name, None)
562
587
 
588
+ def resolve_once_parsed(
589
+ self,
590
+ variable_definitions: Dict[str, SyntaxTree],
591
+ resolved: Optional[Dict[str, Resolvable]] = None,
592
+ unresolvable: Optional[Set[str]] = None,
593
+ update: bool = False,
594
+ ) -> Dict[str, Resolvable]:
595
+ """
596
+ Given a new set of variable definitions, resolve them using the Script, but do not
597
+ add them to the Script itself.
598
+
599
+ Parameters
600
+ ----------
601
+ variable_definitions
602
+ Variables to resolve, but not store in the Script
603
+ resolved
604
+ Optional. Pre-resolved variables that should be used instead of what is in the script.
605
+ unresolvable
606
+ Optional. Unresolvable variables that will be ignored in resolution, including all
607
+ variables with a dependency to them.
608
+ update
609
+ Whether to update the script's state with resolved variables. Defaults to False.
610
+
611
+ Returns
612
+ -------
613
+ Dict[str, Resolvable]
614
+ Dict containing the variable names to their resolved values.
615
+ """
616
+ try:
617
+ self.add_parsed(variable_definitions)
618
+ return self._resolve(
619
+ pre_resolved=resolved,
620
+ unresolvable=unresolvable,
621
+ output_filter=set(list(variable_definitions.keys())),
622
+ update=update,
623
+ ).output
624
+ finally:
625
+ for name in variable_definitions.keys():
626
+ self._variables.pop(name, None)
627
+
563
628
  def get(self, variable_name: str) -> Resolvable:
564
629
  """
565
630
  Parameters
@@ -605,4 +670,4 @@ class Script:
605
670
  Set[str]
606
671
  Names of all functions within the Script.
607
672
  """
608
- return set(_to_function_definition_name(name) for name in self._functions.keys())
673
+ return set(to_function_definition_name(name) for name in self._functions.keys())
@@ -114,6 +114,7 @@ class VariableDependency(ABC):
114
114
  output.add(ParsedCustomFunction(name=arg.name, num_input_args=len(arg.args)))
115
115
  if isinstance(arg, VariableDependency):
116
116
  output.update(arg.custom_functions)
117
+ # if isinstance(arg, Lambda)
117
118
 
118
119
  return output
119
120
 
@@ -189,7 +190,17 @@ class VariableDependency(ABC):
189
190
  -------
190
191
  True if it contains any of the input variables. False otherwise.
191
192
  """
192
- for custom_function in self.custom_functions:
193
+ # If there are lambdas, see if they are custom functions. If so, check them
194
+ custom_functions_to_check = self.custom_functions
195
+ for lambda_func in self.lambdas:
196
+ if lambda_func.value in custom_function_definitions:
197
+ custom_functions_to_check.add(
198
+ ParsedCustomFunction(
199
+ name=lambda_func.value, num_input_args=lambda_func.num_input_args()
200
+ )
201
+ )
202
+
203
+ for custom_function in custom_functions_to_check:
193
204
  if custom_function_definitions[custom_function.name].contains(
194
205
  variables=variables, custom_function_definitions=custom_function_definitions
195
206
  ):
@@ -58,3 +58,24 @@ def validate_custom_function_name(custom_function_name: str) -> None:
58
58
  f"Custom function name '%{custom_function_name}' is invalid:"
59
59
  " The name is used by a built-in function and cannot be overwritten."
60
60
  )
61
+
62
+
63
+ def is_function(override_name: str):
64
+ """
65
+ Whether the definition is a function or not.
66
+ """
67
+ return override_name.startswith("%")
68
+
69
+
70
+ def to_function_name(function_key: str) -> str:
71
+ """
72
+ Drop the % in %custom_function
73
+ """
74
+ return function_key[1:]
75
+
76
+
77
+ def to_function_definition_name(function_key: str) -> str:
78
+ """
79
+ Add % in %custom_function
80
+ """
81
+ return f"%{function_key}"
@@ -8,12 +8,14 @@ from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
8
8
  from ytdl_sub.config.preset import Preset
9
9
  from ytdl_sub.config.preset_options import OutputOptions
10
10
  from ytdl_sub.config.preset_options import YTDLOptions
11
+ from ytdl_sub.config.validators.variable_validation import VariableValidation
11
12
  from ytdl_sub.downloaders.url.validators import MultiUrlValidator
12
13
  from ytdl_sub.entries.variables.override_variables import SubscriptionVariables
13
14
  from ytdl_sub.utils.exceptions import SubscriptionPermissionError
14
15
  from ytdl_sub.utils.file_handler import FileHandler
15
16
  from ytdl_sub.utils.file_handler import FileHandlerTransactionLog
16
17
  from ytdl_sub.utils.logger import Logger
18
+ from ytdl_sub.utils.yaml import dump_yaml
17
19
  from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
18
20
 
19
21
  logger = Logger.get("subscription")
@@ -76,6 +78,14 @@ class BaseSubscription(ABC):
76
78
  }
77
79
  )
78
80
 
81
+ # Validate after adding the subscription name
82
+ self._validated_dict = VariableValidation(
83
+ overrides=self.overrides,
84
+ downloader_options=self.downloader_options,
85
+ output_options=self.output_options,
86
+ plugins=self.plugins,
87
+ ).ensure_proper_usage()
88
+
79
89
  self._enhanced_download_archive: Optional[EnhancedDownloadArchive] = (
80
90
  _initialize_download_archive(
81
91
  output_options=self.output_options,
@@ -88,9 +98,9 @@ class BaseSubscription(ABC):
88
98
  # Add post-archive variables
89
99
  self.overrides.add(
90
100
  {
91
- SubscriptionVariables.subscription_has_download_archive(): f"""{{
92
- %bool({self.download_archive.num_entries > 0})
93
- }}""",
101
+ SubscriptionVariables.subscription_has_download_archive(): (
102
+ f"{{%bool({self.download_archive.num_entries > 0})}}"
103
+ ),
94
104
  }
95
105
  )
96
106
 
@@ -245,3 +255,11 @@ class BaseSubscription(ABC):
245
255
  Subscription in yaml format
246
256
  """
247
257
  return self._preset_options.yaml
258
+
259
+ def resolved_yaml(self) -> str:
260
+ """
261
+ Returns
262
+ -------
263
+ Human-readable, condensed YAML definition of the subscription.
264
+ """
265
+ return dump_yaml(self._validated_dict)
ytdl_sub/utils/script.py CHANGED
@@ -4,7 +4,6 @@ from typing import Any
4
4
  from typing import Dict
5
5
 
6
6
  from ytdl_sub.script.parser import parse
7
- from ytdl_sub.script.script import _is_function
8
7
  from ytdl_sub.script.types.array import UnresolvedArray
9
8
  from ytdl_sub.script.types.function import BuiltInFunction
10
9
  from ytdl_sub.script.types.function import Function
@@ -15,8 +14,10 @@ from ytdl_sub.script.types.resolvable import Float
15
14
  from ytdl_sub.script.types.resolvable import Integer
16
15
  from ytdl_sub.script.types.resolvable import Lambda
17
16
  from ytdl_sub.script.types.resolvable import String
17
+ from ytdl_sub.script.types.syntax_tree import SyntaxTree
18
18
  from ytdl_sub.script.types.variable import Variable
19
19
  from ytdl_sub.script.utils.exceptions import UNREACHABLE
20
+ from ytdl_sub.script.utils.name_validation import is_function
20
21
 
21
22
  # pylint: disable=too-many-return-statements
22
23
 
@@ -30,10 +31,26 @@ class ScriptUtils:
30
31
  sanitized_variables = {
31
32
  f"{name}_sanitized": f"{{%sanitize({name})}}"
32
33
  for name in variables.keys()
33
- if not _is_function(name)
34
+ if not is_function(name)
34
35
  }
35
36
  return dict(variables, **sanitized_variables)
36
37
 
38
+ @classmethod
39
+ def add_sanitized_parsed_variables(
40
+ cls, variables: Dict[str, SyntaxTree]
41
+ ) -> Dict[str, SyntaxTree]:
42
+ """
43
+ Helper to add sanitized variables to a Script
44
+ """
45
+ sanitized_variables = {
46
+ f"{name}_sanitized": SyntaxTree(
47
+ ast=[BuiltInFunction(name="sanitize", args=[Variable(name)])]
48
+ )
49
+ for name in variables.keys()
50
+ if not is_function(name)
51
+ }
52
+ return variables | sanitized_variables
53
+
37
54
  @classmethod
38
55
  def to_script(cls, value: Any, sort_keys: bool = True) -> str:
39
56
  """
@@ -11,6 +11,7 @@ from ytdl_sub.script.types.syntax_tree import SyntaxTree
11
11
  from ytdl_sub.script.utils.exceptions import RuntimeException
12
12
  from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
13
13
  from ytdl_sub.script.utils.exceptions import UserException
14
+ from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError
14
15
  from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException
15
16
  from ytdl_sub.utils.script import ScriptUtils
16
17
  from ytdl_sub.validators.validators import DictValidator
@@ -19,6 +20,8 @@ from ytdl_sub.validators.validators import LiteralDictValidator
19
20
  from ytdl_sub.validators.validators import StringValidator
20
21
  from ytdl_sub.validators.validators import Validator
21
22
 
23
+ # pylint: disable=protected-access
24
+
22
25
 
23
26
  class StringFormatterValidator(StringValidator):
24
27
  """
@@ -51,7 +54,10 @@ class StringFormatterValidator(StringValidator):
51
54
  def __init__(self, name, value: str):
52
55
  super().__init__(name=name, value=value)
53
56
  try:
54
- _ = parse(str(value))
57
+ self._parsed = parse(
58
+ text=str(value),
59
+ name=self.leaf_name,
60
+ )
55
61
  except UserException as exc:
56
62
  raise self._validation_exception(exc) from exc
57
63
 
@@ -65,6 +71,16 @@ class StringFormatterValidator(StringValidator):
65
71
  """
66
72
  return self._value
67
73
 
74
+ @property
75
+ @final
76
+ def parsed(self) -> SyntaxTree:
77
+ """
78
+ Returns
79
+ -------
80
+ The parsed format string.
81
+ """
82
+ return self._parsed
83
+
68
84
  def post_process(self, resolved: str) -> str:
69
85
  """
70
86
  Returns
@@ -167,9 +183,14 @@ class DictFormatterValidator(LiteralDictValidator):
167
183
 
168
184
  @property
169
185
  def dict_with_format_strings(self) -> Dict[str, str]:
170
- """Returns dict with the format strings themselves"""
186
+ """Returns dict with the format strings themselves."""
171
187
  return {key: string_formatter.format_string for key, string_formatter in self.dict.items()}
172
188
 
189
+ @property
190
+ def dict_with_parsed_format_strings(self) -> Dict[str, SyntaxTree]:
191
+ """Returns dict with the parsed format strings."""
192
+ return {key: string_formatter.parsed for key, string_formatter in self.dict.items()}
193
+
173
194
 
174
195
  class OverridesDictFormatterValidator(DictFormatterValidator):
175
196
  """
@@ -199,10 +220,8 @@ def to_variable_dependency_format_string(script: Script, parsed_format_string: S
199
220
  dummy_format_string = ""
200
221
  for var in parsed_format_string.variables:
201
222
  dummy_format_string += f"{{ {var.name} }}"
202
- # pylint: disable=protected-access
203
223
  for variable_dependency in script._variables[var.name].variables:
204
224
  dummy_format_string += f"{{ {variable_dependency.name} }}"
205
- # pylint: enable=protected-access
206
225
  return dummy_format_string
207
226
 
208
227
 
@@ -210,16 +229,15 @@ def _validate_formatter(
210
229
  mock_script: Script,
211
230
  unresolved_variables: Set[str],
212
231
  formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator],
213
- ) -> None:
214
- is_static_formatter = False
215
- unresolvable = unresolved_variables
216
- if isinstance(formatter_validator, OverridesStringFormatterValidator):
217
- is_static_formatter = True
218
- unresolvable = unresolved_variables.union({VARIABLES.entry_metadata.variable_name})
219
-
220
- parsed = parse(
221
- text=formatter_validator.format_string,
222
- )
232
+ ) -> str:
233
+ parsed = formatter_validator.parsed
234
+ if resolved := parsed.maybe_resolvable:
235
+ return resolved.native
236
+
237
+ is_static_formatter = isinstance(formatter_validator, OverridesStringFormatterValidator)
238
+ if is_static_formatter:
239
+ unresolved_variables = unresolved_variables.union({VARIABLES.entry_metadata.variable_name})
240
+
223
241
  variable_names = {var.name for var in parsed.variables}
224
242
  custom_function_names = {f"%{func.name}" for func in parsed.custom_functions}
225
243
 
@@ -233,21 +251,20 @@ def _validate_formatter(
233
251
  "contains the following custom functions that do not exist: "
234
252
  f"{', '.join(sorted(custom_function_names - mock_script.function_names))}"
235
253
  )
236
- if unresolved := variable_names.intersection(unresolvable):
254
+ if unresolved := variable_names.intersection(unresolved_variables):
237
255
  raise StringFormattingVariableNotFoundException(
238
256
  "contains the following variables that are unresolved when executing this "
239
257
  f"formatter: {', '.join(sorted(unresolved))}"
240
258
  )
241
259
  try:
242
- mock_script.resolve_once(
243
- {
244
- "tmp_var": to_variable_dependency_format_string(
245
- script=mock_script, parsed_format_string=parsed
246
- )
247
- },
248
- unresolvable=unresolvable,
249
- update=True,
250
- )
260
+ if is_static_formatter:
261
+ return mock_script.resolve_once_parsed(
262
+ {"tmp_var": formatter_validator.parsed},
263
+ unresolvable=unresolved_variables,
264
+ update=True,
265
+ )["tmp_var"].native
266
+
267
+ return formatter_validator.format_string
251
268
  except RuntimeException as exc:
252
269
  if isinstance(exc, ScriptVariableNotResolved) and is_static_formatter:
253
270
  raise StringFormattingVariableNotFoundException(
@@ -255,45 +272,60 @@ def _validate_formatter(
255
272
  "entry variables"
256
273
  ) from exc
257
274
  raise StringFormattingVariableNotFoundException(exc) from exc
275
+ except UserThrownRuntimeError as exc:
276
+ # Errors are expected for non-static formatters due to missing entry
277
+ # data. Raise otherwise.
278
+ if not is_static_formatter:
279
+ return formatter_validator.format_string
280
+ raise exc
258
281
 
259
282
 
260
283
  def validate_formatters(
261
284
  script: Script,
262
285
  unresolved_variables: Set[str],
263
286
  validator: Validator,
264
- ) -> None:
287
+ ) -> Dict:
265
288
  """
266
289
  Ensure all OverridesStringFormatterValidator's only contain variables from the overrides
267
290
  and resolve.
268
291
  """
292
+ resolved_dict: Dict = {}
293
+
269
294
  if isinstance(validator, DictValidator):
270
- # pylint: disable=protected-access
295
+ resolved_dict[validator.leaf_name] = {}
271
296
  # Usage of protected variables in other validators is fine. The reason to keep
272
297
  # them protected is for readability when using them in subscriptions.
273
298
  for validator_value in validator._validator_dict.values():
274
- validate_formatters(
299
+ resolved_dict[validator.leaf_name] |= validate_formatters(
275
300
  script=script,
276
301
  unresolved_variables=unresolved_variables,
277
302
  validator=validator_value,
278
303
  )
279
- # pylint: enable=protected-access
280
304
  elif isinstance(validator, ListValidator):
305
+ resolved_dict[validator.leaf_name] = []
281
306
  for list_value in validator.list:
282
- validate_formatters(
307
+ list_output = validate_formatters(
283
308
  script=script,
284
309
  unresolved_variables=unresolved_variables,
285
310
  validator=list_value,
286
311
  )
312
+ assert len(list_output) == 1
313
+ resolved_dict[validator.leaf_name].append(list(list_output.values())[0])
287
314
  elif isinstance(validator, (StringFormatterValidator, OverridesStringFormatterValidator)):
288
- _validate_formatter(
315
+ resolved_dict[validator.leaf_name] = _validate_formatter(
289
316
  mock_script=script,
290
317
  unresolved_variables=unresolved_variables,
291
318
  formatter_validator=validator,
292
319
  )
293
320
  elif isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)):
321
+ resolved_dict[validator.leaf_name] = {}
294
322
  for validator_value in validator.dict.values():
295
- _validate_formatter(
323
+ resolved_dict[validator.leaf_name] |= _validate_formatter(
296
324
  mock_script=script,
297
325
  unresolved_variables=unresolved_variables,
298
326
  formatter_validator=validator_value,
299
327
  )
328
+ else:
329
+ resolved_dict[validator.leaf_name] = validator._value
330
+
331
+ return resolved_dict
@@ -95,21 +95,11 @@ class Validator(ABC):
95
95
 
96
96
  @final
97
97
  @property
98
- def _root_name(self) -> str:
98
+ def leaf_name(self) -> str:
99
99
  """
100
100
  Returns
101
101
  -------
102
- "first" from the first.element.of.the.name
103
- """
104
- return self._name.split(".")[0]
105
-
106
- @final
107
- @property
108
- def _leaf_name(self) -> str:
109
- """
110
- Returns
111
- -------
112
- "first" from the first.element.of.the.name
102
+ "name" from the first.element.of.the.name
113
103
  """
114
104
  return self._name.split(".")[-1]
115
105
 
@@ -274,7 +264,7 @@ class DictValidator(Validator):
274
264
  value=self._dict.get(key, default),
275
265
  )
276
266
 
277
- self.__validator_dict[validator_name] = validator_instance
267
+ self.__validator_dict[key] = validator_instance
278
268
  return validator_instance
279
269
 
280
270
  @final
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ytdl-sub
3
- Version: 2025.12.30
3
+ Version: 2025.12.31.post1
4
4
  Summary: Automate downloading metadata generation with YoutubeDL
5
5
  Author: Jesse Bannon
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -1,4 +1,4 @@
1
- ytdl_sub/__init__.py,sha256=-TEMMN5C78roSqtIzCzV1QpBG296lmhcvghcr997QKY,73
1
+ ytdl_sub/__init__.py,sha256=WrW7vK-7xEGvZMFgyjkq_NA4CjXzPSZv3q1IzcatKCI,79
2
2
  ytdl_sub/main.py,sha256=4Rf9wXxSKW7IPnWqG5YtTZ814PjP1n9WtoFDivaainE,1004
3
3
  ytdl_sub/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  ytdl_sub/cli/entrypoint.py,sha256=XXjUH4HiOP_BB2ZA_bNcyt5-o6YLAdZmj0EP3xtOtD8,9496
@@ -12,17 +12,17 @@ ytdl_sub/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  ytdl_sub/config/config_file.py,sha256=SQtVrMIUq2z3WwJVOed4y84JBMQ8aa4pbiBB0Y-YY4M,2781
13
13
  ytdl_sub/config/config_validator.py,sha256=W1dvQD8wI7VOmGOHyaliu8DC6HOjfoGsgUw2MURTZOM,9797
14
14
  ytdl_sub/config/defaults.py,sha256=NTwzlKDkks1LDGNjFMxh91fw5E6T6d_zGsCwODNYJxo,1152
15
- ytdl_sub/config/overrides.py,sha256=oOMrxXSfmNciooLEUM3-GmBzuPhFNZk-2lv4sjPP-u0,9814
16
- ytdl_sub/config/preset.py,sha256=kVT81J1yfTRthYIOBeHVk_WUrdvr_2mqyY3qUpHWKjQ,8602
15
+ ytdl_sub/config/overrides.py,sha256=tCW0RvZ7365MIx5jbH4iDu0XhUP0RCRHGgBJiPiXB1s,10260
16
+ ytdl_sub/config/preset.py,sha256=Msacs60TyDWWzSlvkI3PurRil5vWOGQFK__Ev2HG1l0,9887
17
17
  ytdl_sub/config/preset_options.py,sha256=PBinBBRLTaO4B5Q9bW2WUHAd0-x8fVIUqodOIm-py-c,14207
18
18
  ytdl_sub/config/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  ytdl_sub/config/plugin/plugin.py,sha256=ugUyDnjLKcBy7lOpTbVKaGSuUMvzyyQx3DIcbv3eTkQ,4721
20
20
  ytdl_sub/config/plugin/plugin_mapping.py,sha256=xx9K2EReIU7zdPlogjvIkowy8J2R4TEhMNJyR7_1aE0,7846
21
21
  ytdl_sub/config/plugin/plugin_operation.py,sha256=evRLt-m7LI7q4oQvA4YqlCU0x3-i5MWEWdUn0p7UViw,169
22
- ytdl_sub/config/plugin/preset_plugins.py,sha256=X9_2ZJj7auE-ckL3-biliKO4KV5tCSkn0a1apKtt72o,1530
22
+ ytdl_sub/config/plugin/preset_plugins.py,sha256=AbyYOJwWPK3hy93CfosD4toKJP7yiTVoA1sL4YrppV0,2877
23
23
  ytdl_sub/config/validators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  ytdl_sub/config/validators/options.py,sha256=IweHvzrMWdCEn4oGItDl-X9PBK7L_CmKfD_wuhNctuI,2608
25
- ytdl_sub/config/validators/variable_validation.py,sha256=F9ZTeqeSIvwxFeGbp6EAKy8pYBLJ1qNfhCF7QSccDKY,8599
25
+ ytdl_sub/config/validators/variable_validation.py,sha256=oeNgo96XRR_hCB4vXcJujxPkxyoR6ibwJswHcy0pN08,3606
26
26
  ytdl_sub/downloaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  ytdl_sub/downloaders/source_plugin.py,sha256=dqQpHgeFC_kXjUOAiJKzgtrzSzXPoL2Vvn8ez2sR7QA,2508
28
28
  ytdl_sub/downloaders/ytdl_options_builder.py,sha256=k3cGBZmLMsdlnOj0PafHLJJofGqor7KQiv-B2TUc6mE,1553
@@ -31,7 +31,7 @@ ytdl_sub/downloaders/info_json/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
31
31
  ytdl_sub/downloaders/info_json/info_json_downloader.py,sha256=_FOvM52GsaLhJy4_D9RkpMhsZZyygXrL62CqcxaO5Co,6619
32
32
  ytdl_sub/downloaders/url/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  ytdl_sub/downloaders/url/downloader.py,sha256=wFtR7368t3-NJO0xM0svW9A272wsJY9ii36pfGpSW5U,20703
34
- ytdl_sub/downloaders/url/validators.py,sha256=jQRKli5gLIIn2GDEFgbS5wAKfaZIO3jHTEecMnRBe9M,13024
34
+ ytdl_sub/downloaders/url/validators.py,sha256=ckDXzOPcO4oOdjP96dCJVib5ofe41xjOE3Evlq6vr08,13036
35
35
  ytdl_sub/entries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  ytdl_sub/entries/base_entry.py,sha256=x5CByJIMtwmsZkEe8TipBuQUQO8b2bWMYqyYvRlXGag,5403
37
37
  ytdl_sub/entries/entry.py,sha256=_U0PryLO1QNLexYHG4bcQ1yMgOY1nxKjRNb99-PXg7A,10008
@@ -92,8 +92,8 @@ ytdl_sub/prebuilt_presets/tv_show/tv_show.yaml,sha256=qmAB3jsXY1VsqmtLnLXXte5rz_
92
92
  ytdl_sub/prebuilt_presets/tv_show/tv_show_by_date.yaml,sha256=0OgIOzxSPNVqwcZnL6qT9_azplBhDXePcf0ASNTQJ5Q,7223
93
93
  ytdl_sub/prebuilt_presets/tv_show/tv_show_collection.yaml,sha256=XCtt4i4-zAgyfh-p4SXSGd0FI56lWCrPOMmB6mdf-AE,238393
94
94
  ytdl_sub/script/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
- ytdl_sub/script/parser.py,sha256=30SOHkEFi0rxiW_IrC1w6WzPKuyIf766zYEVHDsndtg,21898
96
- ytdl_sub/script/script.py,sha256=S-H6rYt9lCcxda9tPsElomf48kzCbGwJkdPcTIFahmA,23993
95
+ ytdl_sub/script/parser.py,sha256=LUvmUIxg346njE8-uDx06nOW6ZDX5QS6aUtp4lsgwpk,22115
96
+ ytdl_sub/script/script.py,sha256=4fP5leu1GQdbbOQLzrWGEo_zaIUeZwGY0btFqdpQmpI,26582
97
97
  ytdl_sub/script/script_output.py,sha256=5SIamnI-1D3xMA0qQzjf9xrIy8j6BVhGCKrl_Q1d2M8,1381
98
98
  ytdl_sub/script/functions/__init__.py,sha256=rzl6O5G0IEqFYHQECIM9bRvcuQj-ytC_p-Xl2TTa6j0,2932
99
99
  ytdl_sub/script/functions/array_functions.py,sha256=yg9rcZP67-aVzhu4oZZzUu3-AHiHltbQDWEs8GW1DJ4,7193
@@ -114,14 +114,14 @@ ytdl_sub/script/types/map.py,sha256=Y3zlm__IHcnycGFv0xbU_P7wgiQgtgddatPLQypjCvU,
114
114
  ytdl_sub/script/types/resolvable.py,sha256=OinvpfKsVjgU_mnE0uYONQftUxNLa1zY2jA74bywwBQ,5302
115
115
  ytdl_sub/script/types/syntax_tree.py,sha256=4xWluTMPJqA6yYuQ4XCcncSFt5cDZ9ZIT2AIOOH4bT0,2336
116
116
  ytdl_sub/script/types/variable.py,sha256=aVJ3ocUr3WpDoolOq6y3NV71b3EQQYPAGrIT0FtIqc4,813
117
- ytdl_sub/script/types/variable_dependency.py,sha256=wgNSdAjntziVdlwqxCT7QKwvm43XCA7qxIoFlmD_Tdg,6281
117
+ ytdl_sub/script/types/variable_dependency.py,sha256=J9XzVVrkyt-Ae2acHbb6xFnl6NIeOcL6wLOH9SvzKcA,6796
118
118
  ytdl_sub/script/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
119
119
  ytdl_sub/script/utils/exception_formatters.py,sha256=hZDX2w90vU4lY2xiEM-qxQofQz2dmqPwhkSeWaJRsQ4,4645
120
120
  ytdl_sub/script/utils/exceptions.py,sha256=cag5ZLM6as1w-RsrOwO-oe4YFpwlFu_8U0QNGv_jQ68,2774
121
- ytdl_sub/script/utils/name_validation.py,sha256=ckTZqzeC-PTJRfAJbFLTNEgKchPA6YqLMS9Z280WUeQ,1830
121
+ ytdl_sub/script/utils/name_validation.py,sha256=jLdvHK--BAX2AEshydFwo3PAXfmEKqC_92cz1pMTgWU,2241
122
122
  ytdl_sub/script/utils/type_checking.py,sha256=9EKWE1mWPOlmXWiWTZw-bCRV0FlYk22tNKlgEM6wU_0,10393
123
123
  ytdl_sub/subscriptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
124
- ytdl_sub/subscriptions/base_subscription.py,sha256=JRVyJIoHIt0I7NoEMqYd9FsZU3QF9i8HQAjzQeg_CPM,7179
124
+ ytdl_sub/subscriptions/base_subscription.py,sha256=sZH0U9Kp4jOMFR04sC97DFW7GNo7qWDUn23n0-G8gBk,7814
125
125
  ytdl_sub/subscriptions/subscription.py,sha256=wfzOYVkzmsSH1KYbAMRi6Nhrb2pRaXdhILle7H0hcas,5127
126
126
  ytdl_sub/subscriptions/subscription_download.py,sha256=-eLbClw88tleLdUgP7TtPtnqroE3Zzdx2XLQrgapMDc,18190
127
127
  ytdl_sub/subscriptions/subscription_validators.py,sha256=tSN8YPLkIYXQbmWQ3M6U7xHpNjPn8rtNR9wSLIE2kzM,13606
@@ -138,7 +138,7 @@ ytdl_sub/utils/file_lock.py,sha256=VlDl8QjBEUAHDBHsfp7Q0tR1Me0fFYELqzMtlyZ0MO4,2
138
138
  ytdl_sub/utils/file_path.py,sha256=7RWc4fGj7HH6srzGJ5ph1l5euJfWpIX-2cyC-03YXEs,2539
139
139
  ytdl_sub/utils/logger.py,sha256=g_23ddk1WpQ3-_MOHz-Rlz7xCydK66BbtqkualX1zAQ,8593
140
140
  ytdl_sub/utils/retry.py,sha256=eyhtrfmzwph4uGf4Bwk0UThEIGk4cobwLLuWowGHyK4,1313
141
- ytdl_sub/utils/script.py,sha256=thfjbrWTveJwf7wABBlYw3G8SgT7_JGdAmuv3n5Qty0,5189
141
+ ytdl_sub/utils/script.py,sha256=3sUWCZxVuk5inm6ou3ChFTfeDk7lRnhPSV1wQARj5UI,5777
142
142
  ytdl_sub/utils/scriptable.py,sha256=mSMLVOnBzTAQLZXyBozx2spLwNi6llaT_c2zdS2l1rk,3365
143
143
  ytdl_sub/utils/subtitles.py,sha256=0ICFw7C9H9BIZvsZf0YhD9ygwR68XCdOQWerspR2g3I,85
144
144
  ytdl_sub/utils/system.py,sha256=I9dH46ZhRRpaO0pEPfeOKp968Ub3SAdyZASVavRHrvc,58
@@ -153,14 +153,14 @@ ytdl_sub/validators/regex_validator.py,sha256=jS8No927eg3zcpYEOv5g0gykePV0DCZaIr
153
153
  ytdl_sub/validators/source_variable_validator.py,sha256=ziG4PVIyzj5ky-Okle0FB2d2P5DWs3-jYF81hMvBTEQ,922
154
154
  ytdl_sub/validators/strict_dict_validator.py,sha256=RduK_3pOEbEQQuugAUeKKqM0Tv5x2vxSSb7vROyo2vQ,1661
155
155
  ytdl_sub/validators/string_datetime.py,sha256=GpbBiZH1FHTMeo0Zk134-uMdTMk2JD3Re3TnvuPx2OU,1125
156
- ytdl_sub/validators/string_formatter_validators.py,sha256=CRwZH_fnA5LabSNf973ELSMtWt2q1vQD9ZSPVGnd4Es,10625
156
+ ytdl_sub/validators/string_formatter_validators.py,sha256=dQ_8l1wl101vPkaOlfJuXprYI5EnfHQ6jLWjGk92raA,11858
157
157
  ytdl_sub/validators/string_select_validator.py,sha256=KFXNKWX2J80WGt08m5gVYphPMHYxhHlgfcoXAQMq6zw,1086
158
- ytdl_sub/validators/validators.py,sha256=X9AkECdngEXcME_C25njHaOjtqbfEJMED2eG9Qy3UPs,9352
158
+ ytdl_sub/validators/validators.py,sha256=JC3-c9fSrozFADUY5jqZEhXpM2q3sfserlooQxT2DK8,9133
159
159
  ytdl_sub/ytdl_additions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
160
160
  ytdl_sub/ytdl_additions/enhanced_download_archive.py,sha256=Lsc0wjHdx9d8dYJCskZYAUGDAQ_QzQ-_xbQlyrBSzfk,24884
161
- ytdl_sub-2025.12.30.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
162
- ytdl_sub-2025.12.30.dist-info/METADATA,sha256=iJlju-U5_fduOC8r1k8UBrMMR-i5cvyucuGJj-85ciA,51420
163
- ytdl_sub-2025.12.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
164
- ytdl_sub-2025.12.30.dist-info/entry_points.txt,sha256=K3T5235NlAI-WLmHCg5tzLZHqc33OLN5IY5fOGc9t10,48
165
- ytdl_sub-2025.12.30.dist-info/top_level.txt,sha256=6z-JWazl6jXspC2DNyxOnGnEqYyGzVbgcBDoXfbkUhI,9
166
- ytdl_sub-2025.12.30.dist-info/RECORD,,
161
+ ytdl_sub-2025.12.31.post1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
162
+ ytdl_sub-2025.12.31.post1.dist-info/METADATA,sha256=aCbLYK2DK2szieUQ05O-SzJc0r5erfgVXBjnTGKrW0k,51426
163
+ ytdl_sub-2025.12.31.post1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
164
+ ytdl_sub-2025.12.31.post1.dist-info/entry_points.txt,sha256=K3T5235NlAI-WLmHCg5tzLZHqc33OLN5IY5fOGc9t10,48
165
+ ytdl_sub-2025.12.31.post1.dist-info/top_level.txt,sha256=6z-JWazl6jXspC2DNyxOnGnEqYyGzVbgcBDoXfbkUhI,9
166
+ ytdl_sub-2025.12.31.post1.dist-info/RECORD,,