subliminal 2.4.0__tar.gz → 2.6.0__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 (328) hide show
  1. {subliminal-2.4.0 → subliminal-2.6.0}/.pre-commit-config.yaml +4 -4
  2. {subliminal-2.4.0 → subliminal-2.6.0}/HISTORY.rst +50 -0
  3. {subliminal-2.4.0 → subliminal-2.6.0}/PKG-INFO +8 -3
  4. {subliminal-2.4.0 → subliminal-2.6.0}/README.rst +5 -1
  5. {subliminal-2.4.0 → subliminal-2.6.0}/docs/api/providers.rst +5 -0
  6. subliminal-2.6.0/docs/assets/demo.gif +0 -0
  7. {subliminal-2.4.0 → subliminal-2.6.0}/docs/user/configuration_file.rst +5 -1
  8. {subliminal-2.4.0 → subliminal-2.6.0}/docs/user/how_it_works.rst +1 -0
  9. {subliminal-2.4.0 → subliminal-2.6.0}/hatch.toml +1 -1
  10. {subliminal-2.4.0 → subliminal-2.6.0}/pyproject.toml +3 -1
  11. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/cli/generate_config.py +59 -28
  12. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/core.py +2 -2
  13. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/extensions.py +36 -11
  14. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/providers/__init__.py +1 -2
  15. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/providers/bsplayer.py +1 -1
  16. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/providers/napiprojekt.py +1 -2
  17. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/providers/opensubtitles.py +4 -3
  18. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/providers/opensubtitlescom.py +32 -22
  19. subliminal-2.6.0/src/subliminal/providers/subtis.py +336 -0
  20. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/providers/subtitulamos.py +7 -3
  21. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/refiners/hash.py +12 -8
  22. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/refiners/tvdb.py +1 -23
  23. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/score.py +1 -1
  24. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/utils.py +4 -0
  25. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_get_id.yaml +15 -17
  26. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_get_title.yaml +15 -17
  27. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_get_wrong_id.yaml +11 -13
  28. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_get_wrong_title.yaml +15 -17
  29. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_refine_episode.yaml +11 -13
  30. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_refine_episode_original_series.yaml +68 -80
  31. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_refine_episode_with_country.yaml +17 -19
  32. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_refine_episode_with_country_hoc_us.yaml +15 -18
  33. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_refine_episode_year.yaml +11 -13
  34. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_refine_movie.yaml +49 -54
  35. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_refine_movie_guess_alternative_title.yaml +15 -16
  36. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_search.yaml +11 -13
  37. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_search_page.yaml +11 -13
  38. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_search_type.yaml +11 -13
  39. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_search_wrong_title.yaml +15 -17
  40. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_search_year.yaml +11 -13
  41. subliminal-2.6.0/tests/cassettes/opensubtitlescom/test_query_max_result_pages.yaml +10748 -0
  42. subliminal-2.6.0/tests/cassettes/subtis/test_download_subtitle.yaml +7982 -0
  43. subliminal-2.6.0/tests/cassettes/subtis/test_list_subtitles_hash.yaml +258 -0
  44. subliminal-2.6.0/tests/cassettes/subtis/test_list_subtitles_movie.yaml +997 -0
  45. subliminal-2.6.0/tests/cassettes/subtis/test_list_subtitles_movie_alternative.yaml +682 -0
  46. subliminal-2.6.0/tests/cassettes/subtis/test_list_subtitles_movie_with_size.yaml +367 -0
  47. subliminal-2.6.0/tests/cassettes/subtis/test_list_subtitles_no_match.yaml +632 -0
  48. subliminal-2.6.0/tests/cassettes/subtitulamos/test_list_subtitles_not_exist_series.yaml +110 -0
  49. subliminal-2.6.0/tests/cassettes/tmdb/test_search_movie_page.yaml +313 -0
  50. subliminal-2.6.0/tests/cli/test_cli.py +34 -0
  51. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cli/test_download.py +102 -110
  52. subliminal-2.6.0/tests/cli/test_generate_config.py +57 -0
  53. {subliminal-2.4.0 → subliminal-2.6.0}/tests/conftest.py +225 -6
  54. {subliminal-2.4.0 → subliminal-2.6.0}/tests/providers/test_opensubtitlescom.py +15 -0
  55. {subliminal-2.4.0 → subliminal-2.6.0}/tests/providers/test_providers.py +26 -22
  56. subliminal-2.6.0/tests/providers/test_subtis.py +390 -0
  57. {subliminal-2.4.0 → subliminal-2.6.0}/tests/refiners/test_hash.py +1 -0
  58. {subliminal-2.4.0 → subliminal-2.6.0}/tests/refiners/test_omdb.py +13 -10
  59. {subliminal-2.4.0 → subliminal-2.6.0}/tests/refiners/test_tmdb.py +8 -4
  60. {subliminal-2.4.0 → subliminal-2.6.0}/tests/refiners/test_tvdb.py +0 -22
  61. {subliminal-2.4.0 → subliminal-2.6.0}/tests/test_archives.py +4 -3
  62. {subliminal-2.4.0 → subliminal-2.6.0}/tests/test_core.py +14 -3
  63. {subliminal-2.4.0 → subliminal-2.6.0}/tests/test_provider_pool.py +19 -17
  64. {subliminal-2.4.0 → subliminal-2.6.0}/tests/test_subtitle.py +1 -1
  65. {subliminal-2.4.0 → subliminal-2.6.0}/tests/test_utils.py +1 -0
  66. {subliminal-2.4.0 → subliminal-2.6.0}/tests/test_video.py +2 -1
  67. subliminal-2.4.0/tests/cassettes/subtitulamos/test_list_subtitles_not_exist_series.yaml +0 -114
  68. subliminal-2.4.0/tests/cassettes/tmdb/test_search_movie_page.yaml +0 -146
  69. subliminal-2.4.0/tests/cassettes/tvdb/test_refresh_token.yaml +0 -100
  70. subliminal-2.4.0/tests/cassettes/tvdb/test_token_needs_refresh.yaml +0 -50
  71. subliminal-2.4.0/tests/cli/test_cli.py +0 -32
  72. subliminal-2.4.0/tests/cli/test_generate_config.py +0 -33
  73. {subliminal-2.4.0 → subliminal-2.6.0}/.dockerignore +0 -0
  74. {subliminal-2.4.0 → subliminal-2.6.0}/.github/actions/prepare-tests/action.yaml +0 -0
  75. {subliminal-2.4.0 → subliminal-2.6.0}/.github/workflows/CI.yaml +0 -0
  76. {subliminal-2.4.0 → subliminal-2.6.0}/.github/workflows/coverage.yaml +0 -0
  77. {subliminal-2.4.0 → subliminal-2.6.0}/.github/workflows/docker-build.yaml +0 -0
  78. {subliminal-2.4.0 → subliminal-2.6.0}/.github/workflows/prepare-release-pr.yaml +0 -0
  79. {subliminal-2.4.0 → subliminal-2.6.0}/.github/workflows/publish-release.yaml +0 -0
  80. {subliminal-2.4.0 → subliminal-2.6.0}/.github/workflows/publish.yaml +0 -0
  81. {subliminal-2.4.0 → subliminal-2.6.0}/.github/workflows/scheduled-tests.yaml +0 -0
  82. {subliminal-2.4.0 → subliminal-2.6.0}/.github/workflows/tag-release.yaml +0 -0
  83. {subliminal-2.4.0 → subliminal-2.6.0}/.github/zizmor.yml +0 -0
  84. {subliminal-2.4.0 → subliminal-2.6.0}/.gitignore +0 -0
  85. {subliminal-2.4.0 → subliminal-2.6.0}/.python-version-default +0 -0
  86. {subliminal-2.4.0 → subliminal-2.6.0}/.readthedocs.yaml +0 -0
  87. {subliminal-2.4.0 → subliminal-2.6.0}/CONTRIBUTING.md +0 -0
  88. {subliminal-2.4.0 → subliminal-2.6.0}/Dockerfile +0 -0
  89. {subliminal-2.4.0 → subliminal-2.6.0}/LICENSE +0 -0
  90. {subliminal-2.4.0 → subliminal-2.6.0}/MANIFEST.in +0 -0
  91. {subliminal-2.4.0 → subliminal-2.6.0}/RELEASING.md +0 -0
  92. {subliminal-2.4.0 → subliminal-2.6.0}/changelog.d/.gitignore +0 -0
  93. {subliminal-2.4.0 → subliminal-2.6.0}/docs/Makefile +0 -0
  94. {subliminal-2.4.0 → subliminal-2.6.0}/docs/_intersphinx/README.md +0 -0
  95. {subliminal-2.4.0 → subliminal-2.6.0}/docs/_intersphinx/importlib_metadata_objects.inv +0 -0
  96. {subliminal-2.4.0 → subliminal-2.6.0}/docs/_intersphinx/importlib_metadata_objects.txt +0 -0
  97. {subliminal-2.4.0 → subliminal-2.6.0}/docs/_static/.gitignore +0 -0
  98. {subliminal-2.4.0 → subliminal-2.6.0}/docs/api/cache.rst +0 -0
  99. {subliminal-2.4.0 → subliminal-2.6.0}/docs/api/cli.rst +0 -0
  100. {subliminal-2.4.0 → subliminal-2.6.0}/docs/api/core.rst +0 -0
  101. {subliminal-2.4.0 → subliminal-2.6.0}/docs/api/exceptions.rst +0 -0
  102. {subliminal-2.4.0 → subliminal-2.6.0}/docs/api/extensions.rst +0 -0
  103. {subliminal-2.4.0 → subliminal-2.6.0}/docs/api/refiners.rst +0 -0
  104. {subliminal-2.4.0 → subliminal-2.6.0}/docs/api/score.rst +0 -0
  105. {subliminal-2.4.0 → subliminal-2.6.0}/docs/api/subtitle.rst +0 -0
  106. {subliminal-2.4.0 → subliminal-2.6.0}/docs/api/utils.rst +0 -0
  107. {subliminal-2.4.0 → subliminal-2.6.0}/docs/api/video.rst +0 -0
  108. {subliminal-2.4.0/docs → subliminal-2.6.0/docs/assets}/config.toml +0 -0
  109. {subliminal-2.4.0 → subliminal-2.6.0}/docs/cassettes/test_usage.yaml +0 -0
  110. {subliminal-2.4.0 → subliminal-2.6.0}/docs/conf.py +0 -0
  111. {subliminal-2.4.0 → subliminal-2.6.0}/docs/conftest.py +0 -0
  112. {subliminal-2.4.0 → subliminal-2.6.0}/docs/index.rst +0 -0
  113. {subliminal-2.4.0 → subliminal-2.6.0}/docs/make.bat +0 -0
  114. {subliminal-2.4.0 → subliminal-2.6.0}/docs/user/changelog.rst +0 -0
  115. {subliminal-2.4.0 → subliminal-2.6.0}/docs/user/cli.rst +0 -0
  116. {subliminal-2.4.0 → subliminal-2.6.0}/docs/user/installation.rst +0 -0
  117. {subliminal-2.4.0 → subliminal-2.6.0}/docs/user/provider_guide.rst +0 -0
  118. {subliminal-2.4.0 → subliminal-2.6.0}/docs/user/usage.rst +0 -0
  119. {subliminal-2.4.0 → subliminal-2.6.0}/scripts/.gitignore +0 -0
  120. {subliminal-2.4.0 → subliminal-2.6.0}/scripts/prepare-release-pr.py +0 -0
  121. {subliminal-2.4.0 → subliminal-2.6.0}/scripts/prepare_tests.py +0 -0
  122. {subliminal-2.4.0 → subliminal-2.6.0}/scripts/release.py +0 -0
  123. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/__init__.py +0 -0
  124. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/__main__.py +0 -0
  125. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/archives.py +0 -0
  126. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/cache.py +0 -0
  127. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/cli/__init__.py +0 -0
  128. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/cli/cli.py +0 -0
  129. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/cli/commands/__init__.py +0 -0
  130. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/cli/commands/_format.py +0 -0
  131. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/cli/commands/download_best.py +0 -0
  132. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/cli/helpers.py +0 -0
  133. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/converters/__init__.py +0 -0
  134. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/converters/addic7ed.py +0 -0
  135. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/converters/opensubtitles.py +0 -0
  136. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/converters/opensubtitlescom.py +0 -0
  137. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/converters/subtitulamos.py +0 -0
  138. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/converters/tvsubtitles.py +0 -0
  139. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/exceptions.py +0 -0
  140. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/matches.py +0 -0
  141. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/providers/addic7ed.py +0 -0
  142. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/providers/gestdown.py +0 -0
  143. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/providers/mock.py +0 -0
  144. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/providers/podnapisi.py +0 -0
  145. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/providers/tvsubtitles.py +0 -0
  146. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/py.typed +0 -0
  147. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/refiners/__init__.py +0 -0
  148. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/refiners/metadata.py +0 -0
  149. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/refiners/omdb.py +0 -0
  150. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/refiners/tmdb.py +0 -0
  151. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/subtitle.py +0 -0
  152. {subliminal-2.4.0 → subliminal-2.6.0}/src/subliminal/video.py +0 -0
  153. {subliminal-2.4.0 → subliminal-2.6.0}/tests/__init__.py +0 -0
  154. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_download_subtitle.yaml +0 -0
  155. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_get_show_id.yaml +0 -0
  156. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_get_show_id_country.yaml +0 -0
  157. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_get_show_id_quote_dots_mixed_case.yaml +0 -0
  158. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_get_show_id_with_comma.yaml +0 -0
  159. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_get_show_id_year.yaml +0 -0
  160. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_get_show_ids.yaml +0 -0
  161. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_list_subtitles.yaml +0 -0
  162. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_list_subtitles_episode_alternative_series.yaml +0 -0
  163. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_list_subtitles_show_with_asterisk.yaml +0 -0
  164. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query.yaml +0 -0
  165. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query_no_year.yaml +0 -0
  166. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query_parsing.yaml +0 -0
  167. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query_parsing_colon.yaml +0 -0
  168. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query_parsing_dash.yaml +0 -0
  169. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query_parsing_quote_dots_mixed_case.yaml +0 -0
  170. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query_wrong_series.yaml +0 -0
  171. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query_year.yaml +0 -0
  172. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/bsplayer/test_download_subtitle.yaml +0 -0
  173. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/bsplayer/test_list_subtitles_hash.yaml +0 -0
  174. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/bsplayer/test_login.yaml +0 -0
  175. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/bsplayer/test_logout.yaml +0 -0
  176. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/bsplayer/test_query_hash_size.yaml +0 -0
  177. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/cli/test_cli_download.yaml +0 -0
  178. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_download_subtitle.yaml +0 -0
  179. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_download_with_bom.yaml +0 -0
  180. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_get_title_and_show_id_alternative_name.yaml +0 -0
  181. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_get_title_and_show_id_no_year.yaml +0 -0
  182. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_get_title_and_show_id_only_title.yaml +0 -0
  183. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_get_title_and_show_id_with_tvdb_id.yaml +0 -0
  184. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_list_subtitles.yaml +0 -0
  185. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_list_subtitles_episode_alternative_series.yaml +0 -0
  186. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query.yaml +0 -0
  187. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query_all_series.yaml +0 -0
  188. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query_no_language.yaml +0 -0
  189. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query_parsing.yaml +0 -0
  190. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query_parsing_colon.yaml +0 -0
  191. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query_parsing_dash.yaml +0 -0
  192. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query_parsing_quote_dots_mixed_case.yaml +0 -0
  193. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query_wrong_series.yaml +0 -0
  194. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_search_show_id.yaml +0 -0
  195. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_search_show_id_error.yaml +0 -0
  196. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_search_show_id_incomplete.yaml +0 -0
  197. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_search_show_id_no_year.yaml +0 -0
  198. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_search_show_id_quote.yaml +0 -0
  199. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_search_show_series_tvdb_id.yaml +0 -0
  200. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_show_with_asterisk.yaml +0 -0
  201. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/napiprojekt/test_download_subtitles.yaml +0 -0
  202. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/napiprojekt/test_list_subtitles.yaml +0 -0
  203. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/napiprojekt/test_query_microdvd.yaml +0 -0
  204. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/napiprojekt/test_query_srt.yaml +0 -0
  205. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/napiprojekt/test_query_srt_reencode.yaml +0 -0
  206. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/napiprojekt/test_query_wrong_hash.yaml +0 -0
  207. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_download_subtitle.yaml +0 -0
  208. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_list_subtitles_episode.yaml +0 -0
  209. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_list_subtitles_movie.yaml +0 -0
  210. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_list_subtitles_movie_no_hash.yaml +0 -0
  211. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_login_bad_password.yaml +0 -0
  212. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_login_vip_bad_password.yaml +0 -0
  213. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_login_vip_login.yaml +0 -0
  214. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_no_operation.yaml +0 -0
  215. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_query_hash_size.yaml +0 -0
  216. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_query_imdb_id.yaml +0 -0
  217. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_query_not_enough_information.yaml +0 -0
  218. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_query_query_episode.yaml +0 -0
  219. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_query_query_movie.yaml +0 -0
  220. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_query_query_season_episode.yaml +0 -0
  221. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_query_wrong_hash_wrong_size.yaml +0 -0
  222. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_download_subtitle.yaml +0 -0
  223. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_list_subtitles_episode.yaml +0 -0
  224. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_list_subtitles_movie.yaml +0 -0
  225. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_list_subtitles_movie_no_hash.yaml +0 -0
  226. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_login.yaml +0 -0
  227. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_login_bad_password.yaml +0 -0
  228. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_logout.yaml +0 -0
  229. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_hash_size.yaml +0 -0
  230. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_imdb_id.yaml +0 -0
  231. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_query_episode.yaml +0 -0
  232. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_query_movie.yaml +0 -0
  233. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_query_season_episode.yaml +0 -0
  234. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_tag_movie.yaml +0 -0
  235. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_wrong_hash_wrong_size.yaml +0 -0
  236. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_tag_match.yaml +0 -0
  237. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_user_infos.yaml +0 -0
  238. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/podnapisi/test_download_subtitle.yaml +0 -0
  239. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/podnapisi/test_download_subtitles_with_title_unicode.yaml +0 -0
  240. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/podnapisi/test_list_subtitles_episode.yaml +0 -0
  241. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/podnapisi/test_list_subtitles_episode_alternative_series.yaml +0 -0
  242. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/podnapisi/test_list_subtitles_movie.yaml +0 -0
  243. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/podnapisi/test_query_episode.yaml +0 -0
  244. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/podnapisi/test_query_movie.yaml +0 -0
  245. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/providers/test_download_best_subtitles.yaml +0 -0
  246. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/providers/test_download_best_subtitles_min_score.yaml +0 -0
  247. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/providers/test_download_best_subtitles_only_one.yaml +0 -0
  248. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/providers/test_list_subtitles_providers_download.yaml +0 -0
  249. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_download_subtitle.yaml +0 -0
  250. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_download_subtitle_first_episode.yaml +0 -0
  251. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_download_subtitle_foo.yaml +0 -0
  252. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_download_subtitle_last_season.yaml +0 -0
  253. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_download_subtitle_year.yaml +0 -0
  254. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_list_subtitles_not_exist_episode.yaml +0 -0
  255. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_list_subtitles_not_exist_language.yaml +0 -0
  256. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_list_subtitles_not_exist_season.yaml +0 -0
  257. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_list_subtitles_with_spanish_non_mx_search.yaml +0 -0
  258. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_get_movie_id.yaml +0 -0
  259. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_get_movie_id_failed.yaml +0 -0
  260. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_get_series_id.yaml +0 -0
  261. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_get_series_wrong_id.yaml +0 -0
  262. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_refine_episode.yaml +0 -0
  263. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_refine_episode_original_series.yaml +0 -0
  264. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_refine_episode_with_country.yaml +0 -0
  265. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_refine_episode_with_country_hoc_us.yaml +0 -0
  266. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_refine_episode_year.yaml +0 -0
  267. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_refine_movie.yaml +0 -0
  268. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_refine_movie_guess_alternative_title.yaml +0 -0
  269. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_search_movie.yaml +0 -0
  270. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_search_movie_wrong_title.yaml +0 -0
  271. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_search_movie_year.yaml +0 -0
  272. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_search_series.yaml +0 -0
  273. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_search_series_wrong_name.yaml +0 -0
  274. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_get_series.yaml +0 -0
  275. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_get_series_actors.yaml +0 -0
  276. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_get_series_actors_wrong_id.yaml +0 -0
  277. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_get_series_wrong_id.yaml +0 -0
  278. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_login.yaml +0 -0
  279. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_login_error.yaml +0 -0
  280. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_query_series_episodes.yaml +0 -0
  281. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_query_series_episodes_wrong_season.yaml +0 -0
  282. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine.yaml +0 -0
  283. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_ambiguous.yaml +0 -0
  284. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_ambiguous_2.yaml +0 -0
  285. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_episode_alternative_series.yaml +0 -0
  286. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_episode_no_year.yaml +0 -0
  287. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_episode_partial.yaml +0 -0
  288. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_episode_with_comma.yaml +0 -0
  289. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_episode_with_country.yaml +0 -0
  290. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_episode_with_country_hoc_us.yaml +0 -0
  291. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_episode_year.yaml +0 -0
  292. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_search_series.yaml +0 -0
  293. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_search_series_wrong_name.yaml +0 -0
  294. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_download_subtitle.yaml +0 -0
  295. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_download_subtitle_with_quote.yaml +0 -0
  296. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_get_episode_ids.yaml +0 -0
  297. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_get_episode_ids_wrong_season.yaml +0 -0
  298. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_list_subtitles.yaml +0 -0
  299. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_list_subtitles_episode_alternative_series.yaml +0 -0
  300. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_query.yaml +0 -0
  301. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_query_no_year.yaml +0 -0
  302. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_query_with_quote.yaml +0 -0
  303. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_query_wrong_episode.yaml +0 -0
  304. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_query_wrong_series.yaml +0 -0
  305. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id.yaml +0 -0
  306. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id_ambiguous.yaml +0 -0
  307. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id_error.yaml +0 -0
  308. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id_incomplete.yaml +0 -0
  309. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id_no_year.yaml +0 -0
  310. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id_uk.yaml +0 -0
  311. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id_us.yaml +0 -0
  312. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id_year_in_title.yaml +0 -0
  313. {subliminal-2.4.0 → subliminal-2.6.0}/tests/cli/test_helpers.py +0 -0
  314. {subliminal-2.4.0 → subliminal-2.6.0}/tests/providers/test_addic7ed.py +0 -0
  315. {subliminal-2.4.0 → subliminal-2.6.0}/tests/providers/test_bsplayer.py +0 -0
  316. {subliminal-2.4.0 → subliminal-2.6.0}/tests/providers/test_gestdown.py +0 -0
  317. {subliminal-2.4.0 → subliminal-2.6.0}/tests/providers/test_napiprojekt.py +0 -0
  318. {subliminal-2.4.0 → subliminal-2.6.0}/tests/providers/test_opensubtitles.py +0 -0
  319. {subliminal-2.4.0 → subliminal-2.6.0}/tests/providers/test_podnapisi.py +0 -0
  320. {subliminal-2.4.0 → subliminal-2.6.0}/tests/providers/test_subtitulamos.py +0 -0
  321. {subliminal-2.4.0 → subliminal-2.6.0}/tests/providers/test_tvsubtitles.py +0 -0
  322. {subliminal-2.4.0 → subliminal-2.6.0}/tests/refiners/test_metadata.py +0 -0
  323. {subliminal-2.4.0 → subliminal-2.6.0}/tests/test_cache.py +0 -0
  324. {subliminal-2.4.0 → subliminal-2.6.0}/tests/test_extensions.py +0 -0
  325. {subliminal-2.4.0 → subliminal-2.6.0}/tests/test_matches.py +0 -0
  326. {subliminal-2.4.0 → subliminal-2.6.0}/tests/test_provider.py +0 -0
  327. {subliminal-2.4.0 → subliminal-2.6.0}/tests/test_score.py +0 -0
  328. {subliminal-2.4.0 → subliminal-2.6.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  repos:
2
2
  - repo: https://github.com/pre-commit/pre-commit-hooks
3
- rev: v5.0.0
3
+ rev: v6.0.0
4
4
  hooks:
5
5
  - id: check-toml
6
6
  - id: check-yaml
@@ -8,12 +8,12 @@ repos:
8
8
  - id: end-of-file-fixer
9
9
 
10
10
  - repo: https://github.com/crate-ci/typos
11
- rev: v1.33.1
11
+ rev: v1.40.0
12
12
  hooks:
13
13
  - id: typos
14
14
 
15
15
  - repo: https://github.com/astral-sh/ruff-pre-commit
16
- rev: v0.12.1
16
+ rev: v0.14.9
17
17
  hooks:
18
18
  - id: ruff
19
19
  args: [--fix, --unsafe-fixes]
@@ -33,7 +33,7 @@ repos:
33
33
  require_serial: true
34
34
 
35
35
  - repo: https://github.com/woodruffw/zizmor-pre-commit
36
- rev: v1.10.0
36
+ rev: v1.18.0
37
37
  hooks:
38
38
  - id: zizmor
39
39
 
@@ -2,6 +2,56 @@
2
2
 
3
3
  .. towncrier release notes start
4
4
 
5
+ 2.6.0 (2026-02-20)
6
+ ^^^^^^^^^^^^^^^^^^
7
+
8
+ Provider Changes
9
+ ----------------
10
+
11
+ - OpenSubtitlesCom: replace recursive ``_search()`` implementation to prevent a stack overflow in some edge case (`#1336 <https://github.com/Diaoul/subliminal/issues/1336>`__)
12
+ - Add Subtis provider for Spanish subtitles (`#1337 <https://github.com/Diaoul/subliminal/issues/1337>`__)
13
+
14
+
15
+ Refiner Changes
16
+ ---------------
17
+
18
+ - hash: cache the result of hash_opensubtitles so it can be reused costlessly (`#1340 <https://github.com/Diaoul/subliminal/issues/1340>`__)
19
+
20
+
21
+ Misc
22
+ ----
23
+
24
+ - Fixes for some scheduled tests failures (`#1339 <https://github.com/Diaoul/subliminal/issues/1339>`__)
25
+ - add a gif to show CLI usage (`#1341 <https://github.com/Diaoul/subliminal/issues/1341>`__)
26
+
27
+
28
+ 2.5.0 (2025-12-14)
29
+ ^^^^^^^^^^^^^^^^^^
30
+
31
+ Provider Changes
32
+ ----------------
33
+
34
+ - OpenSubtitlesCom: add a max_result_pages parameter to limit the number of result pages to speed up the search (`#1321 <https://github.com/Diaoul/subliminal/issues/1321>`__)
35
+
36
+
37
+ Documentation
38
+ -------------
39
+
40
+ - fix ``generate_default_config`` for ``click==8.3`` and pin to ``click<8.3`` until another regression is fixed. (`#1318 <https://github.com/Diaoul/subliminal/issues/1318>`__)
41
+
42
+
43
+ Misc
44
+ ----
45
+
46
+ - add support for python 3.14 (`#1317 <https://github.com/Diaoul/subliminal/issues/1317>`__)
47
+ - Fix test with click 8.3.0 running in parallel (`#1320 <https://github.com/Diaoul/subliminal/issues/1320>`__)
48
+ - fix test on big-endian system (`#1323 <https://github.com/Diaoul/subliminal/issues/1323>`__)
49
+ - compatibility with stevedore 5.6.0 (`#1325 <https://github.com/Diaoul/subliminal/issues/1325>`__)
50
+ - fix some tests to pass in a dirty environment (`#1326 <https://github.com/Diaoul/subliminal/issues/1326>`__)
51
+ - update pre-commit hook versions (`#1329 <https://github.com/Diaoul/subliminal/issues/1329>`__)
52
+ - Fix some test so they are more reliable and stable (`#1331 <https://github.com/Diaoul/subliminal/issues/1331>`__)
53
+
54
+
5
55
  2.4.0 (2025-09-03)
6
56
  ^^^^^^^^^^^^^^^^^^
7
57
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: subliminal
3
- Version: 2.4.0
3
+ Version: 2.6.0
4
4
  Summary: Subtitles, faster than your thoughts
5
5
  Project-URL: homepage, https://github.com/Diaoul/subliminal
6
6
  Project-URL: repository, https://github.com/Diaoul/subliminal
@@ -21,14 +21,15 @@ Classifier: Programming Language :: Python :: 3.10
21
21
  Classifier: Programming Language :: Python :: 3.11
22
22
  Classifier: Programming Language :: Python :: 3.12
23
23
  Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
24
25
  Classifier: Topic :: Multimedia :: Video
25
26
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
27
  Requires-Python: >=3.9
27
28
  Requires-Dist: babelfish>=0.6.1
28
29
  Requires-Dist: beautifulsoup4>=4.4.0
29
30
  Requires-Dist: chardet>=5.0
31
+ Requires-Dist: click
30
32
  Requires-Dist: click-option-group>=0.5.6
31
- Requires-Dist: click>=8.0
32
33
  Requires-Dist: defusedxml>=0.7.1
33
34
  Requires-Dist: dogpile-cache>=1.0
34
35
  Requires-Dist: guessit>=3.0.0
@@ -102,6 +103,10 @@ Subtitles, faster than your thoughts.
102
103
 
103
104
  Usage
104
105
  -----
106
+
107
+ .. image:: https://github.com/Diaoul/subliminal/blob/main/docs/assets/demo.gif
108
+ :alt: Demo of the app CLI usage
109
+
105
110
  CLI
106
111
  ^^^
107
112
  Download English subtitles::
@@ -119,7 +124,7 @@ By default it looks for a `subliminal.toml` file in the default configuration fo
119
124
  (see the CLI help for the exact platform-specific default path).
120
125
  Or use the `-c` option to specify the path to the configuration file.
121
126
 
122
- `Look for this example configuration file <https://github.com/Diaoul/subliminal/blob/main/docs/config.toml>`__
127
+ `Look for this example configuration file <https://github.com/Diaoul/subliminal/blob/main/docs/assets/config.toml>`__
123
128
  or use the `generate_default_config` function from the `subliminal.cli` module to generate a
124
129
  configuration file with all the options and their default values::
125
130
 
@@ -30,6 +30,10 @@ Subtitles, faster than your thoughts.
30
30
 
31
31
  Usage
32
32
  -----
33
+
34
+ .. image:: https://github.com/Diaoul/subliminal/blob/main/docs/assets/demo.gif
35
+ :alt: Demo of the app CLI usage
36
+
33
37
  CLI
34
38
  ^^^
35
39
  Download English subtitles::
@@ -47,7 +51,7 @@ By default it looks for a `subliminal.toml` file in the default configuration fo
47
51
  (see the CLI help for the exact platform-specific default path).
48
52
  Or use the `-c` option to specify the path to the configuration file.
49
53
 
50
- `Look for this example configuration file <https://github.com/Diaoul/subliminal/blob/main/docs/config.toml>`__
54
+ `Look for this example configuration file <https://github.com/Diaoul/subliminal/blob/main/docs/assets/config.toml>`__
51
55
  or use the `generate_default_config` function from the `subliminal.cli` module to generate a
52
56
  configuration file with all the options and their default values::
53
57
 
@@ -43,6 +43,11 @@ Subtitulamos
43
43
  .. automodule:: subliminal.providers.subtitulamos
44
44
  :members:
45
45
 
46
+ Subtis
47
+ ------
48
+ .. automodule:: subliminal.providers.subtis
49
+ :members:
50
+
46
51
  TVsubtitles
47
52
  -----------
48
53
  .. automodule:: subliminal.providers.tvsubtitles
Binary file
@@ -24,13 +24,17 @@ CLI arguments (like ``path``) cannot be specified in the configuration file.
24
24
 
25
25
  Options that can be used multiple times (like ``--language``) need to be defined as arrays.
26
26
 
27
+ ``hearing_impaired`` and ``foreign_only`` can take three values: "true", "false" or "[]".
28
+ They mean that hearing impaired (or foreign-only) subtitles will be, respectively,
29
+ ranked first, ranked last or not given any particular rank, for subtitles with identical scores.
30
+
27
31
 
28
32
  Example
29
33
  -------
30
34
 
31
35
  An example of configuration file:
32
36
 
33
- .. literalinclude:: ../config.toml
37
+ .. literalinclude:: ../assets/config.toml
34
38
  :language: toml
35
39
 
36
40
 
@@ -13,6 +13,7 @@ subtitles. Current supported providers are:
13
13
  * OpenSubtitles
14
14
  * OpenSubtitles.com
15
15
  * Podnapisi
16
+ * Subtis
16
17
  * Subtitulamos
17
18
  * TvSubtitles
18
19
 
@@ -80,7 +80,7 @@ description = "Run the tests suite"
80
80
  features = ["tests"]
81
81
 
82
82
  [[envs.tests.matrix]]
83
- python = ["3.9", "3.10", "3.11", "3.12", "3.13"]
83
+ python = ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
84
84
 
85
85
  [envs.tests.env-vars]
86
86
  COVERAGE_PROCESS_START = "pyproject.toml"
@@ -29,6 +29,7 @@ classifiers = [
29
29
  "Programming Language :: Python :: 3.11",
30
30
  "Programming Language :: Python :: 3.12",
31
31
  "Programming Language :: Python :: 3.13",
32
+ "Programming Language :: Python :: 3.14",
32
33
  "Topic :: Software Development :: Libraries :: Python Modules",
33
34
  "Topic :: Multimedia :: Video",
34
35
  ]
@@ -38,7 +39,7 @@ dependencies = [
38
39
  "beautifulsoup4>=4.4.0",
39
40
  "defusedxml>=0.7.1",
40
41
  "chardet>=5.0",
41
- "click>=8.0",
42
+ "click",
42
43
  "click-option-group>=0.5.6",
43
44
  "dogpile.cache>=1.0",
44
45
  "guessit>=3.0.0",
@@ -105,6 +106,7 @@ opensubtitlesvip = "subliminal.providers.opensubtitles:OpenSubtitlesVipProvider"
105
106
  opensubtitlescom = "subliminal.providers.opensubtitlescom:OpenSubtitlesComProvider"
106
107
  opensubtitlescomvip = "subliminal.providers.opensubtitlescom:OpenSubtitlesComVipProvider"
107
108
  podnapisi = "subliminal.providers.podnapisi:PodnapisiProvider"
109
+ subtis = "subliminal.providers.subtis:SubtisProvider"
108
110
  subtitulamos = "subliminal.providers.subtitulamos:SubtitulamosProvider"
109
111
  tvsubtitles = "subliminal.providers.tvsubtitles:TVsubtitlesProvider"
110
112
 
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ from typing import Any
6
7
 
7
8
  import click
8
9
  import tomlkit
@@ -13,6 +14,61 @@ from .cli import subliminal
13
14
  logger = logging.getLogger(__name__)
14
15
 
15
16
 
17
+ def _validate(name: str, value: Any) -> str | None:
18
+ """Validate the value to be sure it is a valid TOML value.
19
+
20
+ E.g., Sentinel.UNSET was introduced in click 8.3 to be used as default value
21
+ and it is not convertible to valid TOML.
22
+ """
23
+ # Create a stub table to try importing the key and value
24
+ stub = tomlkit.table()
25
+ try:
26
+ stub.add(name, value)
27
+ except (ValueError, tomlkit.exceptions.ConvertError):
28
+ return None
29
+
30
+ return stub.as_string().strip('\n')
31
+
32
+
33
+ def _add_value_to_table(
34
+ opt: click.Option,
35
+ table: tomlkit.items.Table,
36
+ *,
37
+ name: str | None = None,
38
+ commented: bool = True,
39
+ ) -> str | None:
40
+ """Add a value to a TOML table."""
41
+ if opt.name is None: # pragma: no cover
42
+ return None
43
+ # Override option name
44
+ opt_name = name if name is not None else opt.name
45
+
46
+ # Add the description of the option as a comment above
47
+ if opt.help:
48
+ table.add(tomlkit.comment(opt.help.capitalize()))
49
+
50
+ # Add the option name with default value
51
+ if opt.default is None:
52
+ # None value is not defined in TOML, it needs to be a comment
53
+ table.add(tomlkit.comment(f'{opt_name} = '))
54
+
55
+ elif (validated := _validate(opt_name, opt.default)) is not None:
56
+ # The default value is valid TOML
57
+ if commented:
58
+ # Add the string to the final table as a comment
59
+ table.add(tomlkit.comment(validated))
60
+ else:
61
+ # Add the key and valid value to the table
62
+ table.add(opt_name, opt.default)
63
+
64
+ else:
65
+ # The default value is not valid TOML, add a comment
66
+ table.add(tomlkit.comment(f'{opt_name} = '))
67
+
68
+ # Return the key to keep track of duplicates
69
+ return opt_name
70
+
71
+
16
72
  def generate_default_config(*, compact: bool = True, commented: bool = True) -> str:
17
73
  """Generate a default configuration file.
18
74
 
@@ -20,31 +76,6 @@ def generate_default_config(*, compact: bool = True, commented: bool = True) ->
20
76
  :param commented: if True, all the options are commented out.
21
77
  :return: the default configuration as a string.
22
78
  """
23
-
24
- def add_value_to_table(opt: click.Option, table: tomlkit.items.Table, *, name: str | None = None) -> str | None:
25
- """Add a value to a TOML table."""
26
- if opt.name is None: # pragma: no cover
27
- return None
28
- # Override option name
29
- opt_name = name if name is not None else opt.name
30
-
31
- table.add(tomlkit.comment((opt.help or opt_name).capitalize()))
32
- # table.add(tomlkit.comment(f'{opt_name} = {opt.default}'))
33
- if opt.default is not None:
34
- if not commented:
35
- table.add(opt_name, opt.default)
36
- else:
37
- # Generate the entry in a dumb table
38
- dumb = tomlkit.table()
39
- dumb.add(opt_name, opt.default)
40
- # Add the string to the final table as a comment
41
- table.add(tomlkit.comment(dumb.as_string().strip('\n')))
42
- else:
43
- table.add(tomlkit.comment(f'{opt_name} = '))
44
-
45
- # Return the key to keep track of duplicates
46
- return opt_name
47
-
48
79
  # Create TOML document
49
80
  doc = tomlkit.document()
50
81
  doc.add(tomlkit.comment('Subliminal default configuration file'))
@@ -62,7 +93,7 @@ def generate_default_config(*, compact: bool = True, commented: bool = True) ->
62
93
  if opt.name in ['version', 'config']:
63
94
  continue
64
95
  # Add key=value to table
65
- add_value_to_table(opt, default)
96
+ _add_value_to_table(opt, default, commented=commented)
66
97
  if not compact: # pragma: no cover
67
98
  default.add(tomlkit.nl())
68
99
  # Adding the table to the document
@@ -85,7 +116,7 @@ def generate_default_config(*, compact: bool = True, commented: bool = True) ->
85
116
  # Duplicated option
86
117
  continue
87
118
  # Add key=value to table
88
- opt_name = add_value_to_table(opt, com_table)
119
+ opt_name = _add_value_to_table(opt, com_table, commented=commented)
89
120
  if opt_name is not None:
90
121
  existing_options.add(opt_name)
91
122
  if not compact: # pragma: no cover
@@ -114,7 +145,7 @@ def generate_default_config(*, compact: bool = True, commented: bool = True) ->
114
145
  continue
115
146
 
116
147
  # Add key=value to table
117
- add_value_to_table(opt, provider_table, name=opt_name)
148
+ _add_value_to_table(opt, provider_table, name=opt_name, commented=commented)
118
149
  if not compact: # pragma: no cover
119
150
  provider_table.add(tomlkit.nl())
120
151
 
@@ -131,12 +131,12 @@ class ProviderPool:
131
131
 
132
132
  """
133
133
  # check video validity
134
- if not provider_manager[provider].plugin.check(video):
134
+ if not provider_manager[provider].plugin.check(video): # type: ignore[attr-defined]
135
135
  logger.info('Skipping provider %r: not a valid video', provider)
136
136
  return []
137
137
 
138
138
  # check supported languages
139
- provider_languages = provider_manager[provider].plugin.check_languages(languages)
139
+ provider_languages = provider_manager[provider].plugin.check_languages(languages) # type: ignore[attr-defined]
140
140
  if not provider_languages:
141
141
  logger.info('Skipping provider %r: no language to search for', provider)
142
142
  return []
@@ -2,15 +2,18 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import inspect
5
6
  import re
6
7
  from importlib.metadata import EntryPoint
7
8
  from typing import TYPE_CHECKING, Any
8
9
 
9
- from stevedore import ExtensionManager # type: ignore[import-untyped]
10
+ from stevedore import ExtensionManager
10
11
 
11
12
  if TYPE_CHECKING:
12
13
  from collections.abc import Sequence
13
14
 
15
+ from stevedore.extension import Extension
16
+
14
17
 
15
18
  class RegistrableExtensionManager(ExtensionManager):
16
19
  """:class:`stevedore.extension.ExtensionManager` with support for registration.
@@ -29,16 +32,21 @@ class RegistrableExtensionManager(ExtensionManager):
29
32
 
30
33
  """
31
34
 
35
+ #: Registered extensions with entry point syntax
32
36
  registered_extensions: list[str]
37
+
38
+ #: Internal extensions with entry point syntax
33
39
  internal_extensions: list[str]
34
40
 
41
+ _old_api: bool
42
+
35
43
  def __init__(self, namespace: str, internal_extensions: Sequence[str], **kwargs: Any) -> None:
36
- #: Registered extensions with entry point syntax
37
44
  self.registered_extensions = []
38
-
39
- #: Internal extensions with entry point syntax
40
45
  self.internal_extensions = list(internal_extensions)
41
46
 
47
+ # 'verify_requirements' argument was remove in stevedore version 5.6.0
48
+ self._old_api = 'verify_requirements' in inspect.signature(self._load_one_plugin).parameters
49
+
42
50
  super().__init__(namespace, **kwargs)
43
51
 
44
52
  def list_entry_points(self) -> list[EntryPoint]:
@@ -60,6 +68,25 @@ class RegistrableExtensionManager(ExtensionManager):
60
68
 
61
69
  return eps
62
70
 
71
+ def __compat_load_one_plugin(self, ep: EntryPoint) -> Extension | None: # pragma: no cover
72
+ if self._old_api:
73
+ # The 'verify_requirements' argument is needed
74
+ return self._load_one_plugin( # type: ignore[call-arg]
75
+ ep,
76
+ invoke_on_load=False,
77
+ invoke_args=(), # type: ignore[arg-type]
78
+ invoke_kwds={},
79
+ verify_requirements=False,
80
+ )
81
+
82
+ # The 'verify_requirements' argument is forbidden
83
+ return self._load_one_plugin( # type: ignore[call-arg]
84
+ ep,
85
+ invoke_on_load=False,
86
+ invoke_args=(), # type: ignore[arg-type]
87
+ invoke_kwds={},
88
+ )
89
+
63
90
  def register(self, entry_point: str) -> None:
64
91
  """Register an extension.
65
92
 
@@ -76,13 +103,10 @@ class RegistrableExtensionManager(ExtensionManager):
76
103
  msg = 'An extension with the same name already exist'
77
104
  raise ValueError(msg)
78
105
 
79
- ext = self._load_one_plugin(
80
- ep,
81
- invoke_on_load=False,
82
- invoke_args=(),
83
- invoke_kwds={},
84
- verify_requirements=False,
85
- )
106
+ ext = self.__compat_load_one_plugin(ep)
107
+ if ext is None: # pragma: no cover
108
+ return
109
+
86
110
  self.extensions.append(ext)
87
111
  if self._extensions_by_name is not None: # pragma: no branch
88
112
  self._extensions_by_name[ext.name] = ext
@@ -133,6 +157,7 @@ provider_manager = RegistrableExtensionManager(
133
157
  'opensubtitlescomvip = subliminal.providers.opensubtitlescom:OpenSubtitlesComVipProvider',
134
158
  'opensubtitlesvip = subliminal.providers.opensubtitles:OpenSubtitlesVipProvider',
135
159
  'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider',
160
+ 'subtis = subliminal.providers.subtis:SubtisProvider',
136
161
  'subtitulamos = subliminal.providers.subtitulamos:SubtitulamosProvider',
137
162
  'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider',
138
163
  ],
@@ -24,7 +24,6 @@ from subliminal.subtitle import Subtitle
24
24
  from subliminal.video import Episode, Movie, Video
25
25
 
26
26
  if TYPE_CHECKING:
27
- import os
28
27
  from collections.abc import Sequence, Set
29
28
  from http.client import HTTPSConnection
30
29
  from types import TracebackType
@@ -146,7 +145,7 @@ class Provider(Generic[S]):
146
145
  user_agent: str = f'Subliminal/{__short_version__}'
147
146
 
148
147
  @staticmethod
149
- def hash_video(video_path: str | os.PathLike) -> str | None:
148
+ def hash_video(video_path: str) -> str | None:
150
149
  """Hash the video to be used by the provider."""
151
150
  return None
152
151
 
@@ -200,7 +200,7 @@ class BSPlayerProvider(Provider):
200
200
  self.search_url = search_url or get_sub_domain()
201
201
 
202
202
  @staticmethod
203
- def hash_video(video_path: str | os.PathLike) -> str | None:
203
+ def hash_video(video_path: str) -> str | None:
204
204
  """Compute a hash using BSPlayer algorithm.
205
205
 
206
206
  :param str video_path: path of the video.
@@ -17,7 +17,6 @@ from subliminal.subtitle import Subtitle, fix_line_ending
17
17
  from . import Provider
18
18
 
19
19
  if TYPE_CHECKING:
20
- import os
21
20
  from collections.abc import Set
22
21
 
23
22
  from subliminal.video import Video
@@ -98,7 +97,7 @@ class NapiProjektProvider(Provider):
98
97
  self.session = None
99
98
 
100
99
  @staticmethod
101
- def hash_video(video_path: str | os.PathLike) -> str | None:
100
+ def hash_video(video_path: str) -> str | None:
102
101
  """Compute a hash using NapiProjekt's algorithm.
103
102
 
104
103
  :param str video_path: path of the video.
@@ -171,8 +171,9 @@ class OpenSubtitlesSubtitle(Subtitle):
171
171
  class OpenSubtitlesProvider(Provider):
172
172
  """OpenSubtitles Provider.
173
173
 
174
- :param str username: username.
175
- :param str password: password.
174
+ :param str username: username
175
+ :param str password: password
176
+ :param int timeout: timeout in seconds. Default to 10.
176
177
 
177
178
  """
178
179
 
@@ -279,7 +280,7 @@ class OpenSubtitlesProvider(Provider):
279
280
  language = Language.fromopensubtitles(subtitle_item['SubLanguageID'])
280
281
  hearing_impaired = bool(int(subtitle_item['SubHearingImpaired']))
281
282
  page_link = subtitle_item['SubtitlesLink']
282
- subtitle_id = int(subtitle_item['IDSubtitleFile'])
283
+ subtitle_id = str(int(subtitle_item['IDSubtitleFile']))
283
284
  matched_by = subtitle_item['MatchedBy']
284
285
  movie_kind = subtitle_item['MovieKind']
285
286
  moviehash = subtitle_item['MovieHash']
@@ -31,7 +31,7 @@ from subliminal.video import Episode, Movie, Video
31
31
  from . import Provider
32
32
 
33
33
  if TYPE_CHECKING:
34
- from collections.abc import Mapping, Set
34
+ from collections.abc import Generator, Mapping, Set
35
35
 
36
36
  C = TypeVar('C', bound=Callable)
37
37
 
@@ -370,8 +370,13 @@ def requires_auth(func: C) -> C:
370
370
  class OpenSubtitlesComProvider(Provider):
371
371
  """OpenSubtitles.com Provider.
372
372
 
373
- :param str username: username.
374
- :param str password: password.
373
+ :param str username: username
374
+ :param str password: password
375
+ :param str apikey: private API key
376
+ :param int max_result_pages: maximum number of result pages to process,
377
+ set to non-zero to search faster, although the correct match can be
378
+ missing. Default to 0 (unlimited).
379
+ :param int timeout: timeout in seconds. Default to 20.
375
380
 
376
381
  """
377
382
 
@@ -385,6 +390,7 @@ class OpenSubtitlesComProvider(Provider):
385
390
  username: str | None
386
391
  password: str | None
387
392
  apikey: str
393
+ max_result_pages: int
388
394
  timeout: int
389
395
  token_expires_at: datetime | None
390
396
  session: Session | None
@@ -395,6 +401,7 @@ class OpenSubtitlesComProvider(Provider):
395
401
  password: str | None = None,
396
402
  *,
397
403
  apikey: str | None = None,
404
+ max_result_pages: int = 0,
398
405
  timeout: int = 20,
399
406
  ) -> None:
400
407
  if any((username, password)) and not all((username, password)):
@@ -404,6 +411,7 @@ class OpenSubtitlesComProvider(Provider):
404
411
  self.username = username
405
412
  self.password = password
406
413
  self.apikey = apikey or OPENSUBTITLESCOM_API_KEY
414
+ self.max_result_pages = max_result_pages
407
415
  self.timeout = timeout
408
416
  self.token_expires_at = None
409
417
  self.session = None
@@ -587,24 +595,28 @@ class OpenSubtitlesComProvider(Provider):
587
595
 
588
596
  return r.json() # type: ignore[no-any-return]
589
597
 
590
- def _search(self, *, page: int = 1, **params: Any) -> list[dict[str, Any]]:
591
- # query the server
592
- logger.info('Searching subtitles %r', params)
598
+ def _search(self, **params: Any) -> Generator[dict[str, Any], None, None]:
599
+ page = 1
593
600
 
594
- # GET request and add page information
595
- response = self.api_get('subtitles', {'page': page, **params})
601
+ while True:
602
+ # Extended params with page
603
+ ext_params = {'page': page, **params}
604
+ logger.info('Searching subtitles %r', ext_params)
605
+ # GET request and add page information
606
+ response = self.api_get('subtitles', ext_params)
607
+ if not response or not response['data']:
608
+ break
609
+ yield from response['data']
596
610
 
597
- if not response or not response['data']:
598
- return []
611
+ page += 1
599
612
 
600
- ret = response['data']
613
+ # check that the maximum number of pages has not been exceeded
614
+ if self.max_result_pages > 0 and page > self.max_result_pages:
615
+ break
601
616
 
602
- # retrieve other pages maybe
603
- if 'total_pages' in response and page < response['total_pages']:
604
- # missing pages
605
- ret.extend(self._search(page=page + 1, **params))
606
-
607
- return ret # type: ignore[no-any-return]
617
+ # check if we fetched all pages already
618
+ if 'total_pages' not in response or page > response['total_pages']:
619
+ break
608
620
 
609
621
  def _make_query(
610
622
  self,
@@ -642,7 +654,8 @@ class OpenSubtitlesComProvider(Provider):
642
654
  if query:
643
655
  criterion.update({'query': query.replace("'", '')})
644
656
 
645
- if season and episode:
657
+ # Check for None to allow season=0 and episode=0
658
+ if season is not None and episode is not None:
646
659
  criterion.update({'season_number': season, 'episode_number': episode})
647
660
  if year:
648
661
  criterion.update({'year': year})
@@ -696,14 +709,11 @@ class OpenSubtitlesComProvider(Provider):
696
709
  # add the language and query the server
697
710
  criterion.update({'languages': ','.join(sorted(lang.opensubtitlescom for lang in languages))})
698
711
 
699
- # query the server
700
- responses = self._search(**criterion)
701
-
702
712
  imdb_match = 'imdb_id' in criterion or 'show_imdb_id' in criterion
703
713
  tmdb_match = 'tmdb_id' in criterion or 'show_tmdb_id' in criterion
704
714
 
705
715
  # loop over subtitle items
706
- for response in responses:
716
+ for response in self._search(**criterion):
707
717
  # read single response
708
718
  subtitle = self.subtitle_class.from_response(
709
719
  response,