subliminal 2.5.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 (325) hide show
  1. {subliminal-2.5.0 → subliminal-2.6.0}/HISTORY.rst +23 -0
  2. {subliminal-2.5.0 → subliminal-2.6.0}/PKG-INFO +6 -2
  3. {subliminal-2.5.0 → subliminal-2.6.0}/README.rst +5 -1
  4. {subliminal-2.5.0 → subliminal-2.6.0}/docs/api/providers.rst +5 -0
  5. subliminal-2.6.0/docs/assets/demo.gif +0 -0
  6. {subliminal-2.5.0 → subliminal-2.6.0}/docs/user/configuration_file.rst +1 -1
  7. {subliminal-2.5.0 → subliminal-2.6.0}/docs/user/how_it_works.rst +1 -0
  8. {subliminal-2.5.0 → subliminal-2.6.0}/pyproject.toml +1 -0
  9. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/extensions.py +1 -0
  10. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/providers/__init__.py +1 -2
  11. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/providers/bsplayer.py +1 -1
  12. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/providers/napiprojekt.py +1 -2
  13. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/providers/opensubtitles.py +0 -1
  14. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/providers/opensubtitlescom.py +20 -25
  15. subliminal-2.6.0/src/subliminal/providers/subtis.py +336 -0
  16. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/providers/subtitulamos.py +7 -3
  17. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/refiners/hash.py +12 -8
  18. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/refiners/tvdb.py +1 -23
  19. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/utils.py +4 -0
  20. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_get_id.yaml +15 -17
  21. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_get_title.yaml +15 -17
  22. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_get_wrong_id.yaml +11 -13
  23. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_get_wrong_title.yaml +15 -17
  24. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_refine_episode.yaml +11 -13
  25. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_refine_episode_original_series.yaml +68 -80
  26. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_refine_episode_with_country.yaml +17 -19
  27. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_refine_episode_with_country_hoc_us.yaml +15 -18
  28. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_refine_episode_year.yaml +11 -13
  29. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_refine_movie.yaml +49 -54
  30. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_refine_movie_guess_alternative_title.yaml +15 -16
  31. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_search.yaml +11 -13
  32. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_search_page.yaml +11 -13
  33. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_search_type.yaml +11 -13
  34. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_search_wrong_title.yaml +15 -17
  35. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/omdb/test_search_year.yaml +11 -13
  36. subliminal-2.6.0/tests/cassettes/subtis/test_download_subtitle.yaml +7982 -0
  37. subliminal-2.6.0/tests/cassettes/subtis/test_list_subtitles_hash.yaml +258 -0
  38. subliminal-2.6.0/tests/cassettes/subtis/test_list_subtitles_movie.yaml +997 -0
  39. subliminal-2.6.0/tests/cassettes/subtis/test_list_subtitles_movie_alternative.yaml +682 -0
  40. subliminal-2.6.0/tests/cassettes/subtis/test_list_subtitles_movie_with_size.yaml +367 -0
  41. subliminal-2.6.0/tests/cassettes/subtis/test_list_subtitles_no_match.yaml +632 -0
  42. subliminal-2.6.0/tests/cassettes/subtitulamos/test_list_subtitles_not_exist_series.yaml +110 -0
  43. {subliminal-2.5.0 → subliminal-2.6.0}/tests/conftest.py +2 -2
  44. {subliminal-2.5.0 → subliminal-2.6.0}/tests/providers/test_providers.py +3 -3
  45. subliminal-2.6.0/tests/providers/test_subtis.py +390 -0
  46. {subliminal-2.5.0 → subliminal-2.6.0}/tests/refiners/test_hash.py +1 -0
  47. {subliminal-2.5.0 → subliminal-2.6.0}/tests/refiners/test_omdb.py +13 -10
  48. {subliminal-2.5.0 → subliminal-2.6.0}/tests/refiners/test_tvdb.py +0 -22
  49. {subliminal-2.5.0 → subliminal-2.6.0}/tests/test_utils.py +1 -0
  50. subliminal-2.5.0/tests/cassettes/subtitulamos/test_list_subtitles_not_exist_series.yaml +0 -114
  51. subliminal-2.5.0/tests/cassettes/tvdb/test_refresh_token.yaml +0 -100
  52. subliminal-2.5.0/tests/cassettes/tvdb/test_token_needs_refresh.yaml +0 -50
  53. {subliminal-2.5.0 → subliminal-2.6.0}/.dockerignore +0 -0
  54. {subliminal-2.5.0 → subliminal-2.6.0}/.github/actions/prepare-tests/action.yaml +0 -0
  55. {subliminal-2.5.0 → subliminal-2.6.0}/.github/workflows/CI.yaml +0 -0
  56. {subliminal-2.5.0 → subliminal-2.6.0}/.github/workflows/coverage.yaml +0 -0
  57. {subliminal-2.5.0 → subliminal-2.6.0}/.github/workflows/docker-build.yaml +0 -0
  58. {subliminal-2.5.0 → subliminal-2.6.0}/.github/workflows/prepare-release-pr.yaml +0 -0
  59. {subliminal-2.5.0 → subliminal-2.6.0}/.github/workflows/publish-release.yaml +0 -0
  60. {subliminal-2.5.0 → subliminal-2.6.0}/.github/workflows/publish.yaml +0 -0
  61. {subliminal-2.5.0 → subliminal-2.6.0}/.github/workflows/scheduled-tests.yaml +0 -0
  62. {subliminal-2.5.0 → subliminal-2.6.0}/.github/workflows/tag-release.yaml +0 -0
  63. {subliminal-2.5.0 → subliminal-2.6.0}/.github/zizmor.yml +0 -0
  64. {subliminal-2.5.0 → subliminal-2.6.0}/.gitignore +0 -0
  65. {subliminal-2.5.0 → subliminal-2.6.0}/.pre-commit-config.yaml +0 -0
  66. {subliminal-2.5.0 → subliminal-2.6.0}/.python-version-default +0 -0
  67. {subliminal-2.5.0 → subliminal-2.6.0}/.readthedocs.yaml +0 -0
  68. {subliminal-2.5.0 → subliminal-2.6.0}/CONTRIBUTING.md +0 -0
  69. {subliminal-2.5.0 → subliminal-2.6.0}/Dockerfile +0 -0
  70. {subliminal-2.5.0 → subliminal-2.6.0}/LICENSE +0 -0
  71. {subliminal-2.5.0 → subliminal-2.6.0}/MANIFEST.in +0 -0
  72. {subliminal-2.5.0 → subliminal-2.6.0}/RELEASING.md +0 -0
  73. {subliminal-2.5.0 → subliminal-2.6.0}/changelog.d/.gitignore +0 -0
  74. {subliminal-2.5.0 → subliminal-2.6.0}/docs/Makefile +0 -0
  75. {subliminal-2.5.0 → subliminal-2.6.0}/docs/_intersphinx/README.md +0 -0
  76. {subliminal-2.5.0 → subliminal-2.6.0}/docs/_intersphinx/importlib_metadata_objects.inv +0 -0
  77. {subliminal-2.5.0 → subliminal-2.6.0}/docs/_intersphinx/importlib_metadata_objects.txt +0 -0
  78. {subliminal-2.5.0 → subliminal-2.6.0}/docs/_static/.gitignore +0 -0
  79. {subliminal-2.5.0 → subliminal-2.6.0}/docs/api/cache.rst +0 -0
  80. {subliminal-2.5.0 → subliminal-2.6.0}/docs/api/cli.rst +0 -0
  81. {subliminal-2.5.0 → subliminal-2.6.0}/docs/api/core.rst +0 -0
  82. {subliminal-2.5.0 → subliminal-2.6.0}/docs/api/exceptions.rst +0 -0
  83. {subliminal-2.5.0 → subliminal-2.6.0}/docs/api/extensions.rst +0 -0
  84. {subliminal-2.5.0 → subliminal-2.6.0}/docs/api/refiners.rst +0 -0
  85. {subliminal-2.5.0 → subliminal-2.6.0}/docs/api/score.rst +0 -0
  86. {subliminal-2.5.0 → subliminal-2.6.0}/docs/api/subtitle.rst +0 -0
  87. {subliminal-2.5.0 → subliminal-2.6.0}/docs/api/utils.rst +0 -0
  88. {subliminal-2.5.0 → subliminal-2.6.0}/docs/api/video.rst +0 -0
  89. {subliminal-2.5.0/docs → subliminal-2.6.0/docs/assets}/config.toml +0 -0
  90. {subliminal-2.5.0 → subliminal-2.6.0}/docs/cassettes/test_usage.yaml +0 -0
  91. {subliminal-2.5.0 → subliminal-2.6.0}/docs/conf.py +0 -0
  92. {subliminal-2.5.0 → subliminal-2.6.0}/docs/conftest.py +0 -0
  93. {subliminal-2.5.0 → subliminal-2.6.0}/docs/index.rst +0 -0
  94. {subliminal-2.5.0 → subliminal-2.6.0}/docs/make.bat +0 -0
  95. {subliminal-2.5.0 → subliminal-2.6.0}/docs/user/changelog.rst +0 -0
  96. {subliminal-2.5.0 → subliminal-2.6.0}/docs/user/cli.rst +0 -0
  97. {subliminal-2.5.0 → subliminal-2.6.0}/docs/user/installation.rst +0 -0
  98. {subliminal-2.5.0 → subliminal-2.6.0}/docs/user/provider_guide.rst +0 -0
  99. {subliminal-2.5.0 → subliminal-2.6.0}/docs/user/usage.rst +0 -0
  100. {subliminal-2.5.0 → subliminal-2.6.0}/hatch.toml +0 -0
  101. {subliminal-2.5.0 → subliminal-2.6.0}/scripts/.gitignore +0 -0
  102. {subliminal-2.5.0 → subliminal-2.6.0}/scripts/prepare-release-pr.py +0 -0
  103. {subliminal-2.5.0 → subliminal-2.6.0}/scripts/prepare_tests.py +0 -0
  104. {subliminal-2.5.0 → subliminal-2.6.0}/scripts/release.py +0 -0
  105. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/__init__.py +0 -0
  106. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/__main__.py +0 -0
  107. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/archives.py +0 -0
  108. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/cache.py +0 -0
  109. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/cli/__init__.py +0 -0
  110. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/cli/cli.py +0 -0
  111. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/cli/commands/__init__.py +0 -0
  112. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/cli/commands/_format.py +0 -0
  113. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/cli/commands/download_best.py +0 -0
  114. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/cli/generate_config.py +0 -0
  115. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/cli/helpers.py +0 -0
  116. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/converters/__init__.py +0 -0
  117. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/converters/addic7ed.py +0 -0
  118. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/converters/opensubtitles.py +0 -0
  119. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/converters/opensubtitlescom.py +0 -0
  120. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/converters/subtitulamos.py +0 -0
  121. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/converters/tvsubtitles.py +0 -0
  122. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/core.py +0 -0
  123. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/exceptions.py +0 -0
  124. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/matches.py +0 -0
  125. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/providers/addic7ed.py +0 -0
  126. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/providers/gestdown.py +0 -0
  127. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/providers/mock.py +0 -0
  128. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/providers/podnapisi.py +0 -0
  129. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/providers/tvsubtitles.py +0 -0
  130. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/py.typed +0 -0
  131. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/refiners/__init__.py +0 -0
  132. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/refiners/metadata.py +0 -0
  133. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/refiners/omdb.py +0 -0
  134. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/refiners/tmdb.py +0 -0
  135. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/score.py +0 -0
  136. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/subtitle.py +0 -0
  137. {subliminal-2.5.0 → subliminal-2.6.0}/src/subliminal/video.py +0 -0
  138. {subliminal-2.5.0 → subliminal-2.6.0}/tests/__init__.py +0 -0
  139. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_download_subtitle.yaml +0 -0
  140. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_get_show_id.yaml +0 -0
  141. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_get_show_id_country.yaml +0 -0
  142. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_get_show_id_quote_dots_mixed_case.yaml +0 -0
  143. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_get_show_id_with_comma.yaml +0 -0
  144. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_get_show_id_year.yaml +0 -0
  145. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_get_show_ids.yaml +0 -0
  146. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_list_subtitles.yaml +0 -0
  147. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_list_subtitles_episode_alternative_series.yaml +0 -0
  148. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_list_subtitles_show_with_asterisk.yaml +0 -0
  149. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query.yaml +0 -0
  150. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query_no_year.yaml +0 -0
  151. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query_parsing.yaml +0 -0
  152. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query_parsing_colon.yaml +0 -0
  153. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query_parsing_dash.yaml +0 -0
  154. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query_parsing_quote_dots_mixed_case.yaml +0 -0
  155. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query_wrong_series.yaml +0 -0
  156. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/addic7ed/test_query_year.yaml +0 -0
  157. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/bsplayer/test_download_subtitle.yaml +0 -0
  158. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/bsplayer/test_list_subtitles_hash.yaml +0 -0
  159. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/bsplayer/test_login.yaml +0 -0
  160. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/bsplayer/test_logout.yaml +0 -0
  161. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/bsplayer/test_query_hash_size.yaml +0 -0
  162. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/cli/test_cli_download.yaml +0 -0
  163. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_download_subtitle.yaml +0 -0
  164. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_download_with_bom.yaml +0 -0
  165. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_get_title_and_show_id_alternative_name.yaml +0 -0
  166. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_get_title_and_show_id_no_year.yaml +0 -0
  167. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_get_title_and_show_id_only_title.yaml +0 -0
  168. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_get_title_and_show_id_with_tvdb_id.yaml +0 -0
  169. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_list_subtitles.yaml +0 -0
  170. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_list_subtitles_episode_alternative_series.yaml +0 -0
  171. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query.yaml +0 -0
  172. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query_all_series.yaml +0 -0
  173. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query_no_language.yaml +0 -0
  174. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query_parsing.yaml +0 -0
  175. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query_parsing_colon.yaml +0 -0
  176. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query_parsing_dash.yaml +0 -0
  177. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query_parsing_quote_dots_mixed_case.yaml +0 -0
  178. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_query_wrong_series.yaml +0 -0
  179. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_search_show_id.yaml +0 -0
  180. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_search_show_id_error.yaml +0 -0
  181. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_search_show_id_incomplete.yaml +0 -0
  182. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_search_show_id_no_year.yaml +0 -0
  183. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_search_show_id_quote.yaml +0 -0
  184. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_search_show_series_tvdb_id.yaml +0 -0
  185. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/gestdown/test_show_with_asterisk.yaml +0 -0
  186. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/napiprojekt/test_download_subtitles.yaml +0 -0
  187. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/napiprojekt/test_list_subtitles.yaml +0 -0
  188. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/napiprojekt/test_query_microdvd.yaml +0 -0
  189. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/napiprojekt/test_query_srt.yaml +0 -0
  190. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/napiprojekt/test_query_srt_reencode.yaml +0 -0
  191. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/napiprojekt/test_query_wrong_hash.yaml +0 -0
  192. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_download_subtitle.yaml +0 -0
  193. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_list_subtitles_episode.yaml +0 -0
  194. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_list_subtitles_movie.yaml +0 -0
  195. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_list_subtitles_movie_no_hash.yaml +0 -0
  196. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_login_bad_password.yaml +0 -0
  197. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_login_vip_bad_password.yaml +0 -0
  198. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_login_vip_login.yaml +0 -0
  199. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_no_operation.yaml +0 -0
  200. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_query_hash_size.yaml +0 -0
  201. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_query_imdb_id.yaml +0 -0
  202. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_query_not_enough_information.yaml +0 -0
  203. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_query_query_episode.yaml +0 -0
  204. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_query_query_movie.yaml +0 -0
  205. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_query_query_season_episode.yaml +0 -0
  206. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitles/test_query_wrong_hash_wrong_size.yaml +0 -0
  207. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_download_subtitle.yaml +0 -0
  208. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_list_subtitles_episode.yaml +0 -0
  209. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_list_subtitles_movie.yaml +0 -0
  210. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_list_subtitles_movie_no_hash.yaml +0 -0
  211. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_login.yaml +0 -0
  212. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_login_bad_password.yaml +0 -0
  213. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_logout.yaml +0 -0
  214. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_hash_size.yaml +0 -0
  215. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_imdb_id.yaml +0 -0
  216. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_max_result_pages.yaml +0 -0
  217. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_query_episode.yaml +0 -0
  218. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_query_movie.yaml +0 -0
  219. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_query_season_episode.yaml +0 -0
  220. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_tag_movie.yaml +0 -0
  221. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_query_wrong_hash_wrong_size.yaml +0 -0
  222. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_tag_match.yaml +0 -0
  223. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/opensubtitlescom/test_user_infos.yaml +0 -0
  224. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/podnapisi/test_download_subtitle.yaml +0 -0
  225. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/podnapisi/test_download_subtitles_with_title_unicode.yaml +0 -0
  226. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/podnapisi/test_list_subtitles_episode.yaml +0 -0
  227. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/podnapisi/test_list_subtitles_episode_alternative_series.yaml +0 -0
  228. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/podnapisi/test_list_subtitles_movie.yaml +0 -0
  229. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/podnapisi/test_query_episode.yaml +0 -0
  230. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/podnapisi/test_query_movie.yaml +0 -0
  231. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/providers/test_download_best_subtitles.yaml +0 -0
  232. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/providers/test_download_best_subtitles_min_score.yaml +0 -0
  233. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/providers/test_download_best_subtitles_only_one.yaml +0 -0
  234. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/providers/test_list_subtitles_providers_download.yaml +0 -0
  235. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_download_subtitle.yaml +0 -0
  236. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_download_subtitle_first_episode.yaml +0 -0
  237. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_download_subtitle_foo.yaml +0 -0
  238. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_download_subtitle_last_season.yaml +0 -0
  239. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_download_subtitle_year.yaml +0 -0
  240. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_list_subtitles_not_exist_episode.yaml +0 -0
  241. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_list_subtitles_not_exist_language.yaml +0 -0
  242. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_list_subtitles_not_exist_season.yaml +0 -0
  243. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/subtitulamos/test_list_subtitles_with_spanish_non_mx_search.yaml +0 -0
  244. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_get_movie_id.yaml +0 -0
  245. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_get_movie_id_failed.yaml +0 -0
  246. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_get_series_id.yaml +0 -0
  247. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_get_series_wrong_id.yaml +0 -0
  248. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_refine_episode.yaml +0 -0
  249. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_refine_episode_original_series.yaml +0 -0
  250. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_refine_episode_with_country.yaml +0 -0
  251. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_refine_episode_with_country_hoc_us.yaml +0 -0
  252. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_refine_episode_year.yaml +0 -0
  253. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_refine_movie.yaml +0 -0
  254. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_refine_movie_guess_alternative_title.yaml +0 -0
  255. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_search_movie.yaml +0 -0
  256. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_search_movie_page.yaml +0 -0
  257. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_search_movie_wrong_title.yaml +0 -0
  258. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_search_movie_year.yaml +0 -0
  259. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_search_series.yaml +0 -0
  260. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tmdb/test_search_series_wrong_name.yaml +0 -0
  261. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_get_series.yaml +0 -0
  262. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_get_series_actors.yaml +0 -0
  263. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_get_series_actors_wrong_id.yaml +0 -0
  264. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_get_series_wrong_id.yaml +0 -0
  265. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_login.yaml +0 -0
  266. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_login_error.yaml +0 -0
  267. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_query_series_episodes.yaml +0 -0
  268. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_query_series_episodes_wrong_season.yaml +0 -0
  269. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine.yaml +0 -0
  270. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_ambiguous.yaml +0 -0
  271. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_ambiguous_2.yaml +0 -0
  272. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_episode_alternative_series.yaml +0 -0
  273. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_episode_no_year.yaml +0 -0
  274. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_episode_partial.yaml +0 -0
  275. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_episode_with_comma.yaml +0 -0
  276. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_episode_with_country.yaml +0 -0
  277. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_episode_with_country_hoc_us.yaml +0 -0
  278. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_refine_episode_year.yaml +0 -0
  279. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_search_series.yaml +0 -0
  280. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvdb/test_search_series_wrong_name.yaml +0 -0
  281. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_download_subtitle.yaml +0 -0
  282. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_download_subtitle_with_quote.yaml +0 -0
  283. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_get_episode_ids.yaml +0 -0
  284. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_get_episode_ids_wrong_season.yaml +0 -0
  285. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_list_subtitles.yaml +0 -0
  286. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_list_subtitles_episode_alternative_series.yaml +0 -0
  287. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_query.yaml +0 -0
  288. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_query_no_year.yaml +0 -0
  289. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_query_with_quote.yaml +0 -0
  290. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_query_wrong_episode.yaml +0 -0
  291. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_query_wrong_series.yaml +0 -0
  292. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id.yaml +0 -0
  293. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id_ambiguous.yaml +0 -0
  294. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id_error.yaml +0 -0
  295. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id_incomplete.yaml +0 -0
  296. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id_no_year.yaml +0 -0
  297. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id_uk.yaml +0 -0
  298. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id_us.yaml +0 -0
  299. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cassettes/tvsubtitles/test_search_show_id_year_in_title.yaml +0 -0
  300. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cli/test_cli.py +0 -0
  301. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cli/test_download.py +0 -0
  302. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cli/test_generate_config.py +0 -0
  303. {subliminal-2.5.0 → subliminal-2.6.0}/tests/cli/test_helpers.py +0 -0
  304. {subliminal-2.5.0 → subliminal-2.6.0}/tests/providers/test_addic7ed.py +0 -0
  305. {subliminal-2.5.0 → subliminal-2.6.0}/tests/providers/test_bsplayer.py +0 -0
  306. {subliminal-2.5.0 → subliminal-2.6.0}/tests/providers/test_gestdown.py +0 -0
  307. {subliminal-2.5.0 → subliminal-2.6.0}/tests/providers/test_napiprojekt.py +0 -0
  308. {subliminal-2.5.0 → subliminal-2.6.0}/tests/providers/test_opensubtitles.py +0 -0
  309. {subliminal-2.5.0 → subliminal-2.6.0}/tests/providers/test_opensubtitlescom.py +0 -0
  310. {subliminal-2.5.0 → subliminal-2.6.0}/tests/providers/test_podnapisi.py +0 -0
  311. {subliminal-2.5.0 → subliminal-2.6.0}/tests/providers/test_subtitulamos.py +0 -0
  312. {subliminal-2.5.0 → subliminal-2.6.0}/tests/providers/test_tvsubtitles.py +0 -0
  313. {subliminal-2.5.0 → subliminal-2.6.0}/tests/refiners/test_metadata.py +0 -0
  314. {subliminal-2.5.0 → subliminal-2.6.0}/tests/refiners/test_tmdb.py +0 -0
  315. {subliminal-2.5.0 → subliminal-2.6.0}/tests/test_archives.py +0 -0
  316. {subliminal-2.5.0 → subliminal-2.6.0}/tests/test_cache.py +0 -0
  317. {subliminal-2.5.0 → subliminal-2.6.0}/tests/test_core.py +0 -0
  318. {subliminal-2.5.0 → subliminal-2.6.0}/tests/test_extensions.py +0 -0
  319. {subliminal-2.5.0 → subliminal-2.6.0}/tests/test_matches.py +0 -0
  320. {subliminal-2.5.0 → subliminal-2.6.0}/tests/test_provider.py +0 -0
  321. {subliminal-2.5.0 → subliminal-2.6.0}/tests/test_provider_pool.py +0 -0
  322. {subliminal-2.5.0 → subliminal-2.6.0}/tests/test_score.py +0 -0
  323. {subliminal-2.5.0 → subliminal-2.6.0}/tests/test_subtitle.py +0 -0
  324. {subliminal-2.5.0 → subliminal-2.6.0}/tests/test_video.py +0 -0
  325. {subliminal-2.5.0 → subliminal-2.6.0}/tox.ini +0 -0
@@ -2,6 +2,29 @@
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
+
5
28
  2.5.0 (2025-12-14)
6
29
  ^^^^^^^^^^^^^^^^^^
7
30
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: subliminal
3
- Version: 2.5.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
@@ -103,6 +103,10 @@ Subtitles, faster than your thoughts.
103
103
 
104
104
  Usage
105
105
  -----
106
+
107
+ .. image:: https://github.com/Diaoul/subliminal/blob/main/docs/assets/demo.gif
108
+ :alt: Demo of the app CLI usage
109
+
106
110
  CLI
107
111
  ^^^
108
112
  Download English subtitles::
@@ -120,7 +124,7 @@ By default it looks for a `subliminal.toml` file in the default configuration fo
120
124
  (see the CLI help for the exact platform-specific default path).
121
125
  Or use the `-c` option to specify the path to the configuration file.
122
126
 
123
- `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>`__
124
128
  or use the `generate_default_config` function from the `subliminal.cli` module to generate a
125
129
  configuration file with all the options and their default values::
126
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
@@ -34,7 +34,7 @@ Example
34
34
 
35
35
  An example of configuration file:
36
36
 
37
- .. literalinclude:: ../config.toml
37
+ .. literalinclude:: ../assets/config.toml
38
38
  :language: toml
39
39
 
40
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
 
@@ -106,6 +106,7 @@ opensubtitlesvip = "subliminal.providers.opensubtitles:OpenSubtitlesVipProvider"
106
106
  opensubtitlescom = "subliminal.providers.opensubtitlescom:OpenSubtitlesComProvider"
107
107
  opensubtitlescomvip = "subliminal.providers.opensubtitlescom:OpenSubtitlesComVipProvider"
108
108
  podnapisi = "subliminal.providers.podnapisi:PodnapisiProvider"
109
+ subtis = "subliminal.providers.subtis:SubtisProvider"
109
110
  subtitulamos = "subliminal.providers.subtitulamos:SubtitulamosProvider"
110
111
  tvsubtitles = "subliminal.providers.tvsubtitles:TVsubtitlesProvider"
111
112
 
@@ -157,6 +157,7 @@ provider_manager = RegistrableExtensionManager(
157
157
  'opensubtitlescomvip = subliminal.providers.opensubtitlescom:OpenSubtitlesComVipProvider',
158
158
  'opensubtitlesvip = subliminal.providers.opensubtitles:OpenSubtitlesVipProvider',
159
159
  'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider',
160
+ 'subtis = subliminal.providers.subtis:SubtisProvider',
160
161
  'subtitulamos = subliminal.providers.subtitulamos:SubtitulamosProvider',
161
162
  'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider',
162
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.
@@ -196,7 +196,6 @@ class OpenSubtitlesProvider(Provider):
196
196
  username: str | None = None,
197
197
  password: str | None = None,
198
198
  *,
199
- max_result_pages: int = 0,
200
199
  timeout: int = 10,
201
200
  ) -> None:
202
201
  transport = TimeoutSafeTransport(timeout=timeout, user_agent='VLSub')
@@ -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
 
@@ -595,30 +595,28 @@ class OpenSubtitlesComProvider(Provider):
595
595
 
596
596
  return r.json() # type: ignore[no-any-return]
597
597
 
598
- def _search(self, *, page: int = 1, **params: Any) -> list[dict[str, Any]]:
599
- # Extended params with page
600
- ext_params = {'page': page, **params}
598
+ def _search(self, **params: Any) -> Generator[dict[str, Any], None, None]:
599
+ page = 1
601
600
 
602
- # query the server
603
- logger.info('Searching subtitles %r', ext_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']
604
610
 
605
- # GET request and add page information
606
- response = self.api_get('subtitles', ext_params)
611
+ page += 1
607
612
 
608
- if not response or not response['data']:
609
- return []
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
610
616
 
611
- ret = response['data']
612
-
613
- # check that the maximum number of pages has not been exceeded
614
- allow_more_pages = self.max_result_pages <= 0 or page < self.max_result_pages
615
-
616
- # retrieve other pages maybe
617
- if allow_more_pages and 'total_pages' in response and page < response['total_pages']:
618
- # missing pages
619
- ret.extend(self._search(page=page + 1, **params))
620
-
621
- 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
622
620
 
623
621
  def _make_query(
624
622
  self,
@@ -711,14 +709,11 @@ class OpenSubtitlesComProvider(Provider):
711
709
  # add the language and query the server
712
710
  criterion.update({'languages': ','.join(sorted(lang.opensubtitlescom for lang in languages))})
713
711
 
714
- # query the server
715
- responses = self._search(**criterion)
716
-
717
712
  imdb_match = 'imdb_id' in criterion or 'show_imdb_id' in criterion
718
713
  tmdb_match = 'tmdb_id' in criterion or 'show_tmdb_id' in criterion
719
714
 
720
715
  # loop over subtitle items
721
- for response in responses:
716
+ for response in self._search(**criterion):
722
717
  # read single response
723
718
  subtitle = self.subtitle_class.from_response(
724
719
  response,
@@ -0,0 +1,336 @@
1
+ """Provider for Subtis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from typing import TYPE_CHECKING, Any, ClassVar, cast
8
+ from urllib.parse import quote
9
+
10
+ from babelfish import Language
11
+ from guessit import guessit
12
+ from requests import Session
13
+ from requests.exceptions import HTTPError, JSONDecodeError, RequestException
14
+
15
+ from subliminal.exceptions import NotInitializedProviderError, ProviderError
16
+ from subliminal.matches import guess_matches
17
+ from subliminal.subtitle import Subtitle
18
+ from subliminal.video import Movie
19
+
20
+ from . import Provider
21
+
22
+ if TYPE_CHECKING:
23
+ from collections.abc import Set
24
+
25
+ from subliminal.video import Video
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ subtis_languages: set[Language] = {
30
+ Language('spa', country)
31
+ for country in [
32
+ 'AR',
33
+ 'BO',
34
+ 'CL',
35
+ 'CO',
36
+ 'CR',
37
+ 'DO',
38
+ 'EC',
39
+ 'GT',
40
+ 'HN',
41
+ 'MX',
42
+ 'NI',
43
+ 'PA',
44
+ 'PE',
45
+ 'PR',
46
+ 'PY',
47
+ 'SV',
48
+ 'US',
49
+ 'UY',
50
+ 'VE',
51
+ ]
52
+ } | {Language('spa')}
53
+
54
+
55
+ class SubtisSubtitle(Subtitle):
56
+ """Subtis Subtitle."""
57
+
58
+ provider_name: ClassVar[str] = 'subtis'
59
+
60
+ def __init__(
61
+ self,
62
+ language: Language,
63
+ subtitle_id: str = '',
64
+ *,
65
+ page_link: str | None = None,
66
+ title: str | None = None,
67
+ download_link: str | None = None,
68
+ is_synced: bool = True,
69
+ video_hash: str | None = None,
70
+ ) -> None:
71
+ super().__init__(
72
+ language=language,
73
+ subtitle_id=subtitle_id,
74
+ hearing_impaired=False,
75
+ page_link=page_link,
76
+ )
77
+ self.title = title
78
+ self.download_link = download_link
79
+ self.is_synced = is_synced
80
+ self.video_hash = video_hash
81
+
82
+ @property
83
+ def info(self) -> str:
84
+ """Information about the subtitle."""
85
+ if not self.title:
86
+ return self.id
87
+ sync_indicator = '' if self.is_synced else ' [fuzzy match]'
88
+ return f'{self.title}{sync_indicator}'
89
+
90
+ def get_matches(self, video: Video) -> set[str]:
91
+ """Get the matches against the `video`."""
92
+ matches: set[str] = set()
93
+
94
+ if not isinstance(video, Movie):
95
+ return matches
96
+
97
+ if self.video_hash and 'subtis' in video.hashes and self.video_hash == video.hashes['subtis']:
98
+ matches.add('hash')
99
+
100
+ if self.is_synced and video.name:
101
+ matches |= guess_matches(video, guessit(os.path.basename(video.name), {'type': 'movie'}))
102
+ elif self.title:
103
+ matches |= guess_matches(video, guessit(self.title, {'type': 'movie'}))
104
+
105
+ return matches
106
+
107
+
108
+ class SubtisProvider(Provider[SubtisSubtitle]):
109
+ """Subtis Provider.
110
+
111
+ Provides Spanish subtitles for movies from the subt.is API.
112
+ """
113
+
114
+ languages: ClassVar[Set[Language]] = subtis_languages
115
+ video_types: ClassVar = (Movie,)
116
+ subtitle_class: ClassVar = SubtisSubtitle
117
+
118
+ server_url: ClassVar[str] = 'https://api.subt.is/v1'
119
+
120
+ timeout: int
121
+ session: Session | None
122
+
123
+ def __init__(self, *, timeout: int = 10) -> None:
124
+ self.timeout = timeout
125
+ self.session = None
126
+
127
+ @staticmethod
128
+ def hash_video(video_path: str) -> str | None:
129
+ """Compute a hash using OpenSubtitles' algorithm.
130
+
131
+ :param video_path: path of the video file.
132
+ :return: the hash string, or None if file is too small or error occurs.
133
+ """
134
+ from subliminal.refiners.hash import hash_opensubtitles
135
+
136
+ video_path = os.fspath(video_path)
137
+ try:
138
+ return hash_opensubtitles(video_path)
139
+ except OSError:
140
+ logger.debug('Could not compute hash for %s', video_path)
141
+ return None
142
+
143
+ def initialize(self) -> None:
144
+ """Initialize the provider."""
145
+ self.session = Session()
146
+ self.session.headers['User-Agent'] = self.user_agent
147
+ self.session.headers['Accept'] = 'application/json'
148
+
149
+ def terminate(self) -> None:
150
+ """Terminate the provider."""
151
+ if self.session is None:
152
+ raise NotInitializedProviderError
153
+ self.session.close()
154
+ self.session = None
155
+
156
+ def _session_request(self, url: str) -> dict[str, Any] | None:
157
+ """Perform a GET request to the provider."""
158
+ if self.session is None:
159
+ raise NotInitializedProviderError
160
+
161
+ try:
162
+ r = self.session.get(url, timeout=self.timeout)
163
+ except RequestException:
164
+ logger.exception('Request error for %s', url)
165
+ raise SubtisError from None
166
+
167
+ if r.status_code in (400, 404):
168
+ return None
169
+
170
+ try:
171
+ r.raise_for_status()
172
+ except HTTPError:
173
+ msg = f'Unexpected status {r.status_code}'
174
+ logger.warning('%s for %s', msg, url)
175
+ raise SubtisError(msg) from None
176
+
177
+ try:
178
+ return cast('dict[str, Any]', r.json())
179
+ except JSONDecodeError:
180
+ msg = 'Invalid JSON response'
181
+ logger.exception('%s for %s', msg, url)
182
+ raise SubtisError(msg) from None
183
+
184
+ def _parse_response(self, data: dict[str, Any]) -> tuple[str, str] | None:
185
+ """Parse API response and extract subtitle link and title.
186
+
187
+ Returns:
188
+ Tuple of (subtitle_link, title_name), or None if required fields are missing.
189
+ """
190
+ subtitle_data = data.get('subtitle')
191
+ if not isinstance(subtitle_data, dict):
192
+ return None
193
+
194
+ subtitle_link = subtitle_data.get('subtitle_link')
195
+ if not isinstance(subtitle_link, str) or not subtitle_link:
196
+ return None
197
+
198
+ title_data = data.get('title', {})
199
+ title_name = title_data.get('title_name', 'Unknown') if isinstance(title_data, dict) else 'Unknown'
200
+
201
+ return subtitle_link, str(title_name)
202
+
203
+ def query(self, video: Movie, languages: Set[Language]) -> list[SubtisSubtitle]:
204
+ """Query the provider for subtitles using cascade search.
205
+
206
+ Cascade order (from most to least specific):
207
+ 1. Hash - OpenSubtitles video hash (most precise)
208
+ 2. Bytes - File size match
209
+ 3. Filename - Exact filename match
210
+ 4. Alternative - Fuzzy match (fallback)
211
+ """
212
+ if not video.name:
213
+ return []
214
+
215
+ language = next((lang for lang in languages if lang.alpha3 == 'spa'), Language('spa'))
216
+
217
+ filename = os.path.basename(video.name)
218
+ encoded_filename = quote(filename, safe='')
219
+
220
+ if video.name and os.path.isfile(video.name):
221
+ video_hash = video.hashes.get('subtis')
222
+ if video_hash:
223
+ hash_url = f'{self.server_url}/subtitle/find/file/hash/{video_hash}'
224
+ logger.info('Searching subtitles by hash for %s', filename)
225
+ data = self._session_request(hash_url)
226
+ if data:
227
+ parsed = self._parse_response(data)
228
+ if parsed:
229
+ subtitle_link, title_name = parsed
230
+ logger.debug('Found subtitle via hash search')
231
+ return [
232
+ SubtisSubtitle(
233
+ language=language,
234
+ subtitle_id=subtitle_link,
235
+ page_link=hash_url,
236
+ title=title_name,
237
+ download_link=subtitle_link,
238
+ is_synced=True,
239
+ video_hash=video_hash,
240
+ )
241
+ ]
242
+
243
+ if video.size:
244
+ bytes_url = f'{self.server_url}/subtitle/find/file/bytes/{video.size}'
245
+ logger.info('Searching subtitles by bytes for %s', filename)
246
+ data = self._session_request(bytes_url)
247
+ if data:
248
+ parsed = self._parse_response(data)
249
+ if parsed:
250
+ subtitle_link, title_name = parsed
251
+ logger.debug('Found subtitle via bytes search')
252
+ return [
253
+ SubtisSubtitle(
254
+ language=language,
255
+ subtitle_id=subtitle_link,
256
+ page_link=bytes_url,
257
+ title=title_name,
258
+ download_link=subtitle_link,
259
+ is_synced=True,
260
+ )
261
+ ]
262
+
263
+ filename_url = f'{self.server_url}/subtitle/find/file/name/{encoded_filename}'
264
+ logger.info('Searching subtitles by filename for %s', filename)
265
+ data = self._session_request(filename_url)
266
+ if data:
267
+ parsed = self._parse_response(data)
268
+ if parsed:
269
+ subtitle_link, title_name = parsed
270
+ logger.debug('Found subtitle via filename search')
271
+ return [
272
+ SubtisSubtitle(
273
+ language=language,
274
+ subtitle_id=subtitle_link,
275
+ page_link=filename_url,
276
+ title=title_name,
277
+ download_link=subtitle_link,
278
+ is_synced=True,
279
+ )
280
+ ]
281
+
282
+ alternative_url = f'{self.server_url}/subtitle/file/alternative/{encoded_filename}'
283
+ logger.info('Searching subtitles (alternative) for %s', filename)
284
+ data = self._session_request(alternative_url)
285
+ if data:
286
+ parsed = self._parse_response(data)
287
+ if parsed:
288
+ subtitle_link, title_name = parsed
289
+ logger.debug('Found subtitle via alternative search (fuzzy)')
290
+ return [
291
+ SubtisSubtitle(
292
+ language=language,
293
+ subtitle_id=subtitle_link,
294
+ page_link=alternative_url,
295
+ title=title_name,
296
+ download_link=subtitle_link,
297
+ is_synced=False,
298
+ )
299
+ ]
300
+
301
+ logger.info('No subtitle found for %s', filename)
302
+ return []
303
+
304
+ def list_subtitles(self, video: Video, languages: Set[Language]) -> list[SubtisSubtitle]:
305
+ """List all the subtitles for the video."""
306
+ if not isinstance(video, Movie):
307
+ return []
308
+
309
+ return self.query(video, languages)
310
+
311
+ def download_subtitle(self, subtitle: SubtisSubtitle) -> None:
312
+ """Download the content of the subtitle."""
313
+ if self.session is None:
314
+ raise NotInitializedProviderError
315
+
316
+ if not subtitle.download_link:
317
+ return
318
+
319
+ logger.info('Downloading subtitle %s', subtitle.download_link)
320
+
321
+ try:
322
+ r = self.session.get(subtitle.download_link, timeout=self.timeout)
323
+ r.raise_for_status()
324
+ except RequestException:
325
+ logger.exception('Download error for %s', subtitle.download_link)
326
+ raise SubtisError from None
327
+
328
+ if not r.content:
329
+ logger.warning('Empty subtitle content from %s', subtitle.download_link)
330
+ return
331
+
332
+ subtitle.set_content(r.content)
333
+
334
+
335
+ class SubtisError(ProviderError):
336
+ """Base class for non-generic :class:`SubtisProvider` exceptions."""
@@ -182,21 +182,25 @@ class SubtitulamosProvider(Provider):
182
182
 
183
183
  @region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
184
184
  def _read_episode_page(
185
- self, series: str, season: int, episode: int, year: int | None = None
185
+ self,
186
+ series: str,
187
+ season: int,
188
+ episode: int,
189
+ year: int | None = None,
186
190
  ) -> tuple[ParserBeautifulSoup, str]:
187
191
  """Search the URL titles by kind for the given `title`, `season` and `episode`.
188
192
 
189
193
  :param str series: Series to search for.
190
194
  :param int season: Season to search for.
191
195
  :param int episode: Episode to search for.
192
- :param int year: Year to search for.
196
+ :param (int | None) year: Year to search for.
193
197
  :return: The episode URL.
194
198
 
195
199
  """
196
200
  logger.info('Searching episode url for %s, season %d, episode %d', series, season, episode)
197
201
 
198
202
  # attempt first with year
199
- series_response = self._query_search(f'{series} ({year})')
203
+ series_response = self._query_search(f'{series} ({year})') if year else []
200
204
  if len(series_response) == 0:
201
205
  series_response = self._query_search(series)
202
206
 
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import logging
6
6
  import os
7
7
  import struct
8
+ from functools import cache
8
9
  from typing import TYPE_CHECKING, Any, cast
9
10
 
10
11
  from subliminal.extensions import get_default_providers, provider_manager
@@ -18,23 +19,26 @@ if TYPE_CHECKING:
18
19
  from subliminal.providers import Provider
19
20
  from subliminal.video import Video
20
21
 
21
- HashFunc: TypeAlias = Callable[[str | os.PathLike], str | None]
22
+ HashFunc: TypeAlias = Callable[[str], str | None]
22
23
 
23
24
  logger = logging.getLogger(__name__)
24
25
 
25
26
 
26
- def hash_opensubtitles(video_path: str | os.PathLike) -> str | None:
27
+ @cache
28
+ def hash_opensubtitles(video_name: str) -> str | None:
27
29
  """Compute a hash using OpenSubtitles' algorithm.
28
30
 
29
- :param (str | os.PathLike) video_path: path of the video.
30
- :return: the hash.
31
- :rtype: str
31
+ The result is cached so it can be reused by several providers without cost.
32
+
33
+ :param str video_name: filename of the video.
34
+ :return: the hash or None if it failed.
35
+ :rtype: str | None
32
36
 
33
37
  """
34
- video_path = os.fspath(video_path)
38
+ video_name = os.fspath(video_name)
35
39
  bytesize = struct.calcsize(b'<q')
36
- with open(video_path, 'rb') as f:
37
- filesize = os.path.getsize(video_path)
40
+ with open(video_name, 'rb') as f:
41
+ filesize = os.path.getsize(video_name)
38
42
  filehash = filesize
39
43
  if filesize < 65536 * 2:
40
44
  return None