├── .dockerignore ├── .github ├── actions │ └── prepare-tests │ │ └── action.yaml ├── workflows │ ├── CI.yaml │ ├── coverage.yaml │ ├── docker-build.yaml │ ├── prepare-release-pr.yaml │ ├── publish-release.yaml │ ├── publish.yaml │ ├── scheduled-tests.yaml │ └── tag-release.yaml └── zizmor.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version-default ├── .readthedocs.yaml ├── CONTRIBUTING.md ├── Dockerfile ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── RELEASING.md ├── changelog.d ├── .gitignore ├── 1250.change.rst └── 1299.cli.rst ├── docs ├── Makefile ├── _intersphinx │ ├── README.md │ ├── importlib_metadata_objects.inv │ └── importlib_metadata_objects.txt ├── _static │ └── .gitignore ├── api │ ├── cache.rst │ ├── cli.rst │ ├── core.rst │ ├── exceptions.rst │ ├── extensions.rst │ ├── providers.rst │ ├── refiners.rst │ ├── score.rst │ ├── subtitle.rst │ ├── utils.rst │ └── video.rst ├── cassettes │ └── test_usage.yaml ├── conf.py ├── config.toml ├── conftest.py ├── index.rst ├── make.bat └── user │ ├── changelog.rst │ ├── cli.rst │ ├── configuration_file.rst │ ├── how_it_works.rst │ ├── installation.rst │ ├── provider_guide.rst │ └── usage.rst ├── hatch.toml ├── pyproject.toml ├── scripts ├── .gitignore ├── prepare-release-pr.py ├── prepare_tests.py └── release.py ├── src └── subliminal │ ├── __init__.py │ ├── __main__.py │ ├── archives.py │ ├── cache.py │ ├── cli │ ├── __init__.py │ ├── cli.py │ ├── commands │ │ ├── __init__.py │ │ ├── _format.py │ │ └── download_best.py │ ├── generate_config.py │ └── helpers.py │ ├── converters │ ├── __init__.py │ ├── addic7ed.py │ ├── opensubtitles.py │ ├── opensubtitlescom.py │ ├── subtitulamos.py │ └── tvsubtitles.py │ ├── core.py │ ├── exceptions.py │ ├── extensions.py │ ├── matches.py │ ├── providers │ ├── __init__.py │ ├── addic7ed.py │ ├── bsplayer.py │ ├── gestdown.py │ ├── mock.py │ ├── napiprojekt.py │ ├── opensubtitles.py │ ├── opensubtitlescom.py │ ├── podnapisi.py │ ├── subtitulamos.py │ └── tvsubtitles.py │ ├── py.typed │ ├── refiners │ ├── __init__.py │ ├── hash.py │ ├── metadata.py │ ├── omdb.py │ ├── tmdb.py │ └── tvdb.py │ ├── score.py │ ├── subtitle.py │ ├── utils.py │ └── video.py ├── tests ├── __init__.py ├── cassettes │ ├── addic7ed │ │ ├── test_download_subtitle.yaml │ │ ├── test_get_show_id.yaml │ │ ├── test_get_show_id_country.yaml │ │ ├── test_get_show_id_quote_dots_mixed_case.yaml │ │ ├── test_get_show_id_with_comma.yaml │ │ ├── test_get_show_id_year.yaml │ │ ├── test_get_show_ids.yaml │ │ ├── test_list_subtitles.yaml │ │ ├── test_list_subtitles_episode_alternative_series.yaml │ │ ├── test_list_subtitles_show_with_asterisk.yaml │ │ ├── test_query.yaml │ │ ├── test_query_no_year.yaml │ │ ├── test_query_parsing.yaml │ │ ├── test_query_parsing_colon.yaml │ │ ├── test_query_parsing_dash.yaml │ │ ├── test_query_parsing_quote_dots_mixed_case.yaml │ │ ├── test_query_wrong_series.yaml │ │ └── test_query_year.yaml │ ├── bsplayer │ │ ├── test_download_subtitle.yaml │ │ ├── test_list_subtitles_hash.yaml │ │ ├── test_login.yaml │ │ ├── test_logout.yaml │ │ └── test_query_hash_size.yaml │ ├── cli │ │ └── test_cli_download.yaml │ ├── gestdown │ │ ├── test_download_subtitle.yaml │ │ ├── test_download_with_bom.yaml │ │ ├── test_get_title_and_show_id_alternative_name.yaml │ │ ├── test_get_title_and_show_id_no_year.yaml │ │ ├── test_get_title_and_show_id_only_title.yaml │ │ ├── test_get_title_and_show_id_with_tvdb_id.yaml │ │ ├── test_list_subtitles.yaml │ │ ├── test_list_subtitles_episode_alternative_series.yaml │ │ ├── test_query.yaml │ │ ├── test_query_all_series.yaml │ │ ├── test_query_no_language.yaml │ │ ├── test_query_parsing.yaml │ │ ├── test_query_parsing_colon.yaml │ │ ├── test_query_parsing_dash.yaml │ │ ├── test_query_parsing_quote_dots_mixed_case.yaml │ │ ├── test_query_wrong_series.yaml │ │ ├── test_search_show_id.yaml │ │ ├── test_search_show_id_error.yaml │ │ ├── test_search_show_id_incomplete.yaml │ │ ├── test_search_show_id_no_year.yaml │ │ ├── test_search_show_id_quote.yaml │ │ ├── test_search_show_series_tvdb_id.yaml │ │ └── test_show_with_asterisk.yaml │ ├── napiprojekt │ │ ├── test_download_subtitles.yaml │ │ ├── test_list_subtitles.yaml │ │ ├── test_query_microdvd.yaml │ │ ├── test_query_srt.yaml │ │ ├── test_query_srt_reencode.yaml │ │ └── test_query_wrong_hash.yaml │ ├── omdb │ │ ├── test_get_id.yaml │ │ ├── test_get_title.yaml │ │ ├── test_get_wrong_id.yaml │ │ ├── test_get_wrong_title.yaml │ │ ├── test_refine_episode.yaml │ │ ├── test_refine_episode_original_series.yaml │ │ ├── test_refine_episode_with_country.yaml │ │ ├── test_refine_episode_with_country_hoc_us.yaml │ │ ├── test_refine_episode_year.yaml │ │ ├── test_refine_movie.yaml │ │ ├── test_refine_movie_guess_alternative_title.yaml │ │ ├── test_search.yaml │ │ ├── test_search_page.yaml │ │ ├── test_search_type.yaml │ │ ├── test_search_wrong_title.yaml │ │ └── test_search_year.yaml │ ├── opensubtitles │ │ ├── test_download_subtitle.yaml │ │ ├── test_list_subtitles_episode.yaml │ │ ├── test_list_subtitles_movie.yaml │ │ ├── test_list_subtitles_movie_no_hash.yaml │ │ ├── test_login_bad_password.yaml │ │ ├── test_login_vip_bad_password.yaml │ │ ├── test_login_vip_login.yaml │ │ ├── test_no_operation.yaml │ │ ├── test_query_hash_size.yaml │ │ ├── test_query_imdb_id.yaml │ │ ├── test_query_not_enough_information.yaml │ │ ├── test_query_query_episode.yaml │ │ ├── test_query_query_movie.yaml │ │ ├── test_query_query_season_episode.yaml │ │ └── test_query_wrong_hash_wrong_size.yaml │ ├── opensubtitlescom │ │ ├── test_download_subtitle.yaml │ │ ├── test_list_subtitles_episode.yaml │ │ ├── test_list_subtitles_movie.yaml │ │ ├── test_list_subtitles_movie_no_hash.yaml │ │ ├── test_login.yaml │ │ ├── test_login_bad_password.yaml │ │ ├── test_logout.yaml │ │ ├── test_query_hash_size.yaml │ │ ├── test_query_imdb_id.yaml │ │ ├── test_query_query_episode.yaml │ │ ├── test_query_query_movie.yaml │ │ ├── test_query_query_season_episode.yaml │ │ ├── test_query_tag_movie.yaml │ │ ├── test_query_wrong_hash_wrong_size.yaml │ │ ├── test_tag_match.yaml │ │ └── test_user_infos.yaml │ ├── podnapisi │ │ ├── test_download_subtitle.yaml │ │ ├── test_download_subtitles_with_title_unicode.yaml │ │ ├── test_list_subtitles_episode.yaml │ │ ├── test_list_subtitles_episode_alternative_series.yaml │ │ ├── test_list_subtitles_movie.yaml │ │ ├── test_query_episode.yaml │ │ └── test_query_movie.yaml │ ├── providers │ │ ├── test_download_best_subtitles.yaml │ │ ├── test_download_best_subtitles_min_score.yaml │ │ ├── test_download_best_subtitles_only_one.yaml │ │ └── test_list_subtitles_providers_download.yaml │ ├── subtitulamos │ │ ├── test_download_subtitle.yaml │ │ ├── test_download_subtitle_first_episode.yaml │ │ ├── test_download_subtitle_foo.yaml │ │ ├── test_download_subtitle_last_season.yaml │ │ ├── test_download_subtitle_year.yaml │ │ ├── test_list_subtitles_not_exist_episode.yaml │ │ ├── test_list_subtitles_not_exist_language.yaml │ │ ├── test_list_subtitles_not_exist_season.yaml │ │ ├── test_list_subtitles_not_exist_series.yaml │ │ └── test_list_subtitles_with_spanish_non_mx_search.yaml │ ├── tmdb │ │ ├── test_get_movie_id.yaml │ │ ├── test_get_movie_id_failed.yaml │ │ ├── test_get_series_id.yaml │ │ ├── test_get_series_wrong_id.yaml │ │ ├── test_refine_episode.yaml │ │ ├── test_refine_episode_original_series.yaml │ │ ├── test_refine_episode_with_country.yaml │ │ ├── test_refine_episode_with_country_hoc_us.yaml │ │ ├── test_refine_episode_year.yaml │ │ ├── test_refine_movie.yaml │ │ ├── test_refine_movie_guess_alternative_title.yaml │ │ ├── test_search_movie.yaml │ │ ├── test_search_movie_page.yaml │ │ ├── test_search_movie_wrong_title.yaml │ │ ├── test_search_movie_year.yaml │ │ ├── test_search_series.yaml │ │ └── test_search_series_wrong_name.yaml │ ├── tvdb │ │ ├── test_get_series.yaml │ │ ├── test_get_series_actors.yaml │ │ ├── test_get_series_actors_wrong_id.yaml │ │ ├── test_get_series_wrong_id.yaml │ │ ├── test_login.yaml │ │ ├── test_login_error.yaml │ │ ├── test_query_series_episodes.yaml │ │ ├── test_query_series_episodes_wrong_season.yaml │ │ ├── test_refine.yaml │ │ ├── test_refine_ambiguous.yaml │ │ ├── test_refine_ambiguous_2.yaml │ │ ├── test_refine_episode_alternative_series.yaml │ │ ├── test_refine_episode_no_year.yaml │ │ ├── test_refine_episode_partial.yaml │ │ ├── test_refine_episode_with_comma.yaml │ │ ├── test_refine_episode_with_country.yaml │ │ ├── test_refine_episode_with_country_hoc_us.yaml │ │ ├── test_refine_episode_year.yaml │ │ ├── test_refresh_token.yaml │ │ ├── test_search_series.yaml │ │ ├── test_search_series_wrong_name.yaml │ │ └── test_token_needs_refresh.yaml │ └── tvsubtitles │ │ ├── test_download_subtitle.yaml │ │ ├── test_download_subtitle_with_quote.yaml │ │ ├── test_get_episode_ids.yaml │ │ ├── test_get_episode_ids_wrong_season.yaml │ │ ├── test_list_subtitles.yaml │ │ ├── test_list_subtitles_episode_alternative_series.yaml │ │ ├── test_query.yaml │ │ ├── test_query_no_year.yaml │ │ ├── test_query_with_quote.yaml │ │ ├── test_query_wrong_episode.yaml │ │ ├── test_query_wrong_series.yaml │ │ ├── test_search_show_id.yaml │ │ ├── test_search_show_id_ambiguous.yaml │ │ ├── test_search_show_id_error.yaml │ │ ├── test_search_show_id_incomplete.yaml │ │ ├── test_search_show_id_no_year.yaml │ │ ├── test_search_show_id_uk.yaml │ │ ├── test_search_show_id_us.yaml │ │ └── test_search_show_id_year_in_title.yaml ├── cli │ ├── test_cli.py │ ├── test_download.py │ ├── test_generate_config.py │ └── test_helpers.py ├── conftest.py ├── providers │ ├── test_addic7ed.py │ ├── test_bsplayer.py │ ├── test_gestdown.py │ ├── test_napiprojekt.py │ ├── test_opensubtitles.py │ ├── test_opensubtitlescom.py │ ├── test_podnapisi.py │ ├── test_providers.py │ ├── test_subtitulamos.py │ └── test_tvsubtitles.py ├── refiners │ ├── test_hash.py │ ├── test_metadata.py │ ├── test_omdb.py │ ├── test_tmdb.py │ └── test_tvdb.py ├── test_archives.py ├── test_cache.py ├── test_core.py ├── test_extensions.py ├── test_matches.py ├── test_provider.py ├── test_provider_pool.py ├── test_score.py ├── test_subtitle.py ├── test_utils.py └── test_video.py └── tox.ini /.dockerignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # Pycharm 61 | .idea 62 | 63 | # Subliminal 64 | docs/ 65 | tests/ 66 | -------------------------------------------------------------------------------- /.github/actions/prepare-tests/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Download data for tests' 2 | description: 'Download .mkv and compress them with rar to be used in tests.' 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - name: Check Runner OS 8 | if: ${{ runner.os != 'Linux' && runner.os != 'Windows' && runner.os != 'macOS'}} 9 | shell: bash 10 | run: | 11 | echo "::error title=⛔ error hint::Support Linux, Windows, and macOS Only" 12 | exit 1 13 | - uses: actions/checkout@v4 14 | with: 15 | persist-credentials: false 16 | fetch-depth: 0 17 | - name: Cache test data 18 | id: cache-test-data 19 | uses: actions/cache@v4 20 | with: 21 | path: tests/data/ 22 | enableCrossOsArchive: true 23 | key: prepare-tests 24 | 25 | - name: Download only from Linux Runner OS 26 | if: ${{ steps.cache-test-data.outputs.cache-hit != 'true' && runner.os != 'Linux' }} 27 | shell: bash 28 | run: | 29 | echo "::error title=⛔ error hint::Download cache only from Linux: ${{ runner.os }}." 30 | exit 1 31 | - name: Install other test software (on Linux only) 32 | if: steps.cache-test-data.outputs.cache-hit != 'true' 33 | uses: awalsh128/cache-apt-pkgs-action@v1 34 | with: 35 | packages: rar 36 | version: 1.0 37 | - uses: actions/setup-python@v5 38 | if: steps.cache-test-data.outputs.cache-hit != 'true' 39 | with: 40 | python-version-file: .python-version-default 41 | cache: pip 42 | - name: Install dependencies 43 | if: steps.cache-test-data.outputs.cache-hit != 'true' 44 | shell: bash 45 | run: | 46 | python -Im pip install --upgrade pip 47 | python -Im pip install tox requests 48 | - name: Download tests data 49 | if: steps.cache-test-data.outputs.cache-hit != 'true' 50 | shell: bash 51 | run: | 52 | tox -e prepare-tests 53 | 54 | # - name: Upload files for test 55 | # uses: actions/upload-artifact@v4 56 | # with: 57 | # name: tests-data 58 | # path: tests/data/ 59 | # include-hidden-files: false 60 | # if-no-files-found: error 61 | # # heavy artifact should be deleted asap 62 | # retention-days: 1 63 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yaml: -------------------------------------------------------------------------------- 1 | name: Post coverage comment 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["CI"] 6 | types: 7 | - completed 8 | 9 | permissions: {} 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | env: 16 | FORCE_COLOR: "1" 17 | PIP_DISABLE_PIP_VERSION_CHECK: "1" 18 | PIP_NO_PYTHON_VERSION_WARNING: "1" 19 | 20 | jobs: 21 | display-coverage: 22 | name: Display coverage comment 23 | runs-on: ubuntu-latest 24 | if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' 25 | permissions: 26 | pull-requests: write 27 | contents: write 28 | actions: read 29 | steps: 30 | # DO NOT run actions/checkout here, for security reasons 31 | # For details, refer to https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 32 | - name: Post comment 33 | uses: py-cov-action/python-coverage-comment-action@v3 34 | with: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | GITHUB_PR_RUN_ID: ${{ github.event.workflow_run.id }} 37 | # Update those if you changed the default values: 38 | # COMMENT_ARTIFACT_NAME: python-coverage-comment-action 39 | # COMMENT_FILENAME: python-coverage-comment-action.txt 40 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "*.*.*" 9 | workflow_call: 10 | inputs: 11 | tag-name: 12 | description: 'Tag name' 13 | required: true 14 | type: 'string' 15 | workflow_dispatch: 16 | inputs: 17 | tag-name: 18 | description: 'Tag name' 19 | required: true 20 | type: 'string' 21 | pull_request: 22 | 23 | env: 24 | REGISTRY: ghcr.io 25 | IMAGE_NAME: ${{ github.repository }} 26 | TEST_IMAGE: subliminal 27 | 28 | jobs: 29 | build-docker: 30 | runs-on: ubuntu-latest 31 | permissions: 32 | contents: read 33 | packages: write 34 | 35 | steps: 36 | - name: Checkout repository 37 | if: github.event_name == 'push' || github.event_name == 'pull_request' 38 | uses: actions/checkout@v4 39 | with: 40 | persist-credentials: false 41 | - name: Checkout repository 42 | if: github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' 43 | uses: actions/checkout@v4 44 | with: 45 | ref: ${{ inputs.tag-name }} 46 | persist-credentials: false 47 | 48 | - name: Set up QEMU 49 | if: | 50 | github.event_name == 'push' 51 | || github.event_name == 'workflow_call' 52 | || github.event_name == 'workflow_dispatch' 53 | uses: docker/setup-qemu-action@v3 54 | 55 | - name: Set up Docker Buildx 56 | uses: docker/setup-buildx-action@v3 57 | with: 58 | cache-binary: false 59 | 60 | - name: Log in to the Container registry 61 | uses: docker/login-action@v3 62 | with: 63 | registry: ${{ env.REGISTRY }} 64 | username: ${{ github.actor }} 65 | password: ${{ secrets.GITHUB_TOKEN }} 66 | 67 | - name: Extract metadata (tags, labels) for Docker 68 | id: meta 69 | uses: docker/metadata-action@v5 70 | with: 71 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 72 | tags: | 73 | type=ref,event=branch 74 | type=semver,pattern={{version}} 75 | type=semver,pattern={{major}}.{{minor}} 76 | type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/0.') }} 77 | 78 | - name: Test building Docker image 79 | uses: docker/build-push-action@v6 80 | if: github.event_name == 'pull_request' 81 | with: 82 | context: . 83 | load: true 84 | tags: ${{ env.TEST_IMAGE }} 85 | cache-from: type=gha 86 | cache-to: type=gha,mode=max 87 | 88 | - name: Build and push Docker image 89 | uses: docker/build-push-action@v6 90 | if: | 91 | github.event_name == 'push' 92 | || github.event_name == 'workflow_call' 93 | || github.event_name == 'workflow_dispatch' 94 | with: 95 | context: . 96 | platforms: linux/amd64,linux/arm64,linux/arm/v7 97 | push: true 98 | tags: ${{ steps.meta.outputs.tags }} 99 | labels: ${{ steps.meta.outputs.labels }} 100 | cache-from: type=gha 101 | cache-to: type=gha,mode=max 102 | 103 | # vim:ts=2:sw=2:et 104 | -------------------------------------------------------------------------------- /.github/workflows/prepare-release-pr.yaml: -------------------------------------------------------------------------------- 1 | name: Prepare release PR 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | branch: 7 | description: 'Branch to base the release from' 8 | required: false 9 | default: 'main' 10 | bump: 11 | description: | 12 | 'Release type: major, minor or patch. ' 13 | 'Leave empty for autommatic detection based on changelog segments.' 14 | required: false 15 | default: '' 16 | prerelease: 17 | description: 'Prerelease (ex: rc1). Leave empty if not a pre-release.' 18 | required: false 19 | default: '' 20 | 21 | env: 22 | FORCE_COLOR: "1" 23 | 24 | jobs: 25 | build: 26 | runs-on: ubuntu-latest 27 | permissions: 28 | contents: write 29 | pull-requests: write 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | with: 34 | fetch-depth: 0 35 | # persist-credentials is needed in order for us to push the release branch. 36 | persist-credentials: true 37 | 38 | - name: Set up Python 39 | uses: actions/setup-python@v5 40 | with: 41 | python-version-file: .python-version-default 42 | cache: pip 43 | 44 | - name: Install dependencies 45 | run: | 46 | python -m pip install --upgrade pip 47 | pip install --upgrade setuptools tox 48 | 49 | - name: Prepare release PR 50 | env: 51 | BRANCH: ${{ github.event.inputs.branch }} 52 | BUMP: ${{ github.event.inputs.bump }} 53 | PRERELEASE: ${{ github.event.inputs.prerelease }} 54 | GH_TOKEN: ${{ github.token }} 55 | run: | 56 | git config user.name 'github-actions[bot]' 57 | git config user.email '41898282+github-actions[bot]@users.noreply.github.com' 58 | tox -e prepare-release-pr -- ${BRANCH} --bump=${BUMP} --prerelease=${PRERELEASE} 59 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Github release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*.*.*" 7 | workflow_call: 8 | inputs: 9 | tag-name: 10 | description: 'Tag name' 11 | required: true 12 | type: 'string' 13 | workflow_dispatch: 14 | inputs: 15 | tag-name: 16 | description: 'Tag name' 17 | required: true 18 | type: 'string' 19 | 20 | permissions: {} 21 | 22 | env: 23 | FORCE_COLOR: "1" 24 | PIP_DISABLE_PIP_VERSION_CHECK: "1" 25 | PIP_NO_PYTHON_VERSION_WARNING: "1" 26 | 27 | jobs: 28 | # Always build & lint package. 29 | build-package: 30 | name: Build & verify package 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | if: github.event_name == 'push' 36 | with: 37 | persist-credentials: false 38 | - name: Checkout 39 | uses: actions/checkout@v4 40 | if: | 41 | github.event_name == 'workflow_dispatch' 42 | || github.event_name == 'workflow_call' 43 | with: 44 | ref: ${{ inputs.tag-name }} 45 | persist-credentials: false 46 | - name: Build dist 47 | uses: hynek/build-and-inspect-python-package@v2 48 | id: baipp 49 | outputs: 50 | tag: ${{ steps.baipp.outputs.package_version }} 51 | 52 | 53 | github-release: 54 | name: Make a GitHub Release 55 | needs: [build-package] 56 | if: github.repository == 'Diaoul/subliminal' 57 | runs-on: ubuntu-latest 58 | permissions: 59 | # IMPORTANT: mandatory for making GitHub Releases 60 | contents: write 61 | id-token: write 62 | 63 | # Publish a Github release when a tag was pushed, or it's called with a tag. 64 | steps: 65 | - name: Checkout 66 | uses: actions/checkout@v4 67 | with: 68 | fetch-tags: true 69 | ref: ${{ needs.build-package.outputs.tag }} 70 | persist-credentials: false 71 | 72 | - name: Download packages built by build-and-inspect-python-package 73 | uses: actions/download-artifact@v4 74 | with: 75 | name: Packages 76 | path: dist 77 | - name: Publish GitHub Release 78 | uses: softprops/action-gh-release@v2 79 | with: 80 | files: dist/* 81 | generate_release_notes: true 82 | draft: true 83 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # Workflow name should be "publish.yaml", as defined in PyPI 2 | name: Publish on PyPI 3 | 4 | on: 5 | release: 6 | types: 7 | - published 8 | 9 | permissions: {} 10 | 11 | env: 12 | FORCE_COLOR: "1" 13 | PIP_DISABLE_PIP_VERSION_CHECK: "1" 14 | PIP_NO_PYTHON_VERSION_WARNING: "1" 15 | 16 | # https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ 17 | jobs: 18 | # Always build & lint package. 19 | build-package: 20 | name: Build & verify package 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | persist-credentials: false 27 | 28 | - uses: hynek/build-and-inspect-python-package@v2 29 | id: baipp 30 | 31 | publish-to-pypi: 32 | name: Publish package to pypi. 33 | needs: [build-package] 34 | runs-on: ubuntu-latest 35 | environment: 36 | name: pypi 37 | url: https://pypi.org/p/subliminal 38 | permissions: 39 | # IMPORTANT: this permission is mandatory for trusted publishing 40 | id-token: write 41 | # only publish to PyPI on Github release published 42 | if: | 43 | github.repository == 'Diaoul/subliminal' 44 | && github.event_name == 'release' 45 | && github.event.action == 'published' 46 | steps: 47 | - name: Download packages built by build-and-inspect-python-package 48 | uses: actions/download-artifact@v4 49 | with: 50 | name: Packages 51 | path: dist 52 | - name: Publish package distributions to PyPI 53 | uses: pypa/gh-action-pypi-publish@release/v1 54 | -------------------------------------------------------------------------------- /.github/workflows/scheduled-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Scheduled Tests 2 | 3 | on: 4 | schedule: 5 | - cron: "12 12 12 * *" # run once a month on the 12th at 12:12 6 | workflow_dispatch: 7 | 8 | permissions: {} 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | env: 15 | FORCE_COLOR: "1" 16 | PIP_DISABLE_PIP_VERSION_CHECK: "1" 17 | PIP_NO_PYTHON_VERSION_WARNING: "1" 18 | 19 | jobs: 20 | test-api: 21 | name: Run tests with deleted requests cassettes 22 | if: | 23 | (github.repository == 'Diaoul/subliminal' && github.event_name == 'schedule') 24 | || github.event_name == 'workflow_dispatch' 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | persist-credentials: false 30 | - uses: actions/setup-python@v5 31 | with: 32 | python-version: "3.x" 33 | - name: Prepare tests 34 | uses: ./.github/actions/prepare-tests 35 | - name: Display downloaded files 36 | run: ls -R tests/data 37 | - name: install 38 | run: | 39 | python -Im pip install -U pip 40 | python -Im pip install -e ".[tests]" 41 | - name: remove cassettes 42 | run: | 43 | rm -rf tests/cassettes 44 | - name: run test 45 | run: | 46 | pytest 47 | -------------------------------------------------------------------------------- /.github/workflows/tag-release.yaml: -------------------------------------------------------------------------------- 1 | name: Tag release 2 | 3 | on: 4 | # pull_request: 5 | # types: 6 | # - closed 7 | workflow_dispatch: 8 | inputs: 9 | version: 10 | description: 'Release tag version.' 11 | type: string 12 | default: NONE 13 | required: true 14 | 15 | permissions: {} 16 | 17 | env: 18 | FORCE_COLOR: "1" 19 | 20 | # https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ 21 | jobs: 22 | # Always build & lint package. 23 | build-package: 24 | name: Build & verify package 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 30 | persist-credentials: false 31 | 32 | - uses: hynek/build-and-inspect-python-package@v2 33 | 34 | define-tag: 35 | name: Define tag 36 | # tag a release after a release PR was accepted 37 | runs-on: ubuntu-latest 38 | steps: 39 | - if: github.event_name == 'workflow_dispatch' 40 | name: from workflow_dispatch 41 | env: 42 | RELEASE_VERSION: ${{ inputs.version }} 43 | run: | 44 | echo "RELEASE_VERSION=${RELEASE_VERSION}" >> $GITHUB_ENV 45 | - if: | 46 | github.event_name == 'pull_request' 47 | && github.event.action == 'closed' 48 | && github.event.pull_request.merged == true 49 | && github.event.pull_request.head.repo.full_name == github.repository 50 | && contains(github.event.pull_request.labels.*.name, 'type/release') 51 | && startsWith(github.head_ref, 'releases/') 52 | name: from merged PR 53 | run: | 54 | echo "RELEASE_VERSION=${GITHUB_HEAD_REF#releases/}" >> $GITHUB_ENV 55 | - name: set output 56 | id: set-output 57 | env: 58 | RELEASE_VERSION: ${{ env.RELEASE_VERSION }} 59 | run: | 60 | echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_OUTPUT 61 | outputs: 62 | tag: ${{ steps.set-output.outputs.RELEASE_VERSION }} 63 | 64 | tag-and-release: 65 | name: Tag and publish a release draft 66 | needs: [define-tag, build-package] 67 | runs-on: ubuntu-latest 68 | permissions: 69 | id-token: write 70 | contents: write 71 | if: ${{ needs.define-tag.outputs.tag != '' }} 72 | env: 73 | RELEASE_VERSION: ${{ needs.define-tag.outputs.tag }} 74 | steps: 75 | - uses: actions/checkout@v4 76 | with: 77 | fetch-depth: 0 78 | # keep credentials to create a tag 79 | persist-credentials: true 80 | 81 | - name: Tag the commit 82 | id: create-tag 83 | run: | 84 | echo "Release version: $RELEASE_VERSION" 85 | git config user.name 'github-actions[bot]' 86 | git config user.email '41898282+github-actions[bot]@users.noreply.github.com' 87 | git tag --annotate --message="Release version $RELEASE_VERSION" $RELEASE_VERSION ${{ github.sha }} 88 | git push origin $RELEASE_VERSION 89 | echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_OUTPUT 90 | outputs: 91 | tag-name: ${{ steps.create-tag.outputs.RELEASE_VERSION }} 92 | 93 | 94 | trigger-release: 95 | # Tagging from an action does not trigger a push>tag event, therefore we use workflow_call 96 | # Publish a release draft after pushing the release tag 97 | needs: [tag-and-release] 98 | uses: ./.github/workflows/publish-release.yaml 99 | with: 100 | tag-name: ${{ needs.tag-and-release.outputs.tag-name }} 101 | 102 | trigger-docker-build: 103 | # Tagging from an action does not trigger a push>tag event, therefore we use workflow_call 104 | # Publish a docker image for the release tag 105 | needs: [tag-and-release] 106 | uses: ./.github/workflows/docker-build.yaml 107 | with: 108 | tag-name: ${{ needs.tag-and-release.outputs.tag-name }} 109 | -------------------------------------------------------------------------------- /.github/zizmor.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | dangerous-triggers: 3 | ignore: 4 | - coverage.yaml:3:1 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # Pycharm 61 | .idea 62 | 63 | # Subliminal 64 | tests/data/mkv/ 65 | tests/data/rar/ 66 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-toml 6 | - id: check-yaml 7 | exclude: ^tests/cassettes/ 8 | - id: end-of-file-fixer 9 | 10 | - repo: https://github.com/crate-ci/typos 11 | rev: v1.30.3 12 | hooks: 13 | - id: typos 14 | 15 | - repo: https://github.com/astral-sh/ruff-pre-commit 16 | rev: v0.11.2 17 | hooks: 18 | - id: ruff 19 | args: [--fix, --unsafe-fixes] 20 | - id: ruff-format 21 | 22 | - repo: https://github.com/abravalheri/validate-pyproject 23 | rev: v0.24.1 24 | hooks: 25 | - id: validate-pyproject 26 | name: validate-pyproject 27 | 28 | - repo: https://github.com/PyCQA/doc8 29 | rev: v1.1.2 30 | hooks: 31 | - id: doc8 32 | description: This hook runs doc8 for linting docs 33 | require_serial: true 34 | 35 | - repo: https://github.com/woodruffw/zizmor-pre-commit 36 | rev: v1.5.2 37 | hooks: 38 | - id: zizmor 39 | 40 | - repo: local 41 | hooks: 42 | - id: changelog-fragment-filenames 43 | name: changelog fragment 44 | language: fail 45 | entry: changelog fragment files must be named *.(breaking|change|provider|refiner|cli|deprecation|doc|misc).rst 46 | exclude: 47 | ^changelog.d/(\..*|towncrier_template.rst|.*\.(breaking|change|provider|refiner|cli|deprecation|doc|misc).rst) 48 | files: ^changelog.d/ 49 | -------------------------------------------------------------------------------- /.python-version-default: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | version: 2 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | # Keep in sync with CI.yaml/docs, hatch.toml and tox/docs. 10 | python: "3.12" 11 | 12 | python: 13 | install: 14 | - method: pip 15 | path: . 16 | extra_requirements: 17 | - docs 18 | 19 | sphinx: 20 | configuration: docs/conf.py 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-alpine 2 | 3 | LABEL org.opencontainers.image.authors="Antoine Bertin " 4 | 5 | # set version label 6 | ARG BUILD_WITH_UNRAR=false 7 | ARG UNRAR_VERSION=6.2.6 8 | 9 | RUN \ 10 | if [ "$BUILD_WITH_UNRAR" = true ]; then \ 11 | apk add -U --update --no-cache --virtual=build-dependencies build-base curl && \ 12 | echo "**** install unrar from source ****" && \ 13 | mkdir /tmp/unrar && \ 14 | curl -o /tmp/unrar.tar.gz -L "https://www.rarlab.com/rar/unrarsrc-${UNRAR_VERSION}.tar.gz" && \ 15 | tar xf /tmp/unrar.tar.gz -C /tmp/unrar --strip-components=1 && \ 16 | cd /tmp/unrar && \ 17 | make && \ 18 | install -v -m755 unrar /usr/local/bin && \ 19 | apk del build-dependencies curl && \ 20 | rm -rf /tmp/unrar /tmp/unrar.tar.gz; \ 21 | fi 22 | 23 | # install libmediainfo for metadata refiner 24 | RUN apk add --no-cache libmediainfo 25 | 26 | WORKDIR /usr/src/app 27 | VOLUME /usr/src/cache 28 | 29 | # Add git for setuptools-scm 30 | RUN apk add --no-cache git 31 | RUN \ 32 | --mount=type=bind,source=.,target=. \ 33 | --mount=type=cache,id=pip,target=/root/.cache/pip \ 34 | python -m pip install . 35 | 36 | ENTRYPOINT ["subliminal", "--cache-dir", "/usr/src/cache"] 37 | CMD ["--help"] 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Antoine Bertin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include HISTORY.rst 3 | include CONTRIBUTING.md 4 | include RELEASING.md 5 | include Dockerfile 6 | include .dockerignore 7 | include .pre-commit-config.yaml 8 | include .readthedocs.yaml 9 | include .python-version-default 10 | include tox.ini 11 | 12 | graft docs 13 | graft tests 14 | graft scripts 15 | 16 | prune changelog.d 17 | prune */__pycache__ 18 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Release procedure 2 | 3 | The git commands assume the following remotes are setup: 4 | 5 | * ``origin``: your own fork of the repository. 6 | * ``upstream``: the ``Diaoul/subliminal`` official repository. 7 | 8 | ## Preparing a new release: Manual method 9 | 10 | There are few steps to follow when making a new release: 11 | 12 | 1. Lint the code, check types, test, check the coverage is high enough 13 | and build and test the documentation. 14 | 15 | 2. Bump the version number, wherever it is, and update ``HISTORY.rst`` 16 | with the changelog fragments. 17 | 18 | 3. Tag the new version with ``git``. 19 | 20 | 4. Publish the source distribution and wheel to Pypi. 21 | 22 | Although this can all be done manually, there is an automated way, 23 | to limit errors. 24 | 25 | ## Preparing a new release: Automatic method 26 | 27 | We use an automated workflow for releases, that uses GitHub workflows and is triggered 28 | by [manually running](https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow) 29 | the [prepare-release-pr workflow](https://github.com/Diaoul/subliminal/actions/workflows/prepare-release-pr.yaml) 30 | on GitHub Actions. 31 | 32 | 1. The automation will decide the new version number based on the following criteria: 33 | 34 | - If there is any ``.breaking.rst`` files in the ``changelog.d`` directory, release a new major release 35 | (e.g. 7.0.0 -> 8.0.0) 36 | - If there are any ``.change.rst`` files in the 37 | ``changelog.d`` directory, release a new minor release 38 | (e.g. 7.0.0 -> 7.1.0) 39 | - Otherwise, release a patch release 40 | (e.g. 7.0.0 -> 7.0.1) 41 | - If the "prerelease" input is set, append the string to the version number 42 | (e.g. 7.0.0 -> 8.0.0rc1, if "major" is set, and "prerelease" is set to `rc1`) 43 | 44 | The choice of the bumped version can be bypassed by the "bump" input 45 | (empty choice means automatic bumped version detection). 46 | 47 | 2. Trigger the workflow with the following inputs: 48 | 49 | - branch: **main** 50 | - bump: [**empty**, major, minor, patch] 51 | - prerelease: empty 52 | 53 | Or via the commandline:: 54 | 55 | gh workflow run prepare-release-pr.yaml -f branch=main -f bump=major -f prerelease= 56 | 57 | Or you can create the PR using ``tox``:: 58 | 59 | tox -e prepare-release-pr -- main --remote=upstream 60 | 61 | The automated workflow will publish a PR for a branch ``release-8.0.0``. 62 | 63 | 64 | ## Preparing a new release: Semi-automatic method 65 | 66 | To release a version ``MAJOR.MINOR.PATCH-PRERELEASE``, follow these steps: 67 | 68 | * Create a branch ``release-MAJOR.MINOR.PATCH-PRERELEASE`` from the ``upstream/main`` branch. 69 | 70 | Ensure your are updated and in a clean working tree. 71 | 72 | * Using ``tox``, generate docs, changelog, announcements:: 73 | 74 | $ tox -e release -- MAJOR.MINOR.PATCH-PRERELEASE 75 | 76 | This will generate a commit with all the changes ready for pushing. 77 | 78 | * Push the ``release-MAJOR.MINOR.PATCH-PRERELEASE`` local branch to the remote 79 | ``upstream/release-MAJOR.MINOR.PATCH-PRERELEASE`` 80 | 81 | * Open a PR for the ``release-MAJOR.MINOR.PATCH-PRERELEASE`` branch targeting ``upstream/main``. 82 | 83 | 84 | ## Releasing 85 | 86 | Both automatic and manual processes described above follow the same steps from this point onward. 87 | 88 | * After all tests pass and the PR has been approved, merge the PR. 89 | 90 | * Then tag and push the new release with ``git tag `` followed by ``git push upstream ``. 91 | Or manually trigger the [tag-release workflow](https://github.com/Diaoul/subliminal/actions/workflows/tag-release.yaml). 92 | 93 | * This new tag will then trigger the 94 | [publish-release workflow](https://github.com/Diaoul/subliminal/actions/workflows/publish-release.yaml), 95 | using the tag as source. 96 | 97 | This job will publish a draft for a Github release. 98 | 99 | * When the Github release draft is published, the 100 | [publish workflow](https://github.com/Diaoul/subliminal/actions/workflows/publish.yaml) will publish to PyPI. 101 | -------------------------------------------------------------------------------- /changelog.d/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | -------------------------------------------------------------------------------- /changelog.d/1250.change.rst: -------------------------------------------------------------------------------- 1 | score: add fps match, remove hearing_impaired match 2 | -------------------------------------------------------------------------------- /changelog.d/1299.cli.rst: -------------------------------------------------------------------------------- 1 | cli: refactor the cli.py file into a folder 2 | -------------------------------------------------------------------------------- /docs/_intersphinx/README.md: -------------------------------------------------------------------------------- 1 | # Creating a Custom Intersphinx Mapping 2 | 3 | Copied from [exhale documentation.](https://github.com/svenevs/exhale/blob/master/docs/_intersphinx/README.md) 4 | 5 | ## The Issue 6 | 7 | The BeautifulSoup intersphinx mapping does not work; see [bug here][bug]. 8 | 9 | [bug]: https://bugs.launchpad.net/beautifulsoup/+bug/1453370 10 | 11 | So we'll just do it manually. 12 | 13 | ## The Tool 14 | 15 | Use the tool `sphobjinv` to do this (`pip install sphobjinv`). 16 | 17 | - [Explanation of syntax][syntax]. 18 | 19 | [syntax]: https://sphinx-objectsinv-encoderdecoder.readthedocs.io/en/latest/syntax.html 20 | 21 | ## How to Reproduce 22 | 23 | 1. Downloaded original objects.inv from bs4 docs 24 | 25 | ```console 26 | $ curl -O https://www.crummy.com/software/BeautifulSoup/bs4/doc/objects.inv 27 | $ mv objects.inv bs4_objects.inv 28 | ``` 29 | 30 | 2. Converted that bad boy to human readable (the file `bs4_objects.txt`). 31 | 32 | ```console 33 | $ sphobjinv convert plain bs4_objects.inv bs4_objects.txt 34 | ``` 35 | 36 | 3. Any time I have a class I want to add a reference for, just add that line to 37 | `bs4_objects.txt`. Recall that there is a specific [syntax][syntax] associated 38 | with these files. 39 | 40 | 4. When you add a new line, simply run (assuming you are in this directory) 41 | 42 | ```console 43 | $ sphobjinv convert zlib bs4_objects.txt bs4_objects.inv 44 | ``` 45 | 46 | 5. Re-run sphinx, usually you need to clean it for the doctree index to be rebuilt. 47 | 48 | ```console 49 | $ make clean html 50 | ``` 51 | 52 | ## Note 53 | 54 | These changes apply because we have **2** things in `conf.py`: 55 | 56 | 1. In the `extensions` list, we have `"sphinx.ext.intersphinx"`. 57 | 2. We have configured our intersphinx mapping to point to here (as opposed to the 58 | one being hosted online): 59 | 60 | ```py 61 | intersphinx_mapping = { 62 | 'bs4': ('https://www.crummy.com/software/BeautifulSoup/bs4/doc/', "_intersphinx/bs4_objects.inv") 63 | } 64 | ``` 65 | 66 | ## In the Documentation 67 | 68 | So now we can do something like 69 | 70 | ```rst 71 | - See :class:`bs4.BeautifulSoup` 72 | - See :meth:`bs4.BeautifulSoup.get_text` 73 | - See :class:`bs4.element.Tag` 74 | ``` 75 | 76 | because we added the following line to our `bs4_objects.txt`: 77 | 78 | ``` 79 | bs4.BeautifulSoup py:class 1 index.html#beautifulsoup - 80 | bs4.BeautifulSoup.get_text py:method 1 index.html#get-text - 81 | bs4.element.Tag py:class 1 index.html#tag - 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/_intersphinx/importlib_metadata_objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Diaoul/subliminal/a1a820f70651e2bbfd5ef85f54840a9390787a0a/docs/_intersphinx/importlib_metadata_objects.inv -------------------------------------------------------------------------------- /docs/_intersphinx/importlib_metadata_objects.txt: -------------------------------------------------------------------------------- 1 | # Sphinx inventory version 2 2 | # Project: importlib_metadata 3 | # Version: 8.0.1.dev0+gf390168.d20240623 4 | # From https://github.com/svenevs/exhale/tree/master/docs/_intersphinx 5 | # The remainder of this file is compressed using zlib. 6 | importlib.metadata.EntryPoint py:class 1 library/importlib.metadata.html#entry-points - 7 | -------------------------------------------------------------------------------- /docs/_static/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | -------------------------------------------------------------------------------- /docs/api/cache.rst: -------------------------------------------------------------------------------- 1 | Cache 2 | ===== 3 | .. module:: subliminal.cache 4 | 5 | .. autodata:: SHOW_EXPIRATION_TIME 6 | :annotation: 7 | 8 | .. autodata:: EPISODE_EXPIRATION_TIME 9 | :annotation: 10 | 11 | .. autodata:: REFINER_EXPIRATION_TIME 12 | :annotation: 13 | 14 | .. data:: region 15 | :annotation: 16 | 17 | The :class:`~dogpile.cache.region.CacheRegion` 18 | 19 | 20 | Refer to dogpile.cache's `region configuration documentation 21 | `_ 22 | to see how to configure the region. 23 | -------------------------------------------------------------------------------- /docs/api/cli.rst: -------------------------------------------------------------------------------- 1 | CLI 2 | === 3 | .. automodule:: subliminal.cli 4 | :members: 5 | :exclude-members: MutexLock, plural 6 | -------------------------------------------------------------------------------- /docs/api/core.rst: -------------------------------------------------------------------------------- 1 | Core 2 | ==== 3 | .. automodule:: subliminal.core 4 | :members: 5 | :exclude-members: ARCHIVE_EXTENSIONS 6 | 7 | .. autodata:: ARCHIVE_EXTENSIONS 8 | :annotation: 9 | -------------------------------------------------------------------------------- /docs/api/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ========== 3 | .. automodule:: subliminal.exceptions 4 | :members: 5 | -------------------------------------------------------------------------------- /docs/api/extensions.rst: -------------------------------------------------------------------------------- 1 | Extensions 2 | ========== 3 | .. automodule:: subliminal.extensions 4 | :members: 5 | -------------------------------------------------------------------------------- /docs/api/providers.rst: -------------------------------------------------------------------------------- 1 | Providers 2 | ========= 3 | .. automodule:: subliminal.providers 4 | :members: 5 | 6 | Addic7ed 7 | -------- 8 | .. automodule:: subliminal.providers.addic7ed 9 | :members: 10 | 11 | BSPlayer 12 | -------- 13 | .. automodule:: subliminal.providers.bsplayer 14 | :members: 15 | 16 | Gestdown 17 | -------- 18 | .. automodule:: subliminal.providers.gestdown 19 | :members: 20 | 21 | NapiProjekt 22 | ----------- 23 | .. automodule:: subliminal.providers.napiprojekt 24 | :members: 25 | 26 | OpenSubtitles 27 | ------------- 28 | .. automodule:: subliminal.providers.opensubtitles 29 | :members: 30 | 31 | OpenSubtitles.com 32 | ----------------- 33 | .. automodule:: subliminal.providers.opensubtitlescom 34 | :members: 35 | 36 | Podnapisi 37 | --------- 38 | .. automodule:: subliminal.providers.podnapisi 39 | :members: 40 | 41 | Subtitulamos 42 | ------------ 43 | .. automodule:: subliminal.providers.subtitulamos 44 | :members: 45 | 46 | TVsubtitles 47 | ----------- 48 | .. automodule:: subliminal.providers.tvsubtitles 49 | :members: 50 | -------------------------------------------------------------------------------- /docs/api/refiners.rst: -------------------------------------------------------------------------------- 1 | .. _refiners: 2 | 3 | Refiners 4 | ======== 5 | .. automodule:: subliminal.refiners 6 | 7 | 8 | Hash 9 | -------- 10 | .. autofunction:: subliminal.refiners.hash.refine 11 | 12 | 13 | Metadata 14 | -------- 15 | .. autofunction:: subliminal.refiners.metadata.refine 16 | 17 | 18 | TVDB 19 | ---- 20 | .. autofunction:: subliminal.refiners.tvdb.refine 21 | 22 | 23 | OMDb 24 | ---- 25 | .. autofunction:: subliminal.refiners.omdb.refine 26 | 27 | 28 | TMDB 29 | ---- 30 | .. autofunction:: subliminal.refiners.tmdb.refine 31 | -------------------------------------------------------------------------------- /docs/api/score.rst: -------------------------------------------------------------------------------- 1 | Score 2 | ===== 3 | .. automodule:: subliminal.score 4 | :members: 5 | -------------------------------------------------------------------------------- /docs/api/subtitle.rst: -------------------------------------------------------------------------------- 1 | Subtitle 2 | ======== 3 | .. automodule:: subliminal.subtitle 4 | :members: 5 | :exclude-members: SUBTITLE_EXTENSIONS 6 | 7 | .. autodata:: SUBTITLE_EXTENSIONS 8 | :annotation: 9 | -------------------------------------------------------------------------------- /docs/api/utils.rst: -------------------------------------------------------------------------------- 1 | Utils 2 | ===== 3 | .. automodule:: subliminal.utils 4 | :members: 5 | -------------------------------------------------------------------------------- /docs/api/video.rst: -------------------------------------------------------------------------------- 1 | Video 2 | ===== 3 | .. automodule:: subliminal.video 4 | :members: 5 | :exclude-members: VIDEO_EXTENSIONS 6 | 7 | .. autodata:: VIDEO_EXTENSIONS 8 | :annotation: 9 | -------------------------------------------------------------------------------- /docs/config.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | cache_dir = "~/.cache/subliminal" 3 | 4 | [provider.opensubtitlescom] 5 | username = "subliminal" 6 | password = "subliminal" 7 | apikey = "mij33pjc3kOlup1qOKxnWWxvle2kFbMH" 8 | 9 | [provider.addic7ed] 10 | username = "subliminal" 11 | password = "subliminal" 12 | timeout = 20 13 | 14 | [refiner.omdb] 15 | apikey = "44d5b275" 16 | 17 | [refiner.tmdb] 18 | apikey = "xxxxxxxxx" 19 | 20 | [download] 21 | provider = ["addic7ed", "opensubtitlescom", "opensubtitles"] 22 | refiner = ["metadata", "hash", "omdb"] 23 | ignore_refiner = ["tmdb"] 24 | language = ["fr", "en", "pt-br"] 25 | foreign_only = false 26 | encoding = "utf-8" 27 | min_score = 50 28 | archives = true 29 | verbose = 3 30 | -------------------------------------------------------------------------------- /docs/conftest.py: -------------------------------------------------------------------------------- 1 | """Docs conftest.py.""" 2 | 3 | import os 4 | from pathlib import Path 5 | from unittest.mock import Mock 6 | 7 | import pytest 8 | from vcr import VCR 9 | 10 | from subliminal.cache import region 11 | 12 | vcr = VCR( 13 | path_transformer=lambda path: path + '.yaml', 14 | record_mode=os.environ.get('VCR_RECORD_MODE', 'once'), 15 | match_on=['method', 'scheme', 'host', 'port', 'path', 'query', 'body'], 16 | cassette_library_dir=os.path.realpath(os.path.join('docs', 'cassettes')), 17 | ) 18 | 19 | 20 | @pytest.fixture(autouse=True, scope='session') 21 | def _configure_region() -> None: 22 | region.configure('dogpile.cache.null') 23 | region.configure = Mock() 24 | 25 | 26 | @pytest.fixture(autouse=True) 27 | def _chdir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: 28 | monkeypatch.chdir(tmp_path) 29 | 30 | 31 | @pytest.fixture(autouse=True) 32 | def use_cassette(request: pytest.FixtureRequest) -> None: 33 | """Use VCR cassette automatically.""" 34 | with vcr.use_cassette('test_' + request.fspath.purebasename): 35 | yield 36 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. subliminal documentation master file, created by 2 | sphinx-quickstart on Sat Jul 11 00:40:28 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to subliminal! 7 | ====================== 8 | Subliminal is a python3 library to search and download subtitles. 9 | It comes with an easy to use yet powerful :abbr:`CLI (command-line interface)` suitable for direct use or cron jobs. 10 | 11 | 12 | Documentation 13 | ------------- 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | user/usage 18 | user/installation 19 | user/how_it_works 20 | user/cli 21 | user/configuration_file 22 | user/provider_guide 23 | 24 | .. toctree:: 25 | :maxdepth: 1 26 | 27 | user/changelog 28 | 29 | 30 | API Documentation 31 | ----------------- 32 | If you are looking for information on a specific function, class or method, this part of the documentation is for you. 33 | 34 | .. toctree:: 35 | :caption: API 36 | :maxdepth: 1 37 | 38 | api/core 39 | api/video 40 | api/subtitle 41 | api/providers 42 | api/refiners 43 | api/extensions 44 | api/score 45 | api/utils 46 | api/cache 47 | api/cli 48 | api/exceptions 49 | 50 | 51 | License 52 | ------- 53 | MIT 54 | 55 | 56 | Indices and tables 57 | ================== 58 | * :ref:`genindex` 59 | * :ref:`modindex` 60 | * :ref:`search` 61 | -------------------------------------------------------------------------------- /docs/user/changelog.rst: -------------------------------------------------------------------------------- 1 | Release notes 2 | ============= 3 | 4 | .. changelog:: 5 | :towncrier: ../../ 6 | :towncrier-skip-if-empty: 7 | :changelog_file: ../../HISTORY.rst 8 | -------------------------------------------------------------------------------- /docs/user/cli.rst: -------------------------------------------------------------------------------- 1 | .. _cli: 2 | 3 | CLI 4 | === 5 | 6 | .. _cli-subliminal: 7 | 8 | subliminal 9 | ---------- 10 | .. program-output:: subliminal --help 11 | 12 | .. _cli-subliminal-download: 13 | 14 | subliminal download 15 | ------------------- 16 | .. program-output:: subliminal download --help 17 | 18 | .. _cli-subliminal-cache: 19 | 20 | subliminal cache 21 | ---------------- 22 | .. program-output:: subliminal cache --help 23 | -------------------------------------------------------------------------------- /docs/user/configuration_file.rst: -------------------------------------------------------------------------------- 1 | .. _configuration-file: 2 | 3 | Configuration file 4 | ================== 5 | 6 | The `TOML `_ format is used for the configuration file. 7 | The path of the configuration file to use can be specified with the ``--config/-c`` 8 | option in the :ref:`cli`. 9 | 10 | Sections 11 | -------- 12 | 13 | The configuration file supports different sections or tables: 14 | 15 | - a ``[default]`` table, corresponding to the options of the :ref:`cli-subliminal-download` command. 16 | 17 | - ``[refiner.]`` and ``[provider.]`` tables to specify refiner and provider options. 18 | 19 | - tables for each CLI sub-commands: ``[cache]``, ``[download]``. 20 | 21 | Each CLI option can be specified in the configuration file, even mandatory options 22 | like ``--language`` for the :ref:`cli-subliminal-download` sub-command. 23 | CLI arguments (like ``path``) cannot be specified in the configuration file. 24 | 25 | Options that can be used multiple times (like ``--language``) need to be defined as arrays. 26 | 27 | 28 | Example 29 | ------- 30 | 31 | An example of configuration file: 32 | 33 | .. literalinclude:: ../config.toml 34 | :language: toml 35 | 36 | 37 | Default configuration 38 | --------------------- 39 | 40 | Here is a list of all the options of the configuration file. 41 | Commented options need a value to be valid. 42 | 43 | .. program-output:: python -c "from subliminal.cli import generate_default_config; print(generate_default_config(commented=False))" 44 | :language: toml 45 | -------------------------------------------------------------------------------- /docs/user/how_it_works.rst: -------------------------------------------------------------------------------- 1 | How it works 2 | ============ 3 | 4 | Providers 5 | --------- 6 | Subliminal uses multiple providers to give users a vast choice and have a better chance to find the best matching 7 | subtitles. Current supported providers are: 8 | 9 | * Addic7ed 10 | * BSPlayer 11 | * Gestdown 12 | * NapiProjekt 13 | * OpenSubtitles 14 | * OpenSubtitles.com 15 | * Podnapisi 16 | * Subtitulamos 17 | * TvSubtitles 18 | 19 | Providers all inherit the same :class:`~subliminal.providers.Provider` base class and thus share the same API. 20 | They are registered on the ``subliminal.providers`` entry point and are exposed through the 21 | :data:`~subliminal.extensions.provider_manager` for easy access. 22 | 23 | To work with multiple providers seamlessly, the :class:`~subliminal.core.ProviderPool` exposes the same API but 24 | distributes it to its providers and :class:`~subliminal.core.AsyncProviderPool` does it asynchronously. 25 | 26 | .. _scoring: 27 | 28 | Scoring 29 | ------- 30 | Rating subtitles and comparing them is probably the most difficult part and this is where subliminal excels with its 31 | powerful scoring algorithm. 32 | 33 | Using `guessit `_ and `knowit `_, subliminal extracts 34 | properties of the video and match them with the properties of the subtitles found with the providers. 35 | 36 | Equations in :mod:`subliminal.score` give a score to each property (called a match). The more matches the video and 37 | the subtitle have, the higher the score computed with :func:`~subliminal.score.compute_score` gets. 38 | 39 | 40 | Libraries 41 | --------- 42 | Various libraries are used by subliminal and are key to its success: 43 | 44 | * `guessit `_ to guess information from filenames 45 | * `knowit `_ to detect embedded subtitles in videos and read other video metadata 46 | * `babelfish `_ to work with languages 47 | * `requests `_ to make human readable HTTP requests 48 | * `BeautifulSoup `_ to parse HTML and XML 49 | * `dogpile.cache `_ to cache intermediate search results 50 | * `stevedore `_ to manage the provider entry point 51 | * `chardet `_ to detect subtitles' encoding 52 | * `srt `_ to validate downloaded SubRip subtitles 53 | * `pysub2 `_ to validate and convert downloaded subtitles to other formats. 54 | -------------------------------------------------------------------------------- /docs/user/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | From Pypi 5 | --------- 6 | 7 | For a better isolation with your system you should use a dedicated virtualenv. 8 | The preferred installation method is to use `pipx `_ that does that for you:: 9 | 10 | $ pipx install subliminal 11 | 12 | Subliminal can be also be installed as a regular python module by running:: 13 | 14 | $ pip install --user subliminal 15 | 16 | From source 17 | ----------- 18 | 19 | If you want to modify the code, `fork `_ this repo, 20 | clone your fork locally and install a development version:: 21 | 22 | $ git clone https://github.com//subliminal 23 | $ cd subliminal 24 | $ pip install --user -e '.[dev,tests,docs]' 25 | 26 | External dependencies 27 | --------------------- 28 | 29 | To extract information about the video files, ``subliminal`` uses `knowit `_. 30 | For better results, make sure one of its provider is installed, for instance `MediaInfo `_. 31 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | latest-release-notes.md 2 | -------------------------------------------------------------------------------- /src/subliminal/__init__.py: -------------------------------------------------------------------------------- 1 | """Subliminal.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | from importlib.metadata import PackageNotFoundError, version 7 | 8 | # Must be first, otherwise we run into ImportError: partially initialized module 9 | try: 10 | __version__ = version('subliminal') 11 | except PackageNotFoundError: 12 | __version__ = 'undefined' 13 | __short_version__: str = '.'.join(__version__.split('.')[:2]) 14 | __title__: str = 'subliminal' 15 | __author__: str = 'Antoine Bertin' 16 | __license__: str = 'MIT' 17 | __copyright__: str = 'Copyright 2016, Antoine Bertin' 18 | 19 | 20 | from .cache import region 21 | from .core import ( 22 | AsyncProviderPool, 23 | ProviderPool, 24 | check_video, 25 | download_best_subtitles, 26 | download_subtitles, 27 | list_subtitles, 28 | refine, 29 | save_subtitles, 30 | scan_video, 31 | scan_videos, 32 | ) 33 | from .exceptions import Error, ProviderError 34 | from .extensions import provider_manager, refiner_manager 35 | from .providers import Provider 36 | from .score import compute_score, get_scores 37 | from .subtitle import SUBTITLE_EXTENSIONS, Subtitle 38 | from .video import VIDEO_EXTENSIONS, Episode, Movie, Video 39 | 40 | logging.getLogger(__name__).addHandler(logging.NullHandler()) 41 | 42 | 43 | __all__ = [ 44 | 'SUBTITLE_EXTENSIONS', 45 | 'VIDEO_EXTENSIONS', 46 | 'AsyncProviderPool', 47 | 'Episode', 48 | 'Error', 49 | 'Movie', 50 | 'Provider', 51 | 'ProviderError', 52 | 'ProviderPool', 53 | 'Subtitle', 54 | 'Video', 55 | 'check_video', 56 | 'compute_score', 57 | 'download_best_subtitles', 58 | 'download_subtitles', 59 | 'get_scores', 60 | 'list_subtitles', 61 | 'provider_manager', 62 | 'refine', 63 | 'refiner_manager', 64 | 'region', 65 | 'save_subtitles', 66 | 'scan_video', 67 | 'scan_videos', 68 | ] 69 | -------------------------------------------------------------------------------- /src/subliminal/__main__.py: -------------------------------------------------------------------------------- 1 | """Module subliminal.""" 2 | 3 | from __future__ import annotations 4 | 5 | from textwrap import dedent 6 | 7 | if not (__name__ == '__main__' and __package__ == 'subliminal'): 8 | import sys 9 | 10 | print( # noqa: T201 11 | dedent( 12 | f""" 13 | 14 | The '__main__' module does not seem to have been run in the context 15 | of a runnable package ... did you forget to add the '-m' flag? 16 | 17 | Usage: {sys.executable} -m subliminal {' '.join(sys.argv[1:])} 18 | 19 | """ 20 | ) 21 | ) 22 | sys.exit(2) 23 | 24 | from subliminal.cli import cli 25 | 26 | cli() 27 | -------------------------------------------------------------------------------- /src/subliminal/cache.py: -------------------------------------------------------------------------------- 1 | """Caching structure.""" 2 | 3 | from __future__ import annotations 4 | 5 | import datetime 6 | 7 | from dogpile.cache import make_region 8 | from dogpile.cache.util import function_key_generator 9 | 10 | #: Expiration time for show caching 11 | SHOW_EXPIRATION_TIME = datetime.timedelta(weeks=3).total_seconds() 12 | 13 | #: Expiration time for episode caching 14 | EPISODE_EXPIRATION_TIME = datetime.timedelta(days=3).total_seconds() 15 | 16 | #: Expiration time for scraper searches 17 | REFINER_EXPIRATION_TIME = datetime.timedelta(weeks=1).total_seconds() 18 | 19 | 20 | def _to_native_str(value: str | bytes) -> str: 21 | """Convert bytes to str.""" 22 | if isinstance(value, bytes): 23 | return value.decode('utf-8') 24 | return str(value) 25 | 26 | 27 | def to_native_str_key_generator(namespace, fn, to_str=_to_native_str): # type: ignore[no-untyped-def] # noqa: ANN201, ANN001 28 | """Convert bytes to str, generator.""" 29 | return function_key_generator(namespace, fn, to_str) # type: ignore[no-untyped-call] 30 | 31 | 32 | region = make_region(function_key_generator=to_native_str_key_generator) 33 | -------------------------------------------------------------------------------- /src/subliminal/cli/__init__.py: -------------------------------------------------------------------------------- 1 | """CLI module.""" 2 | 3 | from .cli import cli 4 | from .generate_config import generate_default_config 5 | 6 | __all__ = ['cli', 'generate_default_config'] 7 | -------------------------------------------------------------------------------- /src/subliminal/cli/commands/__init__.py: -------------------------------------------------------------------------------- 1 | """CLI subcommands.""" 2 | 3 | from .download_best import download 4 | 5 | __all__ = ['download'] 6 | -------------------------------------------------------------------------------- /src/subliminal/cli/commands/_format.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: FBT001 2 | """Helper functions.""" 3 | 4 | from __future__ import annotations 5 | 6 | import logging 7 | import re 8 | from datetime import timedelta 9 | from typing import Any 10 | 11 | import click 12 | from babelfish import Error as BabelfishError # type: ignore[import-untyped] 13 | from babelfish import Language 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class LanguageParamType(click.ParamType): 19 | """:class:`~click.ParamType` for languages that returns a :class:`~babelfish.language.Language`.""" 20 | 21 | name = 'language' 22 | 23 | def convert(self, value: str, param: click.Parameter | None, ctx: click.Context | None) -> Language: 24 | """Convert ietf language to :class:`~babelfish.language.Language`.""" 25 | try: 26 | return Language.fromietf(value) 27 | except BabelfishError: 28 | self.fail(f'{value} is not a valid language', param, ctx) # pragma: no cover 29 | 30 | 31 | class AgeParamType(click.ParamType): 32 | """:class:`~click.ParamType` for age strings that returns a :class:`~datetime.timedelta`. 33 | 34 | An age string is in the form `number + identifier` with possible identifiers: 35 | 36 | * ``w`` for weeks 37 | * ``d`` for days 38 | * ``h`` for hours 39 | 40 | The form can be specified multiple times but only with that identifier ordering. For example: 41 | 42 | * ``1w2d4h`` for 1 week, 2 days and 4 hours 43 | * ``2w`` for 2 weeks 44 | * ``3w6h`` for 3 weeks and 6 hours 45 | """ 46 | 47 | name = 'age' 48 | 49 | def convert(self, value: str, param: click.Parameter | None, ctx: click.Context | None) -> timedelta: 50 | """Convert an age string to :class:`~datetime.timedelta`.""" 51 | match = re.match(r'^(?:(?P\d+?)w)?(?:(?P\d+?)d)?(?:(?P\d+?)h)?$', value) 52 | if not match: 53 | self.fail(f'{value} is not a valid age', param, ctx) 54 | 55 | return timedelta(**{k: int(v) for k, v in match.groupdict(0).items()}) 56 | 57 | 58 | def plural(quantity: int, name: str, *, bold: bool = True, **kwargs: Any) -> str: 59 | """Format a quantity with plural.""" 60 | return '{} {}{}'.format( 61 | click.style(str(quantity), bold=bold, **kwargs), 62 | name, 63 | 's' if quantity > 1 else '', 64 | ) 65 | -------------------------------------------------------------------------------- /src/subliminal/converters/__init__.py: -------------------------------------------------------------------------------- 1 | """Specialized :class:`~babelfiss.LanguageReverseConverter` converters to match the languages of the providers.""" 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | # Tuple of language (alpha3, country, script), with country and script optional 7 | LanguageTuple = tuple[str, str | None, str | None] 8 | -------------------------------------------------------------------------------- /src/subliminal/converters/addic7ed.py: -------------------------------------------------------------------------------- 1 | """Language converter for Addic7ed.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | from babelfish import LanguageReverseConverter, language_converters # type: ignore[import-untyped] 8 | 9 | if TYPE_CHECKING: 10 | from . import LanguageTuple 11 | 12 | 13 | class Addic7edConverter(LanguageReverseConverter): 14 | """Language converter for Addic7ed.""" 15 | 16 | def __init__(self) -> None: 17 | self.name_converter = language_converters['name'] 18 | self.from_addic7ed: dict[str, LanguageTuple] = { 19 | 'Català': ('cat', None, None), 20 | 'Chinese (Simplified)': ('zho', None, None), 21 | 'Chinese (Traditional)': ('zho', None, None), 22 | 'Euskera': ('eus', None, None), 23 | 'French (Canadian)': ('fra', 'CA', None), 24 | 'Galego': ('glg', None, None), 25 | 'Greek': ('ell', None, None), 26 | 'Malay': ('msa', None, None), 27 | 'Portuguese (Brazilian)': ('por', 'BR', None), 28 | 'Serbian (Cyrillic)': ('srp', None, 'Cyrl'), 29 | 'Serbian (Latin)': ('srp', None, None), 30 | 'Spanish (Latin America)': ('spa', None, None), 31 | 'Spanish (Spain)': ('spa', None, None), 32 | } 33 | self.to_addic7ed: dict[LanguageTuple, str] = { 34 | ('cat', None, None): 'Català', 35 | ('zho', None, None): 'Chinese (Simplified)', 36 | ('eus', None, None): 'Euskera', 37 | ('fra', 'CA', None): 'French (Canadian)', 38 | ('glg', None, None): 'Galego', 39 | ('ell', None, None): 'Greek', 40 | ('msa', None, None): 'Malay', 41 | ('por', 'BR', None): 'Portuguese (Brazilian)', 42 | ('srp', None, 'Cyrl'): 'Serbian (Cyrillic)', 43 | } 44 | self.codes = self.name_converter.codes | set(self.from_addic7ed.keys()) 45 | 46 | def convert(self, alpha3: str, country: str | None = None, script: str | None = None) -> str: 47 | """Convert an alpha3 language code with an alpha2 country code and a script code into a custom code.""" 48 | if (alpha3, country, script) in self.to_addic7ed: 49 | return self.to_addic7ed[(alpha3, country, script)] 50 | 51 | return self.name_converter.convert(alpha3, country, script) # type: ignore[no-any-return] 52 | 53 | def reverse(self, code: str) -> LanguageTuple: 54 | """Reverse a custom code into alpha3, country and script code.""" 55 | if code in self.from_addic7ed: 56 | return self.from_addic7ed[code] 57 | 58 | return self.name_converter.reverse(code) # type: ignore[no-any-return] 59 | -------------------------------------------------------------------------------- /src/subliminal/converters/opensubtitles.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 the BabelFish authors. All rights reserved. 2 | # Use of this source code is governed by the 3-clause BSD license 3 | # that can be found in the LICENSE file. 4 | # 5 | """Language converter for OpenSubtitles.""" 6 | 7 | from __future__ import annotations 8 | 9 | from typing import TYPE_CHECKING, cast 10 | 11 | from babelfish import ( # type: ignore[import-untyped] 12 | LanguageReverseConverter, 13 | LanguageReverseError, 14 | language_converters, 15 | ) 16 | from babelfish.converters import CaseInsensitiveDict # type: ignore[import-untyped] 17 | 18 | if TYPE_CHECKING: 19 | from . import LanguageTuple 20 | 21 | 22 | class OpenSubtitlesConverter(LanguageReverseConverter): 23 | """Language converter for OpenSubtitlesCom. 24 | 25 | Originally defined in :mod:`babelfish`. 26 | """ 27 | 28 | codes: set[str] 29 | to_opensubtitles: dict[LanguageTuple, str] 30 | # from_opensubtitles: CaseInsensitiveDict[tuple[str, str | None]] 31 | from_opensubtitles: CaseInsensitiveDict 32 | 33 | def __init__(self) -> None: 34 | self.alpha3b_converter = language_converters['alpha3b'] 35 | self.alpha2_converter = language_converters['alpha2'] 36 | self.to_opensubtitles = { 37 | ('por', 'BR', None): 'pob', 38 | ('gre', None, None): 'ell', 39 | ('srp', None, None): 'scc', 40 | ('srp', 'ME', None): 'mne', 41 | ('srp', None, 'Latn'): 'scc', 42 | ('srp', None, 'Cyrl'): 'scc', 43 | ('spa', 'MX', None): 'spl', 44 | ('chi', None, 'Hant'): 'zht', 45 | ('chi', 'TW', None): 'zht', 46 | } 47 | self.from_opensubtitles = CaseInsensitiveDict( 48 | { 49 | 'pob': ('por', 'BR', None), 50 | 'pb': ('por', 'BR', None), 51 | 'ell': ('ell', None, None), 52 | 'scc': ('srp', None, None), 53 | 'mne': ('srp', 'ME', None), 54 | 'spl': ('spa', 'MX'), 55 | 'zht': ('zho', None, 'Hant'), 56 | }, 57 | ) 58 | self.codes = self.alpha2_converter.codes | self.alpha3b_converter.codes | set(self.from_opensubtitles.keys()) 59 | 60 | def convert(self, alpha3: str, country: str | None = None, script: str | None = None) -> str: 61 | """Convert an alpha3 language code with an alpha2 country code and a script code into a custom code.""" 62 | alpha3b = self.alpha3b_converter.convert(alpha3, country, script) # type: ignore[no-any-return] 63 | if (alpha3b, country, script) in self.to_opensubtitles: 64 | return self.to_opensubtitles[(alpha3b, country, script)] 65 | return alpha3b # type: ignore[no-any-return] 66 | 67 | def reverse(self, code: str) -> LanguageTuple: 68 | """Reverse a custom code into alpha3, country and script code.""" 69 | if code in self.from_opensubtitles: 70 | return self.from_opensubtitles[code] # type: ignore[no-any-return] 71 | for conv in [self.alpha3b_converter, self.alpha2_converter]: 72 | conv = cast('LanguageReverseConverter', conv) 73 | try: 74 | return conv.reverse(code) # type: ignore[no-any-return] 75 | except LanguageReverseError: 76 | pass 77 | raise LanguageReverseError(code) 78 | -------------------------------------------------------------------------------- /src/subliminal/converters/opensubtitlescom.py: -------------------------------------------------------------------------------- 1 | """Language converter for OpenSubtitlesCom.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | from babelfish import LanguageReverseConverter, language_converters # type: ignore[import-untyped] 8 | 9 | if TYPE_CHECKING: 10 | from . import LanguageTuple 11 | 12 | 13 | class OpenSubtitlesComConverter(LanguageReverseConverter): 14 | """Language converter for OpenSubtitlesCom. 15 | 16 | From GET API at: https://api.opensubtitles.com/api/v1/infos/languages 17 | """ 18 | 19 | def __init__(self) -> None: 20 | self.alpha2_converter = language_converters['alpha2'] 21 | self.from_opensubtitlescom: dict[str, tuple[str, str | None]] = { 22 | 'pt-br': ('por', 'BR'), 23 | 'pt-pt': ('por', 'PT'), 24 | 'zh-cn': ('zho', 'CN'), 25 | 'zh-tw': ('zho', 'TW'), 26 | 'ze': ('zho', 'US'), 27 | 'me': ('srp', 'ME'), 28 | 'sy': ('syr', None), 29 | 'ma': ('mni', None), 30 | 'at': ('ast', None), 31 | } 32 | self.to_opensubtitlescom: dict[tuple[str, str | None], str] = { 33 | v: k for k, v in self.from_opensubtitlescom.items() 34 | } 35 | self.codes = self.alpha2_converter.codes | set(self.from_opensubtitlescom.keys()) 36 | 37 | def convert(self, alpha3: str, country: str | None = None, script: str | None = None) -> str: 38 | """Convert an alpha3 language code with an alpha2 country code and a script code into a custom code.""" 39 | if (alpha3, country) in self.to_opensubtitlescom: 40 | return self.to_opensubtitlescom[(alpha3, country)] 41 | 42 | return self.alpha2_converter.convert(alpha3, country, script) # type: ignore[no-any-return] 43 | 44 | def reverse(self, code: str) -> LanguageTuple: 45 | """Reverse a custom code into alpha3, country and script code.""" 46 | code_lower = code.lower() 47 | if code_lower in self.from_opensubtitlescom: 48 | return (*self.from_opensubtitlescom[code_lower], None) 49 | 50 | return self.alpha2_converter.reverse(code) # type: ignore[no-any-return] 51 | -------------------------------------------------------------------------------- /src/subliminal/converters/subtitulamos.py: -------------------------------------------------------------------------------- 1 | """Language converter for Subtitulamos.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | from babelfish import LanguageReverseConverter, language_converters # type: ignore[import-untyped] 8 | 9 | if TYPE_CHECKING: 10 | from . import LanguageTuple 11 | 12 | 13 | class SubtitulamosConverter(LanguageReverseConverter): 14 | """Language converter for Subtitulamos.""" 15 | 16 | def __init__(self) -> None: 17 | self.name_converter = language_converters['name'] 18 | self.from_subtitulamos: dict[str, tuple[str, str | None]] = { 19 | 'Español': ('spa', None), 20 | 'Español (España)': ('spa', None), 21 | 'Español (Latinoamérica)': ('spa', 'MX'), 22 | 'Català': ('cat', None), 23 | 'English': ('eng', None), 24 | 'Galego': ('glg', None), 25 | 'Portuguese': ('por', None), 26 | 'English (US)': ('eng', 'US'), 27 | 'English (UK)': ('eng', 'GB'), 28 | 'Brazilian': ('por', 'BR'), 29 | } 30 | self.to_subtitulamos: dict[tuple[str, str | None], str] = { 31 | item[1]: item[0] for item in self.from_subtitulamos.items() 32 | } | { 33 | ('spa', country): 'Español (Latinoamérica)' 34 | for country in [ 35 | 'AR', # Argentina 36 | 'BO', # Bolivia 37 | 'CL', # Chile 38 | 'CO', # Colombia 39 | 'CR', # Costa Rica 40 | 'DO', # República Dominicana 41 | 'EC', # Ecuador 42 | 'GT', # Guatemala 43 | 'HN', # Honduras 44 | 'NI', # Nicaragua 45 | 'PA', # Panamá 46 | 'PE', # Perú 47 | 'PR', # Puerto Rico 48 | 'PY', # Paraguay 49 | 'SV', # El Salvador 50 | 'US', # United States 51 | 'UY', # Uruguay 52 | 'VE', # Venezuela 53 | ] 54 | } 55 | self.codes = set(self.from_subtitulamos.keys()) 56 | 57 | def convert(self, alpha3: str, country: str | None = None, script: str | None = None) -> str: 58 | """Convert an alpha3 language code with an alpha2 country code and a script code into a custom code.""" 59 | if (alpha3, country) in self.to_subtitulamos: 60 | return self.to_subtitulamos[(alpha3, country)] 61 | 62 | return self.name_converter.convert(alpha3, country, script) # type: ignore[no-any-return] 63 | 64 | def reverse(self, code: str) -> LanguageTuple: 65 | """Reverse a custom code into alpha3, country and script code.""" 66 | if code in self.from_subtitulamos: 67 | return (*self.from_subtitulamos[code], None) 68 | 69 | return self.name_converter.reverse(code) # type: ignore[no-any-return] 70 | -------------------------------------------------------------------------------- /src/subliminal/converters/tvsubtitles.py: -------------------------------------------------------------------------------- 1 | """Language converter for TVsubtitles.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | from babelfish import LanguageReverseConverter, language_converters # type: ignore[import-untyped] 8 | 9 | if TYPE_CHECKING: 10 | from . import LanguageTuple 11 | 12 | 13 | class TVsubtitlesConverter(LanguageReverseConverter): 14 | """Language converter for TVsubtitles.""" 15 | 16 | def __init__(self) -> None: 17 | self.alpha2_converter = language_converters['alpha2'] 18 | self.from_tvsubtitles: dict[str, tuple[str, str | None]] = { 19 | 'br': ('por', 'BR'), 20 | 'ua': ('ukr', None), 21 | 'gr': ('ell', None), 22 | 'cn': ('zho', None), 23 | 'jp': ('jpn', None), 24 | 'cz': ('ces', None), 25 | } 26 | self.to_tvsubtitles: dict[tuple[str, str | None], str] = {v: k for k, v in self.from_tvsubtitles.items()} 27 | self.codes = self.alpha2_converter.codes | set(self.from_tvsubtitles.keys()) 28 | 29 | def convert(self, alpha3: str, country: str | None = None, script: str | None = None) -> str: 30 | """Convert an alpha3 language code with an alpha2 country code and a script code into a custom code.""" 31 | if (alpha3, country) in self.to_tvsubtitles: 32 | return self.to_tvsubtitles[(alpha3, country)] 33 | 34 | return self.alpha2_converter.convert(alpha3, country, script) # type: ignore[no-any-return] 35 | 36 | def reverse(self, code: str) -> LanguageTuple: 37 | """Reverse a custom code into alpha3, country and script code.""" 38 | if code in self.from_tvsubtitles: 39 | return (*self.from_tvsubtitles[code], None) 40 | 41 | return self.alpha2_converter.reverse(code) # type: ignore[no-any-return] 42 | -------------------------------------------------------------------------------- /src/subliminal/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions for Subliminal.""" 2 | 3 | 4 | class Error(Exception): 5 | """Base class for exceptions in subliminal.""" 6 | 7 | pass 8 | 9 | 10 | class ArchiveError(Error): 11 | """Exception raised by reading an archive.""" 12 | 13 | pass 14 | 15 | 16 | class GuessingError(Error, ValueError): 17 | """ValueError raised when guessit cannot guess a video.""" 18 | 19 | pass 20 | 21 | 22 | class ProviderError(Error): 23 | """Exception raised by providers.""" 24 | 25 | pass 26 | 27 | 28 | class NotInitializedProviderError(ProviderError): 29 | """Exception raised by providers when not initialized.""" 30 | 31 | pass 32 | 33 | 34 | class DiscardingError(ProviderError): 35 | """Exception raised by providers that should lead to discard this provider.""" 36 | 37 | pass 38 | 39 | 40 | class ConfigurationError(DiscardingError): 41 | """Exception raised by providers when badly configured.""" 42 | 43 | pass 44 | 45 | 46 | class AuthenticationError(DiscardingError): 47 | """Exception raised by providers when authentication failed.""" 48 | 49 | pass 50 | 51 | 52 | class ServiceUnavailable(DiscardingError): 53 | """Exception raised when status is '503 Service Unavailable'.""" 54 | 55 | pass 56 | 57 | 58 | class DownloadLimitExceeded(DiscardingError): 59 | """Exception raised by providers when download limit is exceeded.""" 60 | 61 | pass 62 | -------------------------------------------------------------------------------- /src/subliminal/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Diaoul/subliminal/a1a820f70651e2bbfd5ef85f54840a9390787a0a/src/subliminal/py.typed -------------------------------------------------------------------------------- /src/subliminal/refiners/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Refiners enrich a :class:`~subliminal.video.Video` object by adding information to it. 3 | 4 | A refiner is a simple function: 5 | 6 | .. py:function:: refine(video, **kwargs) 7 | 8 | :param video: the video to refine. 9 | :type video: :class:`~subliminal.video.Video` 10 | :param kwargs: additional parameters for refiners. 11 | 12 | """ 13 | -------------------------------------------------------------------------------- /src/subliminal/refiners/hash.py: -------------------------------------------------------------------------------- 1 | """Refine the :class:`~subliminal.video.Video` object with video hashes.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | import os 7 | import struct 8 | from typing import TYPE_CHECKING, Any, cast 9 | 10 | from subliminal.extensions import get_default_providers, provider_manager 11 | 12 | if TYPE_CHECKING: 13 | from collections.abc import Sequence, Set 14 | from typing import Callable, TypeAlias 15 | 16 | from babelfish import Language # type: ignore[import-untyped] 17 | 18 | from subliminal.providers import Provider 19 | from subliminal.video import Video 20 | 21 | HashFunc: TypeAlias = Callable[[str | os.PathLike], str | None] 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def hash_opensubtitles(video_path: str | os.PathLike) -> str | None: 27 | """Compute a hash using OpenSubtitles' algorithm. 28 | 29 | :param (str | os.PathLike) video_path: path of the video. 30 | :return: the hash. 31 | :rtype: str 32 | 33 | """ 34 | video_path = os.fspath(video_path) 35 | bytesize = struct.calcsize(b' Video: 70 | """Refine a video computing required hashes for the given providers. 71 | 72 | The following :class:`~subliminal.video.Video` attribute can be found: 73 | 74 | * :attr:`~subliminal.video.Video.hashes` 75 | 76 | :param Video video: the Video to refine. 77 | :param providers: list of providers for which the video hash should be computed. 78 | :param languages: set of languages that need to be compatible with the providers. 79 | 80 | """ 81 | if video.size is None or video.size <= 10485760: 82 | logger.warning('Size is lower than 10MB: hashes not computed') 83 | return video 84 | 85 | providers = providers if providers is not None else get_default_providers() 86 | 87 | logger.debug('Computing hashes for %r', video.name) 88 | for name in providers: 89 | provider = cast('Provider', provider_manager[name].plugin) 90 | if not provider.check_types(video): 91 | continue 92 | 93 | if languages is not None and not provider.check_languages(languages): 94 | continue 95 | 96 | # Try provider static method 97 | h = provider.hash_video(video.name) 98 | 99 | # Try generic hashes 100 | if h is None and name in hash_functions: 101 | h = hash_functions[name](video.name) 102 | 103 | # Add hash 104 | if h is not None: 105 | video.hashes[name] = h 106 | 107 | logger.debug('Computed hashes %r', video.hashes) 108 | return video 109 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Diaoul/subliminal/a1a820f70651e2bbfd5ef85f54840a9390787a0a/tests/__init__.py -------------------------------------------------------------------------------- /tests/cassettes/bsplayer/test_login.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: ' 4 | 5 | BSPlayer 9 | v2.67' 10 | headers: 11 | Accept: 12 | - '*/*' 13 | Accept-Encoding: 14 | - gzip, deflate 15 | Connection: 16 | - close 17 | Content-Length: 18 | - '543' 19 | Content-Type: 20 | - text/xml; charset=utf-8 21 | SOAPAction: 22 | - '"http://api.bsplayer-subtitles.com/v1.php#logIn"' 23 | User-Agent: 24 | - BSPlayer/2.x (1022.12360) 25 | method: POST 26 | uri: http://s1.api.bsplayer-subtitles.com/v1.php 27 | response: 28 | body: 29 | string: ' 30 | 31 | 200OK821e5b660dc9e63df503fdba350dfb69 37 | 38 | ' 39 | headers: 40 | Connection: 41 | - close 42 | Content-Type: 43 | - text/xml; charset=utf-8 44 | Date: 45 | - Thu, 13 Feb 2025 23:29:44 GMT 46 | Server: 47 | - Apache/2.4.62 (Debian) 48 | Vary: 49 | - Accept-Encoding 50 | content-length: 51 | - '684' 52 | status: 53 | code: 200 54 | message: OK 55 | version: 1 56 | -------------------------------------------------------------------------------- /tests/cassettes/gestdown/test_get_title_and_show_id_alternative_name.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept-Encoding: 6 | - gzip, deflate 7 | Connection: 8 | - keep-alive 9 | User-Agent: 10 | - Subliminal/2.2 11 | accept: 12 | - application/json 13 | method: GET 14 | uri: https://api.gestdown.info/shows/search/The%20End%20of%20the%20Fucking%20World 15 | response: 16 | body: 17 | string: '"Couldn''t find show: The End of the Fucking World"' 18 | headers: 19 | CF-RAY: 20 | - 91189a410ca048c8-LHR 21 | Cache-Control: 22 | - public, max-age=43200 23 | Connection: 24 | - keep-alive 25 | Content-Type: 26 | - application/json; charset=utf-8 27 | Date: 28 | - Thu, 13 Feb 2025 23:29:47 GMT 29 | NEL: 30 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 31 | Report-To: 32 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=r%2FvJxkAg6QKaKKhGYUYGMqBThNJHyAW9cflS3mOEBx9lernr6giwYJn6ZMTgxHzMndX9brrH%2BN1UryXHnchqvS4hdqHusdmP7zN%2F4VCHCwMNEPNoZqWbecaUMNqGrSvdjuyOgg%3D%3D"}],"group":"cf-nel","max_age":604800}' 33 | Server: 34 | - cloudflare 35 | Strict-Transport-Security: 36 | - max-age=15552000; includeSubDomains; preload 37 | Transfer-Encoding: 38 | - chunked 39 | X-Content-Type-Options: 40 | - nosniff 41 | alt-svc: 42 | - h3=":443"; ma=86400 43 | cf-cache-status: 44 | - HIT 45 | content-length: 46 | - '50' 47 | server-timing: 48 | - cfL4;desc="?proto=TCP&rtt=33519&min_rtt=32070&rtt_var=13061&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2846&recv_bytes=823&delivery_rate=90302&cwnd=250&unsent_bytes=0&cid=e8d9ba65139567e1&ts=64&x=0" 49 | vary: 50 | - Accept-Encoding 51 | - Accept-Encoding 52 | status: 53 | code: 404 54 | message: Not Found 55 | - request: 56 | body: null 57 | headers: 58 | Accept-Encoding: 59 | - gzip, deflate 60 | Connection: 61 | - keep-alive 62 | User-Agent: 63 | - Subliminal/2.2 64 | accept: 65 | - application/json 66 | method: GET 67 | uri: https://api.gestdown.info/shows/search/The%20end%20of%20the%20f***ing%20world 68 | response: 69 | body: 70 | string: '{"shows":[{"id":"e789f371-ae07-4db4-b9aa-6b34e7e9e7b0","name":"The 71 | End of the F***ing World","nbSeasons":2,"seasons":[1,2],"tvDbId":336522,"tmdbId":74577,"slug":"the-end-of-the-fing-world"}]}' 72 | headers: 73 | Age: 74 | - '2' 75 | CF-RAY: 76 | - 91189a4169395be2-LIS 77 | Cache-Control: 78 | - public, max-age=14400 79 | Connection: 80 | - keep-alive 81 | Content-Type: 82 | - application/json; charset=utf-8 83 | Date: 84 | - Thu, 13 Feb 2025 23:29:47 GMT 85 | NEL: 86 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 87 | Report-To: 88 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=eZ%2FV4xtdoV%2FcHQ1rAEPeHNKF8ZTC4uWyGIwkDAPq4ODZuWBZXyKAFbxiBQKuKPjLcb%2B3xwCSyotkKRC5yMro2tvaE7bq3E4XiELezIk6CMuRMd4lVvFiCo04bw%2B%2BwhYqb9sV9A%3D%3D"}],"group":"cf-nel","max_age":604800}' 89 | Server: 90 | - cloudflare 91 | Strict-Transport-Security: 92 | - max-age=15552000; includeSubDomains; preload 93 | Transfer-Encoding: 94 | - chunked 95 | X-Content-Type-Options: 96 | - nosniff 97 | alt-svc: 98 | - h3=":443"; ma=86400 99 | cf-cache-status: 100 | - HIT 101 | content-length: 102 | - '191' 103 | last-modified: 104 | - Thu, 13 Feb 2025 23:29:45 GMT 105 | server-timing: 106 | - cfL4;desc="?proto=TCP&rtt=3136&min_rtt=2975&rtt_var=1439&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2848&recv_bytes=823&delivery_rate=678537&cwnd=183&unsent_bytes=0&cid=c2ed27014550b1ab&ts=21&x=0" 107 | vary: 108 | - Accept-Encoding 109 | - Accept-Encoding 110 | status: 111 | code: 200 112 | message: OK 113 | version: 1 114 | -------------------------------------------------------------------------------- /tests/cassettes/gestdown/test_get_title_and_show_id_no_year.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept-Encoding: 6 | - gzip, deflate 7 | Connection: 8 | - keep-alive 9 | User-Agent: 10 | - Subliminal/2.2 11 | accept: 12 | - application/json 13 | method: GET 14 | uri: https://api.gestdown.info/shows/external/tvdb/77092 15 | response: 16 | body: 17 | string: '{"shows":[{"id":"226d7f34-a9f5-4fe2-98d7-ecf944243714","name":"Dallas","nbSeasons":11,"seasons":[1,2,3,4,5,6,8,9,11,12,13],"tvDbId":77092,"tmdbId":40,"slug":"dallas"}]}' 18 | headers: 19 | CF-RAY: 20 | - 91189a41ab1b5be0-LIS 21 | Cache-Control: 22 | - public, max-age=14400 23 | Connection: 24 | - keep-alive 25 | Content-Type: 26 | - application/json; charset=utf-8 27 | Date: 28 | - Thu, 13 Feb 2025 23:29:47 GMT 29 | NEL: 30 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 31 | Report-To: 32 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=w6h5csftLSFHDOix%2BbhFlqmS748gwMF3K%2FMlN24WApq9ciE0dXyNz0VoWNDGab5hLyRbv9Eufn0DDXYb1F6qUAMBslL%2FEGgd2ZSQX0BWqQvU6q2nAv%2BrnTJLqXmaNgyTfsuoQw%3D%3D"}],"group":"cf-nel","max_age":604800}' 33 | Server: 34 | - cloudflare 35 | Strict-Transport-Security: 36 | - max-age=15552000; includeSubDomains; preload 37 | Transfer-Encoding: 38 | - chunked 39 | X-Content-Type-Options: 40 | - nosniff 41 | alt-svc: 42 | - h3=":443"; ma=86400 43 | cf-cache-status: 44 | - HIT 45 | content-length: 46 | - '168' 47 | last-modified: 48 | - Thu, 13 Feb 2025 22:54:10 GMT 49 | server-timing: 50 | - cfL4;desc="?proto=TCP&rtt=2875&min_rtt=1409&rtt_var=1282&sent=4&recv=7&lost=0&retrans=0&sent_bytes=2846&recv_bytes=797&delivery_rate=1027679&cwnd=247&unsent_bytes=0&cid=70e06eb04f5b10c9&ts=130&x=0" 51 | vary: 52 | - Accept-Encoding 53 | - Accept-Encoding 54 | status: 55 | code: 200 56 | message: OK 57 | version: 1 58 | -------------------------------------------------------------------------------- /tests/cassettes/gestdown/test_get_title_and_show_id_only_title.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept-Encoding: 6 | - gzip, deflate 7 | Connection: 8 | - keep-alive 9 | User-Agent: 10 | - Subliminal/2.2 11 | accept: 12 | - application/json 13 | method: GET 14 | uri: https://api.gestdown.info/shows/search/Marvel's%20Agents%20of%20S.H.I.E.L.D. 15 | response: 16 | body: 17 | string: '{"shows":[{"id":"8976d3bb-a213-4210-9a03-f4b6d17ce540","name":"Marvel''s 18 | Agents of S.H.I.E.L.D.","nbSeasons":7,"seasons":[1,2,3,4,5,6,7],"tvDbId":263365,"tmdbId":1403,"slug":"marvels-agents-of-shield"},{"id":"9039b72e-89eb-401e-9d03-0419bf7d1273","name":"Marvel''s 19 | Agents of S.H.I.E.L.D.: Slingshot","nbSeasons":0,"seasons":[],"tvDbId":null,"tmdbId":69088,"slug":"marvels-agents-of-shield-slingshot"}]}' 20 | headers: 21 | CF-RAY: 22 | - 91189a3f4b405bea-LIS 23 | Cache-Control: 24 | - public, max-age=14400 25 | Connection: 26 | - keep-alive 27 | Content-Type: 28 | - application/json; charset=utf-8 29 | Date: 30 | - Thu, 13 Feb 2025 23:29:47 GMT 31 | NEL: 32 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 33 | Report-To: 34 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=Uea1Hvut%2BnxgXjELr8MptMoiEP0j7MH4BVvbn4umA3QkYkHeRzwDJWdukb0aQkso4PN9q7WFLorY6xmachyvBQycTWpFvX4j8fTmDGr2lz%2Fu3yU8hb3Ry2r8b99xEdL1el2TwA%3D%3D"}],"group":"cf-nel","max_age":604800}' 35 | Server: 36 | - cloudflare 37 | Strict-Transport-Security: 38 | - max-age=15552000; includeSubDomains; preload 39 | Transfer-Encoding: 40 | - chunked 41 | X-Content-Type-Options: 42 | - nosniff 43 | alt-svc: 44 | - h3=":443"; ma=86400 45 | cf-cache-status: 46 | - MISS 47 | content-length: 48 | - '400' 49 | last-modified: 50 | - Thu, 13 Feb 2025 23:29:47 GMT 51 | server-timing: 52 | - cfL4;desc="?proto=TCP&rtt=3031&min_rtt=2978&rtt_var=1224&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2847&recv_bytes=822&delivery_rate=849515&cwnd=243&unsent_bytes=0&cid=21af27cf8c3e1958&ts=88&x=0" 53 | vary: 54 | - Accept-Encoding 55 | - Accept-Encoding 56 | status: 57 | code: 200 58 | message: OK 59 | version: 1 60 | -------------------------------------------------------------------------------- /tests/cassettes/gestdown/test_get_title_and_show_id_with_tvdb_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept-Encoding: 6 | - gzip, deflate 7 | Connection: 8 | - keep-alive 9 | User-Agent: 10 | - Subliminal/2.2 11 | accept: 12 | - application/json 13 | method: GET 14 | uri: https://api.gestdown.info/shows/external/tvdb/328635 15 | response: 16 | body: 17 | string: '{"shows":[{"id":"6e507429-0994-443f-833e-1b06cbc18705","name":"Alex, 18 | Inc.","nbSeasons":1,"seasons":[1],"tvDbId":328635,"tmdbId":71763,"slug":"alex-inc"}]}' 19 | headers: 20 | CF-Cache-Status: 21 | - HIT 22 | CF-RAY: 23 | - 91189a3ffd2f5bd6-LIS 24 | Cache-Control: 25 | - public, max-age=14400 26 | Connection: 27 | - keep-alive 28 | Content-Type: 29 | - application/json; charset=utf-8 30 | Date: 31 | - Thu, 13 Feb 2025 23:29:47 GMT 32 | Last-Modified: 33 | - Sat, 08 Feb 2025 13:32:07 GMT 34 | NEL: 35 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 36 | Report-To: 37 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=rIFs4g4lEzDxjm5QQuqq%2FvT4TJ0Dp1pVaRElNzqQQAnj4WKKCSnn%2BaD1sF63CDHrVAUTaV3TBraeeTcjwQzszpVX8vLP9A7%2FECqZYn1%2FemPVpZw9wfQ4p7SMi6I3BdX8Fhty6w%3D%3D"}],"group":"cf-nel","max_age":604800}' 38 | Server: 39 | - cloudflare 40 | Strict-Transport-Security: 41 | - max-age=15552000; includeSubDomains; preload 42 | Transfer-Encoding: 43 | - chunked 44 | X-Content-Type-Options: 45 | - nosniff 46 | alt-svc: 47 | - h3=":443"; ma=86400 48 | content-length: 49 | - '154' 50 | server-timing: 51 | - cfL4;desc="?proto=TCP&rtt=3858&min_rtt=3212&rtt_var=1666&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2846&recv_bytes=798&delivery_rate=901618&cwnd=226&unsent_bytes=0&cid=cdd85987a4c9d613&ts=74&x=0" 52 | vary: 53 | - Accept-Encoding 54 | - Accept-Encoding 55 | status: 56 | code: 200 57 | message: OK 58 | version: 1 59 | -------------------------------------------------------------------------------- /tests/cassettes/gestdown/test_query_no_language.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept-Encoding: 6 | - gzip, deflate 7 | Connection: 8 | - keep-alive 9 | User-Agent: 10 | - Subliminal/2.2 11 | accept: 12 | - application/json 13 | method: GET 14 | uri: https://api.gestdown.info/shows/external/tvdb/80379 15 | response: 16 | body: 17 | string: '{"shows":[{"id":"91eb9278-8cf5-4ddd-9111-7f60b15958cb","name":"The 18 | Big Bang Theory","nbSeasons":12,"seasons":[1,2,3,4,5,6,7,8,9,10,11,12],"tvDbId":80379,"tmdbId":1418,"slug":"the-big-bang-theory"}]}' 19 | headers: 20 | Age: 21 | - '3' 22 | CF-RAY: 23 | - 91189a43daf05be8-LIS 24 | Cache-Control: 25 | - public, max-age=14400 26 | Connection: 27 | - keep-alive 28 | Content-Type: 29 | - application/json; charset=utf-8 30 | Date: 31 | - Thu, 13 Feb 2025 23:29:48 GMT 32 | NEL: 33 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 34 | Report-To: 35 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=QwR1l0px5JCLfji4sn6vpNbF7FfYFKklNCD5ZGdkhPXy66c5Wq2ILJA2QH%2BRaDgmBTr3iPAhoC%2FTPAtgS1oVYiMCwpGTmhRk%2Fz2Lp3vwz0Y3pxdH5yCN7jwkpqRo165vXNA5BA%3D%3D"}],"group":"cf-nel","max_age":604800}' 36 | Server: 37 | - cloudflare 38 | Strict-Transport-Security: 39 | - max-age=15552000; includeSubDomains; preload 40 | Transfer-Encoding: 41 | - chunked 42 | X-Content-Type-Options: 43 | - nosniff 44 | alt-svc: 45 | - h3=":443"; ma=86400 46 | cf-cache-status: 47 | - HIT 48 | content-length: 49 | - '198' 50 | last-modified: 51 | - Wed, 12 Feb 2025 22:02:09 GMT 52 | server-timing: 53 | - cfL4;desc="?proto=TCP&rtt=3039&min_rtt=3008&rtt_var=1190&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2847&recv_bytes=797&delivery_rate=888616&cwnd=251&unsent_bytes=0&cid=6c0465c081274957&ts=19&x=0" 54 | vary: 55 | - Accept-Encoding 56 | - Accept-Encoding 57 | status: 58 | code: 200 59 | message: OK 60 | version: 1 61 | -------------------------------------------------------------------------------- /tests/cassettes/gestdown/test_query_wrong_series.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept-Encoding: 6 | - gzip, deflate 7 | Connection: 8 | - keep-alive 9 | User-Agent: 10 | - Subliminal/2.2 11 | accept: 12 | - application/json 13 | method: GET 14 | uri: https://api.gestdown.info/subtitles/get//7/5/eng 15 | response: 16 | body: 17 | string: '' 18 | headers: 19 | CF-Cache-Status: 20 | - EXPIRED 21 | CF-RAY: 22 | - 91189a441cf15bd5-LIS 23 | Cache-Control: 24 | - max-age=7200 25 | Connection: 26 | - keep-alive 27 | Content-Length: 28 | - '0' 29 | Date: 30 | - Thu, 13 Feb 2025 23:29:48 GMT 31 | NEL: 32 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 33 | Report-To: 34 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=xhB4e4QNfvxBRuqjQdXWJPCqTkQWneAC7tmikZq8XzWRz7yox1s3Bb%2BG565y66RNdJ3mIhdbAQkWX8saQatuUqpcaBr2qp3cuu%2B2EnIoIBHnQuW6qRyxmdZ44GOB8wvqxIk8PQ%3D%3D"}],"group":"cf-nel","max_age":604800}' 35 | Server: 36 | - cloudflare 37 | Strict-Transport-Security: 38 | - max-age=15552000; includeSubDomains; preload 39 | X-Content-Type-Options: 40 | - nosniff 41 | alt-svc: 42 | - h3=":443"; ma=86400 43 | server-timing: 44 | - cfL4;desc="?proto=TCP&rtt=3041&min_rtt=2974&rtt_var=1250&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2846&recv_bytes=794&delivery_rate=823663&cwnd=240&unsent_bytes=0&cid=0c1072a1fb741203&ts=75&x=0" 45 | vary: 46 | - Accept-Encoding 47 | status: 48 | code: 404 49 | message: Not Found 50 | version: 1 51 | -------------------------------------------------------------------------------- /tests/cassettes/gestdown/test_search_show_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept-Encoding: 6 | - gzip, deflate 7 | Connection: 8 | - keep-alive 9 | User-Agent: 10 | - Subliminal/2.2 11 | accept: 12 | - application/json 13 | method: GET 14 | uri: https://api.gestdown.info/shows/search/The%20Big%20Bang%20Theory 15 | response: 16 | body: 17 | string: '{"shows":[{"id":"91eb9278-8cf5-4ddd-9111-7f60b15958cb","name":"The 18 | Big Bang Theory","nbSeasons":12,"seasons":[1,2,3,4,5,6,7,8,9,10,11,12],"tvDbId":80379,"tmdbId":1418,"slug":"the-big-bang-theory"}]}' 19 | headers: 20 | CF-Cache-Status: 21 | - HIT 22 | CF-RAY: 23 | - 91189a3b2e245be8-LIS 24 | Cache-Control: 25 | - public, max-age=14400 26 | Connection: 27 | - keep-alive 28 | Content-Type: 29 | - application/json; charset=utf-8 30 | Date: 31 | - Thu, 13 Feb 2025 23:29:46 GMT 32 | Last-Modified: 33 | - Thu, 13 Feb 2025 20:16:17 GMT 34 | NEL: 35 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 36 | Report-To: 37 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=qICbmX3AgwKMtYv%2BsM264kQKAUXoK5fmbFkBE0oRv1%2Fy%2B6wYxcRM92Y0NBddXmoWSXequM3hwacl1z51WqnASungSt8RnL5ACFS9UFlVdy7YJKYk89X51OsoSIhBvRfod9b2mA%3D%3D"}],"group":"cf-nel","max_age":604800}' 38 | Server: 39 | - cloudflare 40 | Strict-Transport-Security: 41 | - max-age=15552000; includeSubDomains; preload 42 | Transfer-Encoding: 43 | - chunked 44 | X-Content-Type-Options: 45 | - nosniff 46 | alt-svc: 47 | - h3=":443"; ma=86400 48 | content-length: 49 | - '198' 50 | server-timing: 51 | - cfL4;desc="?proto=TCP&rtt=3011&min_rtt=2980&rtt_var=1180&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2848&recv_bytes=810&delivery_rate=896039&cwnd=251&unsent_bytes=0&cid=64188512408efddd&ts=61&x=0" 52 | vary: 53 | - Accept-Encoding 54 | - Accept-Encoding 55 | status: 56 | code: 200 57 | message: OK 58 | version: 1 59 | -------------------------------------------------------------------------------- /tests/cassettes/gestdown/test_search_show_id_error.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept-Encoding: 6 | - gzip, deflate 7 | Connection: 8 | - keep-alive 9 | User-Agent: 10 | - Subliminal/2.2 11 | accept: 12 | - application/json 13 | method: GET 14 | uri: https://api.gestdown.info/shows/search/The%20Big%20How%20I%20Met%20Your%20Mother 15 | response: 16 | body: 17 | string: '"Couldn''t find show: The Big How I Met Your Mother"' 18 | headers: 19 | CF-Cache-Status: 20 | - EXPIRED 21 | CF-RAY: 22 | - 91189a3dba095be8-LIS 23 | Cache-Control: 24 | - public, max-age=43200 25 | Connection: 26 | - keep-alive 27 | Content-Type: 28 | - application/json; charset=utf-8 29 | Date: 30 | - Thu, 13 Feb 2025 23:29:47 GMT 31 | NEL: 32 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 33 | Report-To: 34 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=9k5gIvKsfBitsKH8f5nrTi1AJCEhP%2Bu1%2BUFsMKwS9VNzBFCgXgyNnpsW7X40zV0Nhb9f26sIJNrUzEODyeZ1ftX%2Fwf1652D3V9GR3XzqCYAKigjOIURD60YEG1NrCRvJ1M1uLQ%3D%3D"}],"group":"cf-nel","max_age":604800}' 35 | Server: 36 | - cloudflare 37 | Strict-Transport-Security: 38 | - max-age=15552000; includeSubDomains; preload 39 | Transfer-Encoding: 40 | - chunked 41 | X-Content-Type-Options: 42 | - nosniff 43 | alt-svc: 44 | - h3=":443"; ma=86400 45 | content-length: 46 | - '51' 47 | server-timing: 48 | - cfL4;desc="?proto=TCP&rtt=2981&min_rtt=2955&rtt_var=1162&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2848&recv_bytes=826&delivery_rate=913564&cwnd=251&unsent_bytes=0&cid=a839341109cba928&ts=80&x=0" 49 | vary: 50 | - Accept-Encoding 51 | - Accept-Encoding 52 | status: 53 | code: 404 54 | message: Not Found 55 | version: 1 56 | -------------------------------------------------------------------------------- /tests/cassettes/gestdown/test_search_show_id_incomplete.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept-Encoding: 6 | - gzip, deflate 7 | Connection: 8 | - keep-alive 9 | User-Agent: 10 | - Subliminal/2.2 11 | accept: 12 | - application/json 13 | method: GET 14 | uri: https://api.gestdown.info/shows/search/The%20Big%20Bang 15 | response: 16 | body: 17 | string: '{"shows":[{"id":"91eb9278-8cf5-4ddd-9111-7f60b15958cb","name":"The 18 | Big Bang Theory","nbSeasons":12,"seasons":[1,2,3,4,5,6,7,8,9,10,11,12],"tvDbId":80379,"tmdbId":1418,"slug":"the-big-bang-theory"}]}' 19 | headers: 20 | CF-Cache-Status: 21 | - MISS 22 | CF-RAY: 23 | - 91189a3c1e26ef03-LHR 24 | Cache-Control: 25 | - public, max-age=14400 26 | Connection: 27 | - keep-alive 28 | Content-Type: 29 | - application/json; charset=utf-8 30 | Date: 31 | - Thu, 13 Feb 2025 23:29:46 GMT 32 | Last-Modified: 33 | - Thu, 13 Feb 2025 23:29:46 GMT 34 | NEL: 35 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 36 | Report-To: 37 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=PYTdGgvDIjA%2FvdPIdaeMASuLMWKOHIwv0Zvm8JRESGwwHGwoNSpaivB4EIU6ljtIdJp1%2BpA%2FqSHczpg3DWDUREZaKRxkTY8UtHnnJ3j85Iz3PBDEu%2BnoPnjz0NEiJHbwmXOxhg%3D%3D"}],"group":"cf-nel","max_age":604800}' 38 | Server: 39 | - cloudflare 40 | Strict-Transport-Security: 41 | - max-age=15552000; includeSubDomains; preload 42 | Transfer-Encoding: 43 | - chunked 44 | X-Content-Type-Options: 45 | - nosniff 46 | alt-svc: 47 | - h3=":443"; ma=86400 48 | content-length: 49 | - '198' 50 | server-timing: 51 | - cfL4;desc="?proto=TCP&rtt=32029&min_rtt=31981&rtt_var=12089&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2848&recv_bytes=801&delivery_rate=89479&cwnd=242&unsent_bytes=0&cid=385f7f1de8548d1b&ts=85&x=0" 52 | vary: 53 | - Accept-Encoding 54 | - Accept-Encoding 55 | status: 56 | code: 200 57 | message: OK 58 | version: 1 59 | -------------------------------------------------------------------------------- /tests/cassettes/gestdown/test_search_show_id_no_year.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept-Encoding: 6 | - gzip, deflate 7 | Connection: 8 | - keep-alive 9 | User-Agent: 10 | - Subliminal/2.2 11 | accept: 12 | - application/json 13 | method: GET 14 | uri: https://api.gestdown.info/shows/search/Dallas 15 | response: 16 | body: 17 | string: '{"shows":[{"id":"226d7f34-a9f5-4fe2-98d7-ecf944243714","name":"Dallas","nbSeasons":11,"seasons":[1,2,3,4,5,6,8,9,11,12,13],"tvDbId":77092,"tmdbId":40,"slug":"dallas"},{"id":"4e56a466-31a3-4b05-9696-671a1cd6954e","name":"Dallas 18 | (2012)","nbSeasons":3,"seasons":[1,2,3],"tvDbId":242521,"tmdbId":37759,"slug":"dallas-2012"}]}' 19 | headers: 20 | CF-Cache-Status: 21 | - MISS 22 | CF-RAY: 23 | - 91189a3d1a0eedf8-LHR 24 | Cache-Control: 25 | - public, max-age=14400 26 | Connection: 27 | - keep-alive 28 | Content-Type: 29 | - application/json; charset=utf-8 30 | Date: 31 | - Thu, 13 Feb 2025 23:29:47 GMT 32 | Last-Modified: 33 | - Thu, 13 Feb 2025 23:29:47 GMT 34 | NEL: 35 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 36 | Report-To: 37 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=yb2etjFaHp212nP5vSwRqiB4hPwTtAnwcOCHjVga0TknLga%2FCvtt9utAGQTTAzHGFSX90wv4x%2F6f4mw9SHDaEw5fhPRnZ4myZ6eIOBc8hkR3HvVEWvQ0ANn76AGBtDYcQiN1Fw%3D%3D"}],"group":"cf-nel","max_age":604800}' 38 | Server: 39 | - cloudflare 40 | Strict-Transport-Security: 41 | - max-age=15552000; includeSubDomains; preload 42 | Transfer-Encoding: 43 | - chunked 44 | X-Content-Type-Options: 45 | - nosniff 46 | alt-svc: 47 | - h3=":443"; ma=86400 48 | content-length: 49 | - '321' 50 | server-timing: 51 | - cfL4;desc="?proto=TCP&rtt=32042&min_rtt=31978&rtt_var=12121&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2847&recv_bytes=791&delivery_rate=89121&cwnd=251&unsent_bytes=0&cid=ab76998768b8fe85&ts=94&x=0" 52 | vary: 53 | - Accept-Encoding 54 | - Accept-Encoding 55 | status: 56 | code: 200 57 | message: OK 58 | version: 1 59 | -------------------------------------------------------------------------------- /tests/cassettes/gestdown/test_search_show_id_quote.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept-Encoding: 6 | - gzip, deflate 7 | Connection: 8 | - keep-alive 9 | User-Agent: 10 | - Subliminal/2.2 11 | accept: 12 | - application/json 13 | method: GET 14 | uri: https://api.gestdown.info/shows/search/Grey's%20Anatomy 15 | response: 16 | body: 17 | string: '{"shows":[{"id":"cb13bb68-c637-4e0f-b79d-d4f1b4972380","name":"Grey''s 18 | Anatomy","nbSeasons":21,"seasons":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21],"tvDbId":73762,"tmdbId":1416,"slug":"greys-anatomy"}]}' 19 | headers: 20 | CF-Cache-Status: 21 | - MISS 22 | CF-RAY: 23 | - 91189a3e5cc35bd4-LIS 24 | Cache-Control: 25 | - public, max-age=14400 26 | Connection: 27 | - keep-alive 28 | Content-Type: 29 | - application/json; charset=utf-8 30 | Date: 31 | - Thu, 13 Feb 2025 23:29:47 GMT 32 | Last-Modified: 33 | - Thu, 13 Feb 2025 23:29:47 GMT 34 | NEL: 35 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 36 | Report-To: 37 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=r19gGjhTR2MiVlu3yoxOR9UkGKVsssnEbgAKrHWxpRWonfSsiPOSc72QAcw8JqpT4zUMtKjpSndGPXAFKDzVDv%2BQ8%2Fb9KO1lD%2BXBK998wJfXnecuU0HpFQnRLX3mMflrvh8liw%3D%3D"}],"group":"cf-nel","max_age":604800}' 38 | Server: 39 | - cloudflare 40 | Strict-Transport-Security: 41 | - max-age=15552000; includeSubDomains; preload 42 | Transfer-Encoding: 43 | - chunked 44 | X-Content-Type-Options: 45 | - nosniff 46 | alt-svc: 47 | - h3=":443"; ma=86400 48 | content-length: 49 | - '214' 50 | server-timing: 51 | - cfL4;desc="?proto=TCP&rtt=2948&min_rtt=2738&rtt_var=1177&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2847&recv_bytes=801&delivery_rate=1057706&cwnd=241&unsent_bytes=0&cid=2e1bda9ce9ae6fa5&ts=89&x=0" 52 | vary: 53 | - Accept-Encoding 54 | - Accept-Encoding 55 | status: 56 | code: 200 57 | message: OK 58 | version: 1 59 | -------------------------------------------------------------------------------- /tests/cassettes/gestdown/test_search_show_series_tvdb_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept-Encoding: 6 | - gzip, deflate 7 | Connection: 8 | - keep-alive 9 | User-Agent: 10 | - Subliminal/2.2 11 | accept: 12 | - application/json 13 | method: GET 14 | uri: https://api.gestdown.info/shows/external/tvdb/80379 15 | response: 16 | body: 17 | string: '{"shows":[{"id":"91eb9278-8cf5-4ddd-9111-7f60b15958cb","name":"The 18 | Big Bang Theory","nbSeasons":12,"seasons":[1,2,3,4,5,6,7,8,9,10,11,12],"tvDbId":80379,"tmdbId":1418,"slug":"the-big-bang-theory"}]}' 19 | headers: 20 | Age: 21 | - '2' 22 | CF-RAY: 23 | - 91189a3f0d785bda-LIS 24 | Cache-Control: 25 | - public, max-age=14400 26 | Connection: 27 | - keep-alive 28 | Content-Type: 29 | - application/json; charset=utf-8 30 | Date: 31 | - Thu, 13 Feb 2025 23:29:47 GMT 32 | NEL: 33 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 34 | Report-To: 35 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=eFm2qvfCKQeVDSPt%2BqNM9LW4Ak9wJbLJDMooBI7sWBBnynC9FJeJMK8d1EahkeebGIVQdckpimodnLpTHKJWiWvqXTPqWCImyxMIBOeEWlzZIeFRfbqh5fXQ5YJuNKiTJ7Bf2g%3D%3D"}],"group":"cf-nel","max_age":604800}' 36 | Server: 37 | - cloudflare 38 | Strict-Transport-Security: 39 | - max-age=15552000; includeSubDomains; preload 40 | Transfer-Encoding: 41 | - chunked 42 | X-Content-Type-Options: 43 | - nosniff 44 | alt-svc: 45 | - h3=":443"; ma=86400 46 | cf-cache-status: 47 | - HIT 48 | content-length: 49 | - '198' 50 | last-modified: 51 | - Wed, 12 Feb 2025 22:02:09 GMT 52 | server-timing: 53 | - cfL4;desc="?proto=TCP&rtt=2949&min_rtt=2754&rtt_var=1172&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2847&recv_bytes=797&delivery_rate=1051561&cwnd=250&unsent_bytes=0&cid=c604498dad723c34&ts=26&x=0" 54 | vary: 55 | - Accept-Encoding 56 | - Accept-Encoding 57 | status: 58 | code: 200 59 | message: OK 60 | version: 1 61 | -------------------------------------------------------------------------------- /tests/cassettes/napiprojekt/test_query_wrong_hash.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - Subliminal/2.2 13 | method: GET 14 | uri: https://napiprojekt.pl/unit_napisy/dl.php?v=dreambox&kolejka=false&nick=&pass=&napios=Linux&l=PL&f=abcdabdcabcd1234abcd1234abcd123&t=4afc4 15 | response: 16 | body: 17 | string: NPc0 18 | headers: 19 | Cache-Control: 20 | - max-age=1296000 21 | Connection: 22 | - Keep-Alive 23 | Content-Type: 24 | - text/html; charset=UTF-8 25 | Date: 26 | - Thu, 13 Feb 2025 23:29:47 GMT 27 | Expires: 28 | - Fri, 28 Feb 2025 23:29:47 GMT 29 | Keep-Alive: 30 | - timeout=5, max=1000 31 | Server: 32 | - Apache 33 | Vary: 34 | - Accept-Encoding 35 | content-length: 36 | - '4' 37 | status: 38 | code: 200 39 | message: OK 40 | version: 1 41 | -------------------------------------------------------------------------------- /tests/cassettes/omdb/test_get_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - Subliminal/2.2 13 | method: GET 14 | uri: https://www.omdbapi.com/?r=json&v=1&apikey=44d5b275&i=tt0770828&plot=short 15 | response: 16 | body: 17 | string: '{"Title":"Man of Steel","Year":"2013","Rated":"PG-13","Released":"14 18 | Jun 2013","Runtime":"143 min","Genre":"Action, Adventure, Sci-Fi","Director":"Zack 19 | Snyder","Writer":"David S. Goyer, Christopher Nolan, Jerry Siegel","Actors":"Henry 20 | Cavill, Amy Adams, Michael Shannon","Plot":"An alien child is evacuated from 21 | his dying world and sent to Earth to live among humans. His peace is threatened 22 | when other survivors of his home planet invade Earth.","Language":"English","Country":"United 23 | States, United Kingdom","Awards":"Nominated for 1 BAFTA Award7 wins & 46 nominations 24 | total","Poster":"https://m.media-amazon.com/images/M/MV5BMTk5ODk1NDkxMF5BMl5BanBnXkFtZTcwNTA5OTY0OQ@@._V1_SX300.jpg","Ratings":[{"Source":"Internet 25 | Movie Database","Value":"7.1/10"},{"Source":"Rotten Tomatoes","Value":"57%"},{"Source":"Metacritic","Value":"55/100"}],"Metascore":"55","imdbRating":"7.1","imdbVotes":"828,023","imdbID":"tt0770828","Type":"movie","DVD":"N/A","BoxOffice":"$291,045,518","Production":"N/A","Website":"N/A","Response":"True"}' 26 | headers: 27 | CF-Cache-Status: 28 | - MISS 29 | CF-RAY: 30 | - 91189a935c2def44-LHR 31 | Cache-Control: 32 | - public, max-age=86400 33 | Connection: 34 | - keep-alive 35 | Content-Type: 36 | - application/json; charset=utf-8 37 | Date: 38 | - Thu, 13 Feb 2025 23:30:01 GMT 39 | Server: 40 | - cloudflare 41 | Transfer-Encoding: 42 | - chunked 43 | access-control-allow-origin: 44 | - '*' 45 | content-length: 46 | - '1024' 47 | expires: 48 | - Fri, 14 Feb 2025 00:30:01 GMT 49 | last-modified: 50 | - Thu, 13 Feb 2025 23:30:01 GMT 51 | vary: 52 | - '*, Accept-Encoding' 53 | x-aspnet-version: 54 | - 4.0.30319 55 | x-powered-by: 56 | - ASP.NET 57 | status: 58 | code: 200 59 | message: OK 60 | version: 1 61 | -------------------------------------------------------------------------------- /tests/cassettes/omdb/test_get_title.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - Subliminal/2.2 13 | method: GET 14 | uri: https://www.omdbapi.com/?r=json&v=1&apikey=44d5b275&t=Man+of+Steel&plot=short 15 | response: 16 | body: 17 | string: '{"Title":"Man of Steel","Year":"2013","Rated":"PG-13","Released":"14 18 | Jun 2013","Runtime":"143 min","Genre":"Action, Adventure, Sci-Fi","Director":"Zack 19 | Snyder","Writer":"David S. Goyer, Christopher Nolan, Jerry Siegel","Actors":"Henry 20 | Cavill, Amy Adams, Michael Shannon","Plot":"An alien child is evacuated from 21 | his dying world and sent to Earth to live among humans. His peace is threatened 22 | when other survivors of his home planet invade Earth.","Language":"English","Country":"United 23 | States, United Kingdom","Awards":"Nominated for 1 BAFTA Award7 wins & 46 nominations 24 | total","Poster":"https://m.media-amazon.com/images/M/MV5BMTk5ODk1NDkxMF5BMl5BanBnXkFtZTcwNTA5OTY0OQ@@._V1_SX300.jpg","Ratings":[{"Source":"Internet 25 | Movie Database","Value":"7.1/10"},{"Source":"Rotten Tomatoes","Value":"57%"},{"Source":"Metacritic","Value":"55/100"}],"Metascore":"55","imdbRating":"7.1","imdbVotes":"828,023","imdbID":"tt0770828","Type":"movie","DVD":"N/A","BoxOffice":"$291,045,518","Production":"N/A","Website":"N/A","Response":"True"}' 26 | headers: 27 | CF-Cache-Status: 28 | - MISS 29 | CF-RAY: 30 | - 91189a9e1c68ef0b-LHR 31 | Cache-Control: 32 | - public, max-age=86400 33 | Connection: 34 | - keep-alive 35 | Content-Type: 36 | - application/json; charset=utf-8 37 | Date: 38 | - Thu, 13 Feb 2025 23:30:02 GMT 39 | Server: 40 | - cloudflare 41 | Transfer-Encoding: 42 | - chunked 43 | access-control-allow-origin: 44 | - '*' 45 | content-length: 46 | - '1024' 47 | expires: 48 | - Fri, 14 Feb 2025 00:30:02 GMT 49 | last-modified: 50 | - Thu, 13 Feb 2025 23:30:02 GMT 51 | vary: 52 | - '*, Accept-Encoding' 53 | x-aspnet-version: 54 | - 4.0.30319 55 | x-powered-by: 56 | - ASP.NET 57 | status: 58 | code: 200 59 | message: OK 60 | version: 1 61 | -------------------------------------------------------------------------------- /tests/cassettes/omdb/test_get_wrong_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - Subliminal/2.2 13 | method: GET 14 | uri: https://www.omdbapi.com/?r=json&v=1&apikey=44d5b275&i=tt9999999&plot=short 15 | response: 16 | body: 17 | string: '{"Response":"False","Error":"Error getting data."}' 18 | headers: 19 | CF-Cache-Status: 20 | - MISS 21 | CF-RAY: 22 | - 91189a97a904ef1d-LHR 23 | Cache-Control: 24 | - public, max-age=86400 25 | Connection: 26 | - keep-alive 27 | Content-Type: 28 | - application/json; charset=utf-8 29 | Date: 30 | - Thu, 13 Feb 2025 23:30:02 GMT 31 | Server: 32 | - cloudflare 33 | Transfer-Encoding: 34 | - chunked 35 | access-control-allow-origin: 36 | - '*' 37 | content-length: 38 | - '50' 39 | expires: 40 | - Fri, 14 Feb 2025 00:30:01 GMT 41 | last-modified: 42 | - Thu, 13 Feb 2025 23:30:01 GMT 43 | vary: 44 | - '*, Accept-Encoding' 45 | x-aspnet-version: 46 | - 4.0.30319 47 | x-powered-by: 48 | - ASP.NET 49 | status: 50 | code: 200 51 | message: OK 52 | version: 1 53 | -------------------------------------------------------------------------------- /tests/cassettes/omdb/test_get_wrong_title.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - Subliminal/2.2 13 | method: GET 14 | uri: https://www.omdbapi.com/?r=json&v=1&apikey=44d5b275&t=Meen+of+Stal&plot=short 15 | response: 16 | body: 17 | string: '{"Response":"False","Error":"Movie not found!"}' 18 | headers: 19 | Accept-Ranges: 20 | - bytes 21 | CF-Cache-Status: 22 | - MISS 23 | CF-RAY: 24 | - 91189a9ff8f8ef3e-LHR 25 | Cache-Control: 26 | - public, max-age=86400 27 | Connection: 28 | - keep-alive 29 | Content-Length: 30 | - '47' 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Date: 34 | - Thu, 13 Feb 2025 23:30:03 GMT 35 | Server: 36 | - cloudflare 37 | access-control-allow-origin: 38 | - '*' 39 | expires: 40 | - Fri, 14 Feb 2025 00:30:03 GMT 41 | last-modified: 42 | - Thu, 13 Feb 2025 23:30:03 GMT 43 | vary: 44 | - '*, Accept-Encoding' 45 | x-aspnet-version: 46 | - 4.0.30319 47 | x-powered-by: 48 | - ASP.NET 49 | status: 50 | code: 200 51 | message: OK 52 | version: 1 53 | -------------------------------------------------------------------------------- /tests/cassettes/omdb/test_refine_episode.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - Subliminal/2.2 13 | method: GET 14 | uri: https://www.omdbapi.com/?r=json&v=1&apikey=44d5b275&s=the+big+bang+theory&page=1&type=series 15 | response: 16 | body: 17 | string: "{\"Search\":[{\"Title\":\"The Big Bang Theory\",\"Year\":\"2007\u20132019\",\"imdbID\":\"tt0898266\",\"Type\":\"series\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BZjgzY2QyNzItNDhhYi00ZWIwLWFjN2UtZDJkN2MxYWNjMmJjXkEyXkFqcGc@._V1_SX300.jpg\"},{\"Title\":\"The 18 | Big Bang Theory After Show\",\"Year\":\"2015\u2013\",\"imdbID\":\"tt5612894\",\"Type\":\"series\",\"Poster\":\"N/A\"}],\"totalResults\":\"2\",\"Response\":\"True\"}" 19 | headers: 20 | CF-Cache-Status: 21 | - MISS 22 | CF-RAY: 23 | - 91189ab4a8beef40-LHR 24 | Cache-Control: 25 | - public, max-age=86400 26 | Connection: 27 | - keep-alive 28 | Content-Type: 29 | - application/json; charset=utf-8 30 | Date: 31 | - Thu, 13 Feb 2025 23:30:06 GMT 32 | Server: 33 | - cloudflare 34 | Transfer-Encoding: 35 | - chunked 36 | access-control-allow-origin: 37 | - '*' 38 | content-length: 39 | - '377' 40 | expires: 41 | - Fri, 14 Feb 2025 00:30:06 GMT 42 | last-modified: 43 | - Thu, 13 Feb 2025 23:30:06 GMT 44 | vary: 45 | - '*, Accept-Encoding' 46 | x-aspnet-version: 47 | - 4.0.30319 48 | x-powered-by: 49 | - ASP.NET 50 | status: 51 | code: 200 52 | message: OK 53 | version: 1 54 | -------------------------------------------------------------------------------- /tests/cassettes/omdb/test_refine_episode_with_country.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate, br, zstd 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - Subliminal/2.1 13 | method: GET 14 | uri: https://www.omdbapi.com/?r=json&v=1&apikey=44d5b275&s=Shameless&page=1&type=series 15 | response: 16 | body: 17 | string: "{\"Search\":[{\"Title\":\"Shameless\",\"Year\":\"2011\u20132021\",\"imdbID\":\"tt1586680\",\"Type\":\"series\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BZDgxNjQ2MjMtMjk2Yi00M2Q2LWI0ZDktOGM1NWI5YWUzMjk4XkEyXkFqcGdeQXVyOTA3MTMyOTk@._V1_SX300.jpg\"},{\"Title\":\"Shameless\",\"Year\":\"2004\u20132013\",\"imdbID\":\"tt0377260\",\"Type\":\"series\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BZmIzMjAwYWUtNzliMy00MmQ0LTk0ZTItMzBiMTAzY2FjMTNiXkEyXkFqcGdeQXVyNTAyODkwOQ@@._V1_SX300.jpg\"},{\"Title\":\"Shameless 18 | Hall of Shame\",\"Year\":\"2012\u20132021\",\"imdbID\":\"tt13653972\",\"Type\":\"series\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BODZmNjI4MjYtMjRiMy00NzE4LWI5ODItN2JjNzRlMzY0MjlhXkEyXkFqcGdeQXVyNTE1NjY5Mg@@._V1_SX300.jpg\"},{\"Title\":\"Rich 19 | & Shameless\",\"Year\":\"2022\u20132023\",\"imdbID\":\"tt17497942\",\"Type\":\"series\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BMmZhY2I2ZmYtYjRlYi00NDYwLThjMWYtNzQyNDdjZTBhYjI4XkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg\"},{\"Title\":\"Shameless 20 | Comedy\",\"Year\":\"2023\u2013\",\"imdbID\":\"tt20196198\",\"Type\":\"series\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BMWVjM2ZjOWItNzNhOS00N2NhLTg5ZDQtMGRlZDM5ZDY0NTFmXkEyXkFqcGdeQXVyMTAwODYwMDgy._V1_SX300.jpg\"},{\"Title\":\"Shameless 21 | Idealists\",\"Year\":\"2010\u2013\",\"imdbID\":\"tt2539106\",\"Type\":\"series\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BYmU5MTAwNmUtZTBiZi00YWExLTgzMmQtNjM3NzZiMTMwZjA2XkEyXkFqcGdeQXVyMzk2OTI5MDM@._V1_SX300.jpg\"}],\"totalResults\":\"6\",\"Response\":\"True\"}" 22 | headers: 23 | CF-Cache-Status: 24 | - MISS 25 | CF-RAY: 26 | - 887d96c368d099ba-CDG 27 | Cache-Control: 28 | - public, max-age=86400 29 | Connection: 30 | - keep-alive 31 | Content-Encoding: 32 | - gzip 33 | Content-Type: 34 | - application/json; charset=utf-8 35 | Date: 36 | - Wed, 22 May 2024 14:45:15 GMT 37 | Server: 38 | - cloudflare 39 | Transfer-Encoding: 40 | - chunked 41 | access-control-allow-origin: 42 | - '*' 43 | expires: 44 | - Wed, 22 May 2024 15:45:15 GMT 45 | last-modified: 46 | - Wed, 22 May 2024 14:45:15 GMT 47 | vary: 48 | - '*, Accept-Encoding' 49 | x-aspnet-version: 50 | - 4.0.30319 51 | x-powered-by: 52 | - ASP.NET 53 | status: 54 | code: 200 55 | message: OK 56 | version: 1 57 | -------------------------------------------------------------------------------- /tests/cassettes/omdb/test_refine_episode_with_country_hoc_us.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | interactions: 3 | - request: 4 | body: null 5 | headers: 6 | Accept: 7 | - '*/*' 8 | Accept-Encoding: 9 | - gzip, deflate, br, zstd 10 | Connection: 11 | - keep-alive 12 | User-Agent: 13 | - Subliminal/2.1 14 | method: GET 15 | uri: https://www.omdbapi.com/?r=json&v=1&apikey=44d5b275&s=house+of+cards&page=1&type=series 16 | response: 17 | body: 18 | string: "{\"Search\":[{\"Title\":\"House of Cards\",\"Year\":\"2013\u20132018\",\"imdbID\":\"tt1856010\",\"Type\":\"series\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BNmM4ODU1MzItODYyYi00Y2U0LWFjZjItYTRhZWIwOGMyZTRhXkEyXkFqcGdeQXVyNjc2NTQ4Nzk@._V1_SX300.jpg\"},{\"Title\":\"House 19 | of Cards\",\"Year\":\"1990\",\"imdbID\":\"tt0098825\",\"Type\":\"series\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BNmVhMGFlMjUtN2I0NS00MTM4LWFlZTgtMWI1Y2QyMmI1ODdjXkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_SX300.jpg\"},{\"Title\":\"DateSmash: 20 | Bernie Sanders & House of Cards' Frank Underwood\",\"Year\":\"2016\",\"imdbID\":\"tt14397660\",\"Type\":\"series\",\"Poster\":\"N/A\"}],\"totalResults\":\"3\",\"Response\":\"True\"}" 21 | headers: 22 | CF-Cache-Status: 23 | - MISS 24 | CF-RAY: 25 | - 887d96c7fc9003fb-CDG 26 | Cache-Control: 27 | - public, max-age=86400 28 | Connection: 29 | - keep-alive 30 | Content-Encoding: 31 | - gzip 32 | Content-Type: 33 | - application/json; charset=utf-8 34 | Date: 35 | - Wed, 22 May 2024 14:45:16 GMT 36 | Server: 37 | - cloudflare 38 | Transfer-Encoding: 39 | - chunked 40 | access-control-allow-origin: 41 | - '*' 42 | expires: 43 | - Wed, 22 May 2024 15:45:16 GMT 44 | last-modified: 45 | - Wed, 22 May 2024 14:45:16 GMT 46 | vary: 47 | - '*, Accept-Encoding' 48 | x-aspnet-version: 49 | - 4.0.30319 50 | x-powered-by: 51 | - ASP.NET 52 | status: 53 | code: 200 54 | message: OK 55 | version: 1 56 | -------------------------------------------------------------------------------- /tests/cassettes/omdb/test_refine_episode_year.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - Subliminal/2.2 13 | method: GET 14 | uri: https://www.omdbapi.com/?r=json&v=1&apikey=44d5b275&s=dallas&page=1&type=series&y=2012 15 | response: 16 | body: 17 | string: "{\"Search\":[{\"Title\":\"Dallas\",\"Year\":\"2012\u20132014\",\"imdbID\":\"tt1723760\",\"Type\":\"series\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BMjE5NTExNDc5MF5BMl5BanBnXkFtZTcwMjc1NzAwOQ@@._V1_SX300.jpg\"},{\"Title\":\"The 18 | Book of Dallas\",\"Year\":\"2012\u2013\",\"imdbID\":\"tt2373244\",\"Type\":\"series\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BMTU2NzY0MzI2MF5BMl5BanBnXkFtZTcwNzMxMzY0OA@@._V1_SX300.jpg\"},{\"Title\":\"The 19 | Underemployed Adventures of Dallas Heck\",\"Year\":\"2012\u2013\",\"imdbID\":\"tt2673024\",\"Type\":\"series\",\"Poster\":\"N/A\"},{\"Title\":\"Dallas 20 | Roundup Aftershow\",\"Year\":\"2012\u2013\",\"imdbID\":\"tt2715600\",\"Type\":\"series\",\"Poster\":\"N/A\"},{\"Title\":\"Dallas 21 | Live\",\"Year\":\"2012\u20132013\",\"imdbID\":\"tt6872494\",\"Type\":\"series\",\"Poster\":\"N/A\"},{\"Title\":\"John 22 | Barrowman's Dallas\",\"Year\":\"2012\",\"imdbID\":\"tt27329542\",\"Type\":\"series\",\"Poster\":\"N/A\"},{\"Title\":\"Luxury 23 | Dallas\",\"Year\":\"2012\",\"imdbID\":\"tt2332492\",\"Type\":\"series\",\"Poster\":\"N/A\"}],\"totalResults\":\"7\",\"Response\":\"True\"}" 24 | headers: 25 | CF-Cache-Status: 26 | - MISS 27 | CF-RAY: 28 | - 91189acd4decf666-LHR 29 | Cache-Control: 30 | - public, max-age=86400 31 | Connection: 32 | - keep-alive 33 | Content-Type: 34 | - application/json; charset=utf-8 35 | Date: 36 | - Thu, 13 Feb 2025 23:30:10 GMT 37 | Server: 38 | - cloudflare 39 | Transfer-Encoding: 40 | - chunked 41 | access-control-allow-origin: 42 | - '*' 43 | content-length: 44 | - '954' 45 | expires: 46 | - Fri, 14 Feb 2025 00:30:10 GMT 47 | last-modified: 48 | - Thu, 13 Feb 2025 23:30:10 GMT 49 | vary: 50 | - '*, Accept-Encoding' 51 | x-aspnet-version: 52 | - 4.0.30319 53 | x-powered-by: 54 | - ASP.NET 55 | status: 56 | code: 200 57 | message: OK 58 | version: 1 59 | -------------------------------------------------------------------------------- /tests/cassettes/omdb/test_refine_movie_guess_alternative_title.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate, br, zstd 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - Subliminal/2.1 13 | method: GET 14 | uri: https://www.omdbapi.com/?r=json&v=1&apikey=44d5b275&s=Jack+Reacher&page=1&type=movie&y=2016 15 | response: 16 | body: 17 | string: '{"Search":[{"Title":"Jack Reacher: Never Go Back","Year":"2016","imdbID":"tt3393786","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMTA2MDIwNzAyNzReQTJeQWpwZ15BbWU4MDAxNjI5Njkx._V1_SX300.jpg"}],"totalResults":"1","Response":"True"}' 18 | headers: 19 | CF-Cache-Status: 20 | - MISS 21 | CF-RAY: 22 | - 887d96bf7a9f3cf3-CDG 23 | Cache-Control: 24 | - public, max-age=86400 25 | Connection: 26 | - keep-alive 27 | Content-Encoding: 28 | - gzip 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Date: 32 | - Wed, 22 May 2024 14:45:15 GMT 33 | Server: 34 | - cloudflare 35 | Transfer-Encoding: 36 | - chunked 37 | access-control-allow-origin: 38 | - '*' 39 | expires: 40 | - Wed, 22 May 2024 15:45:15 GMT 41 | last-modified: 42 | - Wed, 22 May 2024 14:45:15 GMT 43 | vary: 44 | - '*, Accept-Encoding' 45 | x-aspnet-version: 46 | - 4.0.30319 47 | x-powered-by: 48 | - ASP.NET 49 | status: 50 | code: 200 51 | message: OK 52 | version: 1 53 | -------------------------------------------------------------------------------- /tests/cassettes/omdb/test_search.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - Subliminal/2.2 13 | method: GET 14 | uri: https://www.omdbapi.com/?r=json&v=1&apikey=44d5b275&s=Man+of+Steel&page=1 15 | response: 16 | body: 17 | string: '{"Search":[{"Title":"Man of Steel","Year":"2013","imdbID":"tt0770828","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMTk5ODk1NDkxMF5BMl5BanBnXkFtZTcwNTA5OTY0OQ@@._V1_SX300.jpg"},{"Title":"World 18 | War Two: 1941 and the Man of Steel","Year":"2011","imdbID":"tt5537126","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BNTg4NmI5ZTgtMWU2Yi00ZGYyLTg1OWUtYjVhN2EwODg2YTVjXkEyXkFqcGc@._V1_SX300.jpg"},{"Title":"Superman: 19 | The Man of Steel","Year":"2002","imdbID":"tt1453257","Type":"game","Poster":"https://m.media-amazon.com/images/M/MV5BMWQyOWE4MTYtNTdhYy00ZjYxLThiYmYtMjVlMGI1Y2JjMmRhXkEyXkFqcGdeQXVyNDQ2OTk4MzI@._V1_SX300.jpg"},{"Title":"Man 20 | of Steel: The Yahoo Live Fan Event","Year":"2013","imdbID":"tt3327446","Type":"movie","Poster":"N/A"},{"Title":"Little 21 | Man of Steel","Year":"2013","imdbID":"tt2557780","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BZjE1MjE4MTItZjJjMC00NDFjLWExNjAtMjViMzQzYTA4ZWUzXkEyXkFqcGdeQXVyMzYzNzc1NjY@._V1_SX300.jpg"},{"Title":"Man 22 | of Steel","Year":"1967","imdbID":"tt0263704","Type":"movie","Poster":"N/A"},{"Title":"Christopher 23 | Reeve: The Man of Steel","Year":"2004","imdbID":"tt2108487","Type":"movie","Poster":"N/A"},{"Title":"Man 24 | of Steel Song","Year":"2013","imdbID":"tt2754626","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMjAwNzE4MzU4OF5BMl5BanBnXkFtZTcwNzk1NTk1OQ@@._V1_SX300.jpg"},{"Title":"Chris 25 | Bacon: Man of Steel","Year":"2014","imdbID":"tt3560162","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BNzg2MzQ2MTI3NF5BMl5BanBnXkFtZTgwNjI1OTkyMTE@._V1_SX300.jpg"},{"Title":"Superman: 26 | Man of Steel","Year":"2019","imdbID":"tt10069974","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BODcxNDg4YjYtZjZmYS00NDAwLTg2NjctYTNiMjFiMDRlNmU5XkEyXkFqcGdeQXVyODc5OTgyODQ@._V1_SX300.jpg"}],"totalResults":"32","Response":"True"}' 27 | headers: 28 | CF-Cache-Status: 29 | - MISS 30 | CF-RAY: 31 | - 91189aa3c82bedf7-LHR 32 | Cache-Control: 33 | - public, max-age=86400 34 | Connection: 35 | - keep-alive 36 | Content-Type: 37 | - application/json; charset=utf-8 38 | Date: 39 | - Thu, 13 Feb 2025 23:30:03 GMT 40 | Server: 41 | - cloudflare 42 | Transfer-Encoding: 43 | - chunked 44 | access-control-allow-origin: 45 | - '*' 46 | content-length: 47 | - '1853' 48 | expires: 49 | - Fri, 14 Feb 2025 00:30:03 GMT 50 | last-modified: 51 | - Thu, 13 Feb 2025 23:30:03 GMT 52 | vary: 53 | - '*, Accept-Encoding' 54 | x-aspnet-version: 55 | - 4.0.30319 56 | x-powered-by: 57 | - ASP.NET 58 | status: 59 | code: 200 60 | message: OK 61 | version: 1 62 | -------------------------------------------------------------------------------- /tests/cassettes/omdb/test_search_page.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - Subliminal/2.2 13 | method: GET 14 | uri: https://www.omdbapi.com/?r=json&v=1&apikey=44d5b275&s=Man+of+Steel&page=3 15 | response: 16 | body: 17 | string: '{"Search":[{"Title":"Man of Steel ''Soldier of Steel''","Year":"2013","imdbID":"tt2911220","Type":"game","Poster":"N/A"},{"Title":"Man 18 | of Steel","Year":"2010","imdbID":"tt2215295","Type":"movie","Poster":"N/A"},{"Title":"Kurt 19 | Morrow: Man of Steel","Year":"2015","imdbID":"tt2378339","Type":"movie","Poster":"N/A"},{"Title":"Man 20 | of Steel: Title Sequence","Year":"2013","imdbID":"tt3748934","Type":"movie","Poster":"N/A"},{"Title":"Tales 21 | from Development Hell: Waiting for Man of Steel","Year":"2014","imdbID":"tt3799858","Type":"movie","Poster":"N/A"},{"Title":"The 22 | Heroes Behind Man of Steel: Heroic Storytelling","Year":"2013","imdbID":"tt4158100","Type":"movie","Poster":"N/A"},{"Title":"The 23 | Military Might of Man of Steel","Year":"2013","imdbID":"tt4158128","Type":"movie","Poster":"N/A"},{"Title":"The 24 | Heroes Behind Man of Steel: Composing a Heroic Score","Year":"2013","imdbID":"tt4160502","Type":"movie","Poster":"N/A"},{"Title":"Journey 25 | of Discovery: Creating Man of Steel","Year":"2013","imdbID":"tt4160544","Type":"movie","Poster":"N/A"},{"Title":"The 26 | Sonic Landscape of ''Man of Steel''","Year":"2013","imdbID":"tt5312384","Type":"movie","Poster":"N/A"}],"totalResults":"32","Response":"True"}' 27 | headers: 28 | CF-Cache-Status: 29 | - MISS 30 | CF-RAY: 31 | - 91189ab0d9fcedea-LHR 32 | Cache-Control: 33 | - public, max-age=86400 34 | Connection: 35 | - keep-alive 36 | Content-Type: 37 | - application/json; charset=utf-8 38 | Date: 39 | - Thu, 13 Feb 2025 23:30:06 GMT 40 | Server: 41 | - cloudflare 42 | Transfer-Encoding: 43 | - chunked 44 | access-control-allow-origin: 45 | - '*' 46 | content-length: 47 | - '1199' 48 | expires: 49 | - Fri, 14 Feb 2025 00:30:05 GMT 50 | last-modified: 51 | - Thu, 13 Feb 2025 23:30:05 GMT 52 | vary: 53 | - '*, Accept-Encoding' 54 | x-aspnet-version: 55 | - 4.0.30319 56 | x-powered-by: 57 | - ASP.NET 58 | status: 59 | code: 200 60 | message: OK 61 | version: 1 62 | -------------------------------------------------------------------------------- /tests/cassettes/omdb/test_search_type.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - Subliminal/2.2 13 | method: GET 14 | uri: https://www.omdbapi.com/?r=json&v=1&apikey=44d5b275&s=Man+of+Steel&page=1&type=movie 15 | response: 16 | body: 17 | string: '{"Search":[{"Title":"Man of Steel","Year":"2013","imdbID":"tt0770828","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMTk5ODk1NDkxMF5BMl5BanBnXkFtZTcwNTA5OTY0OQ@@._V1_SX300.jpg"},{"Title":"World 18 | War Two: 1941 and the Man of Steel","Year":"2011","imdbID":"tt5537126","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BNTg4NmI5ZTgtMWU2Yi00ZGYyLTg1OWUtYjVhN2EwODg2YTVjXkEyXkFqcGc@._V1_SX300.jpg"},{"Title":"Man 19 | of Steel: The Yahoo Live Fan Event","Year":"2013","imdbID":"tt3327446","Type":"movie","Poster":"N/A"},{"Title":"Little 20 | Man of Steel","Year":"2013","imdbID":"tt2557780","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BZjE1MjE4MTItZjJjMC00NDFjLWExNjAtMjViMzQzYTA4ZWUzXkEyXkFqcGdeQXVyMzYzNzc1NjY@._V1_SX300.jpg"},{"Title":"Man 21 | of Steel","Year":"1967","imdbID":"tt0263704","Type":"movie","Poster":"N/A"},{"Title":"Christopher 22 | Reeve: The Man of Steel","Year":"2004","imdbID":"tt2108487","Type":"movie","Poster":"N/A"},{"Title":"Man 23 | of Steel Song","Year":"2013","imdbID":"tt2754626","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMjAwNzE4MzU4OF5BMl5BanBnXkFtZTcwNzk1NTk1OQ@@._V1_SX300.jpg"},{"Title":"Chris 24 | Bacon: Man of Steel","Year":"2014","imdbID":"tt3560162","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BNzg2MzQ2MTI3NF5BMl5BanBnXkFtZTgwNjI1OTkyMTE@._V1_SX300.jpg"},{"Title":"Superman: 25 | Man of Steel","Year":"2019","imdbID":"tt10069974","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BODcxNDg4YjYtZjZmYS00NDAwLTg2NjctYTNiMjFiMDRlNmU5XkEyXkFqcGdeQXVyODc5OTgyODQ@._V1_SX300.jpg"},{"Title":"Stalin: 26 | Man of Steel","Year":"2003","imdbID":"tt6643772","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BNGI0NjJiM2QtNDlkNS00NWYxLWI0MjctNjQwZDA4YmY0ZmNkXkEyXkFqcGdeQXVyNzI4MDMyMTU@._V1_SX300.jpg"}],"totalResults":"30","Response":"True"}' 27 | headers: 28 | CF-Cache-Status: 29 | - MISS 30 | CF-RAY: 31 | - 91189aab3fbaede4-LHR 32 | Cache-Control: 33 | - public, max-age=86400 34 | Connection: 35 | - keep-alive 36 | Content-Type: 37 | - application/json; charset=utf-8 38 | Date: 39 | - Thu, 13 Feb 2025 23:30:05 GMT 40 | Server: 41 | - cloudflare 42 | Transfer-Encoding: 43 | - chunked 44 | access-control-allow-origin: 45 | - '*' 46 | content-length: 47 | - '1848' 48 | expires: 49 | - Fri, 14 Feb 2025 00:30:05 GMT 50 | last-modified: 51 | - Thu, 13 Feb 2025 23:30:05 GMT 52 | vary: 53 | - '*, Accept-Encoding' 54 | x-aspnet-version: 55 | - 4.0.30319 56 | x-powered-by: 57 | - ASP.NET 58 | status: 59 | code: 200 60 | message: OK 61 | version: 1 62 | -------------------------------------------------------------------------------- /tests/cassettes/omdb/test_search_wrong_title.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - Subliminal/2.2 13 | method: GET 14 | uri: https://www.omdbapi.com/?r=json&v=1&apikey=44d5b275&s=Meen+of+Stal&page=1 15 | response: 16 | body: 17 | string: '{"Response":"False","Error":"Movie not found!"}' 18 | headers: 19 | Accept-Ranges: 20 | - bytes 21 | CF-Cache-Status: 22 | - MISS 23 | CF-RAY: 24 | - 91189aa77f3decfe-LHR 25 | Cache-Control: 26 | - public, max-age=86400 27 | Connection: 28 | - keep-alive 29 | Content-Length: 30 | - '47' 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Date: 34 | - Thu, 13 Feb 2025 23:30:04 GMT 35 | Server: 36 | - cloudflare 37 | access-control-allow-origin: 38 | - '*' 39 | expires: 40 | - Fri, 14 Feb 2025 00:30:04 GMT 41 | last-modified: 42 | - Thu, 13 Feb 2025 23:30:04 GMT 43 | vary: 44 | - '*, Accept-Encoding' 45 | x-aspnet-version: 46 | - 4.0.30319 47 | x-powered-by: 48 | - ASP.NET 49 | status: 50 | code: 200 51 | message: OK 52 | version: 1 53 | -------------------------------------------------------------------------------- /tests/cassettes/omdb/test_search_year.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - Subliminal/2.2 13 | method: GET 14 | uri: https://www.omdbapi.com/?r=json&v=1&apikey=44d5b275&s=Man+of+Steel&page=1&y=2013 15 | response: 16 | body: 17 | string: '{"Search":[{"Title":"Man of Steel","Year":"2013","imdbID":"tt0770828","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMTk5ODk1NDkxMF5BMl5BanBnXkFtZTcwNTA5OTY0OQ@@._V1_SX300.jpg"},{"Title":"Man 18 | of Steel: The Yahoo Live Fan Event","Year":"2013","imdbID":"tt3327446","Type":"movie","Poster":"N/A"},{"Title":"Little 19 | Man of Steel","Year":"2013","imdbID":"tt2557780","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BZjE1MjE4MTItZjJjMC00NDFjLWExNjAtMjViMzQzYTA4ZWUzXkEyXkFqcGdeQXVyMzYzNzc1NjY@._V1_SX300.jpg"},{"Title":"Man 20 | of Steel Song","Year":"2013","imdbID":"tt2754626","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMjAwNzE4MzU4OF5BMl5BanBnXkFtZTcwNzk1NTk1OQ@@._V1_SX300.jpg"},{"Title":"Man 21 | of Steel ''Soldier of Steel''","Year":"2013","imdbID":"tt2911220","Type":"game","Poster":"N/A"},{"Title":"Man 22 | of Steel: Title Sequence","Year":"2013","imdbID":"tt3748934","Type":"movie","Poster":"N/A"},{"Title":"The 23 | Heroes Behind Man of Steel: Heroic Storytelling","Year":"2013","imdbID":"tt4158100","Type":"movie","Poster":"N/A"},{"Title":"The 24 | Military Might of Man of Steel","Year":"2013","imdbID":"tt4158128","Type":"movie","Poster":"N/A"},{"Title":"The 25 | Heroes Behind Man of Steel: Composing a Heroic Score","Year":"2013","imdbID":"tt4160502","Type":"movie","Poster":"N/A"},{"Title":"Journey 26 | of Discovery: Creating Man of Steel","Year":"2013","imdbID":"tt4160544","Type":"movie","Poster":"N/A"}],"totalResults":"17","Response":"True"}' 27 | headers: 28 | CF-Cache-Status: 29 | - MISS 30 | CF-RAY: 31 | - 91189aaf29d93dae-LHR 32 | Cache-Control: 33 | - public, max-age=86400 34 | Connection: 35 | - keep-alive 36 | Content-Type: 37 | - application/json; charset=utf-8 38 | Date: 39 | - Thu, 13 Feb 2025 23:30:05 GMT 40 | Server: 41 | - cloudflare 42 | Transfer-Encoding: 43 | - chunked 44 | access-control-allow-origin: 45 | - '*' 46 | content-length: 47 | - '1475' 48 | expires: 49 | - Fri, 14 Feb 2025 00:30:05 GMT 50 | last-modified: 51 | - Thu, 13 Feb 2025 23:30:05 GMT 52 | vary: 53 | - '*, Accept-Encoding' 54 | x-aspnet-version: 55 | - 4.0.30319 56 | x-powered-by: 57 | - ASP.NET 58 | status: 59 | code: 200 60 | message: OK 61 | version: 1 62 | -------------------------------------------------------------------------------- /tests/cassettes/opensubtitles/test_login_bad_password.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: ' 4 | 5 | 6 | 7 | LogIn 8 | 9 | 10 | 11 | 12 | 13 | python-subliminal 14 | 15 | 16 | 17 | 18 | 19 | lanimilbus 20 | 21 | 22 | 23 | 24 | 25 | eng 26 | 27 | 28 | 29 | 30 | 31 | VLSub 0.11.1 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ' 40 | headers: 41 | Accept-Encoding: 42 | - gzip 43 | Content-Length: 44 | - '341' 45 | Content-Type: 46 | - text/xml 47 | User-Agent: 48 | - VLSub 49 | method: POST 50 | uri: https://api.opensubtitles.org/xml-rpc 51 | response: 52 | body: 53 | string: tokensJGJwLTJlu-R6kBnvXxltUt5KVbstatus401 54 | Unauthorizedseconds0.237000 55 | headers: 56 | Access-Control-Allow-Credentials: 57 | - '' 58 | Access-Control-Allow-Headers: 59 | - Origin,X-Requested-With,Content-Type,Accept,DNT,Keep-Alive,User-Agent,If-Modified-Since,Cache-Control 60 | Access-Control-Allow-Methods: 61 | - GET, POST, OPTIONS 62 | Access-Control-Allow-Origin: 63 | - '*' 64 | Age: 65 | - '0' 66 | CF-RAY: 67 | - 91189a4afbafedfa-LHR 68 | Connection: 69 | - keep-alive 70 | Content-Type: 71 | - text/xml;charset=UTF-8 72 | Date: 73 | - Thu, 13 Feb 2025 23:29:49 GMT 74 | NEL: 75 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 76 | Report-To: 77 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=dz4ZPiKHrXdwvnKDCnnaDUzGaRxWrpFnMYmc23H%2BukuxeKozzBrVqYQEGCB%2BzXyzP5NkWfRETi2RxsuDiT4imNeWHNCN%2Bh7O3MyRxnfrUPz8EiKm89ZXS6tha%2Bfi8yGnSa0b6P2W6os%3D"}],"group":"cf-nel","max_age":604800}' 78 | Server: 79 | - cloudflare 80 | Set-Cookie: 81 | - PHPSESSID=sJGJwLTJlu-R6kBnvXxltUt5KVb; expires=Fri, 14-Feb-2025 05:29:49 GMT; 82 | Max-Age=21600; path=/; domain=.opensubtitles.org; HttpOnly 83 | Vary: 84 | - Accept-Encoding 85 | X-Cache-Backend: 86 | - lw5 87 | X-Compressed-Content-Length: 88 | - '220' 89 | X-Content-Encoding: 90 | - gzip 91 | X-HTTP-Version: 92 | - 1.0 93 | X-RateLimit-Remaining: 94 | - '23' 95 | X-Uncompressed-Content-Length: 96 | - '390' 97 | X-Var-Cache: 98 | - MISS 99 | X-Via: 100 | - lb2 101 | alt-svc: 102 | - h3=":443"; ma=86400 103 | cf-cache-status: 104 | - DYNAMIC 105 | content-length: 106 | - '390' 107 | server-timing: 108 | - cfL4;desc="?proto=TCP&rtt=32017&min_rtt=32000&rtt_var=12035&sent=4&recv=7&lost=0&retrans=0&sent_bytes=2367&recv_bytes=1124&delivery_rate=90102&cwnd=250&unsent_bytes=0&cid=7e3b75edbc4b5b5d&ts=353&x=0" 109 | status: 110 | code: 200 111 | message: OK 112 | version: 1 113 | -------------------------------------------------------------------------------- /tests/cassettes/opensubtitles/test_login_vip_bad_password.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: ' 4 | 5 | 6 | 7 | LogIn 8 | 9 | 10 | 11 | 12 | 13 | python-subliminal 14 | 15 | 16 | 17 | 18 | 19 | lanimilbus 20 | 21 | 22 | 23 | 24 | 25 | eng 26 | 27 | 28 | 29 | 30 | 31 | VLSub 0.11.1 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ' 40 | headers: 41 | Accept-Encoding: 42 | - gzip 43 | Content-Length: 44 | - '341' 45 | Content-Type: 46 | - text/xml 47 | User-Agent: 48 | - VLSub 49 | method: POST 50 | uri: https://vip-api.opensubtitles.org/xml-rpc 51 | response: 52 | body: 53 | string: tokeno9ZVn79MK,6AJAwKtPaBy8DH7Hfstatus401 54 | Unauthorizedseconds0.142000 55 | headers: 56 | Access-Control-Allow-Credentials: 57 | - '' 58 | Access-Control-Allow-Headers: 59 | - Origin,X-Requested-With,Content-Type,Accept,DNT,Keep-Alive,User-Agent,If-Modified-Since,Cache-Control 60 | Access-Control-Allow-Methods: 61 | - GET, POST, OPTIONS 62 | Access-Control-Allow-Origin: 63 | - '*' 64 | Age: 65 | - '0' 66 | CF-RAY: 67 | - 91189a4ffa915bea-LIS 68 | Connection: 69 | - keep-alive 70 | Content-Type: 71 | - text/xml;charset=UTF-8 72 | Date: 73 | - Thu, 13 Feb 2025 23:29:50 GMT 74 | NEL: 75 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 76 | Report-To: 77 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=HjnwfUaKWqP%2F%2B7Kp3pLrZ218qgtSiwQw9Ck10gG19Ts7ueMB6SiXyfaWIGFvfYWZhOVRZlG9gmPfH65V6HXVX2y5dfeBGWHUC1YN3YzovaAnyMeRRtOG8pSoOJlUXWCqsE3YS9V1PvEJgms2"}],"group":"cf-nel","max_age":604800}' 78 | Server: 79 | - cloudflare 80 | Set-Cookie: 81 | - PHPSESSID=o9ZVn79MK%2C6AJAwKtPaBy8DH7Hf; expires=Fri, 14-Feb-2025 05:29:50 82 | GMT; Max-Age=21600; path=/; domain=.opensubtitles.org; HttpOnly 83 | Vary: 84 | - Accept-Encoding 85 | X-Cache-Backend: 86 | - lw5 87 | X-Compressed-Content-Length: 88 | - '224' 89 | X-Content-Encoding: 90 | - gzip 91 | X-HTTP-Version: 92 | - 1.0 93 | X-RateLimit-Remaining: 94 | - '15' 95 | X-Uncompressed-Content-Length: 96 | - '390' 97 | X-Var-Cache: 98 | - MISS 99 | X-Via: 100 | - lb2 101 | alt-svc: 102 | - h3=":443"; ma=86400 103 | cf-cache-status: 104 | - DYNAMIC 105 | content-length: 106 | - '390' 107 | server-timing: 108 | - cfL4;desc="?proto=TCP&rtt=3026&min_rtt=2974&rtt_var=1219&sent=4&recv=7&lost=0&retrans=0&sent_bytes=2367&recv_bytes=1128&delivery_rate=854277&cwnd=243&unsent_bytes=0&cid=bdc09417e6b65129&ts=263&x=0" 109 | status: 110 | code: 200 111 | message: OK 112 | version: 1 113 | -------------------------------------------------------------------------------- /tests/cassettes/opensubtitles/test_login_vip_login.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: ' 4 | 5 | 6 | 7 | LogIn 8 | 9 | 10 | 11 | 12 | 13 | python-subliminal 14 | 15 | 16 | 17 | 18 | 19 | subliminal 20 | 21 | 22 | 23 | 24 | 25 | eng 26 | 27 | 28 | 29 | 30 | 31 | VLSub 0.11.1 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ' 40 | headers: 41 | Accept-Encoding: 42 | - gzip 43 | Content-Length: 44 | - '341' 45 | Content-Type: 46 | - text/xml 47 | User-Agent: 48 | - VLSub 49 | method: POST 50 | uri: https://vip-api.opensubtitles.org/xml-rpc 51 | response: 52 | body: 53 | string: tokenyg5wAk8inEe6,ZlxiQGrNZztPhbstatus401 54 | Unauthorizedseconds0.221000 55 | headers: 56 | Access-Control-Allow-Credentials: 57 | - '' 58 | Access-Control-Allow-Headers: 59 | - Origin,X-Requested-With,Content-Type,Accept,DNT,Keep-Alive,User-Agent,If-Modified-Since,Cache-Control 60 | Access-Control-Allow-Methods: 61 | - GET, POST, OPTIONS 62 | Access-Control-Allow-Origin: 63 | - '*' 64 | Age: 65 | - '0' 66 | CF-RAY: 67 | - 91189a4d5e635bdc-LIS 68 | Connection: 69 | - keep-alive 70 | Content-Type: 71 | - text/xml;charset=UTF-8 72 | Date: 73 | - Thu, 13 Feb 2025 23:29:49 GMT 74 | NEL: 75 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 76 | Report-To: 77 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=ZRvKY%2FH%2BnNdXQxsNRuPkjN5H94ATR7NJBJWn6jsFIClVZseuVMPNFegiZ%2F9aiAmxEiWtf5eZY8YDHSvaCBjbjpK3ZbEQ6dq0rPAPP4E36tSvfiiqxKpcRnua8YDeYFmuXhg6kTUy4wdxStT8"}],"group":"cf-nel","max_age":604800}' 78 | Server: 79 | - cloudflare 80 | Set-Cookie: 81 | - PHPSESSID=yg5wAk8inEe6%2CZlxiQGrNZztPhb; expires=Fri, 14-Feb-2025 05:29:49 82 | GMT; Max-Age=21600; path=/; domain=.opensubtitles.org; HttpOnly 83 | Vary: 84 | - Accept-Encoding 85 | X-Cache-Backend: 86 | - lw1 87 | X-Compressed-Content-Length: 88 | - '221' 89 | X-Content-Encoding: 90 | - gzip 91 | X-HTTP-Version: 92 | - 1.0 93 | X-RateLimit-Remaining: 94 | - '19' 95 | X-Uncompressed-Content-Length: 96 | - '390' 97 | X-Var-Cache: 98 | - MISS 99 | X-Via: 100 | - lb2 101 | alt-svc: 102 | - h3=":443"; ma=86400 103 | cf-cache-status: 104 | - DYNAMIC 105 | content-length: 106 | - '390' 107 | server-timing: 108 | - cfL4;desc="?proto=TCP&rtt=3000&min_rtt=2951&rtt_var=1142&sent=4&recv=7&lost=0&retrans=0&sent_bytes=2367&recv_bytes=1128&delivery_rate=981362&cwnd=211&unsent_bytes=0&cid=971fdef7955b92a2&ts=343&x=0" 109 | status: 110 | code: 200 111 | message: OK 112 | version: 1 113 | -------------------------------------------------------------------------------- /tests/cassettes/opensubtitlescom/test_login.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"username": "python-subliminal-test", "password": "subliminal"}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Api-Key: 10 | - mij33pjc3kOlup1qOKxnWWxvle2kFbMH 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '64' 15 | Content-Type: 16 | - application/json 17 | User-Agent: 18 | - Subliminal v2.2 19 | method: POST 20 | uri: https://api.opensubtitles.com/api/v1/login 21 | response: 22 | body: 23 | string: '{"user":{"allowed_translations":1,"allowed_downloads":20,"level":"Sub 24 | leecher","user_id":699022,"ext_installed":false,"vip":false},"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJCRDY2Qnl0TmRyZDNhMWl5S3pLaHVGeWRaV2g2UTYwbyIsImV4cCI6MTczOTU3NTEzNX0.pys8y8GBYqkfRxzOMaG6nzuKlXlXEjObvMVvY3nDOYI","status":200,"base_url":"api.opensubtitles.com"}' 25 | headers: 26 | Access-Control-Allow-Headers: 27 | - Origin, Authorization, Accept, Api-Key, Content-Type, X-User-Agent 28 | Access-Control-Allow-Methods: 29 | - GET, HEAD, POST, OPTIONS 30 | Access-Control-Allow-Origin: 31 | - '*' 32 | Age: 33 | - '0' 34 | CF-RAY: 35 | - 91189a6588355bd6-LIS 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate, s-maxage=600 38 | Connection: 39 | - keep-alive 40 | Content-Type: 41 | - application/json; charset=utf-8 42 | Date: 43 | - Thu, 13 Feb 2025 23:29:53 GMT 44 | NEL: 45 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 46 | RateLimit-Limit: 47 | - '1' 48 | RateLimit-Remaining: 49 | - '0' 50 | RateLimit-Reset: 51 | - '1' 52 | Report-To: 53 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=Ir5CFcT7VCWPCeexI%2B6SVmuGMWkVU7E06DLbqwX62Cv%2FbSt9afXeZCjgTrnQwDiNOqhjZxLoU3c%2F682zihkeazsBS2JfdBFdL7N0GD5DkFgFEGn2av%2F0uAZe9e%2BZkYZYv8cUp0OTYZM%3D"}],"group":"cf-nel","max_age":604800}' 54 | Server: 55 | - cloudflare 56 | Strict-Transport-Security: 57 | - max-age=15552000; includeSubDomains; preload 58 | Transfer-Encoding: 59 | - chunked 60 | Via: 61 | - kong/3.3.0 62 | X-Cache-Backend: 63 | - rb12 64 | X-Content-Type-Options: 65 | - nosniff 66 | X-Download-Options: 67 | - noopen 68 | X-Frame-Options: 69 | - SAMEORIGIN 70 | X-Kong-Proxy-Latency: 71 | - '0' 72 | X-Kong-Upstream-Latency: 73 | - '280' 74 | X-RateLimit-Limit-Second: 75 | - '1' 76 | X-RateLimit-Remaining-Second: 77 | - '0' 78 | X-Request-Id: 79 | - 04abe85d-9c68-48a3-968a-51cc08b7c0ff 80 | X-Runtime: 81 | - 0.278007 82 | X-Var-Cache: 83 | - MISS 84 | X-Via: 85 | - lb1 86 | X-Vip-Consumer: 87 | - 'false' 88 | X-Vip-User: 89 | - 'false' 90 | X-XSS-Protection: 91 | - 1; mode=block 92 | alt-svc: 93 | - h3=":443"; ma=86400 94 | cf-cache-status: 95 | - DYNAMIC 96 | content-length: 97 | - '350' 98 | server-timing: 99 | - cfL4;desc="?proto=TCP&rtt=3967&min_rtt=3682&rtt_var=1584&sent=5&recv=7&lost=0&retrans=0&sent_bytes=2855&recv_bytes=958&delivery_rate=786529&cwnd=226&unsent_bytes=0&cid=a072059032243027&ts=360&x=0" 100 | status: 101 | code: 200 102 | message: OK 103 | version: 1 104 | -------------------------------------------------------------------------------- /tests/cassettes/opensubtitlescom/test_login_bad_password.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"username": "python-subliminal-test", "password": "lanimilbus"}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Api-Key: 10 | - mij33pjc3kOlup1qOKxnWWxvle2kFbMH 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '64' 15 | Content-Type: 16 | - application/json 17 | User-Agent: 18 | - Subliminal v2.2 19 | method: POST 20 | uri: https://api.opensubtitles.com/api/v1/login 21 | response: 22 | body: 23 | string: '{"message":"Error, invalid username/password failed:1 remaining:9 ","status":401}' 24 | headers: 25 | Access-Control-Allow-Headers: 26 | - Origin, Authorization, Accept, Api-Key, Content-Type, X-User-Agent 27 | Access-Control-Allow-Methods: 28 | - GET, HEAD, POST, OPTIONS 29 | Access-Control-Allow-Origin: 30 | - '*' 31 | Age: 32 | - '0' 33 | CF-RAY: 34 | - 91189a6e49465bda-LIS 35 | Cache-Control: 36 | - max-age=0, private, must-revalidate, s-maxage=600 37 | Connection: 38 | - keep-alive 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Date: 42 | - Thu, 13 Feb 2025 23:29:55 GMT 43 | NEL: 44 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 45 | RateLimit-Limit: 46 | - '1' 47 | RateLimit-Remaining: 48 | - '0' 49 | RateLimit-Reset: 50 | - '1' 51 | Report-To: 52 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=GhxKaul8wdNx%2BiX34dOhnNB1CkDlHgttDsBl9nA5832BQ1ix2PLbJX8N4GBTfyDQWP2k3j4HQYMBQ226lPnEhaitX%2FIQodec0h9T12bKokAROyhr5NDyYoYvBF6yFE28bmwutIEhCgM%3D"}],"group":"cf-nel","max_age":604800}' 53 | Server: 54 | - cloudflare 55 | Strict-Transport-Security: 56 | - max-age=15552000; includeSubDomains; preload 57 | Transfer-Encoding: 58 | - chunked 59 | Via: 60 | - kong/3.3.0 61 | X-Cache-Backend: 62 | - rb1-temp 63 | X-Content-Type-Options: 64 | - nosniff 65 | X-Download-Options: 66 | - noopen 67 | X-Frame-Options: 68 | - SAMEORIGIN 69 | X-Kong-Proxy-Latency: 70 | - '0' 71 | X-Kong-Upstream-Latency: 72 | - '281' 73 | X-RateLimit-Limit-Second: 74 | - '1' 75 | X-RateLimit-Remaining-Second: 76 | - '0' 77 | X-Request-Id: 78 | - 54dfff14-4d74-40f4-8837-9acddf4dd781 79 | X-Runtime: 80 | - 0.279274 81 | X-Var-Cache: 82 | - MISS 83 | X-Via: 84 | - lb1 85 | X-Vip-Consumer: 86 | - 'false' 87 | X-XSS-Protection: 88 | - 1; mode=block 89 | alt-svc: 90 | - h3=":443"; ma=86400 91 | cf-cache-status: 92 | - DYNAMIC 93 | server-timing: 94 | - cfL4;desc="?proto=TCP&rtt=2801&min_rtt=2740&rtt_var=1071&sent=4&recv=7&lost=0&retrans=0&sent_bytes=2855&recv_bytes=958&delivery_rate=1056934&cwnd=250&unsent_bytes=0&cid=cb6559cb4a3a1ad4&ts=352&x=0" 95 | status: 96 | code: 401 97 | message: Unauthorized 98 | version: 1 99 | -------------------------------------------------------------------------------- /tests/cassettes/opensubtitlescom/test_logout.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"username": "python-subliminal-test", "password": "subliminal"}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Api-Key: 10 | - mij33pjc3kOlup1qOKxnWWxvle2kFbMH 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '64' 15 | Content-Type: 16 | - application/json 17 | User-Agent: 18 | - Subliminal v2.2 19 | method: POST 20 | uri: https://api.opensubtitles.com/api/v1/login 21 | response: 22 | body: 23 | string: '{"user":{"allowed_translations":1,"allowed_downloads":20,"level":"Sub 24 | leecher","user_id":699022,"ext_installed":false,"vip":false},"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJCRDY2Qnl0TmRyZDNhMWl5S3pLaHVGeWRaV2g2UTYwbyIsImV4cCI6MTczOTU3NTEzNX0.pys8y8GBYqkfRxzOMaG6nzuKlXlXEjObvMVvY3nDOYI","status":200,"base_url":"api.opensubtitles.com"}' 25 | headers: 26 | Access-Control-Allow-Headers: 27 | - Origin, Authorization, Accept, Api-Key, Content-Type, X-User-Agent 28 | Access-Control-Allow-Methods: 29 | - GET, HEAD, POST, OPTIONS 30 | Access-Control-Allow-Origin: 31 | - '*' 32 | Age: 33 | - '0' 34 | CF-RAY: 35 | - 91189a76dc115be0-LIS 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate, s-maxage=600 38 | Connection: 39 | - keep-alive 40 | Content-Type: 41 | - application/json; charset=utf-8 42 | Date: 43 | - Thu, 13 Feb 2025 23:29:56 GMT 44 | NEL: 45 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 46 | RateLimit-Limit: 47 | - '1' 48 | RateLimit-Remaining: 49 | - '0' 50 | RateLimit-Reset: 51 | - '1' 52 | Report-To: 53 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=%2Bx3jOY1VnjVf4uyMAHn4hlgszvarf4ycfS%2FU0ZZQOEzMXrlM3tx6WcCQ3hy%2B5z7Oq4Ro1mDfnPiJndqgeL5GE4QUUDLVv5TusEgl7sMSi0CIlT%2FVUId%2FuZiokI3PnuqmC%2F2eHeeaSgE%3D"}],"group":"cf-nel","max_age":604800}' 54 | Server: 55 | - cloudflare 56 | Strict-Transport-Security: 57 | - max-age=15552000; includeSubDomains; preload 58 | Transfer-Encoding: 59 | - chunked 60 | Via: 61 | - kong/3.3.0 62 | X-Cache-Backend: 63 | - rb1 64 | X-Content-Type-Options: 65 | - nosniff 66 | X-Download-Options: 67 | - noopen 68 | X-Frame-Options: 69 | - SAMEORIGIN 70 | X-Kong-Proxy-Latency: 71 | - '0' 72 | X-Kong-Upstream-Latency: 73 | - '320' 74 | X-RateLimit-Limit-Second: 75 | - '1' 76 | X-RateLimit-Remaining-Second: 77 | - '0' 78 | X-Request-Id: 79 | - abf9228c-8f6b-44ae-b6e6-1b64d5ff5d06 80 | X-Runtime: 81 | - 0.317666 82 | X-StackifyID: 83 | - V1|d4e62c5a-097c-477f-9cf3-c8f7cba884ac|C98184|CD5 84 | X-Var-Cache: 85 | - MISS 86 | X-Via: 87 | - lb1 88 | X-Vip-Consumer: 89 | - 'false' 90 | X-Vip-User: 91 | - 'false' 92 | X-XSS-Protection: 93 | - 1; mode=block 94 | alt-svc: 95 | - h3=":443"; ma=86400 96 | cf-cache-status: 97 | - DYNAMIC 98 | content-length: 99 | - '350' 100 | server-timing: 101 | - cfL4;desc="?proto=TCP&rtt=2089&min_rtt=1979&rtt_var=963&sent=5&recv=7&lost=0&retrans=0&sent_bytes=2855&recv_bytes=958&delivery_rate=1011526&cwnd=247&unsent_bytes=0&cid=e32134f724cbb7c5&ts=391&x=0" 102 | status: 103 | code: 200 104 | message: OK 105 | version: 1 106 | -------------------------------------------------------------------------------- /tests/cassettes/subtitulamos/test_list_subtitles_not_exist_series.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | Referer: 12 | - https://www.subtitulamos.tv 13 | User-Agent: 14 | - Subliminal/2.2 15 | method: GET 16 | uri: https://www.subtitulamos.tv/search/query?q=Fake+Show+%281914%29 17 | response: 18 | body: 19 | string: '[]' 20 | headers: 21 | CF-RAY: 22 | - 91189a59cdfd5bec-LIS 23 | Cache-Control: 24 | - no-store, no-cache, must-revalidate 25 | Connection: 26 | - keep-alive 27 | Content-Type: 28 | - text/html; charset=UTF-8 29 | Date: 30 | - Thu, 13 Feb 2025 23:29:51 GMT 31 | Expires: 32 | - Thu, 19 Nov 1981 08:52:00 GMT 33 | NEL: 34 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 35 | Pragma: 36 | - no-cache 37 | Report-To: 38 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=M5sJM2sZfaMoCw5BIHFt6Ns07bj2Zs8XK40Pscv%2B2hLzkaO0XpwzBKHbO6kFx1WGszK9TA6q8OyizZZrYkzY0PZHXBHBXsvkxgVQa2U5woYTwrnhWMb9deefXhup5hY1oTNkEqMH"}],"group":"cf-nel","max_age":604800}' 39 | Server: 40 | - cloudflare 41 | Set-Cookie: 42 | - PHPSESSID=48aefenij92rg03n0fqokr68dt; path=/; HttpOnly 43 | Transfer-Encoding: 44 | - chunked 45 | X-Powered-By: 46 | - PHP/7.4.14 47 | alt-svc: 48 | - h3=":443"; ma=86400 49 | cf-cache-status: 50 | - DYNAMIC 51 | content-length: 52 | - '2' 53 | server-timing: 54 | - cfL4;desc="?proto=TCP&rtt=3908&min_rtt=3524&rtt_var=1596&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2849&recv_bytes=834&delivery_rate=821793&cwnd=241&unsent_bytes=0&cid=0b1c35fccac5daa9&ts=284&x=0" 55 | status: 56 | code: 200 57 | message: OK 58 | - request: 59 | body: null 60 | headers: 61 | Accept: 62 | - '*/*' 63 | Accept-Encoding: 64 | - gzip, deflate 65 | Connection: 66 | - keep-alive 67 | Cookie: 68 | - PHPSESSID=48aefenij92rg03n0fqokr68dt 69 | Referer: 70 | - https://www.subtitulamos.tv 71 | User-Agent: 72 | - Subliminal/2.2 73 | method: GET 74 | uri: https://www.subtitulamos.tv/search/query?q=Fake+Show 75 | response: 76 | body: 77 | string: '[]' 78 | headers: 79 | CF-RAY: 80 | - 91189a5ba9aa5bd5-LIS 81 | Cache-Control: 82 | - no-store, no-cache, must-revalidate 83 | Connection: 84 | - keep-alive 85 | Content-Type: 86 | - text/html; charset=UTF-8 87 | Date: 88 | - Thu, 13 Feb 2025 23:29:52 GMT 89 | Expires: 90 | - Thu, 19 Nov 1981 08:52:00 GMT 91 | NEL: 92 | - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' 93 | Pragma: 94 | - no-cache 95 | Report-To: 96 | - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=xfSsUqS2uKAT4hMZbJU5qVwENE7jK%2FvWcn5kBuNh1wnMwTp%2Fsn5vjnF43ilhtwQzF%2BEgIcuwy8E1iHD%2FZYw9MU36buGLzKhweh33jDY86mb%2BhiJJrAjAq4Hwif1GbfhFklQcvYV3"}],"group":"cf-nel","max_age":604800}' 97 | Server: 98 | - cloudflare 99 | Transfer-Encoding: 100 | - chunked 101 | X-Powered-By: 102 | - PHP/7.4.14 103 | alt-svc: 104 | - h3=":443"; ma=86400 105 | cf-cache-status: 106 | - DYNAMIC 107 | content-length: 108 | - '2' 109 | server-timing: 110 | - cfL4;desc="?proto=TCP&rtt=3024&min_rtt=2988&rtt_var=1193&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2849&recv_bytes=869&delivery_rate=882657&cwnd=240&unsent_bytes=0&cid=ddb79315885ce615&ts=251&x=0" 111 | status: 112 | code: 200 113 | message: OK 114 | version: 1 115 | -------------------------------------------------------------------------------- /tests/cassettes/tmdb/test_get_movie_id_failed.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Type: 14 | - application/json 15 | User-Agent: 16 | - Subliminal/2.2 17 | method: GET 18 | uri: https://api.themoviedb.org/3/search/movie?api_key=3dac925d5d494853ea6ef9161011fbb3&query=Meen+of+Stal&language=en-US&page=1 19 | response: 20 | body: 21 | string: '{"page":1,"results":[],"total_pages":1,"total_results":0}' 22 | headers: 23 | Age: 24 | - '1' 25 | Alt-Svc: 26 | - h3=":443"; ma=86400 27 | Cache-Control: 28 | - public, max-age=25714 29 | Connection: 30 | - keep-alive 31 | Content-Type: 32 | - application/json;charset=utf-8 33 | Date: 34 | - Thu, 13 Feb 2025 23:30:15 GMT 35 | ETag: 36 | - W/"0e23279b004381f72a34159d5c7dfd1f" 37 | Server: 38 | - openresty 39 | Transfer-Encoding: 40 | - chunked 41 | Vary: 42 | - Accept-Encoding,accept-encoding 43 | - Origin 44 | Via: 45 | - 1.1 f13924e40949c7e0a5bd0c7e333695f2.cloudfront.net (CloudFront) 46 | X-Amz-Cf-Id: 47 | - cAgL6kcmAM5C6Ve9fwVgDlf8vSdF6fG4rqeBVJgxR-oRf51e7r7oOQ== 48 | X-Amz-Cf-Pop: 49 | - LIS50-P1 50 | X-Cache: 51 | - Hit from cloudfront 52 | content-length: 53 | - '57' 54 | x-memc: 55 | - MISS, STORE 56 | x-memc-age: 57 | - '0' 58 | x-memc-expires: 59 | - '25714' 60 | x-memc-key: 61 | - 48dfe98b26252d07dc935cf15aba5cd0 62 | status: 63 | code: 200 64 | message: OK 65 | version: 1 66 | -------------------------------------------------------------------------------- /tests/cassettes/tmdb/test_get_series_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Type: 14 | - application/json 15 | User-Agent: 16 | - Subliminal/2.2 17 | method: GET 18 | uri: https://api.themoviedb.org/3/search/tv?api_key=3dac925d5d494853ea6ef9161011fbb3&query=The+Big+Bang+Theory&language=en-US&page=1 19 | response: 20 | body: 21 | string: '{"page":1,"results":[{"adult":false,"backdrop_path":"/sccDflItNho4OiHkzpiDxB2fUFw.jpg","genre_ids":[35],"id":1418,"origin_country":["US"],"original_language":"en","original_name":"The 22 | Big Bang Theory","overview":"Physicists Leonard and Sheldon find their nerd-centric 23 | social circle with pals Howard and Raj expanding when aspiring actress Penny 24 | moves in next door.","popularity":224.564,"poster_path":"/ooBGRQBdbGzBxAVfExiO8r7kloA.jpg","first_air_date":"2007-09-24","name":"The 25 | Big Bang Theory","vote_average":7.9,"vote_count":11492},{"adult":false,"backdrop_path":null,"genre_ids":[],"id":280440,"origin_country":["US"],"original_language":"en","original_name":"The 26 | Big Bang Theory","overview":"The big bang theory behind the scenes","popularity":1.129,"poster_path":null,"first_air_date":"","name":"The 27 | Big Bang Theory","vote_average":0.0,"vote_count":0}],"total_pages":1,"total_results":2}' 28 | headers: 29 | Age: 30 | - '1' 31 | Alt-Svc: 32 | - h3=":443"; ma=86400 33 | Cache-Control: 34 | - public, max-age=15399 35 | Connection: 36 | - keep-alive 37 | Content-Type: 38 | - application/json;charset=utf-8 39 | Date: 40 | - Thu, 13 Feb 2025 23:30:15 GMT 41 | ETag: 42 | - W/"5e486450f4bedab767a6c99839dbbbb7" 43 | Server: 44 | - openresty 45 | Transfer-Encoding: 46 | - chunked 47 | Vary: 48 | - Accept-Encoding,accept-encoding 49 | - Origin 50 | Via: 51 | - 1.1 a1a9ff59f73590e3953b5ce6edfc8aa8.cloudfront.net (CloudFront) 52 | X-Amz-Cf-Id: 53 | - 4rAl_42g1eLnxugcrO2jaYrWGNciOFmGAmGsj8dcmbTmcrDcKm3x2g== 54 | X-Amz-Cf-Pop: 55 | - LIS50-P1 56 | X-Cache: 57 | - Hit from cloudfront 58 | content-length: 59 | - '892' 60 | x-memc: 61 | - HIT 62 | x-memc-age: 63 | - '7556' 64 | x-memc-expires: 65 | - '15399' 66 | x-memc-key: 67 | - c709f17d5f15d43bc80cad8e740ddd85 68 | status: 69 | code: 200 70 | message: OK 71 | version: 1 72 | -------------------------------------------------------------------------------- /tests/cassettes/tmdb/test_get_series_wrong_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Type: 14 | - application/json 15 | User-Agent: 16 | - Subliminal/2.2 17 | method: GET 18 | uri: https://api.themoviedb.org/3/search/tv?api_key=3dac925d5d494853ea6ef9161011fbb3&query=The+Bing+Bag+Theory&language=en-US&page=1 19 | response: 20 | body: 21 | string: '{"page":1,"results":[],"total_pages":1,"total_results":0}' 22 | headers: 23 | Alt-Svc: 24 | - h3=":443"; ma=86400 25 | Cache-Control: 26 | - public, max-age=23602 27 | Connection: 28 | - keep-alive 29 | Content-Type: 30 | - application/json;charset=utf-8 31 | Date: 32 | - Thu, 13 Feb 2025 23:30:15 GMT 33 | ETag: 34 | - W/"0e23279b004381f72a34159d5c7dfd1f" 35 | Server: 36 | - openresty 37 | Transfer-Encoding: 38 | - chunked 39 | Vary: 40 | - Accept-Encoding,accept-encoding 41 | - Origin 42 | Via: 43 | - 1.1 d8d835cce198f21656f532aa7cb25fbe.cloudfront.net (CloudFront) 44 | X-Amz-Cf-Id: 45 | - 5dTQQ8vN8Txk6krsF84La_r06Zljt6AFqVB4GyPgCNMU4Rfve_Ow-A== 46 | X-Amz-Cf-Pop: 47 | - LIS50-P1 48 | X-Cache: 49 | - Hit from cloudfront 50 | content-length: 51 | - '57' 52 | x-memc: 53 | - MISS, STORE 54 | x-memc-age: 55 | - '0' 56 | x-memc-expires: 57 | - '23602' 58 | x-memc-key: 59 | - 29c3efb0f62926bde22e547ff2b93c92 60 | status: 61 | code: 200 62 | message: OK 63 | version: 1 64 | -------------------------------------------------------------------------------- /tests/cassettes/tmdb/test_search_movie_wrong_title.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Type: 14 | - application/json 15 | User-Agent: 16 | - Subliminal/2.2 17 | method: GET 18 | uri: https://api.themoviedb.org/3/search/movie?api_key=3dac925d5d494853ea6ef9161011fbb3&query=Meen+of+Stal&language=en-US&page=1 19 | response: 20 | body: 21 | string: '{"page":1,"results":[],"total_pages":1,"total_results":0}' 22 | headers: 23 | Alt-Svc: 24 | - h3=":443"; ma=86400 25 | Cache-Control: 26 | - public, max-age=25714 27 | Connection: 28 | - keep-alive 29 | Content-Type: 30 | - application/json;charset=utf-8 31 | Date: 32 | - Thu, 13 Feb 2025 23:30:15 GMT 33 | ETag: 34 | - W/"0e23279b004381f72a34159d5c7dfd1f" 35 | Server: 36 | - openresty 37 | Transfer-Encoding: 38 | - chunked 39 | Vary: 40 | - Accept-Encoding,accept-encoding 41 | - Origin 42 | Via: 43 | - 1.1 5d25c31f47a198dbf50acf297a389a00.cloudfront.net (CloudFront) 44 | X-Amz-Cf-Id: 45 | - pFsgrNgwEgA4ifrwN6quvLFXcC6b0CR6u0eVlkuPiHSpAH6FvJiSzw== 46 | X-Amz-Cf-Pop: 47 | - LIS50-P1 48 | X-Cache: 49 | - Miss from cloudfront 50 | content-length: 51 | - '57' 52 | x-memc: 53 | - MISS, STORE 54 | x-memc-age: 55 | - '0' 56 | x-memc-expires: 57 | - '25714' 58 | x-memc-key: 59 | - 48dfe98b26252d07dc935cf15aba5cd0 60 | status: 61 | code: 200 62 | message: OK 63 | version: 1 64 | -------------------------------------------------------------------------------- /tests/cassettes/tmdb/test_search_movie_year.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Type: 14 | - application/json 15 | User-Agent: 16 | - Subliminal/2.2 17 | method: GET 18 | uri: https://api.themoviedb.org/3/search/movie?api_key=3dac925d5d494853ea6ef9161011fbb3&query=Man+of+Steel&language=en-US&page=1&year=2013 19 | response: 20 | body: 21 | string: '{"page":1,"results":[{"adult":false,"backdrop_path":"/69EFgWWPFWbRNHmQgYdSnyJ94Ge.jpg","genre_ids":[28,12,878],"id":49521,"original_language":"en","original_title":"Man 22 | of Steel","overview":"A young boy learns that he has extraordinary powers 23 | and is not of this earth. As a young man, he journeys to discover where he 24 | came from and what he was sent here to do. But the hero in him must emerge 25 | if he is to save the world from annihilation and become the symbol of hope 26 | for all mankind.","popularity":50.299,"poster_path":"/dksTL9NXc3GqPBRHYHcy1aIwjS.jpg","release_date":"2013-06-12","title":"Man 27 | of Steel","video":false,"vote_average":6.629,"vote_count":15242}],"total_pages":1,"total_results":1}' 28 | headers: 29 | Alt-Svc: 30 | - h3=":443"; ma=86400 31 | Cache-Control: 32 | - public, max-age=4350 33 | Connection: 34 | - keep-alive 35 | Content-Type: 36 | - application/json;charset=utf-8 37 | Date: 38 | - Thu, 13 Feb 2025 23:30:15 GMT 39 | ETag: 40 | - W/"7286b69d9653a43b9ab2fc3bc87817e4" 41 | Server: 42 | - openresty 43 | Transfer-Encoding: 44 | - chunked 45 | Vary: 46 | - Accept-Encoding,accept-encoding 47 | - Origin 48 | Via: 49 | - 1.1 27a35654821ee52d8aa69c940ad5de7e.cloudfront.net (CloudFront) 50 | X-Amz-Cf-Id: 51 | - JOgHgmvsL5qz6MP828RqriIrZjduLvtAAbn-m3ri-sTdMWgA9WwhIg== 52 | X-Amz-Cf-Pop: 53 | - LIS50-P1 54 | X-Cache: 55 | - Miss from cloudfront 56 | content-length: 57 | - '695' 58 | x-memc: 59 | - HIT 60 | x-memc-age: 61 | - '20687' 62 | x-memc-expires: 63 | - '4350' 64 | x-memc-key: 65 | - 3fed4b99e74a2821dfdba4d5bacbc98a 66 | status: 67 | code: 200 68 | message: OK 69 | version: 1 70 | -------------------------------------------------------------------------------- /tests/cassettes/tmdb/test_search_series.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Type: 14 | - application/json 15 | User-Agent: 16 | - Subliminal/2.2 17 | method: GET 18 | uri: https://api.themoviedb.org/3/search/tv?api_key=3dac925d5d494853ea6ef9161011fbb3&query=The+Big+Bang+Theory&language=en-US&page=1 19 | response: 20 | body: 21 | string: '{"page":1,"results":[{"adult":false,"backdrop_path":"/sccDflItNho4OiHkzpiDxB2fUFw.jpg","genre_ids":[35],"id":1418,"origin_country":["US"],"original_language":"en","original_name":"The 22 | Big Bang Theory","overview":"Physicists Leonard and Sheldon find their nerd-centric 23 | social circle with pals Howard and Raj expanding when aspiring actress Penny 24 | moves in next door.","popularity":224.564,"poster_path":"/ooBGRQBdbGzBxAVfExiO8r7kloA.jpg","first_air_date":"2007-09-24","name":"The 25 | Big Bang Theory","vote_average":7.9,"vote_count":11492},{"adult":false,"backdrop_path":null,"genre_ids":[],"id":280440,"origin_country":["US"],"original_language":"en","original_name":"The 26 | Big Bang Theory","overview":"The big bang theory behind the scenes","popularity":1.129,"poster_path":null,"first_air_date":"","name":"The 27 | Big Bang Theory","vote_average":0.0,"vote_count":0}],"total_pages":1,"total_results":2}' 28 | headers: 29 | Alt-Svc: 30 | - h3=":443"; ma=86400 31 | Cache-Control: 32 | - public, max-age=15399 33 | Connection: 34 | - keep-alive 35 | Content-Type: 36 | - application/json;charset=utf-8 37 | Date: 38 | - Thu, 13 Feb 2025 23:30:15 GMT 39 | ETag: 40 | - W/"5e486450f4bedab767a6c99839dbbbb7" 41 | Server: 42 | - openresty 43 | Transfer-Encoding: 44 | - chunked 45 | Vary: 46 | - Accept-Encoding,accept-encoding 47 | - Origin 48 | Via: 49 | - 1.1 f13924e40949c7e0a5bd0c7e333695f2.cloudfront.net (CloudFront) 50 | X-Amz-Cf-Id: 51 | - 0LniqRlxXqy1Q3KZ4JJDEpGiUdsfDRAsyNc9O6eoMqERsEHyGYTJFQ== 52 | X-Amz-Cf-Pop: 53 | - LIS50-P1 54 | X-Cache: 55 | - Miss from cloudfront 56 | content-length: 57 | - '892' 58 | x-memc: 59 | - HIT 60 | x-memc-age: 61 | - '7556' 62 | x-memc-expires: 63 | - '15399' 64 | x-memc-key: 65 | - c709f17d5f15d43bc80cad8e740ddd85 66 | status: 67 | code: 200 68 | message: OK 69 | version: 1 70 | -------------------------------------------------------------------------------- /tests/cassettes/tmdb/test_search_series_wrong_name.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Type: 14 | - application/json 15 | User-Agent: 16 | - Subliminal/2.2 17 | method: GET 18 | uri: https://api.themoviedb.org/3/search/tv?api_key=3dac925d5d494853ea6ef9161011fbb3&query=The+Bing+Bag+Theory&language=en-US&page=1 19 | response: 20 | body: 21 | string: '{"page":1,"results":[],"total_pages":1,"total_results":0}' 22 | headers: 23 | Alt-Svc: 24 | - h3=":443"; ma=86400 25 | Cache-Control: 26 | - public, max-age=23602 27 | Connection: 28 | - keep-alive 29 | Content-Type: 30 | - application/json;charset=utf-8 31 | Date: 32 | - Thu, 13 Feb 2025 23:30:15 GMT 33 | ETag: 34 | - W/"0e23279b004381f72a34159d5c7dfd1f" 35 | Server: 36 | - openresty 37 | Transfer-Encoding: 38 | - chunked 39 | Vary: 40 | - Accept-Encoding,accept-encoding 41 | - Origin 42 | Via: 43 | - 1.1 d8d835cce198f21656f532aa7cb25fbe.cloudfront.net (CloudFront) 44 | X-Amz-Cf-Id: 45 | - Pc4KB-64mOSKNJ33RFfciAp0mzassxg7i-wRybXfh_T8tKUq7vXlfA== 46 | X-Amz-Cf-Pop: 47 | - LIS50-P1 48 | X-Cache: 49 | - Miss from cloudfront 50 | content-length: 51 | - '57' 52 | x-memc: 53 | - MISS, STORE 54 | x-memc-age: 55 | - '0' 56 | x-memc-expires: 57 | - '23602' 58 | x-memc-key: 59 | - 29c3efb0f62926bde22e547ff2b93c92 60 | status: 61 | code: 200 62 | message: OK 63 | version: 1 64 | -------------------------------------------------------------------------------- /tests/cassettes/tvdb/test_get_series_actors_wrong_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"apikey": "5EC930FB90DA1ADA", "username": null, "password": null}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '66' 15 | Content-Type: 16 | - application/json 17 | User-Agent: 18 | - Subliminal/2.2 19 | method: POST 20 | uri: https://api.thetvdb.com/login 21 | response: 22 | body: 23 | string: '{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMTcsImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDE3fQ.Es455TPGAju2IUoTiXJPoMMAC3nxEZO02xXHTc1QtvIjcaZ_juLlB_Dr6TyoQ-dsICova3PnduVSTGYsLhsISPZhTqu0gBbiZosMcsDODRLl-B8VVrifpu2W0Cmt1FZzcTIGFB1hIxGAO7bJvVwMTyntyNxhK_3GBRVB2fD94zATWaHWN6veiRUXuNfupJQ2omYf9WqCh4R_ZyrqZvs2QM8zmhmUUoV6_bQjtET-16dO8ErMODUl6V_cY1bKYAXI-XSRpVEWwgXXKtDHVfrG5TDMT_wGhw6R4N5aW0aqmnLqlkxGjD4GwsEQpENtU77XmPAuoyNxTkyJ_mgFi7c0sg"}' 24 | headers: 25 | Connection: 26 | - keep-alive 27 | Content-Length: 28 | - '470' 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Date: 32 | - Thu, 13 Feb 2025 23:30:17 GMT 33 | Vary: 34 | - Accept-Language 35 | Via: 36 | - 1.1 154ecb715e497053770673a9ecb0c104.cloudfront.net (CloudFront) 37 | X-Amz-Cf-Id: 38 | - kTIvJOE6E-0JhYtJE_bhiogdn-CAe7KbmPKsFVWNkENfDgpCDFJDqg== 39 | X-Amz-Cf-Pop: 40 | - MAD51-C3 41 | X-Cache: 42 | - Miss from cloudfront 43 | X-Powered-By: 44 | - Thundar! 45 | X-Thetvdb-Api-Version: 46 | - 3.0.0 47 | status: 48 | code: 200 49 | message: OK 50 | - request: 51 | body: null 52 | headers: 53 | Accept: 54 | - '*/*' 55 | Accept-Encoding: 56 | - gzip, deflate 57 | Accept-Language: 58 | - en 59 | Authorization: 60 | - Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMTcsImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDE3fQ.Es455TPGAju2IUoTiXJPoMMAC3nxEZO02xXHTc1QtvIjcaZ_juLlB_Dr6TyoQ-dsICova3PnduVSTGYsLhsISPZhTqu0gBbiZosMcsDODRLl-B8VVrifpu2W0Cmt1FZzcTIGFB1hIxGAO7bJvVwMTyntyNxhK_3GBRVB2fD94zATWaHWN6veiRUXuNfupJQ2omYf9WqCh4R_ZyrqZvs2QM8zmhmUUoV6_bQjtET-16dO8ErMODUl6V_cY1bKYAXI-XSRpVEWwgXXKtDHVfrG5TDMT_wGhw6R4N5aW0aqmnLqlkxGjD4GwsEQpENtU77XmPAuoyNxTkyJ_mgFi7c0sg 61 | Connection: 62 | - keep-alive 63 | Content-Type: 64 | - application/json 65 | User-Agent: 66 | - Subliminal/2.2 67 | method: GET 68 | uri: https://api.thetvdb.com/series/999999999/actors 69 | response: 70 | body: 71 | string: '{"data":[]}' 72 | headers: 73 | Cache-Control: 74 | - private, max-age=86400 75 | Connection: 76 | - keep-alive 77 | Content-Length: 78 | - '11' 79 | Content-Type: 80 | - application/json; charset=utf-8 81 | Date: 82 | - Thu, 13 Feb 2025 23:30:17 GMT 83 | Vary: 84 | - Accept-Language 85 | Via: 86 | - 1.1 aa81fe1b9ec90b1088d56da5a82deaae.cloudfront.net (CloudFront) 87 | X-Amz-Cf-Id: 88 | - O4fIfU9J2PgDIJD9Uc8FuV7U-lDrR89-L5amPEuoEC_AZWBhrPmK1g== 89 | X-Amz-Cf-Pop: 90 | - MAD51-C3 91 | X-Cache: 92 | - Miss from cloudfront 93 | X-Powered-By: 94 | - Thundar! 95 | X-Thetvdb-Api-Version: 96 | - 3.0.0 97 | status: 98 | code: 200 99 | message: OK 100 | version: 1 101 | -------------------------------------------------------------------------------- /tests/cassettes/tvdb/test_get_series_wrong_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"apikey": "5EC930FB90DA1ADA", "username": null, "password": null}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '66' 15 | Content-Type: 16 | - application/json 17 | User-Agent: 18 | - Subliminal/2.2 19 | method: POST 20 | uri: https://api.thetvdb.com/login 21 | response: 22 | body: 23 | string: '{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMjEsImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDIxfQ.2FuzHyHKlNUYwUE32PvHaR5El9RBf700Kt9jjztWKZ5v3-dkQKlXBpPb5FRAXLrz44mOJek8ZsOyrYSF-Q3AfkslZyEak4PNg99byS4YrIMcm07OwK3-MCZLxCm4-GovoB6Y5JhI1j428mRPEZT4LynknYjhvWAEhbsg42Pjo3n5BJBB_Kd-OO_34dA-rjOgKOKXh2IXki1GTob3gwZXNoUOJCoiD8Cgzdb8nQbo0LYJl2r_n8CggdGPYk4GTcg33w3iFVSMURDhdveitlBKb2ElDzAgZiWpNaHogGnplc5wxpsDGoBhxdi1MoiuoX_1w2R1JSnMuVzXSLmFoebqdQ"}' 24 | headers: 25 | Connection: 26 | - keep-alive 27 | Content-Length: 28 | - '470' 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Date: 32 | - Thu, 13 Feb 2025 23:30:21 GMT 33 | Vary: 34 | - Accept-Language 35 | Via: 36 | - 1.1 f966f79361bddf464daf437364dccfbc.cloudfront.net (CloudFront) 37 | X-Amz-Cf-Id: 38 | - WAVn17iO6m79S5rJMlT8wMfnSI8iYLGPFVNIgjsNNbFq1NK0AlpaFg== 39 | X-Amz-Cf-Pop: 40 | - MAD51-C3 41 | X-Cache: 42 | - Miss from cloudfront 43 | X-Powered-By: 44 | - Thundar! 45 | X-Thetvdb-Api-Version: 46 | - 3.0.0 47 | status: 48 | code: 200 49 | message: OK 50 | - request: 51 | body: null 52 | headers: 53 | Accept: 54 | - '*/*' 55 | Accept-Encoding: 56 | - gzip, deflate 57 | Accept-Language: 58 | - en 59 | Authorization: 60 | - Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMjEsImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDIxfQ.2FuzHyHKlNUYwUE32PvHaR5El9RBf700Kt9jjztWKZ5v3-dkQKlXBpPb5FRAXLrz44mOJek8ZsOyrYSF-Q3AfkslZyEak4PNg99byS4YrIMcm07OwK3-MCZLxCm4-GovoB6Y5JhI1j428mRPEZT4LynknYjhvWAEhbsg42Pjo3n5BJBB_Kd-OO_34dA-rjOgKOKXh2IXki1GTob3gwZXNoUOJCoiD8Cgzdb8nQbo0LYJl2r_n8CggdGPYk4GTcg33w3iFVSMURDhdveitlBKb2ElDzAgZiWpNaHogGnplc5wxpsDGoBhxdi1MoiuoX_1w2R1JSnMuVzXSLmFoebqdQ 61 | Connection: 62 | - keep-alive 63 | Content-Type: 64 | - application/json 65 | User-Agent: 66 | - Subliminal/2.2 67 | method: GET 68 | uri: https://api.thetvdb.com/series/999999999 69 | response: 70 | body: 71 | string: '{"Error":"ID: 999999999 not found"}' 72 | headers: 73 | Cache-Control: 74 | - private, max-age=86400 75 | Connection: 76 | - keep-alive 77 | Content-Length: 78 | - '35' 79 | Content-Type: 80 | - application/json; charset=utf-8 81 | Date: 82 | - Thu, 13 Feb 2025 23:30:21 GMT 83 | Vary: 84 | - Accept-Language 85 | Via: 86 | - 1.1 1dafbe40b0d9e74e368ed415e4fec47a.cloudfront.net (CloudFront) 87 | X-Amz-Cf-Id: 88 | - 5X4-q9nHPMtm8EFlDe2pQthgnxBYFd-_p3fs0nEAgd9U49WQM6FQ3Q== 89 | X-Amz-Cf-Pop: 90 | - MAD51-C3 91 | X-Cache: 92 | - Error from cloudfront 93 | X-Powered-By: 94 | - Thundar! 95 | X-Thetvdb-Api-Version: 96 | - 3.0.0 97 | status: 98 | code: 404 99 | message: Not Found 100 | version: 1 101 | -------------------------------------------------------------------------------- /tests/cassettes/tvdb/test_login.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"apikey": "5EC930FB90DA1ADA", "username": null, "password": null}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '66' 15 | Content-Type: 16 | - application/json 17 | User-Agent: 18 | - Subliminal/2.2 19 | method: POST 20 | uri: https://api.thetvdb.com/login 21 | response: 22 | body: 23 | string: '{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMTcsImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDE3fQ.Es455TPGAju2IUoTiXJPoMMAC3nxEZO02xXHTc1QtvIjcaZ_juLlB_Dr6TyoQ-dsICova3PnduVSTGYsLhsISPZhTqu0gBbiZosMcsDODRLl-B8VVrifpu2W0Cmt1FZzcTIGFB1hIxGAO7bJvVwMTyntyNxhK_3GBRVB2fD94zATWaHWN6veiRUXuNfupJQ2omYf9WqCh4R_ZyrqZvs2QM8zmhmUUoV6_bQjtET-16dO8ErMODUl6V_cY1bKYAXI-XSRpVEWwgXXKtDHVfrG5TDMT_wGhw6R4N5aW0aqmnLqlkxGjD4GwsEQpENtU77XmPAuoyNxTkyJ_mgFi7c0sg"}' 24 | headers: 25 | Connection: 26 | - keep-alive 27 | Content-Length: 28 | - '470' 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Date: 32 | - Thu, 13 Feb 2025 23:30:17 GMT 33 | Vary: 34 | - Accept-Language 35 | Via: 36 | - 1.1 2dfea998d8266b393040dcf24637af0e.cloudfront.net (CloudFront) 37 | X-Amz-Cf-Id: 38 | - LHgAY_X5ReqSAGpTPyBM48r3PfKKniF7WkGeHFStVrc7aY4pTx2VMg== 39 | X-Amz-Cf-Pop: 40 | - MAD51-C3 41 | X-Cache: 42 | - Miss from cloudfront 43 | X-Powered-By: 44 | - Thundar! 45 | X-Thetvdb-Api-Version: 46 | - 3.0.0 47 | status: 48 | code: 200 49 | message: OK 50 | version: 1 51 | -------------------------------------------------------------------------------- /tests/cassettes/tvdb/test_login_error.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"apikey": "1234", "username": null, "password": null}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '54' 15 | Content-Type: 16 | - application/json 17 | User-Agent: 18 | - Subliminal/2.2 19 | method: POST 20 | uri: https://api.thetvdb.com/login 21 | response: 22 | body: 23 | string: '{"Error":"API Key Required"}' 24 | headers: 25 | Connection: 26 | - keep-alive 27 | Content-Length: 28 | - '28' 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Date: 32 | - Thu, 13 Feb 2025 23:30:16 GMT 33 | Vary: 34 | - Accept-Language 35 | Via: 36 | - 1.1 037815db6a622da57fe5824befbc0e0c.cloudfront.net (CloudFront) 37 | Www-Authenticate: 38 | - JWT realm=jwt auth 39 | X-Amz-Cf-Id: 40 | - GY4qSAB9svp70t3jxGA-EshjobsMMoTQXyWF6ozL5sAzCk1j1nl8cQ== 41 | X-Amz-Cf-Pop: 42 | - MAD51-C3 43 | X-Cache: 44 | - Error from cloudfront 45 | X-Powered-By: 46 | - Thundar! 47 | X-Thetvdb-Api-Version: 48 | - 3.0.0 49 | status: 50 | code: 401 51 | message: Unauthorized 52 | version: 1 53 | -------------------------------------------------------------------------------- /tests/cassettes/tvdb/test_query_series_episodes_wrong_season.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"apikey": "5EC930FB90DA1ADA", "username": null, "password": null}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '66' 15 | Content-Type: 16 | - application/json 17 | User-Agent: 18 | - Subliminal/2.2 19 | method: POST 20 | uri: https://api.thetvdb.com/login 21 | response: 22 | body: 23 | string: '{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMTcsImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDE3fQ.Es455TPGAju2IUoTiXJPoMMAC3nxEZO02xXHTc1QtvIjcaZ_juLlB_Dr6TyoQ-dsICova3PnduVSTGYsLhsISPZhTqu0gBbiZosMcsDODRLl-B8VVrifpu2W0Cmt1FZzcTIGFB1hIxGAO7bJvVwMTyntyNxhK_3GBRVB2fD94zATWaHWN6veiRUXuNfupJQ2omYf9WqCh4R_ZyrqZvs2QM8zmhmUUoV6_bQjtET-16dO8ErMODUl6V_cY1bKYAXI-XSRpVEWwgXXKtDHVfrG5TDMT_wGhw6R4N5aW0aqmnLqlkxGjD4GwsEQpENtU77XmPAuoyNxTkyJ_mgFi7c0sg"}' 24 | headers: 25 | Connection: 26 | - keep-alive 27 | Content-Length: 28 | - '470' 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Date: 32 | - Thu, 13 Feb 2025 23:30:17 GMT 33 | Vary: 34 | - Accept-Language 35 | Via: 36 | - 1.1 508a0f3451a34ff5e6a3963c94ef304c.cloudfront.net (CloudFront) 37 | X-Amz-Cf-Id: 38 | - zDGFoR5M8IMS58DRrLYJiFKBUf18M3cLxqOgsJTOpnwYNNqEbnR0Jg== 39 | X-Amz-Cf-Pop: 40 | - MAD51-C3 41 | X-Cache: 42 | - Miss from cloudfront 43 | X-Powered-By: 44 | - Thundar! 45 | X-Thetvdb-Api-Version: 46 | - 3.0.0 47 | status: 48 | code: 200 49 | message: OK 50 | - request: 51 | body: null 52 | headers: 53 | Accept: 54 | - '*/*' 55 | Accept-Encoding: 56 | - gzip, deflate 57 | Accept-Language: 58 | - en 59 | Authorization: 60 | - Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMTcsImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDE3fQ.Es455TPGAju2IUoTiXJPoMMAC3nxEZO02xXHTc1QtvIjcaZ_juLlB_Dr6TyoQ-dsICova3PnduVSTGYsLhsISPZhTqu0gBbiZosMcsDODRLl-B8VVrifpu2W0Cmt1FZzcTIGFB1hIxGAO7bJvVwMTyntyNxhK_3GBRVB2fD94zATWaHWN6veiRUXuNfupJQ2omYf9WqCh4R_ZyrqZvs2QM8zmhmUUoV6_bQjtET-16dO8ErMODUl6V_cY1bKYAXI-XSRpVEWwgXXKtDHVfrG5TDMT_wGhw6R4N5aW0aqmnLqlkxGjD4GwsEQpENtU77XmPAuoyNxTkyJ_mgFi7c0sg 61 | Connection: 62 | - keep-alive 63 | Content-Type: 64 | - application/json 65 | User-Agent: 66 | - Subliminal/2.2 67 | method: GET 68 | uri: https://api.thetvdb.com/series/80379/episodes/query?airedSeason=99&page=1 69 | response: 70 | body: 71 | string: '{"Error":"No results for your query: map[AiredSeason:99]"}' 72 | headers: 73 | Cache-Control: 74 | - private, max-age=86400 75 | Connection: 76 | - keep-alive 77 | Content-Length: 78 | - '58' 79 | Content-Type: 80 | - application/json; charset=utf-8 81 | Date: 82 | - Thu, 13 Feb 2025 23:30:18 GMT 83 | Vary: 84 | - Accept-Language 85 | Via: 86 | - 1.1 ac6c61c5795b4bc9c16e9a11b9a19660.cloudfront.net (CloudFront) 87 | X-Amz-Cf-Id: 88 | - t951WnGKEXp_lAxp0K4NLKiDPoIHo1zKE8rDtzifP-9lxp1GcxAeVA== 89 | X-Amz-Cf-Pop: 90 | - MAD51-C3 91 | X-Cache: 92 | - Error from cloudfront 93 | X-Powered-By: 94 | - Thundar! 95 | X-Thetvdb-Api-Version: 96 | - 3.0.0 97 | status: 98 | code: 404 99 | message: Not Found 100 | version: 1 101 | -------------------------------------------------------------------------------- /tests/cassettes/tvdb/test_refresh_token.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"apikey": "5EC930FB90DA1ADA", "username": null, "password": null}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '66' 15 | Content-Type: 16 | - application/json 17 | User-Agent: 18 | - Subliminal/2.2 19 | method: POST 20 | uri: https://api.thetvdb.com/login 21 | response: 22 | body: 23 | string: '{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMTgsImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDE4fQ.I-flvnRYHdakvoJPuyRAVB6U6ePDHLaV8YdC98u5dVsuvO3P3obJ7E7yAN_x3Mc5f5xyQyHcD11I0JsOcud3NLHt-zM3wknZbMcSqhsy7oNl9OYlELhePRcOMl_EVs_IyyCV9tEjkjTPRoHGrHI2R2DLbOzSWojSqWUkwZemRHvHSsxIaaCIKouP9Slp4B1xAdGlkBklpr0QNhjOU6NkSjioing-gUMc053fDuEJTbaOoH9wMFgYyC6S4cZEEyD2zMmGW_Cqvfgbhy2YGZdAXmWoEQzHepnDct11knRzYFR6t2RwGoziOF1l41kOwfXeUtQQdIvvFuyXdJQSJuU6UA"}' 24 | headers: 25 | Connection: 26 | - keep-alive 27 | Content-Length: 28 | - '470' 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Date: 32 | - Thu, 13 Feb 2025 23:30:18 GMT 33 | Vary: 34 | - Accept-Language 35 | Via: 36 | - 1.1 72dd98bd7ac49e4cde7380f0bf4fad6c.cloudfront.net (CloudFront) 37 | X-Amz-Cf-Id: 38 | - ifzz-4nv7UsnKmNs2C_VpOUplLC8EWbgJC7ZC57ejs49WZeA_l0WBg== 39 | X-Amz-Cf-Pop: 40 | - MAD51-C3 41 | X-Cache: 42 | - Miss from cloudfront 43 | X-Powered-By: 44 | - Thundar! 45 | X-Thetvdb-Api-Version: 46 | - 3.0.0 47 | status: 48 | code: 200 49 | message: OK 50 | - request: 51 | body: null 52 | headers: 53 | Accept: 54 | - '*/*' 55 | Accept-Encoding: 56 | - gzip, deflate 57 | Accept-Language: 58 | - en 59 | Authorization: 60 | - Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMTgsImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDE4fQ.I-flvnRYHdakvoJPuyRAVB6U6ePDHLaV8YdC98u5dVsuvO3P3obJ7E7yAN_x3Mc5f5xyQyHcD11I0JsOcud3NLHt-zM3wknZbMcSqhsy7oNl9OYlELhePRcOMl_EVs_IyyCV9tEjkjTPRoHGrHI2R2DLbOzSWojSqWUkwZemRHvHSsxIaaCIKouP9Slp4B1xAdGlkBklpr0QNhjOU6NkSjioing-gUMc053fDuEJTbaOoH9wMFgYyC6S4cZEEyD2zMmGW_Cqvfgbhy2YGZdAXmWoEQzHepnDct11knRzYFR6t2RwGoziOF1l41kOwfXeUtQQdIvvFuyXdJQSJuU6UA 61 | Connection: 62 | - keep-alive 63 | Content-Type: 64 | - application/json 65 | User-Agent: 66 | - Subliminal/2.2 67 | method: GET 68 | uri: https://api.thetvdb.com/refresh_token 69 | response: 70 | body: 71 | string: '{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMTksImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDE4fQ.D9nhW_HJQQ777HxgoeM5CjjDvVnN-3pBw8L5u6wJIJDpeeF6xs5Wh0CTYoBR4Z2ZrPZXLJvA5FJnGqSSrwFNni99XUyS8j2MZflHlm0LvNobzzoCuOz5jHBySVQBZ42ZNMR06pMUJjgktZojlVjP_eoaEupspeIO5ken8M5K_q09tcvclpYXgCGNUhgSFsswq0geMTymM36DffT0IMDeoDZix8NAfW1UckPGT50sCZTdJesppCITvu3F1gGza-gXLfZHOFbIvSaqu_xCygJfs9a0nh8YPnelKY6dAlbb8UYeFJ9MbZ5BCsGD_i8jyCDj8sXv5lbRhGbdG61xI6nNoA"}' 72 | headers: 73 | Cache-Control: 74 | - private, max-age=86400 75 | Connection: 76 | - keep-alive 77 | Content-Length: 78 | - '470' 79 | Content-Type: 80 | - application/json; charset=utf-8 81 | Date: 82 | - Thu, 13 Feb 2025 23:30:19 GMT 83 | Vary: 84 | - Accept-Language 85 | Via: 86 | - 1.1 ac6c61c5795b4bc9c16e9a11b9a19660.cloudfront.net (CloudFront) 87 | X-Amz-Cf-Id: 88 | - KYKpvlwagErVp6qpa6jKkKS5a5KOXbS37wGffdEvYsd5lX2ETWoXTQ== 89 | X-Amz-Cf-Pop: 90 | - MAD51-C3 91 | X-Cache: 92 | - Miss from cloudfront 93 | X-Powered-By: 94 | - Thundar! 95 | X-Thetvdb-Api-Version: 96 | - 3.0.0 97 | status: 98 | code: 200 99 | message: OK 100 | version: 1 101 | -------------------------------------------------------------------------------- /tests/cassettes/tvdb/test_search_series.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"apikey": "5EC930FB90DA1ADA", "username": null, "password": null}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '66' 15 | Content-Type: 16 | - application/json 17 | User-Agent: 18 | - Subliminal/2.2 19 | method: POST 20 | uri: https://api.thetvdb.com/login 21 | response: 22 | body: 23 | string: '{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMTksImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDE5fQ.X8qNUk8a3GuyzlYxWzm0CoQfavWf3Bkw3VSevhhxebuiJOVuUwNHMsUUJkLeQ15731mRtsSmGOljpRiGDEVv3jg6lQKoiUzLkRVmrbK1ifRxIhjEoPYXSxjTNJs96WGBRqoOlNL1XKzJF0Ey6EAqciw5w-z1EkaLbwYz4HuHn-h3MFN85cCoY8FY3EbA2j2J8vFPhkP69tXorV0j_brX6xQTfZC-rXAAEIASX5BsReM5-mTw80K5igMiJlN3bTMolMt-4zTx_L9NYgGJfwD6muZ6j-8CRDw-I7kltX9e75f9K4aghjhTMsvNUfWCQdAJzvKcKK9qaaRIF6yXKDsepA"}' 24 | headers: 25 | Connection: 26 | - keep-alive 27 | Content-Length: 28 | - '470' 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Date: 32 | - Thu, 13 Feb 2025 23:30:19 GMT 33 | Vary: 34 | - Accept-Language 35 | Via: 36 | - 1.1 cd792529b65c0806dd516f60fbcf5d94.cloudfront.net (CloudFront) 37 | X-Amz-Cf-Id: 38 | - 4-_dPrYrDvfZ9d6Ey6dq6bUSH4fzBkSTE007pSqmQ97BK5GyZPSBDA== 39 | X-Amz-Cf-Pop: 40 | - MAD51-C3 41 | X-Cache: 42 | - Miss from cloudfront 43 | X-Powered-By: 44 | - Thundar! 45 | X-Thetvdb-Api-Version: 46 | - 3.0.0 47 | status: 48 | code: 200 49 | message: OK 50 | - request: 51 | body: null 52 | headers: 53 | Accept: 54 | - '*/*' 55 | Accept-Encoding: 56 | - gzip, deflate 57 | Accept-Language: 58 | - en 59 | Authorization: 60 | - Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMTksImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDE5fQ.X8qNUk8a3GuyzlYxWzm0CoQfavWf3Bkw3VSevhhxebuiJOVuUwNHMsUUJkLeQ15731mRtsSmGOljpRiGDEVv3jg6lQKoiUzLkRVmrbK1ifRxIhjEoPYXSxjTNJs96WGBRqoOlNL1XKzJF0Ey6EAqciw5w-z1EkaLbwYz4HuHn-h3MFN85cCoY8FY3EbA2j2J8vFPhkP69tXorV0j_brX6xQTfZC-rXAAEIASX5BsReM5-mTw80K5igMiJlN3bTMolMt-4zTx_L9NYgGJfwD6muZ6j-8CRDw-I7kltX9e75f9K4aghjhTMsvNUfWCQdAJzvKcKK9qaaRIF6yXKDsepA 61 | Connection: 62 | - keep-alive 63 | Content-Type: 64 | - application/json 65 | User-Agent: 66 | - Subliminal/2.2 67 | method: GET 68 | uri: https://api.thetvdb.com/search/series?name=The+Big+Bang+Theory 69 | response: 70 | body: 71 | string: "{\"data\":[{\"aliases\":[\"La teoria del Big Bang\",\"The Big Bang 72 | Theory\",\"\u56E7\u7537\u5927\u7206\u70B8\"],\"banner\":\"/banners/graphical/80379-g23.jpg\",\"firstAired\":\"2007-09-24\",\"id\":80379,\"image\":\"/banners/posters/80379-18.jpg\",\"network\":\"CBS\",\"overview\":\"A 73 | woman who moves into an apartment across the hall from two brilliant but socially 74 | awkward physicists shows them how little they know about life outside of the 75 | laboratory.\",\"poster\":\"/banners/posters/80379-18.jpg\",\"seriesName\":\"The 76 | Big Bang Theory\",\"slug\":\"the-big-bang-theory\",\"status\":\"Ended\"}]}" 77 | headers: 78 | Cache-Control: 79 | - private, max-age=86400 80 | Connection: 81 | - keep-alive 82 | Content-Length: 83 | - '534' 84 | Content-Type: 85 | - application/json; charset=utf-8 86 | Date: 87 | - Thu, 13 Feb 2025 23:30:19 GMT 88 | Vary: 89 | - Accept-Language 90 | Via: 91 | - 1.1 508a0f3451a34ff5e6a3963c94ef304c.cloudfront.net (CloudFront) 92 | X-Amz-Cf-Id: 93 | - yBQXfqxg9xSP1iYoCL4P7EykIlsS2jGIUwgOZUjwC4FZtVjCIavveA== 94 | X-Amz-Cf-Pop: 95 | - MAD51-C3 96 | X-Cache: 97 | - Miss from cloudfront 98 | X-Powered-By: 99 | - Thundar! 100 | X-Thetvdb-Api-Version: 101 | - 3.0.0 102 | status: 103 | code: 200 104 | message: OK 105 | version: 1 106 | -------------------------------------------------------------------------------- /tests/cassettes/tvdb/test_search_series_wrong_name.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"apikey": "5EC930FB90DA1ADA", "username": null, "password": null}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '66' 15 | Content-Type: 16 | - application/json 17 | User-Agent: 18 | - Subliminal/2.2 19 | method: POST 20 | uri: https://api.thetvdb.com/login 21 | response: 22 | body: 23 | string: '{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMjAsImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDIwfQ.to1lA1te9rHT1Z3WeInLZZBGMgFwE8GrPFFO0pzQGg74-w4oQ_7WCpyyoYKwtEiUx6__OqARhh3Bv4LlyfFzt6ksgNfg13ADpUMH0A8ro4q5dmjLJO4MaMi1reqLvt0uEfWN_DjKT4P_0RAX_tQxultbsCsqsWJxClqSvYO2d2h5Ems7SBTmr-gVvHtqIC-umYTJ7o_0-zptmfjICG1A2Vw0LXOhy_6zq4pRh6ScMKTw7SNQ1KVY_l7LV4dICJJcCJ8chNqf5-U80F8neBO9TxUHNRYlkjpRyUPwzCJKzKkPrkjE99iV_jw7z_cfJwY2B-9CWRJSEr8T-4tkUbSg_w"}' 24 | headers: 25 | Connection: 26 | - keep-alive 27 | Content-Length: 28 | - '470' 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Date: 32 | - Thu, 13 Feb 2025 23:30:20 GMT 33 | Vary: 34 | - Accept-Language 35 | Via: 36 | - 1.1 9d749cb7a21113d40b8187b4b33cf322.cloudfront.net (CloudFront) 37 | X-Amz-Cf-Id: 38 | - IubzYzlMBG5m5rPxM6IhEqDTFMW93a2Txp2fNHWj5XCQOdkye--rmg== 39 | X-Amz-Cf-Pop: 40 | - MAD51-C3 41 | X-Cache: 42 | - Miss from cloudfront 43 | X-Powered-By: 44 | - Thundar! 45 | X-Thetvdb-Api-Version: 46 | - 3.0.0 47 | status: 48 | code: 200 49 | message: OK 50 | - request: 51 | body: null 52 | headers: 53 | Accept: 54 | - '*/*' 55 | Accept-Encoding: 56 | - gzip, deflate 57 | Accept-Language: 58 | - en 59 | Authorization: 60 | - Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMjAsImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDIwfQ.to1lA1te9rHT1Z3WeInLZZBGMgFwE8GrPFFO0pzQGg74-w4oQ_7WCpyyoYKwtEiUx6__OqARhh3Bv4LlyfFzt6ksgNfg13ADpUMH0A8ro4q5dmjLJO4MaMi1reqLvt0uEfWN_DjKT4P_0RAX_tQxultbsCsqsWJxClqSvYO2d2h5Ems7SBTmr-gVvHtqIC-umYTJ7o_0-zptmfjICG1A2Vw0LXOhy_6zq4pRh6ScMKTw7SNQ1KVY_l7LV4dICJJcCJ8chNqf5-U80F8neBO9TxUHNRYlkjpRyUPwzCJKzKkPrkjE99iV_jw7z_cfJwY2B-9CWRJSEr8T-4tkUbSg_w 61 | Connection: 62 | - keep-alive 63 | Content-Type: 64 | - application/json 65 | User-Agent: 66 | - Subliminal/2.2 67 | method: GET 68 | uri: https://api.thetvdb.com/search/series?name=The+Bing+Bag+Theory 69 | response: 70 | body: 71 | string: '{"Error":"Resource not found"}' 72 | headers: 73 | Cache-Control: 74 | - private, max-age=86400 75 | Connection: 76 | - keep-alive 77 | Content-Length: 78 | - '30' 79 | Content-Type: 80 | - application/json; charset=utf-8 81 | Date: 82 | - Thu, 13 Feb 2025 23:30:20 GMT 83 | Vary: 84 | - Accept-Language 85 | Via: 86 | - 1.1 352ab4fc53db02754a30e59a7814a0d2.cloudfront.net (CloudFront) 87 | X-Amz-Cf-Id: 88 | - M58xWsiYyyl1TQVeWI-ELXdojo1glrT0pWJXJd0WpCRg5_qFirHTEw== 89 | X-Amz-Cf-Pop: 90 | - MAD51-C3 91 | X-Cache: 92 | - Error from cloudfront 93 | X-Powered-By: 94 | - Thundar! 95 | X-Thetvdb-Api-Version: 96 | - 3.0.0 97 | status: 98 | code: 404 99 | message: Not Found 100 | version: 1 101 | -------------------------------------------------------------------------------- /tests/cassettes/tvdb/test_token_needs_refresh.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"apikey": "5EC930FB90DA1ADA", "username": null, "password": null}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Accept-Language: 10 | - en 11 | Connection: 12 | - keep-alive 13 | Content-Length: 14 | - '66' 15 | Content-Type: 16 | - application/json 17 | User-Agent: 18 | - Subliminal/2.2 19 | method: POST 20 | uri: https://api.thetvdb.com/login 21 | response: 22 | body: 23 | string: '{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDAwOTQyMTcsImlkIjoic3VibGltaW5hbCIsIm9yaWdfaWF0IjoxNzM5NDg5NDE3fQ.Es455TPGAju2IUoTiXJPoMMAC3nxEZO02xXHTc1QtvIjcaZ_juLlB_Dr6TyoQ-dsICova3PnduVSTGYsLhsISPZhTqu0gBbiZosMcsDODRLl-B8VVrifpu2W0Cmt1FZzcTIGFB1hIxGAO7bJvVwMTyntyNxhK_3GBRVB2fD94zATWaHWN6veiRUXuNfupJQ2omYf9WqCh4R_ZyrqZvs2QM8zmhmUUoV6_bQjtET-16dO8ErMODUl6V_cY1bKYAXI-XSRpVEWwgXXKtDHVfrG5TDMT_wGhw6R4N5aW0aqmnLqlkxGjD4GwsEQpENtU77XmPAuoyNxTkyJ_mgFi7c0sg"}' 24 | headers: 25 | Connection: 26 | - keep-alive 27 | Content-Length: 28 | - '470' 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Date: 32 | - Thu, 13 Feb 2025 23:30:17 GMT 33 | Vary: 34 | - Accept-Language 35 | Via: 36 | - 1.1 2dfea998d8266b393040dcf24637af0e.cloudfront.net (CloudFront) 37 | X-Amz-Cf-Id: 38 | - 8OM8v4OsotRqBSPn8ws5aEZR2lRJKT9sFZ45jBizK782Pfx-7mDB_Q== 39 | X-Amz-Cf-Pop: 40 | - MAD51-C3 41 | X-Cache: 42 | - Miss from cloudfront 43 | X-Powered-By: 44 | - Thundar! 45 | X-Thetvdb-Api-Version: 46 | - 3.0.0 47 | status: 48 | code: 200 49 | message: OK 50 | version: 1 51 | -------------------------------------------------------------------------------- /tests/cli/test_cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from click.testing import CliRunner 5 | 6 | from subliminal.cli.cli import subliminal as subliminal_cli 7 | 8 | # Core test 9 | pytestmark = [ 10 | pytest.mark.core, 11 | ] 12 | 13 | 14 | def test_cli_help() -> None: 15 | runner = CliRunner() 16 | result = runner.invoke(subliminal_cli, ['--help']) 17 | assert result.exit_code == 0 18 | assert result.output.startswith('Usage: subliminal [OPTIONS] COMMAND [ARGS]...') 19 | 20 | 21 | def test_cli_cache() -> None: 22 | runner = CliRunner() 23 | 24 | # Do nothing 25 | result = runner.invoke(subliminal_cli, ['cache']) 26 | assert result.exit_code == 0 27 | assert result.output == 'Nothing done.\n' 28 | 29 | # Clean cache 30 | result = runner.invoke(subliminal_cli, ['cache', '--clear-subliminal']) 31 | assert result.exit_code == 0 32 | assert result.output == "Subliminal's cache cleared.\n" 33 | -------------------------------------------------------------------------------- /tests/cli/test_generate_config.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: PT011, SIM115 2 | from __future__ import annotations 3 | 4 | from difflib import SequenceMatcher 5 | 6 | import pytest 7 | 8 | from subliminal.cli import generate_default_config 9 | 10 | # Core test 11 | pytestmark = [ 12 | pytest.mark.core, 13 | ] 14 | 15 | 16 | def test_generated_config() -> None: 17 | doc = generate_default_config(commented=False) 18 | doc_commented = generate_default_config(commented=True) 19 | 20 | doc_force_commented = ''.join( 21 | [ 22 | f'# {line}' if line and not line.startswith(('[', '#', '\n')) else line 23 | for line in doc.splitlines(keepends=True) 24 | ] 25 | ) 26 | 27 | x = SequenceMatcher(None, doc_commented, doc_force_commented) 28 | blocks = x.get_matching_blocks() 29 | 30 | assert len(blocks) == 2 31 | 32 | length = len(doc_commented) 33 | for match in blocks: 34 | assert match.size == 0 or match.size == length 35 | -------------------------------------------------------------------------------- /tests/cli/test_helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Callable 4 | 5 | import pytest 6 | 7 | from subliminal.cli.helpers import ( 8 | get_argument_doc, 9 | get_parameters_from_signature, 10 | split_doc_args, 11 | ) 12 | 13 | if TYPE_CHECKING: 14 | from babelfish import Language # type: ignore[import-untyped] 15 | 16 | # Core test 17 | pytestmark = pytest.mark.core 18 | 19 | 20 | @pytest.fixture 21 | def docstring() -> str: 22 | """Generate a docstring with arguments.""" 23 | return """Some class or function. 24 | 25 | This object has only one purpose: parsing. 26 | 27 | :param :class:`~babelfish.language.Language` language: the language of the subtitles. 28 | :param str keyword: the query term. 29 | :param (int | None) season: the season number. 30 | :param episode: the episode number. 31 | :type episode: int 32 | :param (int | None) year: the video year and 33 | on another line. 34 | :return: something 35 | """ 36 | 37 | 38 | def test_split_doc_args(docstring: str) -> None: 39 | parts = split_doc_args(docstring) 40 | assert len(parts) == 5 41 | assert all(p.startswith(':param ') for p in parts) 42 | assert '\n' in parts[4] 43 | 44 | 45 | @pytest.mark.parametrize('is_class', [False, True]) 46 | def test_get_argument_doc(docstring: str, is_class: bool) -> None: 47 | obj: Callable 48 | if is_class: 49 | 50 | def obj() -> None: 51 | pass 52 | 53 | else: 54 | 55 | class obj: # type: ignore[no-redef] 56 | pass 57 | 58 | obj.__doc__ = docstring 59 | 60 | d = get_argument_doc(obj) 61 | assert d == { 62 | 'language': 'the language of the subtitles.', 63 | 'keyword': 'the query term.', 64 | 'season': 'the season number.', 65 | 'episode': 'the episode number.', 66 | 'year': 'the video year and on another line.', 67 | } 68 | 69 | 70 | def test_get_parameters_from_signature(docstring: str) -> None: 71 | def fun( 72 | language: Language | None = None, 73 | keyword: str = 'key', 74 | season: int = 0, 75 | episode: int | None = None, 76 | year: str | None = None, 77 | no_desc: bool = False, 78 | ) -> None: 79 | pass 80 | 81 | fun.__doc__ = docstring 82 | 83 | params = get_parameters_from_signature(fun) 84 | 85 | assert len(params) == 6 86 | 87 | assert params[0]['name'] == 'language' 88 | assert params[0]['default'] is None 89 | assert params[0]['annotation'] == 'Language | None' 90 | assert params[0]['desc'] == 'the language of the subtitles.' 91 | 92 | assert params[5]['name'] == 'no_desc' 93 | assert params[5]['default'] is False 94 | assert params[5]['annotation'] == 'bool' 95 | assert params[5]['desc'] is None 96 | -------------------------------------------------------------------------------- /tests/refiners/test_hash.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from babelfish import Language # type: ignore[import-untyped] 4 | 5 | from subliminal.extensions import get_default_providers 6 | from subliminal.refiners.hash import hash_opensubtitles, refine 7 | from subliminal.video import Movie 8 | 9 | 10 | def test_hash_opensubtitles(mkv: dict[str, str]) -> None: 11 | assert hash_opensubtitles(mkv['test1']) == '40b44a7096b71ec3' 12 | 13 | 14 | def test_hash_opensubtitles_too_small(tmp_path: Path) -> None: 15 | path = tmp_path / 'test_too_small.mkv' 16 | path.touch() 17 | assert hash_opensubtitles(str(path)) is None 18 | 19 | 20 | def test_refine_too_small(mkv: dict[str, str]) -> None: 21 | path = mkv['test1'] 22 | video = Movie.fromguess(path, {'type': 'movie', 'title': 'Titanic'}) 23 | 24 | # video too small 25 | assert video.size is None or video.size < 10485760 26 | refine(video, providers=get_default_providers()) 27 | assert len(video.hashes) == 0 28 | 29 | 30 | def test_refine(mkv: dict[str, str]) -> None: 31 | path = mkv['test1'] 32 | video = Movie.fromguess(path, {'type': 'movie', 'title': 'Titanic'}) 33 | 34 | # hide true size 35 | video.size = 10485761 36 | refine(video, providers=get_default_providers()) 37 | 38 | expected = { 39 | 'opensubtitlescom': '40b44a7096b71ec3', 40 | # 'bsplayer': '40b44a7096b71ec3', 41 | 'napiprojekt': '9884a2b66dcb2965d0f45ce84e37b60c', 42 | 'opensubtitles': '40b44a7096b71ec3', 43 | } 44 | assert video.hashes == expected 45 | 46 | 47 | def test_refine_no_language(mkv: dict[str, str]) -> None: 48 | path = mkv['test1'] 49 | video = Movie.fromguess(path, {'type': 'movie', 'title': 'Titanic'}) 50 | 51 | # Napiprojekt only supports polish 52 | languages = {Language('ita')} 53 | 54 | # hide true size 55 | video.size = 10485761 56 | refine(video, providers=['napiprojekt'], languages=languages) 57 | 58 | assert len(video.hashes) == 0 59 | -------------------------------------------------------------------------------- /tests/test_archives.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: PT011, SIM115 2 | from __future__ import annotations 3 | 4 | import os 5 | import sys 6 | from pathlib import Path 7 | 8 | import pytest 9 | 10 | from subliminal.archives import is_supported_archive, scan_archive, scan_archive_rar 11 | from subliminal.core import scan_video_or_archive, scan_videos 12 | from subliminal.exceptions import ArchiveError 13 | 14 | unix_platform = pytest.mark.skipif( 15 | not sys.platform.startswith('linux'), 16 | reason='only on linux platform', 17 | ) 18 | 19 | # Core test 20 | pytestmark = [pytest.mark.core, unix_platform] 21 | 22 | 23 | def test_is_supported_archive(rar: dict[str, str], mkv: dict[str, str]) -> None: 24 | assert is_supported_archive(rar['simple']) 25 | assert not is_supported_archive(mkv['test1']) 26 | 27 | 28 | def test_scan_archive_with_one_video(rar: dict[str, str], mkv: dict[str, str]) -> None: 29 | if 'video' not in rar: 30 | return 31 | rar_file = rar['video'] 32 | actual = scan_archive(rar_file) 33 | 34 | expected = os.fspath(Path(rar_file).parent / Path(mkv['test1']).name) 35 | assert actual.name == expected 36 | 37 | 38 | def test_scan_archive_with_multiple_videos(rar: dict[str, str], mkv: dict[str, str]) -> None: 39 | if 'videos' not in rar: 40 | return 41 | rar_file = rar['videos'] 42 | actual = scan_archive(rar_file) 43 | 44 | expected = os.fspath(Path(rar_file).parent / Path(mkv['test5']).name) 45 | assert actual.name == expected 46 | 47 | 48 | def test_scan_archive_with_no_video(rar: dict[str, str]) -> None: 49 | with pytest.raises(ArchiveError, match='No video in archive'): 50 | scan_archive(rar['simple']) 51 | 52 | 53 | def test_scan_bad_archive(mkv: dict[str, str]) -> None: 54 | with pytest.raises(ArchiveError, match="'.mkv' is not a valid archive"): 55 | scan_archive(mkv['test1']) 56 | 57 | 58 | def test_scan_rar_not_a_file(mkv: dict[str, str]) -> None: 59 | with pytest.raises(ValueError, match="Path does not exist: 'not_a_file.txt'"): 60 | scan_archive_rar('not_a_file.txt') 61 | 62 | 63 | def test_scan_rar_bad_archive(mkv: dict[str, str]) -> None: 64 | with pytest.raises(ValueError, match="'.mkv' is not a valid archive"): 65 | scan_archive_rar(mkv['test1']) 66 | 67 | 68 | def test_scan_password_protected_archive(rar: dict[str, str]) -> None: 69 | with pytest.raises(ArchiveError, match='Rar requires a password'): 70 | scan_archive(rar['pwd-protected']) 71 | 72 | 73 | def test_scan_archive_error( 74 | rar: dict[str, str], 75 | ) -> None: 76 | if 'pwd-protected' not in rar: 77 | return 78 | path = rar['pwd-protected'] 79 | with pytest.raises(ArchiveError): 80 | scan_archive(path) 81 | 82 | with pytest.raises(ValueError, match='Error scanning archive'): 83 | scan_video_or_archive(path) 84 | 85 | 86 | def test_scan_videos_error( 87 | rar: dict[str, str], 88 | caplog: pytest.LogCaptureFixture, 89 | ) -> None: 90 | if 'pwd-protected' not in rar: 91 | return 92 | folder = Path(rar['pwd-protected']).parent 93 | # Test return without error 94 | scan_videos(folder) 95 | 96 | # But error was logged 97 | for record in caplog.records: 98 | assert record.levelname == 'ERROR' 99 | assert 'Error scanning archive' in caplog.text 100 | -------------------------------------------------------------------------------- /tests/test_cache.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import pytest 4 | from dogpile.cache import make_region 5 | 6 | # A Mock version is already provided in conftest.py so no need to configure it again 7 | from subliminal.cache import region as region_custom 8 | 9 | # Core test 10 | pytestmark = pytest.mark.core 11 | 12 | # Configure default dogpile cache 13 | region_dogpile = make_region() 14 | region_dogpile.configure('dogpile.cache.null') 15 | region_dogpile.configure = Mock() # type: ignore[method-assign] 16 | 17 | 18 | str_object = 'The Simpsons-S12E09-HOMЯ' 19 | bytes_object = b'The Simpsons-S12E09-HOM\xd0\xaf' 20 | namespace = 'namespace' 21 | expected_key = 'tests.test_cache:fn|namespace|The Simpsons-S12E09-HOMЯ' # Key is expected as native string 22 | 23 | 24 | def fn() -> None: 25 | pass 26 | 27 | 28 | def test_dogpile_cache_key_generator_unicode_string() -> None: 29 | key = region_dogpile.function_key_generator(namespace, fn)(str_object) 30 | assert key == expected_key 31 | assert isinstance(key, str) 32 | 33 | 34 | def test_dogpile_cache_key_generator_byte_string() -> None: 35 | key = region_dogpile.function_key_generator(namespace, fn)(bytes_object) 36 | assert key == 'tests.test_cache:fn|namespace|' + str(b'The Simpsons-S12E09-HOM\xd0\xaf') 37 | assert key != expected_key # Key is not as expected 38 | assert isinstance(key, str) 39 | 40 | 41 | def test_custom_cache_key_generator_unicode_string() -> None: 42 | key = region_custom.function_key_generator(namespace, fn)(str_object) 43 | assert key == expected_key 44 | assert isinstance(key, str) 45 | 46 | 47 | def test_custom_cache_key_generator_byte_string() -> None: 48 | key = region_custom.function_key_generator(namespace, fn)(bytes_object) 49 | assert key == expected_key 50 | assert isinstance(key, str) 51 | -------------------------------------------------------------------------------- /tests/test_provider.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: PT011 2 | from __future__ import annotations 3 | 4 | import pytest 5 | 6 | from subliminal.providers import FeatureNotFound, ParserBeautifulSoup, Provider 7 | from subliminal.video import Episode, Movie 8 | 9 | # Core test 10 | pytestmark = pytest.mark.core 11 | 12 | 13 | def test_parserbeautifulsoup_reject_features() -> None: 14 | with pytest.raises(ValueError): 15 | ParserBeautifulSoup('', ['lxml', 'html']) 16 | 17 | 18 | def test_parserbeautifulsoup_reject_builder_kwarg() -> None: 19 | with pytest.raises(ValueError): 20 | ParserBeautifulSoup('', ['lxml', 'html.parser'], builder='reject') 21 | 22 | 23 | def test_parserbeautifulsoup_reject_features_kwarg() -> None: 24 | with pytest.raises(ValueError): 25 | ParserBeautifulSoup('', ['lxml', 'html.parser'], features='reject') 26 | 27 | 28 | def test_parserbeautifulsoup_no_parser() -> None: 29 | with pytest.raises(FeatureNotFound): 30 | ParserBeautifulSoup('', ['myparser']) 31 | 32 | 33 | def test_parserbeautifulsoup() -> None: 34 | ParserBeautifulSoup('', ['lxml', 'html.parser']) 35 | 36 | 37 | def test_check_episodes_only(episodes: dict[str, Episode], movies: dict[str, Movie]) -> None: 38 | Provider.video_types = (Episode,) 39 | Provider.required_hash = None 40 | assert Provider.check(movies['man_of_steel']) is False 41 | assert Provider.check(episodes['bbt_s07e05']) is True 42 | 43 | 44 | def test_check_movies_only(episodes: dict[str, Episode], movies: dict[str, Movie]) -> None: 45 | Provider.video_types = (Movie,) 46 | Provider.required_hash = None 47 | assert Provider.check(movies['man_of_steel']) is True 48 | assert Provider.check(episodes['bbt_s07e05']) is False 49 | 50 | 51 | def test_check_required_hash(episodes: dict[str, Episode], movies: dict[str, Movie]) -> None: 52 | Provider.video_types = (Episode, Movie) 53 | Provider.required_hash = 'opensubtitles' 54 | assert Provider.check(movies['man_of_steel']) is True 55 | assert Provider.check(episodes['dallas_s01e03']) is False 56 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | min_version = 4 3 | env_list = 4 | pre-commit, 5 | mypy, 6 | py3{9,13}-tests, 7 | coverage, 8 | docs, 9 | changelog, 10 | 11 | [testenv:.pkg] 12 | pass_env = SETUPTOOLS_SCM_PRETEND_VERSION 13 | 14 | 15 | [testenv] 16 | package = wheel 17 | wheel_build_env = .pkg 18 | extras = tests 19 | pass_env = 20 | COVERAGE_FILE 21 | allowlist_externals = 22 | echo 23 | false 24 | # This and the next few testenvs are a workaround for 25 | # https://github.com/tox-dev/tox/issues/2858. 26 | commands = 27 | echo "Unrecognized environment name {envname}" 28 | false 29 | 30 | 31 | [testenv:prepare-tests] 32 | description = download data for tests 33 | basepython = python3 34 | usedevelop = True 35 | passenv = * 36 | deps = 37 | requests 38 | commands = python scripts/prepare_tests.py {posargs} 39 | 40 | 41 | [testenv:tests] 42 | commands = pytest {posargs:-n auto} 43 | 44 | 45 | [testenv:py3{,.}{9,10,11,12,13}-tests] 46 | commands = {[testenv:tests]commands} 47 | 48 | 49 | [testenv:mypy] 50 | extras = 51 | types 52 | tests 53 | commands = mypy --install-types --non-interactive {posargs:src tests scripts} 54 | 55 | 56 | [testenv:coverage] 57 | usedevelop = true 58 | deps = coverage[toml]>=7 59 | set_env = 60 | COVERAGE_PROCESS_START = pyproject.toml 61 | commands = 62 | python -m pytest --cov=subliminal --cov-report= --cov-fail-under=0 {posargs:-n auto} 63 | coverage report --skip-covered --show-missing --fail-under=80 64 | 65 | 66 | [testenv:py3{,.}{9,10,11,12,13}-coverage] 67 | commands = {[testenv:coverage]commands} 68 | 69 | 70 | [testenv:coverage-core] 71 | usedevelop = true 72 | deps = coverage[toml]>=7 73 | set_env = 74 | COVERAGE_PROCESS_START = pyproject.toml 75 | commands = 76 | python -m pytest \ 77 | -m core --cov=subliminal --cov-report= --cov-fail-under=0 \ 78 | {posargs:-n auto} 79 | coverage report \ 80 | --omit='src/subliminal/__main__.py,'\ 81 | 'src/subliminal/converters/*,src/subliminal/providers/*,src/subliminal/refiners/*' \ 82 | --skip-covered --show-missing --fail-under=100 83 | 84 | 85 | [testenv:py3{,.}{9,10,11,12,13}-coverage-core] 86 | commands = {[testenv:coverage-core]commands} 87 | 88 | 89 | [testenv:pre-commit] 90 | description = 91 | run pre-commit-defined linters under `{basepython}` 92 | skip_install = true 93 | basepython = python3 94 | deps = pre-commit>=2.9.3 95 | commands = pre-commit run --all-files --show-diff-on-failure {posargs:} 96 | setenv = 97 | # pre-commit and tools it launches are not clean of this warning. 98 | PYTHONWARNDEFAULTENCODING= 99 | 100 | 101 | [testenv:docs] 102 | # Keep base_python in-sync with CI.yaml/docs and .readthedocs.yaml. 103 | base_python = python3 104 | extras = docs 105 | allowlist_externals = sphinx-build 106 | commands = 107 | sphinx-build -n -T -W --keep-going -b html -d {envtmpdir}/doctrees docs docs/_build/html 108 | sphinx-build -n -T -W --keep-going -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/html 109 | sphinx-build -n -T -W --keep-going -b doctest -d {envtmpdir}/doctrees docs docs/_build/html 110 | 111 | 112 | [testenv:changelog] 113 | extras = docs 114 | allowlist_externals = towncrier 115 | commands = towncrier build {posargs:--version main --draft} 116 | 117 | [testenv:release] 118 | description = do a release, required posarg of the version number 119 | basepython = python3 120 | extras = docs 121 | usedevelop = True 122 | passenv = * 123 | deps = 124 | colorama 125 | tox 126 | commands = python scripts/release.py {posargs} 127 | 128 | [testenv:prepare-release-pr] 129 | description = prepare a release PR from a manual trigger in GitHub actions 130 | usedevelop = {[testenv:release]usedevelop} 131 | passenv = {[testenv:release]passenv} 132 | deps = {[testenv:release]deps} 133 | commands = python scripts/prepare-release-pr.py {posargs} 134 | --------------------------------------------------------------------------------