ytdl-sub 2025.12.31.post1__tar.gz → 2026.2.3__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 (176) hide show
  1. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/PKG-INFO +3 -3
  2. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/pyproject.toml +2 -2
  3. ytdl_sub-2026.2.3/src/ytdl_sub/__init__.py +1 -0
  4. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/cli/entrypoint.py +9 -0
  5. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/cli/parsers/main.py +11 -0
  6. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/config/overrides.py +17 -33
  7. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/config/plugin/plugin.py +1 -1
  8. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/config/preset.py +9 -2
  9. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/config/preset_options.py +1 -1
  10. ytdl_sub-2026.2.3/src/ytdl_sub/config/validators/variable_validation.py +196 -0
  11. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/downloaders/url/downloader.py +21 -15
  12. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/downloaders/url/validators.py +15 -1
  13. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/entries/script/variable_definitions.py +10 -0
  14. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/entries/script/variable_types.py +1 -9
  15. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/date_range.py +1 -1
  16. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/embed_thumbnail.py +1 -1
  17. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/filter_exclude.py +8 -3
  18. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/filter_include.py +7 -5
  19. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/nfo_tags.py +1 -1
  20. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/square_thumbnail.py +1 -1
  21. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/throttle_protection.py +3 -3
  22. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/video_tags.py +1 -1
  23. ytdl_sub-2026.2.3/src/ytdl_sub/prebuilt_presets/helpers/url.yaml +177 -0
  24. ytdl_sub-2026.2.3/src/ytdl_sub/prebuilt_presets/helpers/url_categorized.yaml +111 -0
  25. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/music/soundcloud.yaml +1 -1
  26. ytdl_sub-2026.2.3/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_collection.yaml +1327 -0
  27. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/functions/numeric_functions.py +17 -0
  28. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/script.py +186 -17
  29. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/types/array.py +22 -1
  30. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/types/function.py +171 -1
  31. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/types/map.py +30 -1
  32. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/types/resolvable.py +8 -0
  33. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/types/syntax_tree.py +34 -1
  34. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/types/variable_dependency.py +97 -15
  35. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/subscriptions/base_subscription.py +15 -4
  36. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/subscriptions/subscription_download.py +2 -2
  37. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/subscriptions/subscription_ytdl_options.py +2 -2
  38. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/script.py +56 -6
  39. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/validators/string_formatter_validators.py +80 -45
  40. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub.egg-info/PKG-INFO +3 -3
  41. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub.egg-info/requires.txt +2 -2
  42. ytdl_sub-2025.12.31.post1/src/ytdl_sub/__init__.py +0 -1
  43. ytdl_sub-2025.12.31.post1/src/ytdl_sub/config/validators/variable_validation.py +0 -91
  44. ytdl_sub-2025.12.31.post1/src/ytdl_sub/prebuilt_presets/helpers/url.yaml +0 -1049
  45. ytdl_sub-2025.12.31.post1/src/ytdl_sub/prebuilt_presets/helpers/url_categorized.yaml +0 -210
  46. ytdl_sub-2025.12.31.post1/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_collection.yaml +0 -5693
  47. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/LICENSE +0 -0
  48. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/README.md +0 -0
  49. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/setup.cfg +0 -0
  50. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/cli/__init__.py +0 -0
  51. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/cli/output_summary.py +0 -0
  52. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/cli/output_transaction_log.py +0 -0
  53. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/cli/parsers/__init__.py +0 -0
  54. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/cli/parsers/cli_to_sub.py +0 -0
  55. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/cli/parsers/dl.py +0 -0
  56. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/config/__init__.py +0 -0
  57. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/config/config_file.py +0 -0
  58. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/config/config_validator.py +0 -0
  59. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/config/defaults.py +0 -0
  60. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/config/plugin/__init__.py +0 -0
  61. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/config/plugin/plugin_mapping.py +0 -0
  62. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/config/plugin/plugin_operation.py +0 -0
  63. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/config/plugin/preset_plugins.py +0 -0
  64. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/config/validators/__init__.py +0 -0
  65. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/config/validators/options.py +0 -0
  66. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/downloaders/__init__.py +0 -0
  67. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/downloaders/info_json/__init__.py +0 -0
  68. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/downloaders/info_json/info_json_downloader.py +0 -0
  69. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/downloaders/source_plugin.py +0 -0
  70. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/downloaders/url/__init__.py +0 -0
  71. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/downloaders/ytdl_options_builder.py +0 -0
  72. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/downloaders/ytdlp.py +0 -0
  73. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/entries/__init__.py +0 -0
  74. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/entries/base_entry.py +0 -0
  75. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/entries/entry.py +0 -0
  76. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/entries/entry_parent.py +0 -0
  77. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/entries/script/__init__.py +0 -0
  78. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/entries/script/custom_functions.py +0 -0
  79. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/entries/script/function_scripts.py +0 -0
  80. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/entries/variables/__init__.py +0 -0
  81. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/entries/variables/override_variables.py +0 -0
  82. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/main.py +0 -0
  83. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/__init__.py +0 -0
  84. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/audio_extract.py +0 -0
  85. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/chapters.py +0 -0
  86. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/file_convert.py +0 -0
  87. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/format.py +0 -0
  88. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/internal/__init__.py +0 -0
  89. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/internal/view.py +0 -0
  90. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/match_filters.py +0 -0
  91. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/music_tags.py +0 -0
  92. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/output_directory_nfo_tags.py +0 -0
  93. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/split_by_chapters.py +0 -0
  94. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/static_nfo_tags.py +0 -0
  95. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/plugins/subtitles.py +0 -0
  96. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/__init__.py +0 -0
  97. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/helpers/__init__.py +0 -0
  98. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/helpers/download_deletion_options.yaml +0 -0
  99. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/helpers/filter_duration.yaml +0 -0
  100. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/helpers/filter_keywords.yaml +0 -0
  101. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/helpers/media_quality.yaml +0 -0
  102. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/helpers/players.yaml +0 -0
  103. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/helpers/throttle_protection.yaml +0 -0
  104. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/helpers/url_bilateral.yaml +0 -0
  105. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/internal/__init__.py +0 -0
  106. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/internal/view.yaml +0 -0
  107. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/music/__init__.py +0 -0
  108. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/music/albums_from_chapters.yaml +0 -0
  109. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/music/albums_from_playlists.yaml +0 -0
  110. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/music/singles.yaml +0 -0
  111. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/music_videos/__init__.py +0 -0
  112. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/music_videos/music_video_base.yaml +0 -0
  113. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/music_videos/music_video_extras.yaml +0 -0
  114. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/music_videos/music_videos.yaml +0 -0
  115. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/tv_show/__init__.py +0 -0
  116. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/tv_show/episode.yaml +0 -0
  117. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/tv_show/tv_show.yaml +0 -0
  118. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_by_date.yaml +0 -0
  119. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/__init__.py +0 -0
  120. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/functions/__init__.py +0 -0
  121. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/functions/array_functions.py +0 -0
  122. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/functions/boolean_functions.py +0 -0
  123. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/functions/conditional_functions.py +0 -0
  124. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/functions/date_functions.py +0 -0
  125. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/functions/error_functions.py +0 -0
  126. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/functions/json_functions.py +0 -0
  127. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/functions/map_functions.py +0 -0
  128. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/functions/print_functions.py +0 -0
  129. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/functions/regex_functions.py +0 -0
  130. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/functions/string_functions.py +0 -0
  131. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/parser.py +0 -0
  132. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/script_output.py +0 -0
  133. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/types/__init__.py +0 -0
  134. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/types/variable.py +0 -0
  135. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/utils/__init__.py +0 -0
  136. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/utils/exception_formatters.py +0 -0
  137. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/utils/exceptions.py +0 -0
  138. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/utils/name_validation.py +0 -0
  139. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/script/utils/type_checking.py +0 -0
  140. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/subscriptions/__init__.py +0 -0
  141. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/subscriptions/subscription.py +0 -0
  142. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/subscriptions/subscription_validators.py +0 -0
  143. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/thread/__init__.py +0 -0
  144. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/thread/log_entries_downloaded_listener.py +0 -0
  145. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/__init__.py +0 -0
  146. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/chapters.py +0 -0
  147. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/datetime.py +0 -0
  148. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/exceptions.py +0 -0
  149. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/ffmpeg.py +0 -0
  150. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/file_handler.py +0 -0
  151. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/file_lock.py +0 -0
  152. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/file_path.py +0 -0
  153. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/logger.py +0 -0
  154. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/retry.py +0 -0
  155. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/scriptable.py +0 -0
  156. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/subtitles.py +0 -0
  157. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/system.py +0 -0
  158. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/thumbnail.py +0 -0
  159. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/xml.py +0 -0
  160. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/utils/yaml.py +0 -0
  161. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/validators/__init__.py +0 -0
  162. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/validators/audo_codec_validator.py +0 -0
  163. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/validators/file_path_validators.py +0 -0
  164. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/validators/nfo_validators.py +0 -0
  165. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/validators/regex_validator.py +0 -0
  166. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/validators/source_variable_validator.py +0 -0
  167. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/validators/strict_dict_validator.py +0 -0
  168. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/validators/string_datetime.py +0 -0
  169. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/validators/string_select_validator.py +0 -0
  170. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/validators/validators.py +0 -0
  171. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/ytdl_additions/__init__.py +0 -0
  172. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py +0 -0
  173. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub.egg-info/SOURCES.txt +0 -0
  174. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub.egg-info/dependency_links.txt +0 -0
  175. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/src/ytdl_sub.egg-info/entry_points.txt +0 -0
  176. {ytdl_sub-2025.12.31.post1 → ytdl_sub-2026.2.3}/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: 2025.12.31.post1
3
+ Version: 2026.2.3
4
4
  Summary: Automate downloading metadata generation with YoutubeDL
5
5
  Author: Jesse Bannon
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -689,7 +689,7 @@ Classifier: Programming Language :: Python :: 3.11
689
689
  Requires-Python: >=3.10
690
690
  Description-Content-Type: text/markdown
691
691
  License-File: LICENSE
692
- Requires-Dist: yt-dlp[default]==2025.12.8
692
+ Requires-Dist: yt-dlp[default]==2026.1.31
693
693
  Requires-Dist: colorama~=0.4
694
694
  Requires-Dist: mergedeep~=1.3
695
695
  Requires-Dist: mediafile~=0.12
@@ -703,7 +703,7 @@ Requires-Dist: black==24.10.0; extra == "lint"
703
703
  Requires-Dist: isort==7.0.0; extra == "lint"
704
704
  Requires-Dist: pylint==4.0.1; extra == "lint"
705
705
  Provides-Extra: docs
706
- Requires-Dist: sphinx<9,>=7; extra == "docs"
706
+ Requires-Dist: sphinx<10,>=7; extra == "docs"
707
707
  Requires-Dist: sphinx-rtd-theme<4,>=2; extra == "docs"
708
708
  Requires-Dist: sphinx-book-theme~=1.0; extra == "docs"
709
709
  Requires-Dist: sphinx-copybutton~=0.5; extra == "docs"
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3.11",
16
16
  ]
17
17
  dependencies = [
18
- "yt-dlp[default]==2025.12.8",
18
+ "yt-dlp[default]==2026.1.31",
19
19
  "colorama~=0.4",
20
20
  "mergedeep~=1.3",
21
21
  "mediafile~=0.12",
@@ -52,7 +52,7 @@ lint = [
52
52
  "pylint==4.0.1",
53
53
  ]
54
54
  docs = [
55
- "sphinx>=7,<9",
55
+ "sphinx>=7,<10",
56
56
  "sphinx-rtd-theme>=2,<4",
57
57
  "sphinx-book-theme~=1.0",
58
58
  "sphinx-copybutton~=0.5",
@@ -0,0 +1 @@
1
+ __pypi_version__ = "2026.02.03";__local_version__ = "2026.02.03+d526545"
@@ -1,5 +1,6 @@
1
1
  import gc
2
2
  import os
3
+ import random
3
4
  import sys
4
5
  from datetime import datetime
5
6
  from pathlib import Path
@@ -75,6 +76,7 @@ def _download_subscriptions_from_yaml_files(
75
76
  subscription_override_dict: Dict,
76
77
  update_with_info_json: bool,
77
78
  dry_run: bool,
79
+ shuffle: bool,
78
80
  ) -> List[Subscription]:
79
81
  """
80
82
  Downloads all subscriptions from one or many subscription yaml files.
@@ -91,6 +93,8 @@ def _download_subscriptions_from_yaml_files(
91
93
  Whether to actually download or update using existing info json
92
94
  dry_run
93
95
  Whether to dry run or not
96
+ shuffle
97
+ Whether to shuffle the subscription download order
94
98
 
95
99
  Returns
96
100
  -------
@@ -112,6 +116,10 @@ def _download_subscriptions_from_yaml_files(
112
116
  subscription_override_dict=subscription_override_dict,
113
117
  )
114
118
 
119
+ if shuffle:
120
+ logger.info("Shuffling subscriptions")
121
+ random.shuffle(subscriptions)
122
+
115
123
  for subscription in subscriptions:
116
124
  with subscription.exception_handling():
117
125
  logger.info(
@@ -253,6 +261,7 @@ def main() -> List[Subscription]:
253
261
  subscription_override_dict=subscription_override_dict,
254
262
  update_with_info_json=args.update_with_info_json,
255
263
  dry_run=args.dry_run,
264
+ shuffle=args.shuffle,
256
265
  )
257
266
 
258
267
  # One-off download
@@ -172,6 +172,10 @@ class SubArguments:
172
172
  short="-o",
173
173
  long="--dl-override",
174
174
  )
175
+ SHUFFLE = CLIArgument(
176
+ short="-sh",
177
+ long="--shuffle",
178
+ )
175
179
 
176
180
 
177
181
  subscription_parser = subparsers.add_parser("sub")
@@ -197,6 +201,13 @@ subscription_parser.add_argument(
197
201
  help="override all subscription config values using `dl` syntax, "
198
202
  "i.e. --dl-override='--ytdl_options.max_downloads 3'",
199
203
  )
204
+ subscription_parser.add_argument(
205
+ SubArguments.SHUFFLE.short,
206
+ SubArguments.SHUFFLE.long,
207
+ action="store_true",
208
+ help="shuffle subscription order when downloading",
209
+ default=False,
210
+ )
200
211
 
201
212
  ###################################################################################################
202
213
  # DOWNLOAD PARSER
@@ -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]]:
@@ -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}})
@@ -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:
@@ -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
@@ -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
  )
@@ -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
  """
@@ -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: