ytdl-sub 2025.12.31__py3-none-any.whl → 2026.1.27__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 (38) hide show
  1. ytdl_sub/__init__.py +1 -1
  2. ytdl_sub/config/overrides.py +17 -33
  3. ytdl_sub/config/plugin/plugin.py +1 -1
  4. ytdl_sub/config/preset_options.py +1 -1
  5. ytdl_sub/config/validators/variable_validation.py +3 -0
  6. ytdl_sub/downloaders/url/downloader.py +21 -15
  7. ytdl_sub/downloaders/url/validators.py +15 -1
  8. ytdl_sub/entries/script/variable_types.py +1 -9
  9. ytdl_sub/plugins/date_range.py +1 -1
  10. ytdl_sub/plugins/embed_thumbnail.py +1 -1
  11. ytdl_sub/plugins/filter_exclude.py +8 -3
  12. ytdl_sub/plugins/filter_include.py +7 -5
  13. ytdl_sub/plugins/nfo_tags.py +1 -1
  14. ytdl_sub/plugins/square_thumbnail.py +1 -1
  15. ytdl_sub/plugins/throttle_protection.py +3 -3
  16. ytdl_sub/plugins/video_tags.py +1 -1
  17. ytdl_sub/prebuilt_presets/helpers/url.yaml +120 -992
  18. ytdl_sub/prebuilt_presets/helpers/url_categorized.yaml +1 -100
  19. ytdl_sub/prebuilt_presets/music/soundcloud.yaml +1 -1
  20. ytdl_sub/prebuilt_presets/tv_show/tv_show_collection.yaml +271 -4637
  21. ytdl_sub/script/functions/numeric_functions.py +17 -0
  22. ytdl_sub/script/script.py +114 -17
  23. ytdl_sub/script/types/array.py +22 -1
  24. ytdl_sub/script/types/function.py +178 -1
  25. ytdl_sub/script/types/map.py +30 -1
  26. ytdl_sub/script/types/resolvable.py +8 -0
  27. ytdl_sub/script/types/syntax_tree.py +30 -1
  28. ytdl_sub/script/types/variable_dependency.py +95 -5
  29. ytdl_sub/subscriptions/subscription_download.py +2 -2
  30. ytdl_sub/subscriptions/subscription_ytdl_options.py +2 -2
  31. ytdl_sub/utils/script.py +54 -3
  32. ytdl_sub/validators/string_formatter_validators.py +47 -26
  33. {ytdl_sub-2025.12.31.dist-info → ytdl_sub-2026.1.27.dist-info}/METADATA +2 -2
  34. {ytdl_sub-2025.12.31.dist-info → ytdl_sub-2026.1.27.dist-info}/RECORD +38 -38
  35. {ytdl_sub-2025.12.31.dist-info → ytdl_sub-2026.1.27.dist-info}/WHEEL +1 -1
  36. {ytdl_sub-2025.12.31.dist-info → ytdl_sub-2026.1.27.dist-info}/entry_points.txt +0 -0
  37. {ytdl_sub-2025.12.31.dist-info → ytdl_sub-2026.1.27.dist-info}/licenses/LICENSE +0 -0
  38. {ytdl_sub-2025.12.31.dist-info → ytdl_sub-2026.1.27.dist-info}/top_level.txt +0 -0
ytdl_sub/__init__.py CHANGED
@@ -1 +1 @@
1
- __pypi_version__ = "2025.12.31";__local_version__ = "2025.12.31+1abe2a4"
1
+ __pypi_version__ = "2026.01.27";__local_version__ = "2026.01.27+d37945d"
@@ -3,6 +3,8 @@ from typing import Dict
3
3
  from typing import Iterable
4
4
  from typing import Optional
5
5
  from typing import Set
6
+ from typing import Type
7
+ from typing import TypeVar
6
8
 
7
9
  from ytdl_sub.entries.entry import Entry
8
10
  from ytdl_sub.entries.script.variable_definitions import VARIABLES
@@ -20,10 +22,11 @@ from ytdl_sub.utils.exceptions import StringFormattingException
20
22
  from ytdl_sub.utils.exceptions import ValidationException
21
23
  from ytdl_sub.utils.script import ScriptUtils
22
24
  from ytdl_sub.utils.scriptable import Scriptable
23
- from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
24
25
  from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
25
26
  from ytdl_sub.validators.string_formatter_validators import UnstructuredDictFormatterValidator
26
27
 
28
+ ExpectedT = TypeVar("ExpectedT")
29
+
27
30
 
28
31
  class Overrides(UnstructuredDictFormatterValidator, Scriptable):
29
32
  """
@@ -207,7 +210,8 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
207
210
  formatter: StringFormatterValidator,
208
211
  entry: Optional[Entry] = None,
209
212
  function_overrides: Optional[Dict[str, str]] = None,
210
- ) -> str:
213
+ expected_type: Type[ExpectedT] = str,
214
+ ) -> ExpectedT:
211
215
  """
212
216
  Parameters
213
217
  ----------
@@ -217,6 +221,8 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
217
221
  Optional. Entry to add source variables to the formatter
218
222
  function_overrides
219
223
  Optional. Explicit values to override the overrides themselves and source variables
224
+ expected_type
225
+ The expected type that should return. Defaults to string.
220
226
 
221
227
  Returns
222
228
  -------
@@ -227,37 +233,15 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
227
233
  StringFormattingException
228
234
  If the formatter that is trying to be resolved cannot
229
235
  """
230
- return formatter.post_process(
231
- str(
232
- self._apply_to_resolvable(
233
- formatter=formatter, entry=entry, function_overrides=function_overrides
234
- )
235
- )
236
+ out = formatter.post_process(
237
+ self._apply_to_resolvable(
238
+ formatter=formatter, entry=entry, function_overrides=function_overrides
239
+ ).native
236
240
  )
237
241
 
238
- def apply_overrides_formatter_to_native(
239
- self,
240
- formatter: OverridesStringFormatterValidator,
241
- ) -> Any:
242
- """
243
- Parameters
244
- ----------
245
- formatter
246
- Overrides formatter to apply
247
-
248
- Returns
249
- -------
250
- The native python form of the resolved variable
251
- """
252
- return self._apply_to_resolvable(
253
- formatter=formatter, entry=None, function_overrides=None
254
- ).native
242
+ if not isinstance(out, expected_type):
243
+ raise StringFormattingException(
244
+ f"Expected type {expected_type.__name__}, but received '{out.__class__.__name__}'"
245
+ )
255
246
 
256
- def evaluate_boolean(
257
- self, formatter: StringFormatterValidator, entry: Optional[Entry] = None
258
- ) -> bool:
259
- """
260
- Apply a formatter, and evaluate it to a boolean
261
- """
262
- output = self.apply_formatter(formatter=formatter, entry=entry)
263
- return ScriptUtils.bool_formatter_output(output)
247
+ return out
@@ -48,7 +48,7 @@ class Plugin(BasePlugin[OptionsValidatorT], Generic[OptionsValidatorT], ABC):
48
48
  Returns True if enabled, False if disabled.
49
49
  """
50
50
  if isinstance(self.plugin_options, ToggleableOptionsDictValidator):
51
- return self.overrides.evaluate_boolean(self.plugin_options.enable)
51
+ return self.overrides.apply_formatter(self.plugin_options.enable, expected_type=bool)
52
52
  return True
53
53
 
54
54
  def ytdl_options_match_filters(self) -> Tuple[List[str], List[str]]:
@@ -63,7 +63,7 @@ class YTDLOptions(UnstructuredOverridesDictFormatterValidator):
63
63
  native python.
64
64
  """
65
65
  out = {
66
- key: overrides.apply_overrides_formatter_to_native(val)
66
+ key: overrides.apply_formatter(val, expected_type=object)
67
67
  for key, val in self.dict.items()
68
68
  }
69
69
  if "cookiefile" in out:
@@ -84,6 +84,9 @@ class VariableValidation:
84
84
  )
85
85
  resolved_subscription["download"] = []
86
86
  for url_output in raw_download_output["download"]:
87
+ if isinstance(url_output["url"], list):
88
+ url_output["url"] = [url for url in url_output["url"] if bool(url)]
89
+
87
90
  if url_output["url"]:
88
91
  resolved_subscription["download"].append(url_output)
89
92
 
@@ -52,12 +52,12 @@ class UrlDownloaderBasePluginExtension(SourcePluginExtension[MultiUrlValidator])
52
52
 
53
53
  if 0 <= input_url_idx < len(self.plugin_options.urls.list):
54
54
  validator = self.plugin_options.urls.list[input_url_idx]
55
- if self.overrides.apply_formatter(validator.url) == entry_input_url:
55
+ if entry_input_url in self.overrides.apply_formatter(validator.url, expected_type=list):
56
56
  return validator
57
57
 
58
58
  # Match the first validator based on the URL, if one exists
59
59
  for validator in self.plugin_options.urls.list:
60
- if self.overrides.apply_formatter(validator.url) == entry_input_url:
60
+ if entry_input_url in self.overrides.apply_formatter(validator.url, expected_type=list):
61
61
  return validator
62
62
 
63
63
  # Return the first validator if none exist
@@ -382,7 +382,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
382
382
  entries_to_iter: List[Optional[Entry]] = entries
383
383
 
384
384
  indices = list(range(len(entries_to_iter)))
385
- if self.overrides.evaluate_boolean(validator.download_reverse):
385
+ if self.overrides.apply_formatter(validator.download_reverse, expected_type=bool):
386
386
  indices = reversed(indices)
387
387
 
388
388
  for idx in indices:
@@ -461,8 +461,8 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
461
461
  ytdl_option_overrides=validator.ytdl_options.to_native_dict(self.overrides)
462
462
  )
463
463
 
464
- include_sibling_metadata = self.overrides.evaluate_boolean(
465
- validator.include_sibling_metadata
464
+ include_sibling_metadata = self.overrides.apply_formatter(
465
+ validator.include_sibling_metadata, expected_type=bool
466
466
  )
467
467
 
468
468
  parents, orphan_entries = self._download_url_metadata(
@@ -487,19 +487,25 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
487
487
  # download the bottom-most urls first since they are top-priority
488
488
  for idx, url_validator in reversed(list(enumerate(self.collection.urls.list))):
489
489
  # URLs can be empty. If they are, then skip
490
- if not (url := self.overrides.apply_formatter(url_validator.url)):
490
+ if not (urls := self.overrides.apply_formatter(url_validator.url, expected_type=list)):
491
491
  continue
492
492
 
493
- for entry in self._download_metadata(url=url, validator=url_validator):
494
- entry.initialize_script(self.overrides).add(
495
- {
496
- v.ytdl_sub_input_url: url,
497
- v.ytdl_sub_input_url_index: idx,
498
- v.ytdl_sub_input_url_count: len(self.collection.urls.list),
499
- }
500
- )
493
+ for url in reversed(urls):
494
+ assert isinstance(url, str)
495
+
496
+ if not url:
497
+ continue
498
+
499
+ for entry in self._download_metadata(url=url, validator=url_validator):
500
+ entry.initialize_script(self.overrides).add(
501
+ {
502
+ v.ytdl_sub_input_url: url,
503
+ v.ytdl_sub_input_url_index: idx,
504
+ v.ytdl_sub_input_url_count: len(self.collection.urls.list),
505
+ }
506
+ )
501
507
 
502
- yield entry
508
+ yield entry
503
509
 
504
510
  def download(self, entry: Entry) -> Optional[Entry]:
505
511
  """
@@ -1,5 +1,6 @@
1
1
  from typing import Any
2
2
  from typing import Dict
3
+ from typing import List
3
4
  from typing import Optional
4
5
  from typing import Set
5
6
 
@@ -43,6 +44,19 @@ class UrlThumbnailListValidator(ListValidator[UrlThumbnailValidator]):
43
44
  _inner_list_type = UrlThumbnailValidator
44
45
 
45
46
 
47
+ class OverridesOneOrManyUrlValidator(OverridesStringFormatterValidator):
48
+ def post_process(self, resolved: Any) -> List[str]:
49
+ if isinstance(resolved, str):
50
+ return [resolved]
51
+ if isinstance(resolved, list):
52
+ for value in resolved:
53
+ if not isinstance(value, str):
54
+ raise self._validation_exception("Must be a string or an array of strings.")
55
+ return resolved
56
+
57
+ raise self._validation_exception("Must be a string or an array of strings.")
58
+
59
+
46
60
  class UrlValidator(StrictDictValidator):
47
61
  _required_keys = {"url"}
48
62
  _optional_keys = {
@@ -68,7 +82,7 @@ class UrlValidator(StrictDictValidator):
68
82
  super().__init__(name, value)
69
83
 
70
84
  # TODO: url validate using yt-dlp IE
71
- self._url = self._validate_key(key="url", validator=OverridesStringFormatterValidator)
85
+ self._url = self._validate_key(key="url", validator=OverridesOneOrManyUrlValidator)
72
86
  self._variables = self._validate_key_if_present(
73
87
  key="variables", validator=DictFormatterValidator, default={}
74
88
  )
@@ -22,7 +22,6 @@ VariableT = TypeVar("VariableT", bound="Variable")
22
22
 
23
23
 
24
24
  def _get(
25
- cast: str,
26
25
  metadata_variable_name: str,
27
26
  metadata_key: str,
28
27
  variable_name: Optional[str],
@@ -47,7 +46,7 @@ def _get(
47
46
  return as_type(
48
47
  variable_name=variable_name or metadata_key,
49
48
  metadata_key=metadata_key,
50
- definition=f"{{ %legacy_bracket_safety(%{cast}({out})) }}",
49
+ definition=f"{{ {out} }}",
51
50
  )
52
51
 
53
52
 
@@ -182,7 +181,6 @@ class MapMetadataVariable(MetadataVariable, MapVariable):
182
181
  Creates a map variable from entry metadata
183
182
  """
184
183
  return _get(
185
- "map",
186
184
  metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME,
187
185
  metadata_key=metadata_key,
188
186
  variable_name=variable_name,
@@ -204,7 +202,6 @@ class ArrayMetadataVariable(MetadataVariable, ArrayVariable):
204
202
  Creates an array variable from entry metadata
205
203
  """
206
204
  return _get(
207
- "array",
208
205
  metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME,
209
206
  metadata_key=metadata_key,
210
207
  variable_name=variable_name,
@@ -226,7 +223,6 @@ class StringMetadataVariable(MetadataVariable, StringVariable):
226
223
  Creates a string variable from entry metadata
227
224
  """
228
225
  return _get(
229
- "string",
230
226
  metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME,
231
227
  metadata_key=metadata_key,
232
228
  variable_name=variable_name,
@@ -245,7 +241,6 @@ class StringMetadataVariable(MetadataVariable, StringVariable):
245
241
  Creates a string variable from playlist metadata
246
242
  """
247
243
  return _get(
248
- "string",
249
244
  metadata_variable_name=PLAYLIST_METADATA_VARIABLE_NAME,
250
245
  metadata_key=metadata_key,
251
246
  variable_name=variable_name,
@@ -264,7 +259,6 @@ class StringMetadataVariable(MetadataVariable, StringVariable):
264
259
  Creates a string variable from source metadata
265
260
  """
266
261
  return _get(
267
- "string",
268
262
  metadata_variable_name=SOURCE_METADATA_VARIABLE_NAME,
269
263
  metadata_key=metadata_key,
270
264
  variable_name=variable_name,
@@ -301,7 +295,6 @@ class IntegerMetadataVariable(MetadataVariable, IntegerVariable):
301
295
  Creates an int variable from entry metadata
302
296
  """
303
297
  return _get(
304
- "int",
305
298
  metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME,
306
299
  metadata_key=metadata_key,
307
300
  variable_name=variable_name,
@@ -320,7 +313,6 @@ class IntegerMetadataVariable(MetadataVariable, IntegerVariable):
320
313
  Creates an int variable from playlist metadata
321
314
  """
322
315
  return _get(
323
- "int",
324
316
  metadata_variable_name=PLAYLIST_METADATA_VARIABLE_NAME,
325
317
  metadata_key=metadata_key,
326
318
  variable_name=variable_name,
@@ -116,7 +116,7 @@ class DateRangePlugin(Plugin[DateRangeOptions]):
116
116
  date_validator=self.plugin_options.after, overrides=self.overrides
117
117
  )
118
118
  after_filter = f"{date_type} >= {after_str}"
119
- if self.overrides.evaluate_boolean(self.plugin_options.breaks):
119
+ if self.overrides.apply_formatter(self.plugin_options.breaks, expected_type=bool):
120
120
  breaking_match_filters.append(after_filter)
121
121
  else:
122
122
  match_filters.append(after_filter)
@@ -33,7 +33,7 @@ class EmbedThumbnailPlugin(Plugin[EmbedThumbnailOptions]):
33
33
 
34
34
  @property
35
35
  def _embed_thumbnail(self) -> bool:
36
- return self.overrides.evaluate_boolean(self.plugin_options)
36
+ return self.overrides.apply_formatter(self.plugin_options, expected_type=bool)
37
37
 
38
38
  @classmethod
39
39
  def _embed_video_thumbnail(cls, entry: Entry) -> None:
@@ -7,13 +7,14 @@ from ytdl_sub.config.validators.options import OptionsValidator
7
7
  from ytdl_sub.entries.entry import Entry
8
8
  from ytdl_sub.utils.exceptions import StringFormattingException
9
9
  from ytdl_sub.utils.logger import Logger
10
- from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
10
+ from ytdl_sub.validators.string_formatter_validators import BooleanFormatterValidator
11
+ from ytdl_sub.validators.validators import ListValidator
11
12
  from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
12
13
 
13
14
  logger = Logger.get("filter-exclude")
14
15
 
15
16
 
16
- class FilterExcludeOptions(ListFormatterValidator, OptionsValidator):
17
+ class FilterExcludeOptions(ListValidator[BooleanFormatterValidator], OptionsValidator):
17
18
  """
18
19
  Applies a conditional OR on any number of filters comprised of either variables or scripts.
19
20
  If any filter evaluates to True, the entry will be excluded.
@@ -29,6 +30,8 @@ class FilterExcludeOptions(ListFormatterValidator, OptionsValidator):
29
30
  { %contains( %lower(description), '#short' ) }
30
31
  """
31
32
 
33
+ _inner_list_type = BooleanFormatterValidator
34
+
32
35
 
33
36
  class FilterExcludePlugin(Plugin[FilterExcludeOptions]):
34
37
  plugin_options_type = FilterExcludeOptions
@@ -52,7 +55,9 @@ class FilterExcludePlugin(Plugin[FilterExcludeOptions]):
52
55
  return entry
53
56
 
54
57
  for formatter in self.plugin_options.list:
55
- should_exclude = self.overrides.evaluate_boolean(formatter=formatter, entry=entry)
58
+ should_exclude = self.overrides.apply_formatter(
59
+ formatter=formatter, entry=entry, expected_type=bool
60
+ )
56
61
 
57
62
  if should_exclude:
58
63
  logger.info(
@@ -7,14 +7,14 @@ from ytdl_sub.config.validators.options import OptionsValidator
7
7
  from ytdl_sub.entries.entry import Entry
8
8
  from ytdl_sub.utils.exceptions import StringFormattingException
9
9
  from ytdl_sub.utils.logger import Logger
10
- from ytdl_sub.utils.script import ScriptUtils
11
- from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
10
+ from ytdl_sub.validators.string_formatter_validators import BooleanFormatterValidator
11
+ from ytdl_sub.validators.validators import ListValidator
12
12
  from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
13
13
 
14
14
  logger = Logger.get("filter-include")
15
15
 
16
16
 
17
- class FilterIncludeOptions(ListFormatterValidator, OptionsValidator):
17
+ class FilterIncludeOptions(ListValidator[BooleanFormatterValidator], OptionsValidator):
18
18
  """
19
19
  Applies a conditional AND on any number of filters comprised of either variables or scripts.
20
20
  If all filters evaluate to True, the entry will be included.
@@ -38,6 +38,8 @@ class FilterIncludeOptions(ListFormatterValidator, OptionsValidator):
38
38
  }
39
39
  """
40
40
 
41
+ _inner_list_type = BooleanFormatterValidator
42
+
41
43
 
42
44
  class FilterIncludePlugin(Plugin[FilterIncludeOptions]):
43
45
  plugin_options_type = FilterIncludeOptions
@@ -61,8 +63,8 @@ class FilterIncludePlugin(Plugin[FilterIncludeOptions]):
61
63
  return entry
62
64
 
63
65
  for formatter in self.plugin_options.list:
64
- should_exclude = ScriptUtils.bool_formatter_output(
65
- self.overrides.apply_formatter(formatter=formatter, entry=entry)
66
+ should_exclude = self.overrides.apply_formatter(
67
+ formatter=formatter, entry=entry, expected_type=bool
66
68
  )
67
69
  if not should_exclude:
68
70
  logger.info(
@@ -140,7 +140,7 @@ class SharedNfoTagsPlugin(Plugin[SharedNfoTagsOptions], ABC):
140
140
  if not nfo_tags:
141
141
  return
142
142
 
143
- if self.overrides.evaluate_boolean(self.plugin_options.kodi_safe):
143
+ if self.overrides.apply_formatter(self.plugin_options.kodi_safe, expected_type=bool):
144
144
  nfo_root = to_max_3_byte_utf8_string(nfo_root)
145
145
  nfo_tags = {
146
146
  to_max_3_byte_utf8_string(key): [
@@ -31,7 +31,7 @@ class SquareThumbnailPlugin(Plugin[SquareThumbnailOptions]):
31
31
 
32
32
  @property
33
33
  def _square_thumbnail(self) -> bool:
34
- return self.overrides.evaluate_boolean(self.plugin_options)
34
+ return self.overrides.apply_formatter(self.plugin_options, expected_type=bool)
35
35
 
36
36
  @classmethod
37
37
  def _convert_to_square_thumbnail(cls, entry: Entry) -> None:
@@ -42,8 +42,8 @@ class _RandomizedRangeValidator(StrictDictValidator, ABC):
42
42
  )
43
43
 
44
44
  def _randomized_float(self, overrides: Overrides, entry: Optional[Entry] = None) -> float:
45
- actualized_min = float(overrides.apply_formatter(self._min, entry=entry))
46
- actualized_max = float(overrides.apply_formatter(self._max, entry=entry))
45
+ actualized_min = overrides.apply_formatter(self._min, entry=entry, expected_type=float)
46
+ actualized_max = overrides.apply_formatter(self._max, entry=entry, expected_type=float)
47
47
 
48
48
  if actualized_min < 0:
49
49
  raise self._validation_exception(
@@ -70,7 +70,7 @@ class _RandomizedRangeValidator(StrictDictValidator, ABC):
70
70
  -------
71
71
  Max possible value
72
72
  """
73
- actualized_max = float(overrides.apply_formatter(self._max, entry=entry))
73
+ actualized_max = overrides.apply_formatter(self._max, entry=entry, expected_type=float)
74
74
  if actualized_max < 0:
75
75
  raise self._validation_exception(
76
76
  f"max must be greater than zero, received {actualized_max}"
@@ -34,7 +34,7 @@ class VideoTagsPlugin(Plugin[VideoTagsOptions]):
34
34
  Tags the entry's audio file using values defined in the metadata options
35
35
  """
36
36
  tags_to_write: Dict[str, str] = {}
37
- for tag_name, tag_formatter in self.plugin_options.dict.items():
37
+ for tag_name, tag_formatter in sorted(self.plugin_options.dict.items()):
38
38
  tag_value = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry)
39
39
  tags_to_write[tag_name] = tag_value
40
40