ytdl-sub 2025.9.27.post2__py3-none-any.whl → 2025.12.31__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.
Files changed (41) hide show
  1. ytdl_sub/__init__.py +1 -1
  2. ytdl_sub/cli/entrypoint.py +6 -1
  3. ytdl_sub/cli/parsers/cli_to_sub.py +64 -0
  4. ytdl_sub/cli/parsers/main.py +4 -0
  5. ytdl_sub/config/config_validator.py +8 -0
  6. ytdl_sub/config/defaults.py +2 -0
  7. ytdl_sub/config/overrides.py +44 -13
  8. ytdl_sub/config/plugin/preset_plugins.py +33 -0
  9. ytdl_sub/config/preset.py +36 -8
  10. ytdl_sub/config/preset_options.py +33 -2
  11. ytdl_sub/config/validators/variable_validation.py +23 -147
  12. ytdl_sub/downloaders/url/downloader.py +22 -21
  13. ytdl_sub/downloaders/url/validators.py +19 -2
  14. ytdl_sub/entries/script/variable_definitions.py +1 -0
  15. ytdl_sub/entries/variables/override_variables.py +1 -4
  16. ytdl_sub/plugins/subtitles.py +32 -3
  17. ytdl_sub/prebuilt_presets/helpers/download_deletion_options.yaml +1 -2
  18. ytdl_sub/prebuilt_presets/helpers/url.yaml +201 -0
  19. ytdl_sub/prebuilt_presets/tv_show/tv_show_collection.yaml +5206 -644
  20. ytdl_sub/script/parser.py +6 -1
  21. ytdl_sub/script/script.py +117 -46
  22. ytdl_sub/script/types/function.py +11 -6
  23. ytdl_sub/script/types/syntax_tree.py +28 -2
  24. ytdl_sub/script/types/variable_dependency.py +4 -4
  25. ytdl_sub/script/utils/name_validation.py +21 -0
  26. ytdl_sub/script/utils/type_checking.py +2 -1
  27. ytdl_sub/subscriptions/base_subscription.py +29 -3
  28. ytdl_sub/subscriptions/subscription_download.py +3 -0
  29. ytdl_sub/utils/exceptions.py +4 -0
  30. ytdl_sub/utils/ffmpeg.py +5 -1
  31. ytdl_sub/utils/file_handler.py +49 -0
  32. ytdl_sub/utils/script.py +19 -2
  33. ytdl_sub/validators/string_formatter_validators.py +63 -30
  34. ytdl_sub/validators/validators.py +3 -13
  35. ytdl_sub/ytdl_additions/enhanced_download_archive.py +20 -0
  36. {ytdl_sub-2025.9.27.post2.dist-info → ytdl_sub-2025.12.31.dist-info}/METADATA +4 -4
  37. {ytdl_sub-2025.9.27.post2.dist-info → ytdl_sub-2025.12.31.dist-info}/RECORD +41 -40
  38. {ytdl_sub-2025.9.27.post2.dist-info → ytdl_sub-2025.12.31.dist-info}/WHEEL +0 -0
  39. {ytdl_sub-2025.9.27.post2.dist-info → ytdl_sub-2025.12.31.dist-info}/entry_points.txt +0 -0
  40. {ytdl_sub-2025.9.27.post2.dist-info → ytdl_sub-2025.12.31.dist-info}/licenses/LICENSE +0 -0
  41. {ytdl_sub-2025.9.27.post2.dist-info → ytdl_sub-2025.12.31.dist-info}/top_level.txt +0 -0
ytdl_sub/__init__.py CHANGED
@@ -1 +1 @@
1
- __pypi_version__ = "2025.09.27.post2";__local_version__ = "2025.09.27+cc3a36e"
1
+ __pypi_version__ = "2025.12.31";__local_version__ = "2025.12.31+1abe2a4"
@@ -12,6 +12,7 @@ from yt_dlp.utils import sanitize_filename
12
12
  from ytdl_sub.cli.output_summary import output_summary
13
13
  from ytdl_sub.cli.output_transaction_log import _maybe_validate_transaction_log_file
14
14
  from ytdl_sub.cli.output_transaction_log import output_transaction_log
15
+ from ytdl_sub.cli.parsers.cli_to_sub import print_cli_to_sub
15
16
  from ytdl_sub.cli.parsers.dl import DownloadArgsParser
16
17
  from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME
17
18
  from ytdl_sub.cli.parsers.main import parser
@@ -206,6 +207,10 @@ def main() -> List[Subscription]:
206
207
 
207
208
  args, extra_args = parser.parse_known_args()
208
209
 
210
+ if args.subparser == "cli-to-sub":
211
+ print_cli_to_sub(args=extra_args)
212
+ return []
213
+
209
214
  # Load the config
210
215
  if args.config:
211
216
  config = ConfigFile.from_file_path(args.config)
@@ -263,7 +268,7 @@ def main() -> List[Subscription]:
263
268
  _view_url_from_cli(config=config, url=args.url, split_chapters=args.split_chapters)
264
269
  )
265
270
  else:
266
- raise ValidationException("Must provide one of the commands: sub, dl, view")
271
+ raise ValidationException("Must provide one of the commands: sub, dl, view, cli-to-sub")
267
272
 
268
273
  if not args.suppress_transaction_log:
269
274
  output_transaction_log(
@@ -0,0 +1,64 @@
1
+ from typing import List
2
+
3
+ import yt_dlp
4
+ import yt_dlp.options
5
+
6
+ from ytdl_sub.utils.logger import Logger
7
+ from ytdl_sub.utils.yaml import dump_yaml
8
+
9
+ logger = Logger.get()
10
+
11
+ # pylint: disable=missing-function-docstring
12
+
13
+ ##############################################################
14
+ # --- BEGIN ----
15
+ # Copy of https://github.com/yt-dlp/yt-dlp/blob/master/devscripts/cli_to_api.py
16
+
17
+ create_parser = yt_dlp.options.create_parser
18
+
19
+
20
+ def parse_patched_options(opts):
21
+
22
+ patched_parser = create_parser()
23
+ patched_parser.defaults.update(
24
+ {
25
+ "ignoreerrors": False,
26
+ "retries": 0,
27
+ "fragment_retries": 0,
28
+ "extract_flat": False,
29
+ "concat_playlist": "never",
30
+ "update_self": False,
31
+ }
32
+ )
33
+ yt_dlp.options.create_parser = lambda: patched_parser
34
+ try:
35
+ return yt_dlp.parse_options(opts)
36
+ finally:
37
+ yt_dlp.options.create_parser = create_parser
38
+
39
+
40
+ default_opts = parse_patched_options([]).ydl_opts
41
+
42
+
43
+ def cli_to_api(opts, cli_defaults=False):
44
+ opts = (yt_dlp.parse_options if cli_defaults else parse_patched_options)(opts).ydl_opts
45
+
46
+ diff = {k: v for k, v in opts.items() if default_opts[k] != v}
47
+ if "postprocessors" in diff:
48
+ diff["postprocessors"] = [
49
+ pp for pp in diff["postprocessors"] if pp not in default_opts["postprocessors"]
50
+ ]
51
+ return diff
52
+
53
+
54
+ # --- END ----
55
+ ##############################################################
56
+
57
+
58
+ def print_cli_to_sub(args: List[str]) -> None:
59
+ api_args = cli_to_api(args)
60
+ if not api_args:
61
+ logger.info("Does not resolve to any yt-dlp args")
62
+ return
63
+
64
+ print(dump_yaml({"ytdl_options": api_args}))
@@ -221,3 +221,7 @@ view_parser.add_argument(
221
221
  help="View source variables after splitting by chapters",
222
222
  )
223
223
  view_parser.add_argument("url", help="URL to view source variables for")
224
+
225
+ ###################################################################################################
226
+ # CLI-TO-SUB PARSER
227
+ cli_to_sub_parser = subparsers.add_parser("cli-to-sub")
@@ -12,6 +12,8 @@ from ytdl_sub.config.defaults import DEFAULT_FFPROBE_PATH
12
12
  from ytdl_sub.config.defaults import DEFAULT_LOCK_DIRECTORY
13
13
  from ytdl_sub.config.defaults import MAX_FILE_NAME_BYTES
14
14
  from ytdl_sub.prebuilt_presets import PREBUILT_PRESETS
15
+ from ytdl_sub.utils.exceptions import SubscriptionPermissionError
16
+ from ytdl_sub.utils.file_handler import FileHandler
15
17
  from ytdl_sub.validators.file_path_validators import FFmpegFileValidator
16
18
  from ytdl_sub.validators.file_path_validators import FFprobeFileValidator
17
19
  from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
@@ -147,6 +149,12 @@ class ConfigOptions(StrictDictValidator):
147
149
  key="file_name_max_bytes", validator=IntValidator, default=MAX_FILE_NAME_BYTES
148
150
  )
149
151
 
152
+ if not FileHandler.is_path_writable(self.working_directory):
153
+ raise SubscriptionPermissionError(
154
+ "ytdl-sub does not have permissions to the working directory: "
155
+ f"{self.working_directory}"
156
+ )
157
+
150
158
  @property
151
159
  def working_directory(self) -> str:
152
160
  """
@@ -2,6 +2,8 @@ import os
2
2
 
3
3
  from ytdl_sub.utils.system import IS_WINDOWS
4
4
 
5
+ # pylint: disable=invalid-name
6
+
5
7
 
6
8
  def _existing_path(*paths: str) -> str:
7
9
  """
@@ -1,17 +1,19 @@
1
1
  from typing import Any
2
2
  from typing import Dict
3
+ from typing import Iterable
3
4
  from typing import Optional
4
5
  from typing import Set
5
6
 
6
- import mergedeep
7
-
8
7
  from ytdl_sub.entries.entry import Entry
9
8
  from ytdl_sub.entries.script.variable_definitions import VARIABLES
10
9
  from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_NAMES
11
10
  from ytdl_sub.entries.variables.override_variables import OverrideHelpers
12
11
  from ytdl_sub.script.parser import parse
13
12
  from ytdl_sub.script.script import Script
13
+ from ytdl_sub.script.types.function import BuiltInFunction
14
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
15
17
  from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
16
18
  from ytdl_sub.utils.exceptions import InvalidVariableNameException
17
19
  from ytdl_sub.utils.exceptions import StringFormattingException
@@ -88,6 +90,24 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
88
90
 
89
91
  return True
90
92
 
93
+ def ensure_variable_names_not_a_plugin(self, plugin_names: Iterable[str]) -> None:
94
+ """
95
+ Throws an error if an override variable or function has the same name as a
96
+ preset key. This is to avoid confusion when accidentally defining things in
97
+ overrides that are meant to be in the preset.
98
+ """
99
+ for name in self.keys:
100
+ if name.startswith("%"):
101
+ name = name[1:]
102
+
103
+ if name in plugin_names:
104
+ raise self._validation_exception(
105
+ f"Override variable with name {name} cannot be used since it is"
106
+ " the name of a plugin. Perhaps you meant to define it as a plugin? If so,"
107
+ " indent it left to make it at the same level as overrides.",
108
+ exception_class=InvalidVariableNameException,
109
+ )
110
+
91
111
  def ensure_variable_name_valid(self, name: str) -> None:
92
112
  """
93
113
  Ensures the variable name does not collide with any entry variables or built-in functions.
@@ -115,29 +135,35 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
115
135
  )
116
136
 
117
137
  def initial_variables(
118
- self, unresolved_variables: Optional[Dict[str, str]] = None
119
- ) -> Dict[str, str]:
138
+ self, unresolved_variables: Optional[Dict[str, SyntaxTree]] = None
139
+ ) -> Dict[str, SyntaxTree]:
120
140
  """
121
141
  Returns
122
142
  -------
123
143
  Variables and format strings for all Override variables + additional variables (Optional)
124
144
  """
125
- initial_variables: Dict[str, str] = {}
126
- mergedeep.merge(
127
- initial_variables,
128
- self.dict_with_format_strings,
129
- unresolved_variables if unresolved_variables else {},
130
- )
131
- 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)
132
149
 
133
150
  def initialize_script(self, unresolved_variables: Set[str]) -> "Overrides":
134
151
  """
135
152
  Initialize the override script with any unresolved variables
136
153
  """
137
- self.script.add(
154
+ self.script.add_parsed(
138
155
  self.initial_variables(
139
156
  unresolved_variables={
140
- 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
+ )
141
167
  for var_name in unresolved_variables
142
168
  }
143
169
  )
@@ -158,10 +184,15 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
158
184
  script = entry.script
159
185
  unresolvable = entry.unresolvable
160
186
 
187
+ # Update the script internally so long as we are not supplying overrides
188
+ # that could alter the script with one-off state
189
+ update = function_overrides is None
190
+
161
191
  try:
162
192
  return script.resolve_once(
163
193
  dict({"tmp_var": formatter.format_string}, **(function_overrides or {})),
164
194
  unresolvable=unresolvable,
195
+ update=update,
165
196
  )["tmp_var"]
166
197
  except ScriptVariableNotResolved as exc:
167
198
  raise StringFormattingException(
@@ -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,13 +223,10 @@ 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
-
197
- VariableValidation(
198
- downloader_options=self.downloader_options,
199
- output_options=self.output_options,
200
- plugins=self.plugins,
201
- ).initialize_preset_overrides(overrides=self.overrides).ensure_proper_usage()
226
+ self.overrides = self._initialize_overrides_script(
227
+ overrides=self._validate_key(key="overrides", validator=Overrides, default={})
228
+ )
229
+ self.overrides.ensure_variable_names_not_a_plugin(plugin_names=PRESET_KEYS)
202
230
 
203
231
  @property
204
232
  def name(self) -> str:
@@ -8,6 +8,9 @@ from ytdl_sub.config.overrides import Overrides
8
8
  from ytdl_sub.config.plugin.plugin_operation import PluginOperation
9
9
  from ytdl_sub.config.validators.options import OptionsDictValidator
10
10
  from ytdl_sub.entries.script.variable_definitions import VARIABLES as v
11
+ from ytdl_sub.utils.exceptions import SubscriptionPermissionError
12
+ from ytdl_sub.utils.exceptions import ValidationException
13
+ from ytdl_sub.utils.file_handler import FileHandler
11
14
  from ytdl_sub.validators.file_path_validators import OverridesStringFormatterFilePathValidator
12
15
  from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator
13
16
  from ytdl_sub.validators.string_datetime import StringDatetimeValidator
@@ -57,12 +60,24 @@ class YTDLOptions(UnstructuredOverridesDictFormatterValidator):
57
60
  def to_native_dict(self, overrides: Overrides) -> Dict:
58
61
  """
59
62
  Materializes the entire ytdl-options dict from OverrideStringFormatters into
60
- native python
63
+ native python.
61
64
  """
62
- return {
65
+ out = {
63
66
  key: overrides.apply_overrides_formatter_to_native(val)
64
67
  for key, val in self.dict.items()
65
68
  }
69
+ if "cookiefile" in out:
70
+ if not FileHandler.is_file_existent(out["cookiefile"]):
71
+ raise ValidationException(
72
+ f"Specified cookiefile {out['cookiefile']} but it does not exist as a file."
73
+ )
74
+
75
+ if not FileHandler.is_file_readable(out["cookiefile"]):
76
+ raise SubscriptionPermissionError(
77
+ f"Cannot read cookiefile {out['cookiefile']} due to permissions issue."
78
+ )
79
+
80
+ return out
66
81
 
67
82
 
68
83
  # Disable for proper docstring formatting
@@ -107,6 +122,7 @@ class OutputOptions(OptionsDictValidator):
107
122
  "keep_max_files",
108
123
  "download_archive_standardized_date",
109
124
  "keep_files_date_eval",
125
+ "preserve_mtime",
110
126
  }
111
127
 
112
128
  @classmethod
@@ -170,6 +186,10 @@ class OutputOptions(OptionsDictValidator):
170
186
  default=f"{{{v.upload_date_standardized.variable_name}}}",
171
187
  )
172
188
 
189
+ self._preserve_mtime = self._validate_key_if_present(
190
+ key="preserve_mtime", validator=BoolValidator, default=False
191
+ )
192
+
173
193
  if (
174
194
  self._keep_files_before or self._keep_files_after or self._keep_max_files
175
195
  ) and not self.maintain_download_archive:
@@ -309,6 +329,17 @@ class OutputOptions(OptionsDictValidator):
309
329
  """
310
330
  return self._keep_max_files
311
331
 
332
+ @property
333
+ def preserve_mtime(self) -> bool:
334
+ """
335
+ :expected type: Optional[Boolean]
336
+ :description:
337
+ Preserve the video's original upload time as the file modification time.
338
+ When True, sets the file's mtime to match the video's upload_date from
339
+ yt-dlp metadata. Defaults to False.
340
+ """
341
+ return self._preserve_mtime.value
342
+
312
343
  def added_variables(self, unresolved_variables: Set[str]) -> Dict[PluginOperation, Set[str]]:
313
344
  return {
314
345
  # PluginOperation.MODIFY_ENTRY_METADATA: {
@@ -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