ytdl-sub 2026.1.30__tar.gz → 2026.2.2__tar.gz

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 (173) hide show
  1. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/PKG-INFO +1 -1
  2. ytdl_sub-2026.2.2/src/ytdl_sub/__init__.py +1 -0
  3. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/config/preset.py +9 -2
  4. ytdl_sub-2026.2.2/src/ytdl_sub/config/validators/variable_validation.py +196 -0
  5. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/entries/script/variable_definitions.py +10 -0
  6. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/script.py +99 -27
  7. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/types/function.py +0 -7
  8. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/types/syntax_tree.py +4 -0
  9. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/types/variable_dependency.py +4 -1
  10. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/subscriptions/base_subscription.py +15 -4
  11. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/validators/string_formatter_validators.py +33 -19
  12. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub.egg-info/PKG-INFO +1 -1
  13. ytdl_sub-2026.1.30/src/ytdl_sub/__init__.py +0 -1
  14. ytdl_sub-2026.1.30/src/ytdl_sub/config/validators/variable_validation.py +0 -94
  15. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/LICENSE +0 -0
  16. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/README.md +0 -0
  17. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/pyproject.toml +0 -0
  18. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/setup.cfg +0 -0
  19. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/cli/__init__.py +0 -0
  20. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/cli/entrypoint.py +0 -0
  21. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/cli/output_summary.py +0 -0
  22. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/cli/output_transaction_log.py +0 -0
  23. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/cli/parsers/__init__.py +0 -0
  24. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/cli/parsers/cli_to_sub.py +0 -0
  25. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/cli/parsers/dl.py +0 -0
  26. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/cli/parsers/main.py +0 -0
  27. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/config/__init__.py +0 -0
  28. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/config/config_file.py +0 -0
  29. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/config/config_validator.py +0 -0
  30. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/config/defaults.py +0 -0
  31. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/config/overrides.py +0 -0
  32. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/config/plugin/__init__.py +0 -0
  33. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/config/plugin/plugin.py +0 -0
  34. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/config/plugin/plugin_mapping.py +0 -0
  35. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/config/plugin/plugin_operation.py +0 -0
  36. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/config/plugin/preset_plugins.py +0 -0
  37. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/config/preset_options.py +0 -0
  38. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/config/validators/__init__.py +0 -0
  39. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/config/validators/options.py +0 -0
  40. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/downloaders/__init__.py +0 -0
  41. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/downloaders/info_json/__init__.py +0 -0
  42. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/downloaders/info_json/info_json_downloader.py +0 -0
  43. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/downloaders/source_plugin.py +0 -0
  44. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/downloaders/url/__init__.py +0 -0
  45. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/downloaders/url/downloader.py +0 -0
  46. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/downloaders/url/validators.py +0 -0
  47. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/downloaders/ytdl_options_builder.py +0 -0
  48. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/downloaders/ytdlp.py +0 -0
  49. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/entries/__init__.py +0 -0
  50. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/entries/base_entry.py +0 -0
  51. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/entries/entry.py +0 -0
  52. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/entries/entry_parent.py +0 -0
  53. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/entries/script/__init__.py +0 -0
  54. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/entries/script/custom_functions.py +0 -0
  55. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/entries/script/function_scripts.py +0 -0
  56. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/entries/script/variable_types.py +0 -0
  57. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/entries/variables/__init__.py +0 -0
  58. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/entries/variables/override_variables.py +0 -0
  59. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/main.py +0 -0
  60. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/__init__.py +0 -0
  61. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/audio_extract.py +0 -0
  62. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/chapters.py +0 -0
  63. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/date_range.py +0 -0
  64. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/embed_thumbnail.py +0 -0
  65. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/file_convert.py +0 -0
  66. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/filter_exclude.py +0 -0
  67. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/filter_include.py +0 -0
  68. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/format.py +0 -0
  69. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/internal/__init__.py +0 -0
  70. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/internal/view.py +0 -0
  71. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/match_filters.py +0 -0
  72. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/music_tags.py +0 -0
  73. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/nfo_tags.py +0 -0
  74. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/output_directory_nfo_tags.py +0 -0
  75. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/split_by_chapters.py +0 -0
  76. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/square_thumbnail.py +0 -0
  77. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/static_nfo_tags.py +0 -0
  78. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/subtitles.py +0 -0
  79. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/throttle_protection.py +0 -0
  80. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/plugins/video_tags.py +0 -0
  81. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/__init__.py +0 -0
  82. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/helpers/__init__.py +0 -0
  83. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/helpers/download_deletion_options.yaml +0 -0
  84. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/helpers/filter_duration.yaml +0 -0
  85. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/helpers/filter_keywords.yaml +0 -0
  86. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/helpers/media_quality.yaml +0 -0
  87. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/helpers/players.yaml +0 -0
  88. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/helpers/throttle_protection.yaml +0 -0
  89. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/helpers/url.yaml +0 -0
  90. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/helpers/url_bilateral.yaml +0 -0
  91. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/helpers/url_categorized.yaml +0 -0
  92. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/internal/__init__.py +0 -0
  93. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/internal/view.yaml +0 -0
  94. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/music/__init__.py +0 -0
  95. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/music/albums_from_chapters.yaml +0 -0
  96. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/music/albums_from_playlists.yaml +0 -0
  97. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/music/singles.yaml +0 -0
  98. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/music/soundcloud.yaml +0 -0
  99. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/music_videos/__init__.py +0 -0
  100. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/music_videos/music_video_base.yaml +0 -0
  101. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/music_videos/music_video_extras.yaml +0 -0
  102. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/music_videos/music_videos.yaml +0 -0
  103. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/tv_show/__init__.py +0 -0
  104. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/tv_show/episode.yaml +0 -0
  105. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/tv_show/tv_show.yaml +0 -0
  106. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_by_date.yaml +0 -0
  107. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_collection.yaml +0 -0
  108. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/__init__.py +0 -0
  109. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/functions/__init__.py +0 -0
  110. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/functions/array_functions.py +0 -0
  111. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/functions/boolean_functions.py +0 -0
  112. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/functions/conditional_functions.py +0 -0
  113. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/functions/date_functions.py +0 -0
  114. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/functions/error_functions.py +0 -0
  115. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/functions/json_functions.py +0 -0
  116. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/functions/map_functions.py +0 -0
  117. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/functions/numeric_functions.py +0 -0
  118. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/functions/print_functions.py +0 -0
  119. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/functions/regex_functions.py +0 -0
  120. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/functions/string_functions.py +0 -0
  121. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/parser.py +0 -0
  122. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/script_output.py +0 -0
  123. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/types/__init__.py +0 -0
  124. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/types/array.py +0 -0
  125. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/types/map.py +0 -0
  126. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/types/resolvable.py +0 -0
  127. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/types/variable.py +0 -0
  128. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/utils/__init__.py +0 -0
  129. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/utils/exception_formatters.py +0 -0
  130. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/utils/exceptions.py +0 -0
  131. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/utils/name_validation.py +0 -0
  132. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/script/utils/type_checking.py +0 -0
  133. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/subscriptions/__init__.py +0 -0
  134. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/subscriptions/subscription.py +0 -0
  135. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/subscriptions/subscription_download.py +0 -0
  136. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/subscriptions/subscription_validators.py +0 -0
  137. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/subscriptions/subscription_ytdl_options.py +0 -0
  138. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/thread/__init__.py +0 -0
  139. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/thread/log_entries_downloaded_listener.py +0 -0
  140. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/__init__.py +0 -0
  141. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/chapters.py +0 -0
  142. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/datetime.py +0 -0
  143. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/exceptions.py +0 -0
  144. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/ffmpeg.py +0 -0
  145. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/file_handler.py +0 -0
  146. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/file_lock.py +0 -0
  147. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/file_path.py +0 -0
  148. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/logger.py +0 -0
  149. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/retry.py +0 -0
  150. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/script.py +0 -0
  151. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/scriptable.py +0 -0
  152. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/subtitles.py +0 -0
  153. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/system.py +0 -0
  154. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/thumbnail.py +0 -0
  155. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/xml.py +0 -0
  156. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/utils/yaml.py +0 -0
  157. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/validators/__init__.py +0 -0
  158. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/validators/audo_codec_validator.py +0 -0
  159. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/validators/file_path_validators.py +0 -0
  160. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/validators/nfo_validators.py +0 -0
  161. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/validators/regex_validator.py +0 -0
  162. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/validators/source_variable_validator.py +0 -0
  163. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/validators/strict_dict_validator.py +0 -0
  164. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/validators/string_datetime.py +0 -0
  165. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/validators/string_select_validator.py +0 -0
  166. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/validators/validators.py +0 -0
  167. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/ytdl_additions/__init__.py +0 -0
  168. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py +0 -0
  169. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub.egg-info/SOURCES.txt +0 -0
  170. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub.egg-info/dependency_links.txt +0 -0
  171. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub.egg-info/entry_points.txt +0 -0
  172. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub.egg-info/requires.txt +0 -0
  173. {ytdl_sub-2026.1.30 → ytdl_sub-2026.2.2}/src/ytdl_sub.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ytdl-sub
3
- Version: 2026.1.30
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
@@ -0,0 +1 @@
1
+ __pypi_version__ = "2026.02.02";__local_version__ = "2026.02.02+c163f97"
@@ -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}})
@@ -0,0 +1,196 @@
1
+ from typing import Dict
2
+ from typing import List
3
+ from typing import Set
4
+
5
+ from ytdl_sub.config.overrides import Overrides
6
+ from ytdl_sub.config.plugin.plugin_mapping import PluginMapping
7
+ from ytdl_sub.config.plugin.plugin_operation import PluginOperation
8
+ from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
9
+ from ytdl_sub.config.preset_options import OutputOptions
10
+ from ytdl_sub.config.validators.options import OptionsValidator
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
17
+ from ytdl_sub.validators.string_formatter_validators import validate_formatters
18
+
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
+
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
+
83
+ def __init__(
84
+ self,
85
+ overrides: Overrides,
86
+ downloader_options: MultiUrlValidator,
87
+ output_options: OutputOptions,
88
+ plugins: PresetPlugins,
89
+ resolution_level: int = ResolutionLevel.RESOLVE,
90
+ ):
91
+ self.overrides = overrides
92
+ self.downloader_options = downloader_options
93
+ self.output_options = output_options
94
+ self.plugins = plugins
95
+
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(
104
+ additional_options=[self.output_options, self.downloader_options]
105
+ )
106
+ self._resolution_level = resolution_level
107
+
108
+ self._apply_resolution_level()
109
+
110
+ def _add_runtime_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> None:
111
+ """
112
+ Add dummy variables for script validation
113
+ """
114
+ added_variables = options.added_variables(
115
+ unresolved_variables=self.unresolved_runtime_variables,
116
+ ).get(plugin_op, set())
117
+ modified_variables = options.modified_variables().get(plugin_op, set())
118
+
119
+ self.unresolved_runtime_variables -= added_variables | modified_variables
120
+
121
+ def ensure_proper_usage(self, partial_resolve_formatters: bool = False) -> Dict:
122
+ """
123
+ Validate variables resolve as plugins are executed, and return
124
+ a mock script which contains actualized added variables from the plugins
125
+ """
126
+ resolved_subscription: Dict = {}
127
+
128
+ self._add_runtime_variables(PluginOperation.DOWNLOADER, options=self.downloader_options)
129
+
130
+ # Always add output options first
131
+ self._add_runtime_variables(
132
+ PluginOperation.MODIFY_ENTRY_METADATA, options=self.output_options
133
+ )
134
+
135
+ # Metadata variables to be added
136
+ for plugin_options in PluginMapping.order_options_by(
137
+ self.plugins.zipped(), PluginOperation.MODIFY_ENTRY_METADATA
138
+ ):
139
+ self._add_runtime_variables(
140
+ PluginOperation.MODIFY_ENTRY_METADATA, options=plugin_options
141
+ )
142
+
143
+ for plugin_options in PluginMapping.order_options_by(
144
+ self.plugins.zipped(), PluginOperation.MODIFY_ENTRY
145
+ ):
146
+ self._add_runtime_variables(PluginOperation.MODIFY_ENTRY, options=plugin_options)
147
+
148
+ # Validate that any formatter in the plugin options can resolve
149
+ resolved_subscription |= validate_formatters(
150
+ script=self.script,
151
+ unresolved_variables=self.unresolved_variables,
152
+ unresolved_runtime_variables=self.unresolved_runtime_variables,
153
+ validator=plugin_options,
154
+ partial_resolve_formatters=partial_resolve_formatters,
155
+ )
156
+
157
+ resolved_subscription |= validate_formatters(
158
+ script=self.script,
159
+ unresolved_variables=self.unresolved_variables,
160
+ unresolved_runtime_variables=self.unresolved_runtime_variables,
161
+ validator=self.output_options,
162
+ partial_resolve_formatters=partial_resolve_formatters,
163
+ )
164
+
165
+ # TODO: make this a function
166
+ raw_download_output = validate_formatters(
167
+ script=self.script,
168
+ unresolved_variables=self.unresolved_variables,
169
+ unresolved_runtime_variables=self.unresolved_runtime_variables,
170
+ validator=self.downloader_options.urls,
171
+ partial_resolve_formatters=partial_resolve_formatters,
172
+ )
173
+ resolved_subscription["download"] = []
174
+ for url_output in raw_download_output["download"]:
175
+ if isinstance(url_output["url"], list):
176
+ url_output["url"] = [url for url in url_output["url"] if bool(url)]
177
+
178
+ if url_output["url"]:
179
+ resolved_subscription["download"].append(url_output)
180
+
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
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
  """
@@ -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.30
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
@@ -1 +0,0 @@
1
- __pypi_version__ = "2026.01.30";__local_version__ = "2026.01.30+6b7805c"