ytdl-sub 2026.1.27.post1__py3-none-any.whl → 2026.2.2__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__ = "2026.01.27.post1";__local_version__ = "2026.01.27+d6eda27"
1
+ __pypi_version__ = "2026.02.02";__local_version__ = "2026.02.02+c163f97"
ytdl_sub/config/preset.py CHANGED
@@ -255,11 +255,18 @@ class Preset(_PresetShell):
255
255
  """
256
256
  return cls(config=config, name=preset_name, value=preset_dict)
257
257
 
258
- @property
259
- def yaml(self) -> str:
258
+ def yaml(self, subscription_only: bool) -> str:
260
259
  """
260
+ Parameters
261
+ ----------
262
+ subscription_only:
263
+ Only include the subscription contents, not the surrounding boiler-plate.
264
+
261
265
  Returns
262
266
  -------
263
267
  Preset in YAML format
264
268
  """
269
+ if subscription_only:
270
+ return dump_yaml(self._value)
271
+
265
272
  return dump_yaml({"presets": {self._name: self._value}})
@@ -1,4 +1,6 @@
1
1
  from typing import Dict
2
+ from typing import List
3
+ from typing import Set
2
4
 
3
5
  from ytdl_sub.config.overrides import Overrides
4
6
  from ytdl_sub.config.plugin.plugin_mapping import PluginMapping
@@ -7,80 +9,166 @@ from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
7
9
  from ytdl_sub.config.preset_options import OutputOptions
8
10
  from ytdl_sub.config.validators.options import OptionsValidator
9
11
  from ytdl_sub.downloaders.url.validators import MultiUrlValidator
12
+ from ytdl_sub.entries.script.variable_definitions import UNRESOLVED_VARIABLES
13
+ from ytdl_sub.entries.script.variable_definitions import VARIABLES
14
+ from ytdl_sub.script.script import Script
15
+ from ytdl_sub.script.utils.name_validation import is_function
16
+ from ytdl_sub.utils.script import ScriptUtils
10
17
  from ytdl_sub.validators.string_formatter_validators import validate_formatters
11
18
 
12
19
 
20
+ class ResolutionLevel:
21
+ ORIGINAL = 0
22
+ FILL = 1
23
+ RESOLVE = 2
24
+ INTERNAL = 3
25
+
26
+ @classmethod
27
+ def name_of(cls, resolution_level: int) -> str:
28
+ """
29
+ Name of the resolution level.
30
+ """
31
+ if resolution_level == cls.ORIGINAL:
32
+ return "original"
33
+ if resolution_level == cls.FILL:
34
+ return "fill"
35
+ if resolution_level == cls.RESOLVE:
36
+ return "resolve"
37
+ if resolution_level == cls.INTERNAL:
38
+ return "internal"
39
+ raise ValueError("Invalid resolution level")
40
+
41
+ @classmethod
42
+ def all(cls) -> List[int]:
43
+ """
44
+ All possible resolution levels.
45
+ """
46
+ return [cls.ORIGINAL, cls.FILL, cls.RESOLVE, cls.INTERNAL]
47
+
48
+
13
49
  class VariableValidation:
50
+
51
+ def _get_resolve_partial_filter(self) -> Set[str]:
52
+ # Exclude sanitized variables from partial validation. This lessens the work
53
+ # and prevents double-evaluation, which can lead to bad behavior like double-prints.
54
+ return {
55
+ name
56
+ for name in self.script.variable_names
57
+ if name not in self.unresolved_variables and not name.endswith("_sanitized")
58
+ }
59
+
60
+ def _apply_resolution_level(self) -> None:
61
+ if self._resolution_level == ResolutionLevel.FILL:
62
+ self.unresolved_variables |= VARIABLES.variable_names(include_sanitized=True)
63
+ # Only partial resolve definitions that are already resolved
64
+ self.unresolved_variables |= {
65
+ name
66
+ for name in self.overrides.keys
67
+ if not is_function(name) and not self.script.definition_of(name).maybe_resolvable
68
+ }
69
+ elif self._resolution_level == ResolutionLevel.RESOLVE:
70
+ # Partial resolve everything, but not including internal variables
71
+ self.unresolved_variables |= VARIABLES.variable_names(include_sanitized=True)
72
+ elif self._resolution_level == ResolutionLevel.INTERNAL:
73
+ # Partial resolve everything including internal variables
74
+ pass
75
+ else:
76
+ raise ValueError("Invalid resolution level for validation")
77
+
78
+ self.script = self.script.resolve_partial(
79
+ unresolvable=self.unresolved_variables,
80
+ output_filter=self._get_resolve_partial_filter(),
81
+ )
82
+
14
83
  def __init__(
15
84
  self,
16
85
  overrides: Overrides,
17
86
  downloader_options: MultiUrlValidator,
18
87
  output_options: OutputOptions,
19
88
  plugins: PresetPlugins,
89
+ resolution_level: int = ResolutionLevel.RESOLVE,
20
90
  ):
21
91
  self.overrides = overrides
22
92
  self.downloader_options = downloader_options
23
93
  self.output_options = output_options
24
94
  self.plugins = plugins
25
95
 
26
- self.script = self.overrides.script
27
- self.unresolved_variables = self.plugins.get_all_variables(
96
+ self.script: Script = self.overrides.script
97
+ self.unresolved_variables = (
98
+ self.plugins.get_all_variables(
99
+ additional_options=[self.output_options, self.downloader_options]
100
+ )
101
+ | UNRESOLVED_VARIABLES
102
+ )
103
+ self.unresolved_runtime_variables = self.plugins.get_all_variables(
28
104
  additional_options=[self.output_options, self.downloader_options]
29
105
  )
106
+ self._resolution_level = resolution_level
107
+
108
+ self._apply_resolution_level()
30
109
 
31
- def _add_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> None:
110
+ def _add_runtime_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> None:
32
111
  """
33
112
  Add dummy variables for script validation
34
113
  """
35
114
  added_variables = options.added_variables(
36
- unresolved_variables=self.unresolved_variables,
115
+ unresolved_variables=self.unresolved_runtime_variables,
37
116
  ).get(plugin_op, set())
38
117
  modified_variables = options.modified_variables().get(plugin_op, set())
39
118
 
40
- self.unresolved_variables -= added_variables | modified_variables
119
+ self.unresolved_runtime_variables -= added_variables | modified_variables
41
120
 
42
- def ensure_proper_usage(self) -> Dict:
121
+ def ensure_proper_usage(self, partial_resolve_formatters: bool = False) -> Dict:
43
122
  """
44
123
  Validate variables resolve as plugins are executed, and return
45
124
  a mock script which contains actualized added variables from the plugins
46
125
  """
47
-
48
126
  resolved_subscription: Dict = {}
49
127
 
50
- self._add_variables(PluginOperation.DOWNLOADER, options=self.downloader_options)
128
+ self._add_runtime_variables(PluginOperation.DOWNLOADER, options=self.downloader_options)
51
129
 
52
130
  # Always add output options first
53
- self._add_variables(PluginOperation.MODIFY_ENTRY_METADATA, options=self.output_options)
131
+ self._add_runtime_variables(
132
+ PluginOperation.MODIFY_ENTRY_METADATA, options=self.output_options
133
+ )
54
134
 
55
135
  # Metadata variables to be added
56
136
  for plugin_options in PluginMapping.order_options_by(
57
137
  self.plugins.zipped(), PluginOperation.MODIFY_ENTRY_METADATA
58
138
  ):
59
- self._add_variables(PluginOperation.MODIFY_ENTRY_METADATA, options=plugin_options)
139
+ self._add_runtime_variables(
140
+ PluginOperation.MODIFY_ENTRY_METADATA, options=plugin_options
141
+ )
60
142
 
61
143
  for plugin_options in PluginMapping.order_options_by(
62
144
  self.plugins.zipped(), PluginOperation.MODIFY_ENTRY
63
145
  ):
64
- self._add_variables(PluginOperation.MODIFY_ENTRY, options=plugin_options)
146
+ self._add_runtime_variables(PluginOperation.MODIFY_ENTRY, options=plugin_options)
65
147
 
66
148
  # Validate that any formatter in the plugin options can resolve
67
149
  resolved_subscription |= validate_formatters(
68
150
  script=self.script,
69
151
  unresolved_variables=self.unresolved_variables,
152
+ unresolved_runtime_variables=self.unresolved_runtime_variables,
70
153
  validator=plugin_options,
154
+ partial_resolve_formatters=partial_resolve_formatters,
71
155
  )
72
156
 
73
157
  resolved_subscription |= validate_formatters(
74
158
  script=self.script,
75
159
  unresolved_variables=self.unresolved_variables,
160
+ unresolved_runtime_variables=self.unresolved_runtime_variables,
76
161
  validator=self.output_options,
162
+ partial_resolve_formatters=partial_resolve_formatters,
77
163
  )
78
164
 
79
165
  # TODO: make this a function
80
166
  raw_download_output = validate_formatters(
81
167
  script=self.script,
82
168
  unresolved_variables=self.unresolved_variables,
169
+ unresolved_runtime_variables=self.unresolved_runtime_variables,
83
170
  validator=self.downloader_options.urls,
171
+ partial_resolve_formatters=partial_resolve_formatters,
84
172
  )
85
173
  resolved_subscription["download"] = []
86
174
  for url_output in raw_download_output["download"]:
@@ -90,5 +178,19 @@ class VariableValidation:
90
178
  if url_output["url"]:
91
179
  resolved_subscription["download"].append(url_output)
92
180
 
93
- assert not self.unresolved_variables
181
+ # TODO: make function
182
+ resolved_subscription["overrides"] = {}
183
+ for name in self.overrides.keys:
184
+ value = self.script.definition_of(name)
185
+ if name in self.script.function_names:
186
+ # Keep custom functions as-is
187
+ resolved_subscription["overrides"][name] = self.overrides.dict_with_format_strings[
188
+ name
189
+ ]
190
+ elif resolved := value.maybe_resolvable:
191
+ resolved_subscription["overrides"][name] = resolved.native
192
+ else:
193
+ resolved_subscription["overrides"][name] = ScriptUtils.to_native_script(value)
194
+
195
+ assert not self.unresolved_runtime_variables
94
196
  return resolved_subscription
@@ -1135,6 +1135,16 @@ class VariableDefinitions(
1135
1135
  ]
1136
1136
  }
1137
1137
 
1138
+ @cache
1139
+ def variable_names(self, include_sanitized: bool):
1140
+ """
1141
+ Returns all variable names, and can include sanitized.
1142
+ """
1143
+ var_names: Set[str] = self.scripts().keys()
1144
+ if include_sanitized:
1145
+ var_names |= {f"{name}_sanitized" for name in var_names}
1146
+ return var_names
1147
+
1138
1148
  @cache
1139
1149
  def injected_variables(self) -> Set[MetadataVariable]:
1140
1150
  """
ytdl_sub/script/script.py CHANGED
@@ -1,5 +1,4 @@
1
1
  # pylint: disable=missing-raises-doc
2
- import copy
3
2
  from collections import defaultdict
4
3
  from typing import Dict
5
4
  from typing import List
@@ -693,6 +692,18 @@ class Script:
693
692
 
694
693
  raise RuntimeException(f"Tried to get unresolved variable {variable_name}")
695
694
 
695
+ def definition_of(self, name: str) -> SyntaxTree:
696
+ """
697
+ Returns
698
+ -------
699
+ The definition of the variable or function.
700
+ """
701
+ if name.startswith("%") and name[1:] in self._functions:
702
+ return self._functions[name[1:]]
703
+ if name in self._variables:
704
+ return self._variables[name]
705
+ raise RuntimeException(f"Tried to get non-existent definition with name {name}")
706
+
696
707
  @property
697
708
  def variable_names(self) -> Set[str]:
698
709
  """
@@ -713,35 +724,33 @@ class Script:
713
724
  """
714
725
  return set(to_function_definition_name(name) for name in self._functions.keys())
715
726
 
716
- def resolve_partial(
727
+ def _resolve_partial_loop(
717
728
  self,
718
- unresolvable: Optional[Set[str]] = None,
719
- ) -> "Script":
720
- """
721
- Returns
722
- -------
723
- New (deep-copied) script that resolves inner variables as much
724
- as possible.
725
- """
726
- unresolvable: Set[str] = unresolvable or {}
727
- resolved: Dict[Variable, Resolvable] = {}
728
- unresolved: Dict[Variable, Argument] = {
729
- Variable(name): definition
730
- for name, definition in self._variables.items()
731
- if name not in unresolvable
732
- }
729
+ output_filter: Optional[Set[str]],
730
+ resolved: Dict[Variable, Resolvable],
731
+ unresolved: Dict[Variable, Argument],
732
+ unresolvable: Optional[Set[str]],
733
+ ):
734
+ to_partially_resolve: Set[Variable] = (
735
+ {Variable(name) for name in output_filter} if output_filter else set(unresolved.keys())
736
+ )
733
737
 
734
738
  partially_resolved = True
735
739
  while partially_resolved:
736
740
 
737
741
  partially_resolved = False
738
742
 
739
- for variable in list(unresolved.keys()):
743
+ for variable in list(to_partially_resolve):
740
744
  definition = unresolved[variable]
741
-
742
745
  maybe_resolved = definition
746
+
743
747
  if isinstance(definition, Variable) and definition.name not in unresolvable:
744
- maybe_resolved = resolved.get(definition, unresolved[definition])
748
+ if definition in resolved:
749
+ maybe_resolved = resolved[definition]
750
+ elif definition in unresolved:
751
+ maybe_resolved = unresolved[definition]
752
+ else:
753
+ raise UNREACHABLE
745
754
  elif isinstance(definition, VariableDependency):
746
755
  maybe_resolved = definition.partial_resolve(
747
756
  resolved_variables=resolved,
@@ -752,6 +761,7 @@ class Script:
752
761
  if isinstance(maybe_resolved, Resolvable):
753
762
  resolved[variable] = maybe_resolved
754
763
  del unresolved[variable]
764
+ to_partially_resolve.remove(variable)
755
765
  partially_resolved = True
756
766
  else:
757
767
  unresolved[variable] = maybe_resolved
@@ -760,11 +770,73 @@ class Script:
760
770
  # which means we can iterate again
761
771
  partially_resolved |= definition != maybe_resolved
762
772
 
763
- return copy.deepcopy(self).add_parsed(
764
- {var_name: self._variables[var_name] for var_name in unresolvable}
765
- | {
766
- var.name: ResolvedSyntaxTree(ast=[definition])
767
- for var, definition in resolved.items()
768
- }
769
- | {var.name: SyntaxTree(ast=[definition]) for var, definition in unresolved.items()}
773
+ def _resolve_partial(
774
+ self,
775
+ unresolvable: Optional[Set[str]] = None,
776
+ output_filter: Optional[Set[str]] = None,
777
+ ) -> Dict[str, SyntaxTree]:
778
+ """
779
+ Returns
780
+ -------
781
+ New (deep-copied) script that resolves inner variables as much
782
+ as possible.
783
+ """
784
+ unresolvable: Set[str] = unresolvable or {}
785
+ resolved: Dict[Variable, Resolvable] = {}
786
+ unresolved: Dict[Variable, Argument] = {
787
+ Variable(name): definition
788
+ for name, definition in self._variables.items()
789
+ if name not in unresolvable
790
+ }
791
+
792
+ self._resolve_partial_loop(
793
+ output_filter=output_filter,
794
+ resolved=resolved,
795
+ unresolved=unresolved,
796
+ unresolvable=unresolvable,
770
797
  )
798
+
799
+ if output_filter:
800
+ out: Dict[str, SyntaxTree] = {}
801
+ for name in output_filter:
802
+ variable_name = Variable(name)
803
+ if variable_name in resolved:
804
+ out[name] = ResolvedSyntaxTree(ast=[resolved[variable_name]])
805
+ else:
806
+ out[name] = SyntaxTree(ast=[unresolved[variable_name]])
807
+
808
+ return out
809
+
810
+ return {
811
+ var.name: ResolvedSyntaxTree(ast=[definition]) for var, definition in resolved.items()
812
+ } | {var.name: SyntaxTree(ast=[definition]) for var, definition in unresolved.items()}
813
+
814
+ def resolve_partial(
815
+ self,
816
+ unresolvable: Optional[Set[str]] = None,
817
+ output_filter: Optional[Set[str]] = None,
818
+ ) -> "Script":
819
+ """
820
+ Updates the internal script to resolve as much as possible.
821
+ """
822
+ out = self._resolve_partial(unresolvable=unresolvable, output_filter=output_filter)
823
+ for var_name, definition in out.items():
824
+ self._variables[var_name] = definition
825
+
826
+ return self
827
+
828
+ def resolve_partial_once(
829
+ self, variable_definitions: Dict[str, SyntaxTree], unresolvable: Optional[Set[str]] = None
830
+ ) -> Dict[str, SyntaxTree]:
831
+ """
832
+ Partially resolves the input variable definitions as much as possible.
833
+ """
834
+ try:
835
+ self.add_parsed(variable_definitions)
836
+ return self._resolve_partial(
837
+ unresolvable=unresolvable,
838
+ output_filter=set(list(variable_definitions.keys())),
839
+ )
840
+ finally:
841
+ for name in variable_definitions.keys():
842
+ self._variables.pop(name, None)
@@ -371,13 +371,6 @@ class BuiltInFunction(Function, BuiltInFunctionType):
371
371
  If the conditional partially resolvable enough to warrant evaluation,
372
372
  perform it here.
373
373
  """
374
- if self.is_subset_of(
375
- variables=resolved_variables, custom_function_definitions=custom_functions
376
- ):
377
- return self.resolve(
378
- resolved_variables=resolved_variables,
379
- custom_functions=custom_functions,
380
- )
381
374
 
382
375
  if self.name == "if":
383
376
  maybe_resolvable_arg, is_resolvable = VariableDependency.try_partial_resolve(
@@ -55,6 +55,10 @@ class SyntaxTree(VariableDependency):
55
55
  custom_functions=custom_functions,
56
56
  )
57
57
 
58
+ # If no arguments, must be empty string
59
+ if len(maybe_resolvable_values) == 0:
60
+ return String(value="")
61
+
58
62
  # Mimic the above resolve behavior
59
63
  if len(maybe_resolvable_values) > 1:
60
64
  return BuiltInFunction(name="concat", args=maybe_resolvable_values)
@@ -279,8 +279,11 @@ class VariableDependency(ABC):
279
279
  if not isinstance(maybe_resolvable_args[-1], Resolvable):
280
280
  is_resolvable = False
281
281
  elif isinstance(arg, Variable):
282
- if arg not in resolved_variables:
282
+ if arg in resolved_variables:
283
+ maybe_resolvable_args[-1] = resolved_variables[arg]
284
+ else:
283
285
  is_resolvable = False
286
+ # Could be un unresolvable
284
287
  if arg in unresolved_variables:
285
288
  maybe_resolvable_args[-1] = unresolved_variables[arg]
286
289
 
@@ -8,6 +8,7 @@ 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 ResolutionLevel
11
12
  from ytdl_sub.config.validators.variable_validation import VariableValidation
12
13
  from ytdl_sub.downloaders.url.validators import MultiUrlValidator
13
14
  from ytdl_sub.entries.variables.override_variables import SubscriptionVariables
@@ -79,7 +80,7 @@ class BaseSubscription(ABC):
79
80
  )
80
81
 
81
82
  # Validate after adding the subscription name
82
- self._validated_dict = VariableValidation(
83
+ _ = VariableValidation(
83
84
  overrides=self.overrides,
84
85
  downloader_options=self.downloader_options,
85
86
  output_options=self.output_options,
@@ -254,12 +255,22 @@ class BaseSubscription(ABC):
254
255
  -------
255
256
  Subscription in yaml format
256
257
  """
257
- return self._preset_options.yaml
258
+ return self._preset_options.yaml(subscription_only=False)
258
259
 
259
- def resolved_yaml(self) -> str:
260
+ def resolved_yaml(self, resolution_level: int = ResolutionLevel.RESOLVE) -> str:
260
261
  """
261
262
  Returns
262
263
  -------
263
264
  Human-readable, condensed YAML definition of the subscription.
264
265
  """
265
- return dump_yaml(self._validated_dict)
266
+ if resolution_level == ResolutionLevel.ORIGINAL:
267
+ return self._preset_options.yaml(subscription_only=True)
268
+
269
+ out = VariableValidation(
270
+ overrides=self.overrides,
271
+ downloader_options=self.downloader_options,
272
+ output_options=self.output_options,
273
+ plugins=self.plugins,
274
+ resolution_level=resolution_level,
275
+ ).ensure_proper_usage(partial_resolve_formatters=True)
276
+ return dump_yaml(out)
@@ -5,14 +5,12 @@ from typing import Set
5
5
  from typing import Union
6
6
  from typing import final
7
7
 
8
- from ytdl_sub.entries.script.variable_definitions import VARIABLES
9
8
  from ytdl_sub.script.parser import parse
10
9
  from ytdl_sub.script.script import Script
11
10
  from ytdl_sub.script.types.syntax_tree import SyntaxTree
12
11
  from ytdl_sub.script.utils.exceptions import RuntimeException
13
12
  from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
14
13
  from ytdl_sub.script.utils.exceptions import UserException
15
- from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError
16
14
  from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException
17
15
  from ytdl_sub.utils.script import ScriptUtils
18
16
  from ytdl_sub.validators.validators import DictValidator
@@ -244,15 +242,15 @@ class UnstructuredOverridesDictFormatterValidator(UnstructuredDictFormatterValid
244
242
  def _validate_formatter(
245
243
  mock_script: Script,
246
244
  unresolved_variables: Set[str],
245
+ unresolved_runtime_variables: Set[str],
247
246
  formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator],
248
- ) -> str:
247
+ partial_resolve_entry_formatters: bool,
248
+ ) -> Any:
249
249
  parsed = formatter_validator.parsed
250
250
  if resolved := parsed.maybe_resolvable:
251
- return resolved.native
251
+ return formatter_validator.post_process(resolved.native)
252
252
 
253
253
  is_static_formatter = isinstance(formatter_validator, OverridesStringFormatterValidator)
254
- if is_static_formatter:
255
- unresolved_variables = unresolved_variables.union({VARIABLES.entry_metadata.variable_name})
256
254
 
257
255
  variable_names = {var.name for var in parsed.variables}
258
256
  custom_function_names = {f"%{func.name}" for func in parsed.custom_functions}
@@ -272,20 +270,32 @@ def _validate_formatter(
272
270
  "contains the following custom functions that do not exist: "
273
271
  f"{', '.join(sorted(custom_function_names - mock_script.function_names))}"
274
272
  )
275
- if unresolved := variable_names.intersection(unresolved_variables):
273
+ if unresolved := variable_names.intersection(unresolved_runtime_variables):
276
274
  raise StringFormattingVariableNotFoundException(
277
275
  "contains the following variables that are unresolved when executing this "
278
276
  f"formatter: {', '.join(sorted(unresolved))}"
279
277
  )
278
+
279
+ if partial_resolve_entry_formatters and not is_static_formatter:
280
+ parsed = mock_script.resolve_partial_once(
281
+ variable_definitions={"tmp_var": formatter_validator.parsed},
282
+ unresolvable=unresolved_variables,
283
+ )["tmp_var"]
284
+
280
285
  try:
281
286
  if is_static_formatter:
282
- return mock_script.resolve_once_parsed(
283
- {"tmp_var": formatter_validator.parsed},
284
- unresolvable=unresolved_variables,
285
- update=True,
286
- )["tmp_var"].native
287
+ return formatter_validator.post_process(
288
+ mock_script.resolve_once_parsed(
289
+ {"tmp_var": formatter_validator.parsed},
290
+ unresolvable=unresolved_variables,
291
+ update=True,
292
+ )["tmp_var"].native
293
+ )
294
+
295
+ if maybe_resolved := parsed.maybe_resolvable:
296
+ return formatter_validator.post_process(maybe_resolved)
287
297
 
288
- return formatter_validator.format_string
298
+ return ScriptUtils.to_native_script(parsed)
289
299
  except RuntimeException as exc:
290
300
  if isinstance(exc, ScriptVariableNotResolved) and is_static_formatter:
291
301
  raise StringFormattingVariableNotFoundException(
@@ -293,18 +303,14 @@ def _validate_formatter(
293
303
  "entry variables"
294
304
  ) from exc
295
305
  raise StringFormattingVariableNotFoundException(exc) from exc
296
- except UserThrownRuntimeError as exc:
297
- # Errors are expected for non-static formatters due to missing entry
298
- # data. Raise otherwise.
299
- if not is_static_formatter:
300
- return formatter_validator.format_string
301
- raise exc
302
306
 
303
307
 
304
308
  def validate_formatters(
305
309
  script: Script,
306
310
  unresolved_variables: Set[str],
311
+ unresolved_runtime_variables: Set[str],
307
312
  validator: Validator,
313
+ partial_resolve_formatters: bool,
308
314
  ) -> Dict:
309
315
  """
310
316
  Ensure all OverridesStringFormatterValidator's only contain variables from the overrides
@@ -320,7 +326,9 @@ def validate_formatters(
320
326
  resolved_dict[validator.leaf_name] |= validate_formatters(
321
327
  script=script,
322
328
  unresolved_variables=unresolved_variables,
329
+ unresolved_runtime_variables=unresolved_runtime_variables,
323
330
  validator=validator_value,
331
+ partial_resolve_formatters=partial_resolve_formatters,
324
332
  )
325
333
  elif isinstance(validator, ListValidator):
326
334
  resolved_dict[validator.leaf_name] = []
@@ -328,7 +336,9 @@ def validate_formatters(
328
336
  list_output = validate_formatters(
329
337
  script=script,
330
338
  unresolved_variables=unresolved_variables,
339
+ unresolved_runtime_variables=unresolved_runtime_variables,
331
340
  validator=list_value,
341
+ partial_resolve_formatters=partial_resolve_formatters,
332
342
  )
333
343
  assert len(list_output) == 1
334
344
  resolved_dict[validator.leaf_name].append(list(list_output.values())[0])
@@ -336,7 +346,9 @@ def validate_formatters(
336
346
  resolved_dict[validator.leaf_name] = _validate_formatter(
337
347
  mock_script=script,
338
348
  unresolved_variables=unresolved_variables,
349
+ unresolved_runtime_variables=unresolved_runtime_variables,
339
350
  formatter_validator=validator,
351
+ partial_resolve_entry_formatters=partial_resolve_formatters,
340
352
  )
341
353
  elif isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)):
342
354
  resolved_dict[validator.leaf_name] = {}
@@ -344,7 +356,9 @@ def validate_formatters(
344
356
  resolved_dict[validator.leaf_name] |= _validate_formatter(
345
357
  mock_script=script,
346
358
  unresolved_variables=unresolved_variables,
359
+ unresolved_runtime_variables=unresolved_runtime_variables,
347
360
  formatter_validator=validator_value,
361
+ partial_resolve_entry_formatters=partial_resolve_formatters,
348
362
  )
349
363
  else:
350
364
  resolved_dict[validator.leaf_name] = validator._value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ytdl-sub
3
- Version: 2026.1.27.post1
3
+ Version: 2026.2.2
4
4
  Summary: Automate downloading metadata generation with YoutubeDL
5
5
  Author: Jesse Bannon
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -689,7 +689,7 @@ Classifier: Programming Language :: Python :: 3.11
689
689
  Requires-Python: >=3.10
690
690
  Description-Content-Type: text/markdown
691
691
  License-File: LICENSE
692
- Requires-Dist: yt-dlp[default]==2025.12.8
692
+ Requires-Dist: yt-dlp[default]==2026.1.29
693
693
  Requires-Dist: colorama~=0.4
694
694
  Requires-Dist: mergedeep~=1.3
695
695
  Requires-Dist: mediafile~=0.12
@@ -1,4 +1,4 @@
1
- ytdl_sub/__init__.py,sha256=ebvRc0usukkW5E6dJtD_XVxJ2ji_AGCQe2mpRtDsimE,79
1
+ ytdl_sub/__init__.py,sha256=0fwTHDr6XaKwXXTdbuAQ0QP_VQIpRpjxuILwDho9QF8,73
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
@@ -13,7 +13,7 @@ ytdl_sub/config/config_file.py,sha256=SQtVrMIUq2z3WwJVOed4y84JBMQ8aa4pbiBB0Y-YY4
13
13
  ytdl_sub/config/config_validator.py,sha256=W1dvQD8wI7VOmGOHyaliu8DC6HOjfoGsgUw2MURTZOM,9797
14
14
  ytdl_sub/config/defaults.py,sha256=NTwzlKDkks1LDGNjFMxh91fw5E6T6d_zGsCwODNYJxo,1152
15
15
  ytdl_sub/config/overrides.py,sha256=kwSsvVACKYq5qjnuFFXrWDfT2DJph9n3XC6uzKFGlVA,9786
16
- ytdl_sub/config/preset.py,sha256=Msacs60TyDWWzSlvkI3PurRil5vWOGQFK__Ev2HG1l0,9887
16
+ ytdl_sub/config/preset.py,sha256=iGDVNiPilqA8_802kK7d7KE9VFdW7D8fjZNScAfuKqc,10123
17
17
  ytdl_sub/config/preset_options.py,sha256=Nl1E148Asi2tak6dNqoXgyKdAtf3-Xa2sERrh9wjiJY,14209
18
18
  ytdl_sub/config/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  ytdl_sub/config/plugin/plugin.py,sha256=gsjTcB8cOO097FhJYJD5zPlfEvoPzL6K5VAAqkM3gYc,4740
@@ -22,7 +22,7 @@ ytdl_sub/config/plugin/plugin_operation.py,sha256=evRLt-m7LI7q4oQvA4YqlCU0x3-i5M
22
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=qoFzTyoLJPLr4zYvhVR5UHXSsieFIaTUc9zWN5GtK30,3743
25
+ ytdl_sub/config/validators/variable_validation.py,sha256=rlAM0rZq5DX0v0DYbB-jQJ2KYwNvbjqXQLdcqJ2zKXE,8021
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
@@ -39,7 +39,7 @@ ytdl_sub/entries/entry_parent.py,sha256=qsj7GracXkq1VyJ3cE9hNum4EJ2bDIAI0p0m_5ql
39
39
  ytdl_sub/entries/script/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
40
  ytdl_sub/entries/script/custom_functions.py,sha256=TyfiZilUKB424NXjDCwgOmYBMyqji_QIyk0TTjPC_zU,6963
41
41
  ytdl_sub/entries/script/function_scripts.py,sha256=iPIgTpIzXv5PRnfG8gi42zP6YKUIGPtN9KuMpjHWRTM,719
42
- ytdl_sub/entries/script/variable_definitions.py,sha256=dz4znpZ4op4ALtnYw2vy7ePW6z76A6TI994BFlBaOmo,43561
42
+ ytdl_sub/entries/script/variable_definitions.py,sha256=ecE1m-oup07zTgaLqOGgbzYPLTiT1X_xAf05G48dKjU,43891
43
43
  ytdl_sub/entries/script/variable_types.py,sha256=SMPm52pYbw-c9uFX65pzpIYCecVd0oO0w1htmV6oTTM,9634
44
44
  ytdl_sub/entries/variables/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  ytdl_sub/entries/variables/override_variables.py,sha256=LjWtora5FT9hSka3COP6RySBdpQ39ogiwbJgE-Z_X5Y,5923
@@ -93,7 +93,7 @@ ytdl_sub/prebuilt_presets/tv_show/tv_show_by_date.yaml,sha256=0OgIOzxSPNVqwcZnL6
93
93
  ytdl_sub/prebuilt_presets/tv_show/tv_show_collection.yaml,sha256=MRAxKnh_MvQFJTisRYGsbsUlDmHNLMuV5tlK6iczmqs,49826
94
94
  ytdl_sub/script/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
95
  ytdl_sub/script/parser.py,sha256=LUvmUIxg346njE8-uDx06nOW6ZDX5QS6aUtp4lsgwpk,22115
96
- ytdl_sub/script/script.py,sha256=frzZNxx42dIcpB6QGyAAb2uE2fI-pmywEBiYJQVXZG4,30526
96
+ ytdl_sub/script/script.py,sha256=qDZ7GgUdod2qFNDOs1rlHiiDXeamK9NQAJr_y5NQ4Hg,33179
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
@@ -109,19 +109,19 @@ ytdl_sub/script/functions/regex_functions.py,sha256=d6omjhD9FjkP0BVjUaMsJfXVt-re
109
109
  ytdl_sub/script/functions/string_functions.py,sha256=rZbOuP2V9FvoKzMG94R7vsvj-GxHK_hwgVDMa_Yeftw,6095
110
110
  ytdl_sub/script/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
111
111
  ytdl_sub/script/types/array.py,sha256=2NQhAzAE5aRtlKh7elSFwDgN6-cQLYK1rteLlQE2GYI,2475
112
- ytdl_sub/script/types/function.py,sha256=Yq6CL7eZEvJNW4agERwjB8Bmaw-GZFhVsZPn81ai_48,19433
112
+ ytdl_sub/script/types/function.py,sha256=hDMEN_em2aCqUVzkmSVewMvNUSJciKoIoE5tsExXsVc,19152
113
113
  ytdl_sub/script/types/map.py,sha256=Hw053W-2kxqqW0ece2QsBu_xs7HmsKlNkZwYoBiYlc0,3405
114
114
  ytdl_sub/script/types/resolvable.py,sha256=YeMEhPTRTDSr5AVmK4NRpUbqxh1YQT6a2dGUIPKoFkI,5482
115
- ytdl_sub/script/types/syntax_tree.py,sha256=bkZqKLOj_yKUFBq8uLQ2i4Rcre01aD1_A-yOrHQt_9w,3479
115
+ ytdl_sub/script/types/syntax_tree.py,sha256=UZg5XaY6DBOZWtCFaV4lNd9-e6xCU-WW_LkN8r_NrGw,3610
116
116
  ytdl_sub/script/types/variable.py,sha256=aVJ3ocUr3WpDoolOq6y3NV71b3EQQYPAGrIT0FtIqc4,813
117
- ytdl_sub/script/types/variable_dependency.py,sha256=ZKDDY9AeYhH_kVJgY-Td-cmt3USDkmMEBLpcn0-8pLs,9861
117
+ ytdl_sub/script/types/variable_dependency.py,sha256=sr0AELRyt2rEWNSBBHo5l9ANmRTRQF4NBcIHlyH6AFg,9998
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
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=sZH0U9Kp4jOMFR04sC97DFW7GNo7qWDUn23n0-G8gBk,7814
124
+ ytdl_sub/subscriptions/base_subscription.py,sha256=Qf9hf-QP-0GvEap8Pif5vMOLJa_NsGQ3WvtcPyjaJ3g,8374
125
125
  ytdl_sub/subscriptions/subscription.py,sha256=wfzOYVkzmsSH1KYbAMRi6Nhrb2pRaXdhILle7H0hcas,5127
126
126
  ytdl_sub/subscriptions/subscription_download.py,sha256=2774G3psa38MBhWjrLYtUmV9g_SelVCHctzd7avhxOU,18204
127
127
  ytdl_sub/subscriptions/subscription_validators.py,sha256=tSN8YPLkIYXQbmWQ3M6U7xHpNjPn8rtNR9wSLIE2kzM,13606
@@ -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=d4laCipImcfrZlEYbXaJkvr5iRojsDZIIlBSt4w6L4c,12395
156
+ ytdl_sub/validators/string_formatter_validators.py,sha256=FdFaLPVjUjyzRU9_b31AnTd6BNfD59Z3AXBWjZQ6X6w,13143
157
157
  ytdl_sub/validators/string_select_validator.py,sha256=KFXNKWX2J80WGt08m5gVYphPMHYxhHlgfcoXAQMq6zw,1086
158
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-2026.1.27.post1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
162
- ytdl_sub-2026.1.27.post1.dist-info/METADATA,sha256=Js-5QjiqELzd_bcqhdepjnzrAYzfvnzWq4saFp9N78s,51426
163
- ytdl_sub-2026.1.27.post1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
164
- ytdl_sub-2026.1.27.post1.dist-info/entry_points.txt,sha256=K3T5235NlAI-WLmHCg5tzLZHqc33OLN5IY5fOGc9t10,48
165
- ytdl_sub-2026.1.27.post1.dist-info/top_level.txt,sha256=6z-JWazl6jXspC2DNyxOnGnEqYyGzVbgcBDoXfbkUhI,9
166
- ytdl_sub-2026.1.27.post1.dist-info/RECORD,,
161
+ ytdl_sub-2026.2.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
162
+ ytdl_sub-2026.2.2.dist-info/METADATA,sha256=rhyPTeqmuM3eB0rW6JlE5enc428Fx7Zo1OiPh20Kr1g,51419
163
+ ytdl_sub-2026.2.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
164
+ ytdl_sub-2026.2.2.dist-info/entry_points.txt,sha256=K3T5235NlAI-WLmHCg5tzLZHqc33OLN5IY5fOGc9t10,48
165
+ ytdl_sub-2026.2.2.dist-info/top_level.txt,sha256=6z-JWazl6jXspC2DNyxOnGnEqYyGzVbgcBDoXfbkUhI,9
166
+ ytdl_sub-2026.2.2.dist-info/RECORD,,