subliminal 2.3.2__tar.gz → 2.5.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.
- {subliminal-2.3.2 → subliminal-2.5.0}/.github/workflows/publish-release.yaml +2 -30
- subliminal-2.5.0/.github/zizmor.yml +13 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/.pre-commit-config.yaml +5 -5
- {subliminal-2.3.2 → subliminal-2.5.0}/HISTORY.rst +54 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/PKG-INFO +11 -3
- {subliminal-2.3.2 → subliminal-2.5.0}/README.rst +8 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/conf.py +1 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/user/configuration_file.rst +5 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/user/usage.rst +2 -2
- {subliminal-2.3.2 → subliminal-2.5.0}/hatch.toml +1 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/pyproject.toml +4 -4
- subliminal-2.5.0/src/subliminal/cli/__init__.py +6 -0
- subliminal-2.5.0/src/subliminal/cli/cli.py +241 -0
- subliminal-2.5.0/src/subliminal/cli/commands/__init__.py +5 -0
- subliminal-2.5.0/src/subliminal/cli/commands/_format.py +63 -0
- subliminal-2.5.0/src/subliminal/cli/commands/download_best.py +632 -0
- subliminal-2.5.0/src/subliminal/cli/generate_config.py +162 -0
- subliminal-2.5.0/src/subliminal/cli/helpers.py +230 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/core.py +2 -2
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/extensions.py +35 -11
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/providers/opensubtitles.py +5 -3
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/providers/opensubtitlescom.py +21 -6
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/providers/subtitulamos.py +1 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/refiners/omdb.py +23 -20
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/refiners/tvdb.py +8 -3
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/score.py +47 -52
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/utils.py +11 -45
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/video.py +2 -2
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_download_subtitle.yaml +5 -5
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_list_subtitles_episode.yaml +4 -4
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_list_subtitles_movie.yaml +4 -4
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_list_subtitles_movie_no_hash.yaml +4 -4
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_login_bad_password.yaml +1 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_login_vip_bad_password.yaml +1 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_login_vip_login.yaml +1 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_no_operation.yaml +4 -4
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_query_hash_size.yaml +4 -4
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_query_imdb_id.yaml +4 -4
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_query_not_enough_information.yaml +3 -3
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_query_query_episode.yaml +4 -4
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_query_query_movie.yaml +4 -4
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_query_query_season_episode.yaml +4 -4
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitles/test_query_wrong_hash_wrong_size.yaml +4 -4
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_download_subtitle.yaml +7 -7
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_list_subtitles_episode.yaml +2 -2
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_list_subtitles_movie.yaml +4 -4
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_list_subtitles_movie_no_hash.yaml +1 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_login.yaml +1 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_login_bad_password.yaml +1 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_logout.yaml +1 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_query_hash_size.yaml +1 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_query_imdb_id.yaml +1 -1
- subliminal-2.5.0/tests/cassettes/opensubtitlescom/test_query_max_result_pages.yaml +10748 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_query_query_episode.yaml +2 -2
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_query_query_movie.yaml +1 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_query_query_season_episode.yaml +2 -2
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_query_tag_movie.yaml +1 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_tag_match.yaml +3 -3
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_user_infos.yaml +2 -2
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/providers/test_list_subtitles_providers_download.yaml +8 -8
- subliminal-2.5.0/tests/cassettes/tmdb/test_search_movie_page.yaml +313 -0
- subliminal-2.5.0/tests/cli/test_cli.py +34 -0
- subliminal-2.3.2/tests/test_cli.py → subliminal-2.5.0/tests/cli/test_download.py +102 -173
- subliminal-2.5.0/tests/cli/test_generate_config.py +57 -0
- subliminal-2.5.0/tests/cli/test_helpers.py +95 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/conftest.py +232 -6
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/providers/test_opensubtitlescom.py +15 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/providers/test_providers.py +23 -19
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/refiners/test_tmdb.py +8 -4
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/test_archives.py +6 -6
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/test_core.py +14 -4
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/test_provider_pool.py +19 -17
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/test_subtitle.py +1 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/test_utils.py +20 -66
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/test_video.py +17 -1
- {subliminal-2.3.2 → subliminal-2.5.0}/tox.ini +0 -8
- subliminal-2.3.2/.github/zizmor.yml +0 -4
- subliminal-2.3.2/scripts/generate-gh-release-notes.py +0 -86
- subliminal-2.3.2/src/subliminal/cli.py +0 -1169
- subliminal-2.3.2/tests/cassettes/tmdb/test_search_movie_page.yaml +0 -146
- {subliminal-2.3.2 → subliminal-2.5.0}/.dockerignore +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/.github/actions/prepare-tests/action.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/.github/workflows/CI.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/.github/workflows/coverage.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/.github/workflows/docker-build.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/.github/workflows/prepare-release-pr.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/.github/workflows/publish.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/.github/workflows/scheduled-tests.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/.github/workflows/tag-release.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/.gitignore +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/.python-version-default +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/.readthedocs.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/CONTRIBUTING.md +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/Dockerfile +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/LICENSE +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/MANIFEST.in +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/RELEASING.md +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/changelog.d/.gitignore +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/Makefile +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/_intersphinx/README.md +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/_intersphinx/importlib_metadata_objects.inv +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/_intersphinx/importlib_metadata_objects.txt +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/_static/.gitignore +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/api/cache.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/api/cli.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/api/core.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/api/exceptions.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/api/extensions.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/api/providers.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/api/refiners.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/api/score.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/api/subtitle.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/api/utils.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/api/video.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/cassettes/test_usage.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/config.toml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/conftest.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/index.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/make.bat +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/user/changelog.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/user/cli.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/user/how_it_works.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/user/installation.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/docs/user/provider_guide.rst +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/scripts/.gitignore +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/scripts/prepare-release-pr.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/scripts/prepare_tests.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/scripts/release.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/__init__.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/__main__.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/archives.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/cache.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/converters/__init__.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/converters/addic7ed.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/converters/opensubtitles.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/converters/opensubtitlescom.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/converters/subtitulamos.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/converters/tvsubtitles.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/exceptions.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/matches.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/providers/__init__.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/providers/addic7ed.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/providers/bsplayer.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/providers/gestdown.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/providers/mock.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/providers/napiprojekt.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/providers/podnapisi.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/providers/tvsubtitles.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/py.typed +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/refiners/__init__.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/refiners/hash.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/refiners/metadata.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/refiners/tmdb.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/src/subliminal/subtitle.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/__init__.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_download_subtitle.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_get_show_id.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_get_show_id_country.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_get_show_id_quote_dots_mixed_case.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_get_show_id_with_comma.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_get_show_id_year.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_get_show_ids.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_list_subtitles.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_list_subtitles_episode_alternative_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_list_subtitles_show_with_asterisk.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_query.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_query_no_year.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_query_parsing.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_query_parsing_colon.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_query_parsing_dash.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_query_parsing_quote_dots_mixed_case.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_query_wrong_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/addic7ed/test_query_year.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/bsplayer/test_download_subtitle.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/bsplayer/test_list_subtitles_hash.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/bsplayer/test_login.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/bsplayer/test_logout.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/bsplayer/test_query_hash_size.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/cli/test_cli_download.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_download_subtitle.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_download_with_bom.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_get_title_and_show_id_alternative_name.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_get_title_and_show_id_no_year.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_get_title_and_show_id_only_title.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_get_title_and_show_id_with_tvdb_id.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_list_subtitles.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_list_subtitles_episode_alternative_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_query.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_query_all_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_query_no_language.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_query_parsing.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_query_parsing_colon.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_query_parsing_dash.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_query_parsing_quote_dots_mixed_case.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_query_wrong_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_search_show_id.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_search_show_id_error.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_search_show_id_incomplete.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_search_show_id_no_year.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_search_show_id_quote.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_search_show_series_tvdb_id.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/gestdown/test_show_with_asterisk.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/napiprojekt/test_download_subtitles.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/napiprojekt/test_list_subtitles.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/napiprojekt/test_query_microdvd.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/napiprojekt/test_query_srt.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/napiprojekt/test_query_srt_reencode.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/napiprojekt/test_query_wrong_hash.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_get_id.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_get_title.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_get_wrong_id.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_get_wrong_title.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_refine_episode.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_refine_episode_original_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_refine_episode_with_country.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_refine_episode_with_country_hoc_us.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_refine_episode_year.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_refine_movie.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_refine_movie_guess_alternative_title.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_search.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_search_page.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_search_type.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_search_wrong_title.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/omdb/test_search_year.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/opensubtitlescom/test_query_wrong_hash_wrong_size.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/podnapisi/test_download_subtitle.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/podnapisi/test_download_subtitles_with_title_unicode.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/podnapisi/test_list_subtitles_episode.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/podnapisi/test_list_subtitles_episode_alternative_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/podnapisi/test_list_subtitles_movie.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/podnapisi/test_query_episode.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/podnapisi/test_query_movie.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/providers/test_download_best_subtitles.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/providers/test_download_best_subtitles_min_score.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/providers/test_download_best_subtitles_only_one.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/subtitulamos/test_download_subtitle.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/subtitulamos/test_download_subtitle_first_episode.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/subtitulamos/test_download_subtitle_foo.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/subtitulamos/test_download_subtitle_last_season.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/subtitulamos/test_download_subtitle_year.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/subtitulamos/test_list_subtitles_not_exist_episode.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/subtitulamos/test_list_subtitles_not_exist_language.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/subtitulamos/test_list_subtitles_not_exist_season.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/subtitulamos/test_list_subtitles_not_exist_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/subtitulamos/test_list_subtitles_with_spanish_non_mx_search.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_get_movie_id.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_get_movie_id_failed.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_get_series_id.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_get_series_wrong_id.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_refine_episode.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_refine_episode_original_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_refine_episode_with_country.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_refine_episode_with_country_hoc_us.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_refine_episode_year.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_refine_movie.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_refine_movie_guess_alternative_title.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_search_movie.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_search_movie_wrong_title.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_search_movie_year.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_search_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tmdb/test_search_series_wrong_name.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_get_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_get_series_actors.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_get_series_actors_wrong_id.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_get_series_wrong_id.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_login.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_login_error.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_query_series_episodes.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_query_series_episodes_wrong_season.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_refine.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_refine_ambiguous.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_refine_ambiguous_2.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_refine_episode_alternative_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_refine_episode_no_year.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_refine_episode_partial.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_refine_episode_with_comma.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_refine_episode_with_country.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_refine_episode_with_country_hoc_us.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_refine_episode_year.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_refresh_token.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_search_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_search_series_wrong_name.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvdb/test_token_needs_refresh.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_download_subtitle.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_download_subtitle_with_quote.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_get_episode_ids.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_get_episode_ids_wrong_season.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_list_subtitles.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_list_subtitles_episode_alternative_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_query.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_query_no_year.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_query_with_quote.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_query_wrong_episode.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_query_wrong_series.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_search_show_id.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_search_show_id_ambiguous.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_search_show_id_error.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_search_show_id_incomplete.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_search_show_id_no_year.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_search_show_id_uk.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_search_show_id_us.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/cassettes/tvsubtitles/test_search_show_id_year_in_title.yaml +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/providers/test_addic7ed.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/providers/test_bsplayer.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/providers/test_gestdown.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/providers/test_napiprojekt.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/providers/test_opensubtitles.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/providers/test_podnapisi.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/providers/test_subtitulamos.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/providers/test_tvsubtitles.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/refiners/test_hash.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/refiners/test_metadata.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/refiners/test_omdb.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/refiners/test_tvdb.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/test_cache.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/test_extensions.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/test_matches.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/test_provider.py +0 -0
- {subliminal-2.3.2 → subliminal-2.5.0}/tests/test_score.py +0 -0
|
@@ -35,7 +35,6 @@ jobs:
|
|
|
35
35
|
if: github.event_name == 'push'
|
|
36
36
|
with:
|
|
37
37
|
persist-credentials: false
|
|
38
|
-
|
|
39
38
|
- name: Checkout
|
|
40
39
|
uses: actions/checkout@v4
|
|
41
40
|
if: |
|
|
@@ -44,34 +43,13 @@ jobs:
|
|
|
44
43
|
with:
|
|
45
44
|
ref: ${{ inputs.tag-name }}
|
|
46
45
|
persist-credentials: false
|
|
47
|
-
|
|
48
46
|
- name: Build dist
|
|
49
47
|
uses: hynek/build-and-inspect-python-package@v2
|
|
50
48
|
id: baipp
|
|
51
|
-
|
|
52
|
-
- name: Install tox
|
|
53
|
-
run: |
|
|
54
|
-
python -m pip install --upgrade pip
|
|
55
|
-
pip install --upgrade tox
|
|
56
|
-
|
|
57
|
-
- name: Generate Release Notes
|
|
58
|
-
env:
|
|
59
|
-
RELEASE_VERSION: ${{ steps.baipp.outputs.package_version }}
|
|
60
|
-
run: |
|
|
61
|
-
sudo apt-get install pandoc
|
|
62
|
-
# use package_version computed in step 'baipp'
|
|
63
|
-
tox -e generate-gh-release-notes -- "$RELEASE_VERSION" latest-release-notes.md
|
|
64
|
-
|
|
65
|
-
- name: Upload release notes
|
|
66
|
-
uses: actions/upload-artifact@v4
|
|
67
|
-
with:
|
|
68
|
-
name: latest-release-notes
|
|
69
|
-
path: latest-release-notes.md
|
|
70
|
-
include-hidden-files: false
|
|
71
|
-
if-no-files-found: error
|
|
72
49
|
outputs:
|
|
73
50
|
tag: ${{ steps.baipp.outputs.package_version }}
|
|
74
51
|
|
|
52
|
+
|
|
75
53
|
github-release:
|
|
76
54
|
name: Make a GitHub Release
|
|
77
55
|
needs: [build-package]
|
|
@@ -96,15 +74,9 @@ jobs:
|
|
|
96
74
|
with:
|
|
97
75
|
name: Packages
|
|
98
76
|
path: dist
|
|
99
|
-
- name: Download release notes
|
|
100
|
-
uses: actions/download-artifact@v4
|
|
101
|
-
with:
|
|
102
|
-
name: latest-release-notes
|
|
103
|
-
path: latest-release-notes.md
|
|
104
|
-
|
|
105
77
|
- name: Publish GitHub Release
|
|
106
78
|
uses: softprops/action-gh-release@v2
|
|
107
79
|
with:
|
|
108
80
|
files: dist/*
|
|
109
|
-
|
|
81
|
+
generate_release_notes: true
|
|
110
82
|
draft: true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
repos:
|
|
2
2
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
-
rev:
|
|
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.
|
|
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.
|
|
16
|
+
rev: v0.14.9
|
|
17
17
|
hooks:
|
|
18
18
|
- id: ruff
|
|
19
19
|
args: [--fix, --unsafe-fixes]
|
|
@@ -26,14 +26,14 @@ repos:
|
|
|
26
26
|
name: validate-pyproject
|
|
27
27
|
|
|
28
28
|
- repo: https://github.com/PyCQA/doc8
|
|
29
|
-
rev:
|
|
29
|
+
rev: v2.0.0
|
|
30
30
|
hooks:
|
|
31
31
|
- id: doc8
|
|
32
32
|
description: This hook runs doc8 for linting docs
|
|
33
33
|
require_serial: true
|
|
34
34
|
|
|
35
35
|
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
|
36
|
-
rev: v1.
|
|
36
|
+
rev: v1.18.0
|
|
37
37
|
hooks:
|
|
38
38
|
- id: zizmor
|
|
39
39
|
|
|
@@ -2,6 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
.. towncrier release notes start
|
|
4
4
|
|
|
5
|
+
2.5.0 (2025-12-14)
|
|
6
|
+
^^^^^^^^^^^^^^^^^^
|
|
7
|
+
|
|
8
|
+
Provider Changes
|
|
9
|
+
----------------
|
|
10
|
+
|
|
11
|
+
- 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>`__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Documentation
|
|
15
|
+
-------------
|
|
16
|
+
|
|
17
|
+
- 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>`__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Misc
|
|
21
|
+
----
|
|
22
|
+
|
|
23
|
+
- add support for python 3.14 (`#1317 <https://github.com/Diaoul/subliminal/issues/1317>`__)
|
|
24
|
+
- Fix test with click 8.3.0 running in parallel (`#1320 <https://github.com/Diaoul/subliminal/issues/1320>`__)
|
|
25
|
+
- fix test on big-endian system (`#1323 <https://github.com/Diaoul/subliminal/issues/1323>`__)
|
|
26
|
+
- compatibility with stevedore 5.6.0 (`#1325 <https://github.com/Diaoul/subliminal/issues/1325>`__)
|
|
27
|
+
- fix some tests to pass in a dirty environment (`#1326 <https://github.com/Diaoul/subliminal/issues/1326>`__)
|
|
28
|
+
- update pre-commit hook versions (`#1329 <https://github.com/Diaoul/subliminal/issues/1329>`__)
|
|
29
|
+
- Fix some test so they are more reliable and stable (`#1331 <https://github.com/Diaoul/subliminal/issues/1331>`__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
2.4.0 (2025-09-03)
|
|
33
|
+
^^^^^^^^^^^^^^^^^^
|
|
34
|
+
|
|
35
|
+
Changes
|
|
36
|
+
-------
|
|
37
|
+
|
|
38
|
+
- score: add fps match, remove hearing_impaired match (`#1250 <https://github.com/Diaoul/subliminal/issues/1250>`__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
CLI Changes
|
|
42
|
+
-----------
|
|
43
|
+
|
|
44
|
+
- cli: refactor the cli.py file into a folder (`#1299 <https://github.com/Diaoul/subliminal/issues/1299>`__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
Documentation
|
|
48
|
+
-------------
|
|
49
|
+
|
|
50
|
+
- doc: add a section in the README to run subliminal with the ``--debug`` flag. (`#1308 <https://github.com/Diaoul/subliminal/issues/1308>`__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
Misc
|
|
54
|
+
----
|
|
55
|
+
|
|
56
|
+
- ensure Episode.series is always a string, not a list from guessit bug (`#1304 <https://github.com/Diaoul/subliminal/issues/1304>`__)
|
|
57
|
+
|
|
58
|
+
|
|
5
59
|
2.3.2 (2025-05-08)
|
|
6
60
|
^^^^^^^^^^^^^^^^^^
|
|
7
61
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: subliminal
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.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
|
|
@@ -56,7 +57,6 @@ Provides-Extra: tests
|
|
|
56
57
|
Requires-Dist: colorama; extra == 'tests'
|
|
57
58
|
Requires-Dist: coverage[toml]>=7; extra == 'tests'
|
|
58
59
|
Requires-Dist: importlib-metadata>=4.6; (python_version < '3.10') and extra == 'tests'
|
|
59
|
-
Requires-Dist: pypandoc; extra == 'tests'
|
|
60
60
|
Requires-Dist: pytest-cov; extra == 'tests'
|
|
61
61
|
Requires-Dist: pytest-xdist; extra == 'tests'
|
|
62
62
|
Requires-Dist: pytest>=6.0; extra == 'tests'
|
|
@@ -159,6 +159,14 @@ Run subliminal in a docker container::
|
|
|
159
159
|
|
|
160
160
|
$ docker run --rm --name subliminal -v subliminal_cache:/usr/src/cache -v /tvshows:/tvshows -it ghcr.io/diaoul/subliminal download -l en /tvshows/The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
|
|
161
161
|
|
|
162
|
+
Debugging
|
|
163
|
+
^^^^^^^^^
|
|
164
|
+
|
|
165
|
+
By default, subliminal output is minimal. Run with the `--debug` flag before the `download` command to get more information::
|
|
166
|
+
|
|
167
|
+
$ subliminal --debug download -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
|
|
168
|
+
|
|
169
|
+
|
|
162
170
|
Installation
|
|
163
171
|
------------
|
|
164
172
|
For a better isolation with your system you should use a dedicated virtualenv.
|
|
@@ -86,6 +86,14 @@ Run subliminal in a docker container::
|
|
|
86
86
|
|
|
87
87
|
$ docker run --rm --name subliminal -v subliminal_cache:/usr/src/cache -v /tvshows:/tvshows -it ghcr.io/diaoul/subliminal download -l en /tvshows/The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
|
|
88
88
|
|
|
89
|
+
Debugging
|
|
90
|
+
^^^^^^^^^
|
|
91
|
+
|
|
92
|
+
By default, subliminal output is minimal. Run with the `--debug` flag before the `download` command to get more information::
|
|
93
|
+
|
|
94
|
+
$ subliminal --debug download -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
|
|
95
|
+
|
|
96
|
+
|
|
89
97
|
Installation
|
|
90
98
|
------------
|
|
91
99
|
For a better isolation with your system you should use a dedicated virtualenv.
|
|
@@ -304,7 +304,7 @@ intersphinx_mapping = {
|
|
|
304
304
|
'dogpilecache': ('https://dogpilecache.sqlalchemy.org/en/latest', None),
|
|
305
305
|
'stevedore': ('https://docs.openstack.org/stevedore/latest', None),
|
|
306
306
|
'click': ('https://click.palletsprojects.com/en/latest/', None),
|
|
307
|
-
'requests': ('https://
|
|
307
|
+
'requests': ('https://requests.readthedocs.io/en/latest/', None),
|
|
308
308
|
'sympy': ('https://docs.sympy.org/latest/', None),
|
|
309
309
|
'bs4': ('https://www.crummy.com/software/BeautifulSoup/bs4/doc/', None),
|
|
310
310
|
'importlib.metadata': ('https://docs.python.org/3/', '_intersphinx/importlib_metadata_objects.inv'),
|
|
@@ -24,6 +24,10 @@ 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
|
-------
|
|
@@ -40,5 +44,5 @@ Default configuration
|
|
|
40
44
|
Here is a list of all the options of the configuration file.
|
|
41
45
|
Commented options need a value to be valid.
|
|
42
46
|
|
|
43
|
-
.. program-output:: python -c "from subliminal.cli import generate_default_config; print(generate_default_config(commented=False))"
|
|
47
|
+
.. program-output:: python -c "from subliminal.cli.generate_config import generate_default_config; print(generate_default_config(commented=False))"
|
|
44
48
|
:language: toml
|
|
@@ -146,8 +146,8 @@ And then compute a score with those matches with :func:`~subliminal.score.comput
|
|
|
146
146
|
|
|
147
147
|
>>> for s in subtitles[video]:
|
|
148
148
|
... {s: compute_score(s, video)}
|
|
149
|
-
{<PodnapisiSubtitle 'ZtAW' [hu]>:
|
|
150
|
-
{<PodnapisiSubtitle 'ONAW' [hu]>:
|
|
149
|
+
{<PodnapisiSubtitle 'ZtAW' [hu]>: 941}
|
|
150
|
+
{<PodnapisiSubtitle 'ONAW' [hu]>: 922}
|
|
151
151
|
|
|
152
152
|
Now you should have a better idea about which one you should choose.
|
|
153
153
|
|
|
@@ -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
|
|
42
|
+
"click",
|
|
42
43
|
"click-option-group>=0.5.6",
|
|
43
44
|
"dogpile.cache>=1.0",
|
|
44
45
|
"guessit>=3.0.0",
|
|
@@ -72,7 +73,6 @@ tests = [
|
|
|
72
73
|
"pytest-xdist",
|
|
73
74
|
"sympy",
|
|
74
75
|
"colorama", # used in scripts/
|
|
75
|
-
"pypandoc", # used in scripts/
|
|
76
76
|
"vcrpy>=5", # keep synchronized with docs dependencies
|
|
77
77
|
"win32-setctime; sys_platform=='win32'",
|
|
78
78
|
"importlib_metadata>=4.6; python_version<'3.10'",
|
|
@@ -275,12 +275,12 @@ showcontent = true
|
|
|
275
275
|
[[tool.towncrier.type]]
|
|
276
276
|
directory = "doc"
|
|
277
277
|
name = "Documentation"
|
|
278
|
-
showcontent =
|
|
278
|
+
showcontent = true
|
|
279
279
|
|
|
280
280
|
[[tool.towncrier.type]]
|
|
281
281
|
directory = "misc"
|
|
282
282
|
name = "Misc"
|
|
283
|
-
showcontent =
|
|
283
|
+
showcontent = true
|
|
284
284
|
|
|
285
285
|
|
|
286
286
|
# https://mypy.readthedocs.io/en/stable/config_file.html
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# ruff: noqa: FBT001
|
|
2
|
+
"""Subliminal uses `click <https://click.palletsprojects.com>`_ to provide a :abbr:`CLI (command-line interface)`."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import pathlib
|
|
9
|
+
import warnings
|
|
10
|
+
from datetime import timedelta
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
from platformdirs import PlatformDirs
|
|
16
|
+
|
|
17
|
+
from subliminal import (
|
|
18
|
+
__version__,
|
|
19
|
+
region,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .commands import download
|
|
23
|
+
from .helpers import (
|
|
24
|
+
MutexLock,
|
|
25
|
+
options_from_providers,
|
|
26
|
+
options_from_refiners,
|
|
27
|
+
providers_config,
|
|
28
|
+
read_configuration,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def configure(ctx: click.Context, param: click.Parameter | None, filename: str | os.PathLike) -> None:
|
|
35
|
+
"""Update :class:`click.Context` based on a configuration file."""
|
|
36
|
+
config = read_configuration(filename)
|
|
37
|
+
|
|
38
|
+
ctx.obj = config['obj']
|
|
39
|
+
ctx.default_map = config['default_map']
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
dirs = PlatformDirs('subliminal')
|
|
43
|
+
cache_file = 'subliminal.dbm'
|
|
44
|
+
default_config_path = dirs.user_config_path / 'subliminal.toml'
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@click.group(
|
|
48
|
+
context_settings={'max_content_width': 100},
|
|
49
|
+
epilog='Suggestions and bug reports are greatly appreciated: https://github.com/Diaoul/subliminal/',
|
|
50
|
+
)
|
|
51
|
+
@click.option(
|
|
52
|
+
'-c',
|
|
53
|
+
'--config',
|
|
54
|
+
type=click.Path(dir_okay=False, path_type=pathlib.Path),
|
|
55
|
+
default=default_config_path,
|
|
56
|
+
callback=configure,
|
|
57
|
+
show_default=True,
|
|
58
|
+
is_eager=True,
|
|
59
|
+
expose_value=False,
|
|
60
|
+
show_envvar=True,
|
|
61
|
+
help='Path to the TOML configuration file.',
|
|
62
|
+
)
|
|
63
|
+
@click.option(
|
|
64
|
+
'--cache-dir',
|
|
65
|
+
type=click.Path(writable=True, file_okay=False),
|
|
66
|
+
default=dirs.user_cache_dir,
|
|
67
|
+
show_default=True,
|
|
68
|
+
expose_value=True,
|
|
69
|
+
help='Path to the cache directory.',
|
|
70
|
+
)
|
|
71
|
+
@providers_config.option(
|
|
72
|
+
'--addic7ed',
|
|
73
|
+
type=click.STRING,
|
|
74
|
+
nargs=2,
|
|
75
|
+
metavar='USERNAME PASSWORD',
|
|
76
|
+
help='DEPRECATED: Addic7ed configuration.',
|
|
77
|
+
)
|
|
78
|
+
@providers_config.option(
|
|
79
|
+
'--opensubtitles',
|
|
80
|
+
type=click.STRING,
|
|
81
|
+
nargs=2,
|
|
82
|
+
metavar='USERNAME PASSWORD',
|
|
83
|
+
help='DEPRECATED: OpenSubtitles configuration.',
|
|
84
|
+
)
|
|
85
|
+
@providers_config.option(
|
|
86
|
+
'--opensubtitlescom',
|
|
87
|
+
type=click.STRING,
|
|
88
|
+
nargs=2,
|
|
89
|
+
metavar='USERNAME PASSWORD',
|
|
90
|
+
help='DEPRECATED: OpenSubtitlesCom configuration.',
|
|
91
|
+
)
|
|
92
|
+
@options_from_providers
|
|
93
|
+
@options_from_refiners
|
|
94
|
+
@click.option('--debug', is_flag=True, help='Print useful information for debugging subliminal and for reporting bugs.')
|
|
95
|
+
@click.option(
|
|
96
|
+
'--logfile',
|
|
97
|
+
type=str,
|
|
98
|
+
default='',
|
|
99
|
+
help=(
|
|
100
|
+
'If defined, record information to the specified log file. '
|
|
101
|
+
'If the file already exists, new logs are appended to the file.'
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
@click.option(
|
|
105
|
+
'--logfile-level',
|
|
106
|
+
type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']),
|
|
107
|
+
default='DEBUG',
|
|
108
|
+
help='The logging level used for the log file.',
|
|
109
|
+
)
|
|
110
|
+
@click.version_option(__version__)
|
|
111
|
+
@click.pass_context
|
|
112
|
+
def subliminal(
|
|
113
|
+
ctx: click.Context,
|
|
114
|
+
/,
|
|
115
|
+
cache_dir: str,
|
|
116
|
+
debug: bool,
|
|
117
|
+
logfile: os.PathLike[str] | None,
|
|
118
|
+
logfile_level: str,
|
|
119
|
+
addic7ed: tuple[str, str],
|
|
120
|
+
opensubtitles: tuple[str, str],
|
|
121
|
+
opensubtitlescom: tuple[str, str],
|
|
122
|
+
**kwargs: Any,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Subtitles, faster than your thoughts."""
|
|
125
|
+
# make sure to expand user
|
|
126
|
+
cache_dir_path = Path(cache_dir).expanduser()
|
|
127
|
+
# create cache directory
|
|
128
|
+
try:
|
|
129
|
+
cache_dir_path.mkdir(parents=True)
|
|
130
|
+
except OSError: # pragma: no cover
|
|
131
|
+
if not cache_dir_path.is_dir():
|
|
132
|
+
raise
|
|
133
|
+
|
|
134
|
+
# configure cache
|
|
135
|
+
region.configure(
|
|
136
|
+
'dogpile.cache.dbm',
|
|
137
|
+
expiration_time=timedelta(days=30),
|
|
138
|
+
arguments={'filename': os.fspath(cache_dir_path / cache_file), 'lock_factory': MutexLock},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Set the logger level to DEBUG in case debug or logfile is defined
|
|
142
|
+
subliminal_logger = logging.getLogger('subliminal')
|
|
143
|
+
subliminal_logger.setLevel(logging.DEBUG)
|
|
144
|
+
|
|
145
|
+
# configure logging file
|
|
146
|
+
if logfile:
|
|
147
|
+
# make sure to expand user
|
|
148
|
+
logfile_path = Path(logfile).expanduser()
|
|
149
|
+
try:
|
|
150
|
+
# make sure the parent directories exist
|
|
151
|
+
logfile_path.parent.mkdir(parents=True)
|
|
152
|
+
except OSError: # pragma: no cover
|
|
153
|
+
if not logfile_path.parent.is_dir():
|
|
154
|
+
raise
|
|
155
|
+
|
|
156
|
+
file_handler = logging.FileHandler(logfile_path)
|
|
157
|
+
file_handler.setFormatter(
|
|
158
|
+
logging.Formatter(
|
|
159
|
+
'%(asctime)s | %(levelname)-8s | %(name)s | %(message)s',
|
|
160
|
+
datefmt='%Y-%m-%d %H:%M:%S',
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
file_handler.setLevel(logfile_level)
|
|
164
|
+
subliminal_logger.addHandler(file_handler)
|
|
165
|
+
|
|
166
|
+
# configure logging
|
|
167
|
+
if debug:
|
|
168
|
+
handler = logging.StreamHandler()
|
|
169
|
+
handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
|
|
170
|
+
handler.setLevel(logging.DEBUG)
|
|
171
|
+
subliminal_logger.addHandler(handler)
|
|
172
|
+
# log about the config file
|
|
173
|
+
msg = ctx.obj['debug_message']
|
|
174
|
+
logger.info(msg)
|
|
175
|
+
|
|
176
|
+
ctx.obj['debug'] = debug
|
|
177
|
+
|
|
178
|
+
# create provider and refiner configs
|
|
179
|
+
provider_configs: dict[str, dict[str, Any]] = {}
|
|
180
|
+
refiner_configs: dict[str, dict[str, Any]] = {}
|
|
181
|
+
|
|
182
|
+
for k, v in kwargs.items():
|
|
183
|
+
try_split = k.split('__')
|
|
184
|
+
if len(try_split) != 3: # pragma: no cover
|
|
185
|
+
click.echo(f'Unknown option: {k}={v}')
|
|
186
|
+
continue
|
|
187
|
+
group, plugin, key = try_split
|
|
188
|
+
if group == '_provider':
|
|
189
|
+
provider_configs.setdefault(plugin, {})[key] = v
|
|
190
|
+
|
|
191
|
+
elif group == '_refiner': # pragma: no branch
|
|
192
|
+
refiner_configs.setdefault(plugin, {})[key] = v
|
|
193
|
+
|
|
194
|
+
ctx.obj['provider_configs'] = provider_configs
|
|
195
|
+
ctx.obj['refiner_configs'] = refiner_configs
|
|
196
|
+
|
|
197
|
+
# Deprecated options
|
|
198
|
+
# To be remove in next version
|
|
199
|
+
deprecated_options = {
|
|
200
|
+
'addic7ed': addic7ed,
|
|
201
|
+
'opensubtitles': opensubtitles,
|
|
202
|
+
'opensubtitlescom': opensubtitlescom,
|
|
203
|
+
}
|
|
204
|
+
for provider, option_value in deprecated_options.items():
|
|
205
|
+
if option_value is not None: # pragma: no cover
|
|
206
|
+
msg = (
|
|
207
|
+
f'option --{provider} is deprecated, use --provider.{provider}.username and '
|
|
208
|
+
f'--provider.{provider}.password'
|
|
209
|
+
)
|
|
210
|
+
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
|
211
|
+
|
|
212
|
+
provider_configs[provider]['username'] = option_value[0]
|
|
213
|
+
provider_configs[provider]['password'] = option_value[1]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@subliminal.command()
|
|
217
|
+
@click.option(
|
|
218
|
+
'--clear-subliminal',
|
|
219
|
+
is_flag=True,
|
|
220
|
+
help='Clear subliminal cache. Use this ONLY if your cache is corrupted or if you experience issues.',
|
|
221
|
+
)
|
|
222
|
+
@click.pass_context
|
|
223
|
+
def cache(ctx: click.Context, clear_subliminal: bool) -> None:
|
|
224
|
+
"""Cache management."""
|
|
225
|
+
if clear_subliminal and ctx.parent and 'cache_dir' in ctx.parent.params:
|
|
226
|
+
cache_dir_path = Path(ctx.parent.params['cache_dir'])
|
|
227
|
+
for file in (cache_dir_path / cache_file).glob('*'): # pragma: no cover
|
|
228
|
+
file.unlink()
|
|
229
|
+
click.echo("Subliminal's cache cleared.")
|
|
230
|
+
else:
|
|
231
|
+
click.echo('Nothing done.')
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# Add commands
|
|
235
|
+
# Download best subtitle
|
|
236
|
+
subliminal.add_command(download)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def cli() -> None: # pragma: no cover
|
|
240
|
+
"""CLI that recognizes environment variables."""
|
|
241
|
+
subliminal(auto_envvar_prefix='SUBLIMINAL')
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Helper functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from babelfish import Error as BabelfishError # type: ignore[import-untyped]
|
|
12
|
+
from babelfish import Language
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LanguageParamType(click.ParamType):
|
|
18
|
+
""":class:`~click.ParamType` for languages that returns a :class:`~babelfish.language.Language`."""
|
|
19
|
+
|
|
20
|
+
name = 'language'
|
|
21
|
+
|
|
22
|
+
def convert(self, value: str, param: click.Parameter | None, ctx: click.Context | None) -> Language:
|
|
23
|
+
"""Convert ietf language to :class:`~babelfish.language.Language`."""
|
|
24
|
+
try:
|
|
25
|
+
return Language.fromietf(value)
|
|
26
|
+
except BabelfishError:
|
|
27
|
+
self.fail(f'{value} is not a valid language', param, ctx) # pragma: no cover
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AgeParamType(click.ParamType):
|
|
31
|
+
""":class:`~click.ParamType` for age strings that returns a :class:`~datetime.timedelta`.
|
|
32
|
+
|
|
33
|
+
An age string is in the form `number + identifier` with possible identifiers:
|
|
34
|
+
|
|
35
|
+
* ``w`` for weeks
|
|
36
|
+
* ``d`` for days
|
|
37
|
+
* ``h`` for hours
|
|
38
|
+
|
|
39
|
+
The form can be specified multiple times but only with that identifier ordering. For example:
|
|
40
|
+
|
|
41
|
+
* ``1w2d4h`` for 1 week, 2 days and 4 hours
|
|
42
|
+
* ``2w`` for 2 weeks
|
|
43
|
+
* ``3w6h`` for 3 weeks and 6 hours
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
name = 'age'
|
|
47
|
+
|
|
48
|
+
def convert(self, value: str, param: click.Parameter | None, ctx: click.Context | None) -> timedelta:
|
|
49
|
+
"""Convert an age string to :class:`~datetime.timedelta`."""
|
|
50
|
+
match = re.match(r'^(?:(?P<weeks>\d+?)w)?(?:(?P<days>\d+?)d)?(?:(?P<hours>\d+?)h)?$', value)
|
|
51
|
+
if not match:
|
|
52
|
+
self.fail(f'{value} is not a valid age', param, ctx)
|
|
53
|
+
|
|
54
|
+
return timedelta(**{k: int(v) for k, v in match.groupdict(0).items()})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def plural(quantity: int, name: str, *, bold: bool = True, **kwargs: Any) -> str:
|
|
58
|
+
"""Format a quantity with plural."""
|
|
59
|
+
return '{} {}{}'.format(
|
|
60
|
+
click.style(str(quantity), bold=bold, **kwargs),
|
|
61
|
+
name,
|
|
62
|
+
's' if quantity > 1 else '',
|
|
63
|
+
)
|