Merge pull request #346 from jdepoix/feature/github-actions
migrated CI to GitHub Actions and setup project to use poetry
This commit is contained in:
commit
ceda81b968
30
.coveragerc
30
.coveragerc
|
@ -1,30 +0,0 @@
|
|||
[run]
|
||||
source = youtube_transcript_api
|
||||
|
||||
|
||||
[report]
|
||||
omit =
|
||||
*/__main__.py
|
||||
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
|
||||
# Don't complain about missing debug-only code:
|
||||
def __unicode__
|
||||
def __repr__
|
||||
if self\.debug
|
||||
|
||||
# Don't complain if tests don't hit defensive assertion code:
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
if 0:
|
||||
if __name__ == .__main__.:
|
||||
|
||||
# Don't complain about empty stubs of abstract methods
|
||||
@abstractmethod
|
||||
@abstractclassmethod
|
||||
@abstractstaticmethod
|
||||
|
||||
show_missing = True
|
|
@ -0,0 +1,95 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
static-checks:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install poetry poethepoet
|
||||
poetry install --only dev
|
||||
- name: Format
|
||||
run: poe ci-format
|
||||
- name: Lint
|
||||
run: poe lint
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install poetry poethepoet
|
||||
poetry install --with test
|
||||
- name: Run tests
|
||||
run: |
|
||||
poe ci-test
|
||||
- name: Report intermediate coverage report
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
file: coverage.xml
|
||||
format: cobertura
|
||||
flag-name: run-python-${{ matrix.python-version }}
|
||||
parallel: true
|
||||
|
||||
coverage:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Finalize coverage report
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
parallel-finished: true
|
||||
carryforward: "run-python-3.8,run-python-3.9,run-python-3.10,run-python-3.11,run-python-3.12,run-python-3.13"
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install poetry poethepoet
|
||||
poetry install --with test
|
||||
- name: Check coverage
|
||||
run: poe coverage
|
||||
|
||||
publish:
|
||||
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
|
||||
needs: [coverage, static-checks]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install poetry
|
||||
poetry install
|
||||
- name: Build
|
||||
run: poetry build
|
||||
- name: Publish
|
||||
run: poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN }}
|
|
@ -7,3 +7,5 @@ build
|
|||
*.egg-info
|
||||
upload_new_version.sh
|
||||
.coverage
|
||||
coverage.xml
|
||||
.DS_STORE
|
15
.travis.yml
15
.travis.yml
|
@ -1,15 +0,0 @@
|
|||
language: python
|
||||
python:
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
- "3.7.11"
|
||||
- "3.8"
|
||||
install:
|
||||
- pip install --upgrade pip
|
||||
- pip install --upgrade setuptools
|
||||
- pip install -r requirements.txt
|
||||
- pip install urllib3==1.26.6
|
||||
script:
|
||||
- coverage run -m unittest discover
|
||||
after_success:
|
||||
- coveralls
|
31
README.md
31
README.md
|
@ -7,8 +7,8 @@
|
|||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=BAENLEW8VUJ6G&source=url">
|
||||
<img src="https://img.shields.io/badge/Donate-PayPal-green.svg" alt="Donate">
|
||||
</a>
|
||||
<a href="https://app.travis-ci.com/jdepoix/youtube-transcript-api">
|
||||
<img src="https://travis-ci.com/jdepoix/youtube-transcript-api.svg?branch=master" alt="Build Status">
|
||||
<a href="https://github.com/jdepoix/youtube-transcript-api/actions">
|
||||
<img src="https://github.com/jdepoix/youtube-transcript-api/actions/workflows/ci.yml/badge.svg?branch=master" alt="Build Status">
|
||||
</a>
|
||||
<a href="https://coveralls.io/github/jdepoix/youtube-transcript-api?branch=master">
|
||||
<img src="https://coveralls.io/repos/github/jdepoix/youtube-transcript-api/badge.svg?branch=master" alt="Coverage Status">
|
||||
|
@ -49,12 +49,6 @@ It is recommended to [install this module by using pip](https://pypi.org/project
|
|||
pip install youtube-transcript-api
|
||||
```
|
||||
|
||||
If you want to use it from source, you'll have to install the dependencies manually:
|
||||
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
You can either integrate this module [into an existing application](#api) or just use it via a [CLI](#cli).
|
||||
|
||||
## API
|
||||
|
@ -371,11 +365,30 @@ Using the CLI:
|
|||
youtube_transcript_api <first_video_id> <second_video_id> --cookies /path/to/your/cookies.txt
|
||||
```
|
||||
|
||||
|
||||
## Warning
|
||||
|
||||
This code uses an undocumented part of the YouTube API, which is called by the YouTube web-client. So there is no guarantee that it won't stop working tomorrow, if they change how things work. I will however do my best to make things working again as soon as possible if that happens. So if it stops working, let me know!
|
||||
|
||||
## Contributing
|
||||
|
||||
To setup the project locally run (requires [poetry](https://python-poetry.org/docs/) to be installed):
|
||||
```shell
|
||||
poetry install --with test,dev
|
||||
```
|
||||
|
||||
There's [poe](https://github.com/nat-n/poethepoet?tab=readme-ov-file#quick-start) tasks to run tests, coverage, the linter and formatter (you'll need to pass all of those for the build to pass):
|
||||
```shell
|
||||
poe test
|
||||
poe coverage
|
||||
poe format
|
||||
poe lint
|
||||
```
|
||||
|
||||
If you just want to make sure that your code passes all the necessary checks to get a green build, you can simply run:
|
||||
```shell
|
||||
poe precommit
|
||||
```
|
||||
|
||||
## Donations
|
||||
|
||||
If this project makes you happy by reducing your development time, you can make me happy by treating me to a cup of coffee, or become a [Sponsor of this project](https://github.com/sponsors/jdepoix) :)
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
.venv/bin/coverage run -m unittest discover && .venv/bin/coverage report
|
|
@ -0,0 +1,415 @@
|
|||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.8.30"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
|
||||
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.0"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
files = [
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"},
|
||||
{file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"},
|
||||
{file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"},
|
||||
{file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"},
|
||||
{file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"},
|
||||
{file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"},
|
||||
{file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"},
|
||||
{file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"},
|
||||
{file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.6.1"
|
||||
description = "Code coverage measurement for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"},
|
||||
{file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"},
|
||||
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"},
|
||||
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"},
|
||||
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"},
|
||||
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"},
|
||||
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"},
|
||||
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"},
|
||||
{file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"},
|
||||
{file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"},
|
||||
{file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"},
|
||||
{file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"},
|
||||
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"},
|
||||
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"},
|
||||
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"},
|
||||
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"},
|
||||
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"},
|
||||
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"},
|
||||
{file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"},
|
||||
{file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"},
|
||||
{file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"},
|
||||
{file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"},
|
||||
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"},
|
||||
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"},
|
||||
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"},
|
||||
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"},
|
||||
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"},
|
||||
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"},
|
||||
{file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"},
|
||||
{file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"},
|
||||
{file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"},
|
||||
{file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"},
|
||||
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"},
|
||||
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"},
|
||||
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"},
|
||||
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"},
|
||||
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"},
|
||||
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"},
|
||||
{file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"},
|
||||
{file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"},
|
||||
{file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"},
|
||||
{file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"},
|
||||
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"},
|
||||
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"},
|
||||
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"},
|
||||
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"},
|
||||
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"},
|
||||
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"},
|
||||
{file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"},
|
||||
{file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"},
|
||||
{file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"},
|
||||
{file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"},
|
||||
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"},
|
||||
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"},
|
||||
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"},
|
||||
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"},
|
||||
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"},
|
||||
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"},
|
||||
{file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"},
|
||||
{file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"},
|
||||
{file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"},
|
||||
{file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"},
|
||||
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"},
|
||||
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"},
|
||||
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"},
|
||||
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"},
|
||||
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"},
|
||||
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"},
|
||||
{file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"},
|
||||
{file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"},
|
||||
{file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"},
|
||||
{file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.2"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
|
||||
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpretty"
|
||||
version = "1.1.4"
|
||||
description = "HTTP client mock for Python"
|
||||
optional = false
|
||||
python-versions = ">=3"
|
||||
files = [
|
||||
{file = "httpretty-1.1.4.tar.gz", hash = "sha256:20de0e5dd5a18292d36d928cc3d6e52f8b2ac73daec40d41eb62dee154933b68"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
||||
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mock"
|
||||
version = "5.1.0"
|
||||
description = "Rolling backport of unittest.mock for all Pythons"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744"},
|
||||
{file = "mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
build = ["blurb", "twine", "wheel"]
|
||||
docs = ["sphinx"]
|
||||
test = ["pytest", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.2"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.5.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.3"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
|
||||
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=1.5,<2"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
|
||||
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
charset-normalizer = ">=2,<4"
|
||||
idna = ">=2.5,<4"
|
||||
urllib3 = ">=1.21.1,<3"
|
||||
|
||||
[package.extras]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.9"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"},
|
||||
{file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"},
|
||||
{file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"},
|
||||
{file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"},
|
||||
{file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"},
|
||||
{file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"},
|
||||
{file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.2"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"},
|
||||
{file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.2.3"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
|
||||
{file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
||||
h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.8,<3.14"
|
||||
content-hash = "370c5c5f94f6000e0fdb76190a3aabd5acadf804802ca70dba41787d306799b4"
|
|
@ -0,0 +1,94 @@
|
|||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "youtube-transcript-api"
|
||||
version = "0.6.2"
|
||||
description = "This is an python API which allows you to get the transcripts/subtitles for a given YouTube video. It also works for automatically generated subtitles, supports translating subtitles and it does not require a headless browser, like other selenium based solutions do!"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
authors = [
|
||||
"Jonas Depoix <jonas.depoix@web.de>",
|
||||
]
|
||||
homepage = "https://github.com/jdepoix/youtube-transcript-api"
|
||||
repository = "https://github.com/jdepoix/youtube-transcript-api"
|
||||
keywords = [
|
||||
"cli",
|
||||
"subtitle",
|
||||
"subtitles",
|
||||
"transcript",
|
||||
"transcripts",
|
||||
"youtube",
|
||||
"youtube-api",
|
||||
"youtube-subtitles",
|
||||
"youtube-transcripts",
|
||||
]
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
youtube_transcript_api = "youtube_transcript_api.__main__:main"
|
||||
|
||||
[tool.poe.tasks]
|
||||
test = "pytest youtube_transcript_api"
|
||||
ci-test.shell = "coverage run -m unittest discover && coverage xml"
|
||||
coverage.shell = "coverage run -m unittest discover && coverage report -m --fail-under=100"
|
||||
format = "ruff format youtube_transcript_api"
|
||||
ci-format = "ruff format youtube_transcript_api --check"
|
||||
lint = "ruff check youtube_transcript_api"
|
||||
precommit.shell = "poe format && poe lint && poe coverage"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.8,<3.14"
|
||||
requests = "*"
|
||||
|
||||
[tool.poetry.group.test]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
pytest = "^8.3.3"
|
||||
coverage = "^7.6.1"
|
||||
mock = "^5.1.0"
|
||||
httpretty = "^1.1.4"
|
||||
|
||||
[tool.poetry.group.dev]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.6.8"
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["youtube_transcript_api"]
|
||||
|
||||
[tool.coverage.report]
|
||||
omit = ["*/__main__.py", "youtube_transcript_api/test/*"]
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
|
||||
# Don't complain about missing debug-only code:
|
||||
"def __unicode__",
|
||||
"def __repr__",
|
||||
"if self\\.debug",
|
||||
|
||||
# Don't complain if tests don't hit defensive assertion code:
|
||||
"raise AssertionError",
|
||||
"raise NotImplementedError",
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
"if 0:",
|
||||
"if __name__ == .__main__.:",
|
||||
|
||||
# Don't complain about empty stubs of abstract methods
|
||||
"@abstractmethod",
|
||||
"@abstractclassmethod",
|
||||
"@abstractstaticmethod"
|
||||
]
|
||||
show_missing = true
|
|
@ -1,7 +0,0 @@
|
|||
requests
|
||||
|
||||
# testing
|
||||
mock==3.0.5
|
||||
httpretty==1.1.4
|
||||
coveralls==1.11.1
|
||||
coverage==5.2.1
|
59
setup.py
59
setup.py
|
@ -1,59 +0,0 @@
|
|||
import os
|
||||
|
||||
import unittest
|
||||
|
||||
import setuptools
|
||||
|
||||
|
||||
def _get_file_content(file_name):
|
||||
with open(file_name, 'r') as file_handler:
|
||||
return file_handler.read()
|
||||
|
||||
def get_long_description():
|
||||
return _get_file_content('README.md')
|
||||
|
||||
|
||||
def get_test_suite():
|
||||
test_loader = unittest.TestLoader()
|
||||
test_suite = test_loader.discover(
|
||||
'test', pattern='test_*.py',
|
||||
top_level_dir='{dirname}/youtube_transcript_api'.format(dirname=os.path.dirname(__file__))
|
||||
)
|
||||
return test_suite
|
||||
|
||||
|
||||
setuptools.setup(
|
||||
name="youtube_transcript_api",
|
||||
version="0.6.2",
|
||||
author="Jonas Depoix",
|
||||
author_email="jonas.depoix@web.de",
|
||||
description="This is an python API which allows you to get the transcripts/subtitles for a given YouTube video. It also works for automatically generated subtitles, supports translating subtitles and it does not require a headless browser, like other selenium based solutions do!",
|
||||
long_description=get_long_description(),
|
||||
long_description_content_type="text/markdown",
|
||||
keywords="youtube-api subtitles youtube transcripts transcript subtitle youtube-subtitles youtube-transcripts cli",
|
||||
url="https://github.com/jdepoix/youtube-transcript-api",
|
||||
packages=setuptools.find_packages(),
|
||||
classifiers=(
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
),
|
||||
install_requires=[
|
||||
'requests',
|
||||
],
|
||||
tests_require=[
|
||||
'mock',
|
||||
'httpretty',
|
||||
'coverage',
|
||||
'coveralls',
|
||||
],
|
||||
test_suite='setup.get_test_suite',
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'youtube_transcript_api = youtube_transcript_api.__main__:main',
|
||||
],
|
||||
},
|
||||
)
|
|
@ -1,3 +1,4 @@
|
|||
# ruff: noqa: F401
|
||||
from ._api import YouTubeTranscriptApi
|
||||
from ._transcripts import TranscriptList, Transcript
|
||||
from ._errors import (
|
||||
|
|
|
@ -11,5 +11,5 @@ def main():
|
|||
print(YouTubeTranscriptCli(sys.argv[1:]).run())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import requests
|
||||
|
||||
try: # pragma: no cover
|
||||
import http.cookiejar as cookiejar
|
||||
|
||||
CookieLoadError = (FileNotFoundError, cookiejar.LoadError)
|
||||
except ImportError: # pragma: no cover
|
||||
import cookielib as cookiejar
|
||||
|
||||
CookieLoadError = IOError
|
||||
|
||||
from ._transcripts import TranscriptListFetcher
|
||||
|
||||
from ._errors import (
|
||||
CookiePathInvalid,
|
||||
CookiesInvalid
|
||||
)
|
||||
from ._errors import CookiePathInvalid, CookiesInvalid
|
||||
|
||||
|
||||
class YouTubeTranscriptApi(object):
|
||||
|
@ -71,8 +71,15 @@ class YouTubeTranscriptApi(object):
|
|||
return TranscriptListFetcher(http_client).fetch(video_id)
|
||||
|
||||
@classmethod
|
||||
def get_transcripts(cls, video_ids, languages=('en',), continue_after_error=False, proxies=None,
|
||||
cookies=None, preserve_formatting=False):
|
||||
def get_transcripts(
|
||||
cls,
|
||||
video_ids,
|
||||
languages=("en",),
|
||||
continue_after_error=False,
|
||||
proxies=None,
|
||||
cookies=None,
|
||||
preserve_formatting=False,
|
||||
):
|
||||
"""
|
||||
Retrieves the transcripts for a list of videos.
|
||||
|
||||
|
@ -102,7 +109,9 @@ class YouTubeTranscriptApi(object):
|
|||
|
||||
for video_id in video_ids:
|
||||
try:
|
||||
data[video_id] = cls.get_transcript(video_id, languages, proxies, cookies, preserve_formatting)
|
||||
data[video_id] = cls.get_transcript(
|
||||
video_id, languages, proxies, cookies, preserve_formatting
|
||||
)
|
||||
except Exception as exception:
|
||||
if not continue_after_error:
|
||||
raise exception
|
||||
|
@ -112,7 +121,14 @@ class YouTubeTranscriptApi(object):
|
|||
return data, unretrievable_videos
|
||||
|
||||
@classmethod
|
||||
def get_transcript(cls, video_id, languages=('en',), proxies=None, cookies=None, preserve_formatting=False):
|
||||
def get_transcript(
|
||||
cls,
|
||||
video_id,
|
||||
languages=("en",),
|
||||
proxies=None,
|
||||
cookies=None,
|
||||
preserve_formatting=False,
|
||||
):
|
||||
"""
|
||||
Retrieves the transcript for a single video. This is just a shortcut for calling::
|
||||
|
||||
|
@ -134,7 +150,11 @@ class YouTubeTranscriptApi(object):
|
|||
:rtype [{'text': str, 'start': float, 'end': float}]:
|
||||
"""
|
||||
assert isinstance(video_id, str), "`video_id` must be a string"
|
||||
return cls.list_transcripts(video_id, proxies, cookies).find_transcript(languages).fetch(preserve_formatting=preserve_formatting)
|
||||
return (
|
||||
cls.list_transcripts(video_id, proxies, cookies)
|
||||
.find_transcript(languages)
|
||||
.fetch(preserve_formatting=preserve_formatting)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _load_cookies(cls, cookies, video_id):
|
||||
|
|
|
@ -13,10 +13,10 @@ class YouTubeTranscriptCli(object):
|
|||
parsed_args = self._parse_args()
|
||||
|
||||
if parsed_args.exclude_manually_created and parsed_args.exclude_generated:
|
||||
return ''
|
||||
return ""
|
||||
|
||||
proxies = None
|
||||
if parsed_args.http_proxy != '' or parsed_args.https_proxy != '':
|
||||
if parsed_args.http_proxy != "" or parsed_args.https_proxy != "":
|
||||
proxies = {"http": parsed_args.http_proxy, "https": parsed_args.https_proxy}
|
||||
|
||||
cookies = parsed_args.cookies
|
||||
|
@ -26,25 +26,41 @@ class YouTubeTranscriptCli(object):
|
|||
|
||||
for video_id in parsed_args.video_ids:
|
||||
try:
|
||||
transcripts.append(self._fetch_transcript(parsed_args, proxies, cookies, video_id))
|
||||
transcripts.append(
|
||||
self._fetch_transcript(parsed_args, proxies, cookies, video_id)
|
||||
)
|
||||
except Exception as exception:
|
||||
exceptions.append(exception)
|
||||
|
||||
return '\n\n'.join(
|
||||
return "\n\n".join(
|
||||
[str(exception) for exception in exceptions]
|
||||
+ ([FormatterLoader().load(parsed_args.format).format_transcripts(transcripts)] if transcripts else [])
|
||||
+ (
|
||||
[
|
||||
FormatterLoader()
|
||||
.load(parsed_args.format)
|
||||
.format_transcripts(transcripts)
|
||||
]
|
||||
if transcripts
|
||||
else []
|
||||
)
|
||||
)
|
||||
|
||||
def _fetch_transcript(self, parsed_args, proxies, cookies, video_id):
|
||||
transcript_list = YouTubeTranscriptApi.list_transcripts(video_id, proxies=proxies, cookies=cookies)
|
||||
transcript_list = YouTubeTranscriptApi.list_transcripts(
|
||||
video_id, proxies=proxies, cookies=cookies
|
||||
)
|
||||
|
||||
if parsed_args.list_transcripts:
|
||||
return str(transcript_list)
|
||||
|
||||
if parsed_args.exclude_manually_created:
|
||||
transcript = transcript_list.find_generated_transcript(parsed_args.languages)
|
||||
transcript = transcript_list.find_generated_transcript(
|
||||
parsed_args.languages
|
||||
)
|
||||
elif parsed_args.exclude_generated:
|
||||
transcript = transcript_list.find_manually_created_transcript(parsed_args.languages)
|
||||
transcript = transcript_list.find_manually_created_transcript(
|
||||
parsed_args.languages
|
||||
)
|
||||
else:
|
||||
transcript = transcript_list.find_transcript(parsed_args.languages)
|
||||
|
||||
|
@ -56,80 +72,84 @@ class YouTubeTranscriptCli(object):
|
|||
def _parse_args(self):
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
'This is an python API which allows you to get the transcripts/subtitles for a given YouTube video. '
|
||||
'It also works for automatically generated subtitles and it does not require a headless browser, like '
|
||||
'other selenium based solutions do!'
|
||||
"This is an python API which allows you to get the transcripts/subtitles for a given YouTube video. "
|
||||
"It also works for automatically generated subtitles and it does not require a headless browser, like "
|
||||
"other selenium based solutions do!"
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
'--list-transcripts',
|
||||
action='store_const',
|
||||
"--list-transcripts",
|
||||
action="store_const",
|
||||
const=True,
|
||||
default=False,
|
||||
help='This will list the languages in which the given videos are available in.',
|
||||
help="This will list the languages in which the given videos are available in.",
|
||||
)
|
||||
parser.add_argument('video_ids', nargs='+', type=str, help='List of YouTube video IDs.')
|
||||
parser.add_argument(
|
||||
'--languages',
|
||||
nargs='*',
|
||||
default=['en',],
|
||||
"video_ids", nargs="+", type=str, help="List of YouTube video IDs."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--languages",
|
||||
nargs="*",
|
||||
default=[
|
||||
"en",
|
||||
],
|
||||
type=str,
|
||||
help=(
|
||||
'A list of language codes in a descending priority. For example, if this is set to "de en" it will '
|
||||
'first try to fetch the german transcript (de) and then fetch the english transcript (en) if it fails '
|
||||
'to do so. As I can\'t provide a complete list of all working language codes with full certainty, you '
|
||||
'may have to play around with the language codes a bit, to find the one which is working for you!'
|
||||
"first try to fetch the german transcript (de) and then fetch the english transcript (en) if it fails "
|
||||
"to do so. As I can't provide a complete list of all working language codes with full certainty, you "
|
||||
"may have to play around with the language codes a bit, to find the one which is working for you!"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--exclude-generated',
|
||||
action='store_const',
|
||||
"--exclude-generated",
|
||||
action="store_const",
|
||||
const=True,
|
||||
default=False,
|
||||
help='If this flag is set transcripts which have been generated by YouTube will not be retrieved.',
|
||||
help="If this flag is set transcripts which have been generated by YouTube will not be retrieved.",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--exclude-manually-created',
|
||||
action='store_const',
|
||||
"--exclude-manually-created",
|
||||
action="store_const",
|
||||
const=True,
|
||||
default=False,
|
||||
help='If this flag is set transcripts which have been manually created will not be retrieved.',
|
||||
help="If this flag is set transcripts which have been manually created will not be retrieved.",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--format',
|
||||
"--format",
|
||||
type=str,
|
||||
default='pretty',
|
||||
default="pretty",
|
||||
choices=tuple(FormatterLoader.TYPES.keys()),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--translate',
|
||||
default='',
|
||||
"--translate",
|
||||
default="",
|
||||
help=(
|
||||
'The language code for the language you want this transcript to be translated to. Use the '
|
||||
'--list-transcripts feature to find out which languages are translatable and which translation '
|
||||
'languages are available.'
|
||||
)
|
||||
"The language code for the language you want this transcript to be translated to. Use the "
|
||||
"--list-transcripts feature to find out which languages are translatable and which translation "
|
||||
"languages are available."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--http-proxy',
|
||||
default='',
|
||||
metavar='URL',
|
||||
help='Use the specified HTTP proxy.'
|
||||
"--http-proxy",
|
||||
default="",
|
||||
metavar="URL",
|
||||
help="Use the specified HTTP proxy.",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--https-proxy',
|
||||
default='',
|
||||
metavar='URL',
|
||||
help='Use the specified HTTPS proxy.'
|
||||
"--https-proxy",
|
||||
default="",
|
||||
metavar="URL",
|
||||
help="Use the specified HTTPS proxy.",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--cookies',
|
||||
"--cookies",
|
||||
default=None,
|
||||
help='The cookie file that will be used for authorization with youtube.'
|
||||
help="The cookie file that will be used for authorization with youtube.",
|
||||
)
|
||||
|
||||
return self._sanitize_video_ids(parser.parse_args(self._args))
|
||||
|
||||
def _sanitize_video_ids(self, args):
|
||||
args.video_ids = [video_id.replace('\\', '') for video_id in args.video_ids]
|
||||
args.video_ids = [video_id.replace("\\", "") for video_id in args.video_ids]
|
||||
return args
|
||||
|
|
|
@ -5,16 +5,17 @@ class CouldNotRetrieveTranscript(Exception):
|
|||
"""
|
||||
Raised if a transcript could not be retrieved.
|
||||
"""
|
||||
ERROR_MESSAGE = '\nCould not retrieve a transcript for the video {video_url}!'
|
||||
CAUSE_MESSAGE_INTRO = ' This is most likely caused by:\n\n{cause}'
|
||||
CAUSE_MESSAGE = ''
|
||||
|
||||
ERROR_MESSAGE = "\nCould not retrieve a transcript for the video {video_url}!"
|
||||
CAUSE_MESSAGE_INTRO = " This is most likely caused by:\n\n{cause}"
|
||||
CAUSE_MESSAGE = ""
|
||||
GITHUB_REFERRAL = (
|
||||
'\n\nIf you are sure that the described cause is not responsible for this error '
|
||||
'and that a transcript should be retrievable, please create an issue at '
|
||||
'https://github.com/jdepoix/youtube-transcript-api/issues. '
|
||||
'Please add which version of youtube_transcript_api you are using '
|
||||
'and provide the information needed to replicate the error. '
|
||||
'Also make sure that there are no open issues which already describe your problem!'
|
||||
"\n\nIf you are sure that the described cause is not responsible for this error "
|
||||
"and that a transcript should be retrievable, please create an issue at "
|
||||
"https://github.com/jdepoix/youtube-transcript-api/issues. "
|
||||
"Please add which version of youtube_transcript_api you are using "
|
||||
"and provide the information needed to replicate the error. "
|
||||
"Also make sure that there are no open issues which already describe your problem!"
|
||||
)
|
||||
|
||||
def __init__(self, video_id):
|
||||
|
@ -23,10 +24,14 @@ class CouldNotRetrieveTranscript(Exception):
|
|||
|
||||
def _build_error_message(self):
|
||||
cause = self.cause
|
||||
error_message = self.ERROR_MESSAGE.format(video_url=WATCH_URL.format(video_id=self.video_id))
|
||||
error_message = self.ERROR_MESSAGE.format(
|
||||
video_url=WATCH_URL.format(video_id=self.video_id)
|
||||
)
|
||||
|
||||
if cause:
|
||||
error_message += self.CAUSE_MESSAGE_INTRO.format(cause=cause) + self.GITHUB_REFERRAL
|
||||
error_message += (
|
||||
self.CAUSE_MESSAGE_INTRO.format(cause=cause) + self.GITHUB_REFERRAL
|
||||
)
|
||||
|
||||
return error_message
|
||||
|
||||
|
@ -36,7 +41,7 @@ class CouldNotRetrieveTranscript(Exception):
|
|||
|
||||
|
||||
class YouTubeRequestFailed(CouldNotRetrieveTranscript):
|
||||
CAUSE_MESSAGE = 'Request to YouTube failed: {reason}'
|
||||
CAUSE_MESSAGE = "Request to YouTube failed: {reason}"
|
||||
|
||||
def __init__(self, video_id, http_error):
|
||||
self.reason = str(http_error)
|
||||
|
@ -50,12 +55,12 @@ class YouTubeRequestFailed(CouldNotRetrieveTranscript):
|
|||
|
||||
|
||||
class VideoUnavailable(CouldNotRetrieveTranscript):
|
||||
CAUSE_MESSAGE = 'The video is no longer available'
|
||||
CAUSE_MESSAGE = "The video is no longer available"
|
||||
|
||||
|
||||
class InvalidVideoId(CouldNotRetrieveTranscript):
|
||||
CAUSE_MESSAGE = (
|
||||
'You provided an invalid video id. Make sure you are using the video id and NOT the url!\n\n'
|
||||
"You provided an invalid video id. Make sure you are using the video id and NOT the url!\n\n"
|
||||
'Do NOT run: `YouTubeTranscriptApi.get_transcript("https://www.youtube.com/watch?v=1234")`\n'
|
||||
'Instead run: `YouTubeTranscriptApi.get_transcript("1234")`'
|
||||
)
|
||||
|
@ -63,48 +68,48 @@ class InvalidVideoId(CouldNotRetrieveTranscript):
|
|||
|
||||
class TooManyRequests(CouldNotRetrieveTranscript):
|
||||
CAUSE_MESSAGE = (
|
||||
'YouTube is receiving too many requests from this IP and now requires solving a captcha to continue. '
|
||||
'One of the following things can be done to work around this:\n\
|
||||
- Manually solve the captcha in a browser and export the cookie. '
|
||||
'Read here how to use that cookie with '
|
||||
'youtube-transcript-api: https://github.com/jdepoix/youtube-transcript-api#cookies\n\
|
||||
"YouTube is receiving too many requests from this IP and now requires solving a captcha to continue. "
|
||||
"One of the following things can be done to work around this:\n\
|
||||
- Manually solve the captcha in a browser and export the cookie. "
|
||||
"Read here how to use that cookie with "
|
||||
"youtube-transcript-api: https://github.com/jdepoix/youtube-transcript-api#cookies\n\
|
||||
- Use a different IP address\n\
|
||||
- Wait until the ban on your IP has been lifted'
|
||||
- Wait until the ban on your IP has been lifted"
|
||||
)
|
||||
|
||||
|
||||
class TranscriptsDisabled(CouldNotRetrieveTranscript):
|
||||
CAUSE_MESSAGE = 'Subtitles are disabled for this video'
|
||||
CAUSE_MESSAGE = "Subtitles are disabled for this video"
|
||||
|
||||
|
||||
class NoTranscriptAvailable(CouldNotRetrieveTranscript):
|
||||
CAUSE_MESSAGE = 'No transcripts are available for this video'
|
||||
CAUSE_MESSAGE = "No transcripts are available for this video"
|
||||
|
||||
|
||||
class NotTranslatable(CouldNotRetrieveTranscript):
|
||||
CAUSE_MESSAGE = 'The requested language is not translatable'
|
||||
CAUSE_MESSAGE = "The requested language is not translatable"
|
||||
|
||||
|
||||
class TranslationLanguageNotAvailable(CouldNotRetrieveTranscript):
|
||||
CAUSE_MESSAGE = 'The requested translation language is not available'
|
||||
CAUSE_MESSAGE = "The requested translation language is not available"
|
||||
|
||||
|
||||
class CookiePathInvalid(CouldNotRetrieveTranscript):
|
||||
CAUSE_MESSAGE = 'The provided cookie file was unable to be loaded'
|
||||
CAUSE_MESSAGE = "The provided cookie file was unable to be loaded"
|
||||
|
||||
|
||||
class CookiesInvalid(CouldNotRetrieveTranscript):
|
||||
CAUSE_MESSAGE = 'The cookies provided are not valid (may have expired)'
|
||||
CAUSE_MESSAGE = "The cookies provided are not valid (may have expired)"
|
||||
|
||||
|
||||
class FailedToCreateConsentCookie(CouldNotRetrieveTranscript):
|
||||
CAUSE_MESSAGE = 'Failed to automatically give consent to saving cookies'
|
||||
CAUSE_MESSAGE = "Failed to automatically give consent to saving cookies"
|
||||
|
||||
|
||||
class NoTranscriptFound(CouldNotRetrieveTranscript):
|
||||
CAUSE_MESSAGE = (
|
||||
'No transcripts were found for any of the requested language codes: {requested_language_codes}\n\n'
|
||||
'{transcript_data}'
|
||||
"No transcripts were found for any of the requested language codes: {requested_language_codes}\n\n"
|
||||
"{transcript_data}"
|
||||
)
|
||||
|
||||
def __init__(self, video_id, requested_language_codes, transcript_data):
|
||||
|
|
|
@ -1 +1 @@
|
|||
WATCH_URL = 'https://www.youtube.com/watch?v={video_id}'
|
||||
WATCH_URL = "https://www.youtube.com/watch?v={video_id}"
|
||||
|
|
|
@ -2,8 +2,9 @@ import sys
|
|||
|
||||
# This can only be tested by using different python versions, therefore it is not covered by coverage.py
|
||||
if sys.version_info.major == 2: # pragma: no cover
|
||||
# ruff: noqa: F821
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf-8')
|
||||
sys.setdefaultencoding("utf-8")
|
||||
|
||||
import json
|
||||
|
||||
|
@ -52,7 +53,7 @@ class TranscriptListFetcher(object):
|
|||
splitted_html = html.split('"captions":')
|
||||
|
||||
if len(splitted_html) <= 1:
|
||||
if video_id.startswith('http://') or video_id.startswith('https://'):
|
||||
if video_id.startswith("http://") or video_id.startswith("https://"):
|
||||
raise InvalidVideoId(video_id)
|
||||
if 'class="g-recaptcha"' in html:
|
||||
raise TooManyRequests(video_id)
|
||||
|
@ -62,12 +63,12 @@ class TranscriptListFetcher(object):
|
|||
raise TranscriptsDisabled(video_id)
|
||||
|
||||
captions_json = json.loads(
|
||||
splitted_html[1].split(',"videoDetails')[0].replace('\n', '')
|
||||
).get('playerCaptionsTracklistRenderer')
|
||||
splitted_html[1].split(',"videoDetails')[0].replace("\n", "")
|
||||
).get("playerCaptionsTracklistRenderer")
|
||||
if captions_json is None:
|
||||
raise TranscriptsDisabled(video_id)
|
||||
|
||||
if 'captionTracks' not in captions_json:
|
||||
if "captionTracks" not in captions_json:
|
||||
raise NoTranscriptAvailable(video_id)
|
||||
|
||||
return captions_json
|
||||
|
@ -76,7 +77,9 @@ class TranscriptListFetcher(object):
|
|||
match = re.search('name="v" value="(.*?)"', html)
|
||||
if match is None:
|
||||
raise FailedToCreateConsentCookie(video_id)
|
||||
self._http_client.cookies.set('CONSENT', 'YES+' + match.group(1), domain='.youtube.com')
|
||||
self._http_client.cookies.set(
|
||||
"CONSENT", "YES+" + match.group(1), domain=".youtube.com"
|
||||
)
|
||||
|
||||
def _fetch_video_html(self, video_id):
|
||||
html = self._fetch_html(video_id)
|
||||
|
@ -88,7 +91,9 @@ class TranscriptListFetcher(object):
|
|||
return html
|
||||
|
||||
def _fetch_html(self, video_id):
|
||||
response = self._http_client.get(WATCH_URL.format(video_id=video_id), headers={'Accept-Language': 'en-US'})
|
||||
response = self._http_client.get(
|
||||
WATCH_URL.format(video_id=video_id), headers={"Accept-Language": "en-US"}
|
||||
)
|
||||
return unescape(_raise_http_errors(response, video_id).text)
|
||||
|
||||
|
||||
|
@ -98,7 +103,13 @@ class TranscriptList(object):
|
|||
for a given YouTube video. Also it provides functionality to search for a transcript in a given language.
|
||||
"""
|
||||
|
||||
def __init__(self, video_id, manually_created_transcripts, generated_transcripts, translation_languages):
|
||||
def __init__(
|
||||
self,
|
||||
video_id,
|
||||
manually_created_transcripts,
|
||||
generated_transcripts,
|
||||
translation_languages,
|
||||
):
|
||||
"""
|
||||
The constructor is only for internal use. Use the static build method instead.
|
||||
|
||||
|
@ -132,28 +143,29 @@ class TranscriptList(object):
|
|||
"""
|
||||
translation_languages = [
|
||||
{
|
||||
'language': translation_language['languageName']['simpleText'],
|
||||
'language_code': translation_language['languageCode'],
|
||||
} for translation_language in captions_json.get('translationLanguages', [])
|
||||
"language": translation_language["languageName"]["simpleText"],
|
||||
"language_code": translation_language["languageCode"],
|
||||
}
|
||||
for translation_language in captions_json.get("translationLanguages", [])
|
||||
]
|
||||
|
||||
manually_created_transcripts = {}
|
||||
generated_transcripts = {}
|
||||
|
||||
for caption in captions_json['captionTracks']:
|
||||
if caption.get('kind', '') == 'asr':
|
||||
for caption in captions_json["captionTracks"]:
|
||||
if caption.get("kind", "") == "asr":
|
||||
transcript_dict = generated_transcripts
|
||||
else:
|
||||
transcript_dict = manually_created_transcripts
|
||||
|
||||
transcript_dict[caption['languageCode']] = Transcript(
|
||||
transcript_dict[caption["languageCode"]] = Transcript(
|
||||
http_client,
|
||||
video_id,
|
||||
caption['baseUrl'],
|
||||
caption['name']['simpleText'],
|
||||
caption['languageCode'],
|
||||
caption.get('kind', '') == 'asr',
|
||||
translation_languages if caption.get('isTranslatable', False) else [],
|
||||
caption["baseUrl"],
|
||||
caption["name"]["simpleText"],
|
||||
caption["languageCode"],
|
||||
caption.get("kind", "") == "asr",
|
||||
translation_languages if caption.get("isTranslatable", False) else [],
|
||||
)
|
||||
|
||||
return TranscriptList(
|
||||
|
@ -164,7 +176,10 @@ class TranscriptList(object):
|
|||
)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(list(self._manually_created_transcripts.values()) + list(self._generated_transcripts.values()))
|
||||
return iter(
|
||||
list(self._manually_created_transcripts.values())
|
||||
+ list(self._generated_transcripts.values())
|
||||
)
|
||||
|
||||
def find_transcript(self, language_codes):
|
||||
"""
|
||||
|
@ -180,7 +195,10 @@ class TranscriptList(object):
|
|||
:rtype Transcript:
|
||||
:raises: NoTranscriptFound
|
||||
"""
|
||||
return self._find_transcript(language_codes, [self._manually_created_transcripts, self._generated_transcripts])
|
||||
return self._find_transcript(
|
||||
language_codes,
|
||||
[self._manually_created_transcripts, self._generated_transcripts],
|
||||
)
|
||||
|
||||
def find_generated_transcript(self, language_codes):
|
||||
"""
|
||||
|
@ -208,7 +226,9 @@ class TranscriptList(object):
|
|||
:rtype Transcript:
|
||||
:raises: NoTranscriptFound
|
||||
"""
|
||||
return self._find_transcript(language_codes, [self._manually_created_transcripts])
|
||||
return self._find_transcript(
|
||||
language_codes, [self._manually_created_transcripts]
|
||||
)
|
||||
|
||||
def _find_transcript(self, language_codes, transcript_dicts):
|
||||
for language_code in language_codes:
|
||||
|
@ -216,44 +236,54 @@ class TranscriptList(object):
|
|||
if language_code in transcript_dict:
|
||||
return transcript_dict[language_code]
|
||||
|
||||
raise NoTranscriptFound(
|
||||
self.video_id,
|
||||
language_codes,
|
||||
self
|
||||
)
|
||||
raise NoTranscriptFound(self.video_id, language_codes, self)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
'For this video ({video_id}) transcripts are available in the following languages:\n\n'
|
||||
'(MANUALLY CREATED)\n'
|
||||
'{available_manually_created_transcript_languages}\n\n'
|
||||
'(GENERATED)\n'
|
||||
'{available_generated_transcripts}\n\n'
|
||||
'(TRANSLATION LANGUAGES)\n'
|
||||
'{available_translation_languages}'
|
||||
"For this video ({video_id}) transcripts are available in the following languages:\n\n"
|
||||
"(MANUALLY CREATED)\n"
|
||||
"{available_manually_created_transcript_languages}\n\n"
|
||||
"(GENERATED)\n"
|
||||
"{available_generated_transcripts}\n\n"
|
||||
"(TRANSLATION LANGUAGES)\n"
|
||||
"{available_translation_languages}"
|
||||
).format(
|
||||
video_id=self.video_id,
|
||||
available_manually_created_transcript_languages=self._get_language_description(
|
||||
str(transcript) for transcript in self._manually_created_transcripts.values()
|
||||
str(transcript)
|
||||
for transcript in self._manually_created_transcripts.values()
|
||||
),
|
||||
available_generated_transcripts=self._get_language_description(
|
||||
str(transcript) for transcript in self._generated_transcripts.values()
|
||||
),
|
||||
available_translation_languages=self._get_language_description(
|
||||
'{language_code} ("{language}")'.format(
|
||||
language=translation_language['language'],
|
||||
language_code=translation_language['language_code'],
|
||||
) for translation_language in self._translation_languages
|
||||
language=translation_language["language"],
|
||||
language_code=translation_language["language_code"],
|
||||
)
|
||||
for translation_language in self._translation_languages
|
||||
),
|
||||
)
|
||||
|
||||
def _get_language_description(self, transcript_strings):
|
||||
description = '\n'.join(' - {transcript}'.format(transcript=transcript) for transcript in transcript_strings)
|
||||
return description if description else 'None'
|
||||
description = "\n".join(
|
||||
" - {transcript}".format(transcript=transcript)
|
||||
for transcript in transcript_strings
|
||||
)
|
||||
return description if description else "None"
|
||||
|
||||
|
||||
class Transcript(object):
|
||||
def __init__(self, http_client, video_id, url, language, language_code, is_generated, translation_languages):
|
||||
def __init__(
|
||||
self,
|
||||
http_client,
|
||||
video_id,
|
||||
url,
|
||||
language,
|
||||
language_code,
|
||||
is_generated,
|
||||
translation_languages,
|
||||
):
|
||||
"""
|
||||
You probably don't want to initialize this directly. Usually you'll access Transcript objects using a
|
||||
TranscriptList.
|
||||
|
@ -276,7 +306,7 @@ class Transcript(object):
|
|||
self.is_generated = is_generated
|
||||
self.translation_languages = translation_languages
|
||||
self._translation_languages_dict = {
|
||||
translation_language['language_code']: translation_language['language']
|
||||
translation_language["language_code"]: translation_language["language"]
|
||||
for translation_language in translation_languages
|
||||
}
|
||||
|
||||
|
@ -288,7 +318,9 @@ class Transcript(object):
|
|||
:return: a list of dictionaries containing the 'text', 'start' and 'duration' keys
|
||||
:rtype [{'text': str, 'start': float, 'end': float}]:
|
||||
"""
|
||||
response = self._http_client.get(self._url, headers={'Accept-Language': 'en-US'})
|
||||
response = self._http_client.get(
|
||||
self._url, headers={"Accept-Language": "en-US"}
|
||||
)
|
||||
return _TranscriptParser(preserve_formatting=preserve_formatting).parse(
|
||||
_raise_http_errors(response, self.video_id).text,
|
||||
)
|
||||
|
@ -297,7 +329,7 @@ class Transcript(object):
|
|||
return '{language_code} ("{language}"){translation_description}'.format(
|
||||
language=self.language,
|
||||
language_code=self.language_code,
|
||||
translation_description='[TRANSLATABLE]' if self.is_translatable else ''
|
||||
translation_description="[TRANSLATABLE]" if self.is_translatable else "",
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -314,7 +346,9 @@ class Transcript(object):
|
|||
return Transcript(
|
||||
self._http_client,
|
||||
self.video_id,
|
||||
'{url}&tlang={language_code}'.format(url=self._url, language_code=language_code),
|
||||
"{url}&tlang={language_code}".format(
|
||||
url=self._url, language_code=language_code
|
||||
),
|
||||
self._translation_languages_dict[language_code],
|
||||
language_code,
|
||||
True,
|
||||
|
@ -324,16 +358,16 @@ class Transcript(object):
|
|||
|
||||
class _TranscriptParser(object):
|
||||
_FORMATTING_TAGS = [
|
||||
'strong', # important
|
||||
'em', # emphasized
|
||||
'b', # bold
|
||||
'i', # italic
|
||||
'mark', # marked
|
||||
'small', # smaller
|
||||
'del', # deleted
|
||||
'ins', # inserted
|
||||
'sub', # subscript
|
||||
'sup', # superscript
|
||||
"strong", # important
|
||||
"em", # emphasized
|
||||
"b", # bold
|
||||
"i", # italic
|
||||
"mark", # marked
|
||||
"small", # smaller
|
||||
"del", # deleted
|
||||
"ins", # inserted
|
||||
"sub", # subscript
|
||||
"sup", # superscript
|
||||
]
|
||||
|
||||
def __init__(self, preserve_formatting=False):
|
||||
|
@ -341,19 +375,19 @@ class _TranscriptParser(object):
|
|||
|
||||
def _get_html_regex(self, preserve_formatting):
|
||||
if preserve_formatting:
|
||||
formats_regex = '|'.join(self._FORMATTING_TAGS)
|
||||
formats_regex = r'<\/?(?!\/?(' + formats_regex + r')\b).*?\b>'
|
||||
formats_regex = "|".join(self._FORMATTING_TAGS)
|
||||
formats_regex = r"<\/?(?!\/?(" + formats_regex + r")\b).*?\b>"
|
||||
html_regex = re.compile(formats_regex, re.IGNORECASE)
|
||||
else:
|
||||
html_regex = re.compile(r'<[^>]*>', re.IGNORECASE)
|
||||
html_regex = re.compile(r"<[^>]*>", re.IGNORECASE)
|
||||
return html_regex
|
||||
|
||||
def parse(self, plain_data):
|
||||
return [
|
||||
{
|
||||
'text': re.sub(self._html_regex, '', unescape(xml_element.text)),
|
||||
'start': float(xml_element.attrib['start']),
|
||||
'duration': float(xml_element.attrib.get('dur', '0.0')),
|
||||
"text": re.sub(self._html_regex, "", unescape(xml_element.text)),
|
||||
"start": float(xml_element.attrib["start"]),
|
||||
"duration": float(xml_element.attrib.get("dur", "0.0")),
|
||||
}
|
||||
for xml_element in ElementTree.fromstring(plain_data)
|
||||
if xml_element.text is not None
|
||||
|
|
|
@ -12,12 +12,16 @@ class Formatter(object):
|
|||
"""
|
||||
|
||||
def format_transcript(self, transcript, **kwargs):
|
||||
raise NotImplementedError('A subclass of Formatter must implement ' \
|
||||
'their own .format_transcript() method.')
|
||||
raise NotImplementedError(
|
||||
"A subclass of Formatter must implement "
|
||||
"their own .format_transcript() method."
|
||||
)
|
||||
|
||||
def format_transcripts(self, transcripts, **kwargs):
|
||||
raise NotImplementedError('A subclass of Formatter must implement ' \
|
||||
'their own .format_transcripts() method.')
|
||||
raise NotImplementedError(
|
||||
"A subclass of Formatter must implement "
|
||||
"their own .format_transcripts() method."
|
||||
)
|
||||
|
||||
|
||||
class PrettyPrintFormatter(Formatter):
|
||||
|
@ -68,7 +72,7 @@ class TextFormatter(Formatter):
|
|||
:return: all transcript text lines separated by newline breaks.'
|
||||
:rtype str
|
||||
"""
|
||||
return '\n'.join(line['text'] for line in transcript)
|
||||
return "\n".join(line["text"] for line in transcript)
|
||||
|
||||
def format_transcripts(self, transcripts, **kwargs):
|
||||
"""Converts a list of transcripts into plain text with no timestamps.
|
||||
|
@ -77,20 +81,29 @@ class TextFormatter(Formatter):
|
|||
:return: all transcript text lines separated by newline breaks.'
|
||||
:rtype str
|
||||
"""
|
||||
return '\n\n\n'.join([self.format_transcript(transcript, **kwargs) for transcript in transcripts])
|
||||
return "\n\n\n".join(
|
||||
[self.format_transcript(transcript, **kwargs) for transcript in transcripts]
|
||||
)
|
||||
|
||||
|
||||
class _TextBasedFormatter(TextFormatter):
|
||||
def _format_timestamp(self, hours, mins, secs, ms):
|
||||
raise NotImplementedError('A subclass of _TextBasedFormatter must implement ' \
|
||||
'their own .format_timestamp() method.')
|
||||
raise NotImplementedError(
|
||||
"A subclass of _TextBasedFormatter must implement "
|
||||
"their own .format_timestamp() method."
|
||||
)
|
||||
|
||||
def _format_transcript_header(self, lines):
|
||||
raise NotImplementedError('A subclass of _TextBasedFormatter must implement ' \
|
||||
'their own _format_transcript_header method.')
|
||||
raise NotImplementedError(
|
||||
"A subclass of _TextBasedFormatter must implement "
|
||||
"their own _format_transcript_header method."
|
||||
)
|
||||
|
||||
def _format_transcript_helper(self, i, time_text, line):
|
||||
raise NotImplementedError('A subclass of _TextBasedFormatter must implement ' \
|
||||
'their own _format_transcript_helper method.')
|
||||
raise NotImplementedError(
|
||||
"A subclass of _TextBasedFormatter must implement "
|
||||
"their own _format_transcript_helper method."
|
||||
)
|
||||
|
||||
def _seconds_to_timestamp(self, time):
|
||||
"""Helper that converts `time` into a transcript cue timestamp.
|
||||
|
@ -122,13 +135,14 @@ class _TextBasedFormatter(TextFormatter):
|
|||
"""
|
||||
lines = []
|
||||
for i, line in enumerate(transcript):
|
||||
end = line['start'] + line['duration']
|
||||
end = line["start"] + line["duration"]
|
||||
time_text = "{} --> {}".format(
|
||||
self._seconds_to_timestamp(line['start']),
|
||||
self._seconds_to_timestamp(line["start"]),
|
||||
self._seconds_to_timestamp(
|
||||
transcript[i + 1]['start']
|
||||
if i < len(transcript) - 1 and transcript[i + 1]['start'] < end else end
|
||||
)
|
||||
transcript[i + 1]["start"]
|
||||
if i < len(transcript) - 1 and transcript[i + 1]["start"] < end
|
||||
else end
|
||||
),
|
||||
)
|
||||
lines.append(self._format_transcript_helper(i, time_text, line))
|
||||
|
||||
|
@ -143,7 +157,7 @@ class SRTFormatter(_TextBasedFormatter):
|
|||
return "\n\n".join(lines) + "\n"
|
||||
|
||||
def _format_transcript_helper(self, i, time_text, line):
|
||||
return "{}\n{}\n{}".format(i + 1, time_text, line['text'])
|
||||
return "{}\n{}\n{}".format(i + 1, time_text, line["text"])
|
||||
|
||||
|
||||
class WebVTTFormatter(_TextBasedFormatter):
|
||||
|
@ -154,29 +168,29 @@ class WebVTTFormatter(_TextBasedFormatter):
|
|||
return "WEBVTT\n\n" + "\n\n".join(lines) + "\n"
|
||||
|
||||
def _format_transcript_helper(self, i, time_text, line):
|
||||
return "{}\n{}".format(time_text, line['text'])
|
||||
return "{}\n{}".format(time_text, line["text"])
|
||||
|
||||
|
||||
class FormatterLoader(object):
|
||||
TYPES = {
|
||||
'json': JSONFormatter,
|
||||
'pretty': PrettyPrintFormatter,
|
||||
'text': TextFormatter,
|
||||
'webvtt': WebVTTFormatter,
|
||||
'srt' : SRTFormatter,
|
||||
"json": JSONFormatter,
|
||||
"pretty": PrettyPrintFormatter,
|
||||
"text": TextFormatter,
|
||||
"webvtt": WebVTTFormatter,
|
||||
"srt": SRTFormatter,
|
||||
}
|
||||
|
||||
class UnknownFormatterType(Exception):
|
||||
def __init__(self, formatter_type):
|
||||
super(FormatterLoader.UnknownFormatterType, self).__init__(
|
||||
'The format \'{formatter_type}\' is not supported. '
|
||||
'Choose one of the following formats: {supported_formatter_types}'.format(
|
||||
"The format '{formatter_type}' is not supported. "
|
||||
"Choose one of the following formats: {supported_formatter_types}".format(
|
||||
formatter_type=formatter_type,
|
||||
supported_formatter_types=', '.join(FormatterLoader.TYPES.keys()),
|
||||
supported_formatter_types=", ".join(FormatterLoader.TYPES.keys()),
|
||||
)
|
||||
)
|
||||
|
||||
def load(self, formatter_type='pretty'):
|
||||
def load(self, formatter_type="pretty"):
|
||||
"""
|
||||
Loads the Formatter for the given formatter type.
|
||||
|
||||
|
|
|
@ -25,8 +25,9 @@ from youtube_transcript_api import (
|
|||
|
||||
|
||||
def load_asset(filename):
|
||||
filepath = '{dirname}/assets/{filename}'.format(
|
||||
dirname=os.path.dirname(__file__), filename=filename)
|
||||
filepath = "{dirname}/assets/{filename}".format(
|
||||
dirname=os.path.dirname(__file__), filename=filename
|
||||
)
|
||||
|
||||
with open(filepath, mode="rb") as file:
|
||||
return file.read()
|
||||
|
@ -37,13 +38,13 @@ class TestYouTubeTranscriptApi(TestCase):
|
|||
httpretty.enable()
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/watch',
|
||||
body=load_asset('youtube.html.static')
|
||||
"https://www.youtube.com/watch",
|
||||
body=load_asset("youtube.html.static"),
|
||||
)
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/api/timedtext',
|
||||
body=load_asset('transcript.xml.static')
|
||||
"https://www.youtube.com/api/timedtext",
|
||||
body=load_asset("transcript.xml.static"),
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -51,306 +52,362 @@ class TestYouTubeTranscriptApi(TestCase):
|
|||
httpretty.disable()
|
||||
|
||||
def test_get_transcript(self):
|
||||
transcript = YouTubeTranscriptApi.get_transcript('GJLlxj_dtq8')
|
||||
transcript = YouTubeTranscriptApi.get_transcript("GJLlxj_dtq8")
|
||||
|
||||
self.assertEqual(
|
||||
transcript,
|
||||
[
|
||||
{'text': 'Hey, this is just a test', 'start': 0.0, 'duration': 1.54},
|
||||
{'text': 'this is not the original transcript', 'start': 1.54, 'duration': 4.16},
|
||||
{'text': 'just something shorter, I made up for testing', 'start': 5.7, 'duration': 3.239}
|
||||
]
|
||||
{"text": "Hey, this is just a test", "start": 0.0, "duration": 1.54},
|
||||
{
|
||||
"text": "this is not the original transcript",
|
||||
"start": 1.54,
|
||||
"duration": 4.16,
|
||||
},
|
||||
{
|
||||
"text": "just something shorter, I made up for testing",
|
||||
"start": 5.7,
|
||||
"duration": 3.239,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def test_get_transcript_formatted(self):
|
||||
transcript = YouTubeTranscriptApi.get_transcript('GJLlxj_dtq8', preserve_formatting=True)
|
||||
transcript = YouTubeTranscriptApi.get_transcript(
|
||||
"GJLlxj_dtq8", preserve_formatting=True
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
transcript,
|
||||
[
|
||||
{'text': 'Hey, this is just a test', 'start': 0.0, 'duration': 1.54},
|
||||
{'text': 'this is <i>not</i> the original transcript', 'start': 1.54, 'duration': 4.16},
|
||||
{'text': 'just something shorter, I made up for testing', 'start': 5.7, 'duration': 3.239}
|
||||
]
|
||||
{"text": "Hey, this is just a test", "start": 0.0, "duration": 1.54},
|
||||
{
|
||||
"text": "this is <i>not</i> the original transcript",
|
||||
"start": 1.54,
|
||||
"duration": 4.16,
|
||||
},
|
||||
{
|
||||
"text": "just something shorter, I made up for testing",
|
||||
"start": 5.7,
|
||||
"duration": 3.239,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def test_list_transcripts(self):
|
||||
transcript_list = YouTubeTranscriptApi.list_transcripts('GJLlxj_dtq8')
|
||||
transcript_list = YouTubeTranscriptApi.list_transcripts("GJLlxj_dtq8")
|
||||
|
||||
language_codes = {transcript.language_code for transcript in transcript_list}
|
||||
|
||||
self.assertEqual(language_codes, {'zh', 'de', 'en', 'hi', 'ja', 'ko', 'es', 'cs', 'en'})
|
||||
self.assertEqual(
|
||||
language_codes, {"zh", "de", "en", "hi", "ja", "ko", "es", "cs", "en"}
|
||||
)
|
||||
|
||||
def test_list_transcripts__find_manually_created(self):
|
||||
transcript_list = YouTubeTranscriptApi.list_transcripts('GJLlxj_dtq8')
|
||||
transcript = transcript_list.find_manually_created_transcript(['cs'])
|
||||
transcript_list = YouTubeTranscriptApi.list_transcripts("GJLlxj_dtq8")
|
||||
transcript = transcript_list.find_manually_created_transcript(["cs"])
|
||||
|
||||
self.assertFalse(transcript.is_generated)
|
||||
|
||||
|
||||
def test_list_transcripts__find_generated(self):
|
||||
transcript_list = YouTubeTranscriptApi.list_transcripts('GJLlxj_dtq8')
|
||||
transcript_list = YouTubeTranscriptApi.list_transcripts("GJLlxj_dtq8")
|
||||
|
||||
with self.assertRaises(NoTranscriptFound):
|
||||
transcript_list.find_generated_transcript(['cs'])
|
||||
transcript_list.find_generated_transcript(["cs"])
|
||||
|
||||
transcript = transcript_list.find_generated_transcript(['en'])
|
||||
transcript = transcript_list.find_generated_transcript(["en"])
|
||||
|
||||
self.assertTrue(transcript.is_generated)
|
||||
|
||||
def test_list_transcripts__url_as_video_id(self):
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/watch',
|
||||
body=load_asset('youtube_transcripts_disabled.html.static')
|
||||
"https://www.youtube.com/watch",
|
||||
body=load_asset("youtube_transcripts_disabled.html.static"),
|
||||
)
|
||||
|
||||
with self.assertRaises(InvalidVideoId):
|
||||
YouTubeTranscriptApi.list_transcripts('https://www.youtube.com/watch?v=GJLlxj_dtq8')
|
||||
|
||||
YouTubeTranscriptApi.list_transcripts(
|
||||
"https://www.youtube.com/watch?v=GJLlxj_dtq8"
|
||||
)
|
||||
|
||||
def test_list_transcripts__no_translation_languages_provided(self):
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/watch',
|
||||
body=load_asset('youtube_no_translation_languages.html.static')
|
||||
"https://www.youtube.com/watch",
|
||||
body=load_asset("youtube_no_translation_languages.html.static"),
|
||||
)
|
||||
|
||||
transcript_list = YouTubeTranscriptApi.list_transcripts('GJLlxj_dtq8')
|
||||
transcript_list = YouTubeTranscriptApi.list_transcripts("GJLlxj_dtq8")
|
||||
for transcript in transcript_list:
|
||||
self.assertEqual(len(transcript.translation_languages), 0)
|
||||
|
||||
|
||||
def test_translate_transcript(self):
|
||||
transcript = YouTubeTranscriptApi.list_transcripts('GJLlxj_dtq8').find_transcript(['en'])
|
||||
transcript = YouTubeTranscriptApi.list_transcripts(
|
||||
"GJLlxj_dtq8"
|
||||
).find_transcript(["en"])
|
||||
|
||||
translated_transcript = transcript.translate('af')
|
||||
translated_transcript = transcript.translate("af")
|
||||
|
||||
self.assertEqual(translated_transcript.language_code, 'af')
|
||||
self.assertIn('&tlang=af', translated_transcript._url)
|
||||
self.assertEqual(translated_transcript.language_code, "af")
|
||||
self.assertIn("&tlang=af", translated_transcript._url)
|
||||
|
||||
def test_translate_transcript__translation_language_not_available(self):
|
||||
transcript = YouTubeTranscriptApi.list_transcripts('GJLlxj_dtq8').find_transcript(['en'])
|
||||
transcript = YouTubeTranscriptApi.list_transcripts(
|
||||
"GJLlxj_dtq8"
|
||||
).find_transcript(["en"])
|
||||
|
||||
with self.assertRaises(TranslationLanguageNotAvailable):
|
||||
transcript.translate('xyz')
|
||||
transcript.translate("xyz")
|
||||
|
||||
def test_translate_transcript__not_translatable(self):
|
||||
transcript = YouTubeTranscriptApi.list_transcripts('GJLlxj_dtq8').find_transcript(['en'])
|
||||
transcript = YouTubeTranscriptApi.list_transcripts(
|
||||
"GJLlxj_dtq8"
|
||||
).find_transcript(["en"])
|
||||
transcript.translation_languages = []
|
||||
|
||||
with self.assertRaises(NotTranslatable):
|
||||
transcript.translate('af')
|
||||
transcript.translate("af")
|
||||
|
||||
def test_get_transcript__correct_language_is_used(self):
|
||||
YouTubeTranscriptApi.get_transcript('GJLlxj_dtq8', ['de', 'en'])
|
||||
YouTubeTranscriptApi.get_transcript("GJLlxj_dtq8", ["de", "en"])
|
||||
query_string = httpretty.last_request().querystring
|
||||
|
||||
self.assertIn('lang', query_string)
|
||||
self.assertEqual(len(query_string['lang']), 1)
|
||||
self.assertEqual(query_string['lang'][0], 'de')
|
||||
self.assertIn("lang", query_string)
|
||||
self.assertEqual(len(query_string["lang"]), 1)
|
||||
self.assertEqual(query_string["lang"][0], "de")
|
||||
|
||||
def test_get_transcript__fallback_language_is_used(self):
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/watch',
|
||||
body=load_asset('youtube_ww1_nl_en.html.static')
|
||||
"https://www.youtube.com/watch",
|
||||
body=load_asset("youtube_ww1_nl_en.html.static"),
|
||||
)
|
||||
|
||||
YouTubeTranscriptApi.get_transcript('F1xioXWb8CY', ['de', 'en'])
|
||||
YouTubeTranscriptApi.get_transcript("F1xioXWb8CY", ["de", "en"])
|
||||
query_string = httpretty.last_request().querystring
|
||||
|
||||
self.assertIn('lang', query_string)
|
||||
self.assertEqual(len(query_string['lang']), 1)
|
||||
self.assertEqual(query_string['lang'][0], 'en')
|
||||
self.assertIn("lang", query_string)
|
||||
self.assertEqual(len(query_string["lang"]), 1)
|
||||
self.assertEqual(query_string["lang"][0], "en")
|
||||
|
||||
def test_get_transcript__create_consent_cookie_if_needed(self):
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/watch',
|
||||
body=load_asset('youtube_consent_page.html.static')
|
||||
"https://www.youtube.com/watch",
|
||||
body=load_asset("youtube_consent_page.html.static"),
|
||||
)
|
||||
|
||||
YouTubeTranscriptApi.get_transcript('F1xioXWb8CY')
|
||||
YouTubeTranscriptApi.get_transcript("F1xioXWb8CY")
|
||||
self.assertEqual(len(httpretty.latest_requests()), 3)
|
||||
for request in httpretty.latest_requests()[1:]:
|
||||
self.assertEqual(request.headers['cookie'], 'CONSENT=YES+cb.20210328-17-p0.de+FX+119')
|
||||
self.assertEqual(
|
||||
request.headers["cookie"], "CONSENT=YES+cb.20210328-17-p0.de+FX+119"
|
||||
)
|
||||
|
||||
def test_get_transcript__exception_if_create_consent_cookie_failed(self):
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/watch',
|
||||
body=load_asset('youtube_consent_page.html.static')
|
||||
"https://www.youtube.com/watch",
|
||||
body=load_asset("youtube_consent_page.html.static"),
|
||||
)
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/watch',
|
||||
body=load_asset('youtube_consent_page.html.static')
|
||||
"https://www.youtube.com/watch",
|
||||
body=load_asset("youtube_consent_page.html.static"),
|
||||
)
|
||||
|
||||
with self.assertRaises(FailedToCreateConsentCookie):
|
||||
YouTubeTranscriptApi.get_transcript('F1xioXWb8CY')
|
||||
YouTubeTranscriptApi.get_transcript("F1xioXWb8CY")
|
||||
|
||||
def test_get_transcript__exception_if_consent_cookie_age_invalid(self):
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/watch',
|
||||
body=load_asset('youtube_consent_page_invalid.html.static')
|
||||
"https://www.youtube.com/watch",
|
||||
body=load_asset("youtube_consent_page_invalid.html.static"),
|
||||
)
|
||||
|
||||
with self.assertRaises(FailedToCreateConsentCookie):
|
||||
YouTubeTranscriptApi.get_transcript('F1xioXWb8CY')
|
||||
YouTubeTranscriptApi.get_transcript("F1xioXWb8CY")
|
||||
|
||||
def test_get_transcript__exception_if_video_unavailable(self):
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/watch',
|
||||
body=load_asset('youtube_video_unavailable.html.static')
|
||||
"https://www.youtube.com/watch",
|
||||
body=load_asset("youtube_video_unavailable.html.static"),
|
||||
)
|
||||
|
||||
with self.assertRaises(VideoUnavailable):
|
||||
YouTubeTranscriptApi.get_transcript('abc')
|
||||
YouTubeTranscriptApi.get_transcript("abc")
|
||||
|
||||
def test_get_transcript__exception_if_youtube_request_fails(self):
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/watch',
|
||||
status=500
|
||||
httpretty.GET, "https://www.youtube.com/watch", status=500
|
||||
)
|
||||
|
||||
with self.assertRaises(YouTubeRequestFailed):
|
||||
YouTubeTranscriptApi.get_transcript('abc')
|
||||
YouTubeTranscriptApi.get_transcript("abc")
|
||||
|
||||
def test_get_transcript__exception_if_youtube_request_limit_reached(self):
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/watch',
|
||||
body=load_asset('youtube_too_many_requests.html.static')
|
||||
"https://www.youtube.com/watch",
|
||||
body=load_asset("youtube_too_many_requests.html.static"),
|
||||
)
|
||||
|
||||
with self.assertRaises(TooManyRequests):
|
||||
YouTubeTranscriptApi.get_transcript('abc')
|
||||
YouTubeTranscriptApi.get_transcript("abc")
|
||||
|
||||
def test_get_transcript__exception_if_transcripts_disabled(self):
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/watch',
|
||||
body=load_asset('youtube_transcripts_disabled.html.static')
|
||||
"https://www.youtube.com/watch",
|
||||
body=load_asset("youtube_transcripts_disabled.html.static"),
|
||||
)
|
||||
|
||||
with self.assertRaises(TranscriptsDisabled):
|
||||
YouTubeTranscriptApi.get_transcript('dsMFmonKDD4')
|
||||
YouTubeTranscriptApi.get_transcript("dsMFmonKDD4")
|
||||
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/watch',
|
||||
body=load_asset('youtube_transcripts_disabled2.html.static')
|
||||
"https://www.youtube.com/watch",
|
||||
body=load_asset("youtube_transcripts_disabled2.html.static"),
|
||||
)
|
||||
with self.assertRaises(TranscriptsDisabled):
|
||||
YouTubeTranscriptApi.get_transcript('Fjg5lYqvzUs')
|
||||
YouTubeTranscriptApi.get_transcript("Fjg5lYqvzUs")
|
||||
|
||||
def test_get_transcript__exception_if_language_unavailable(self):
|
||||
with self.assertRaises(NoTranscriptFound):
|
||||
YouTubeTranscriptApi.get_transcript('GJLlxj_dtq8', languages=['cz'])
|
||||
YouTubeTranscriptApi.get_transcript("GJLlxj_dtq8", languages=["cz"])
|
||||
|
||||
def test_get_transcript__exception_if_no_transcript_available(self):
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'https://www.youtube.com/watch',
|
||||
body=load_asset('youtube_no_transcript_available.html.static')
|
||||
"https://www.youtube.com/watch",
|
||||
body=load_asset("youtube_no_transcript_available.html.static"),
|
||||
)
|
||||
|
||||
with self.assertRaises(NoTranscriptAvailable):
|
||||
YouTubeTranscriptApi.get_transcript('MwBPvcYFY2E')
|
||||
YouTubeTranscriptApi.get_transcript("MwBPvcYFY2E")
|
||||
|
||||
def test_get_transcript__with_proxy(self):
|
||||
proxies = {'http': '', 'https:': ''}
|
||||
transcript = YouTubeTranscriptApi.get_transcript(
|
||||
'GJLlxj_dtq8', proxies=proxies
|
||||
)
|
||||
proxies = {"http": "", "https:": ""}
|
||||
transcript = YouTubeTranscriptApi.get_transcript("GJLlxj_dtq8", proxies=proxies)
|
||||
self.assertEqual(
|
||||
transcript,
|
||||
[
|
||||
{'text': 'Hey, this is just a test', 'start': 0.0, 'duration': 1.54},
|
||||
{'text': 'this is not the original transcript', 'start': 1.54, 'duration': 4.16},
|
||||
{'text': 'just something shorter, I made up for testing', 'start': 5.7, 'duration': 3.239}
|
||||
]
|
||||
{"text": "Hey, this is just a test", "start": 0.0, "duration": 1.54},
|
||||
{
|
||||
"text": "this is not the original transcript",
|
||||
"start": 1.54,
|
||||
"duration": 4.16,
|
||||
},
|
||||
{
|
||||
"text": "just something shorter, I made up for testing",
|
||||
"start": 5.7,
|
||||
"duration": 3.239,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def test_get_transcript__with_cookies(self):
|
||||
dirname, filename = os.path.split(os.path.abspath(__file__))
|
||||
cookies = dirname + '/example_cookies.txt'
|
||||
transcript = YouTubeTranscriptApi.get_transcript('GJLlxj_dtq8', cookies=cookies)
|
||||
cookies = dirname + "/example_cookies.txt"
|
||||
transcript = YouTubeTranscriptApi.get_transcript("GJLlxj_dtq8", cookies=cookies)
|
||||
|
||||
self.assertEqual(
|
||||
transcript,
|
||||
[
|
||||
{'text': 'Hey, this is just a test', 'start': 0.0, 'duration': 1.54},
|
||||
{'text': 'this is not the original transcript', 'start': 1.54, 'duration': 4.16},
|
||||
{'text': 'just something shorter, I made up for testing', 'start': 5.7, 'duration': 3.239}
|
||||
]
|
||||
{"text": "Hey, this is just a test", "start": 0.0, "duration": 1.54},
|
||||
{
|
||||
"text": "this is not the original transcript",
|
||||
"start": 1.54,
|
||||
"duration": 4.16,
|
||||
},
|
||||
{
|
||||
"text": "just something shorter, I made up for testing",
|
||||
"start": 5.7,
|
||||
"duration": 3.239,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def test_get_transcript__assertionerror_if_input_not_string(self):
|
||||
with self.assertRaises(AssertionError):
|
||||
YouTubeTranscriptApi.get_transcript(['video_id_1', 'video_id_2'])
|
||||
YouTubeTranscriptApi.get_transcript(["video_id_1", "video_id_2"])
|
||||
|
||||
def test_get_transcripts__assertionerror_if_input_not_list(self):
|
||||
with self.assertRaises(AssertionError):
|
||||
YouTubeTranscriptApi.get_transcripts('video_id_1')
|
||||
YouTubeTranscriptApi.get_transcripts("video_id_1")
|
||||
|
||||
@patch('youtube_transcript_api.YouTubeTranscriptApi.get_transcript')
|
||||
@patch("youtube_transcript_api.YouTubeTranscriptApi.get_transcript")
|
||||
def test_get_transcripts(self, mock_get_transcript):
|
||||
video_id_1 = 'video_id_1'
|
||||
video_id_2 = 'video_id_2'
|
||||
languages = ['de', 'en']
|
||||
video_id_1 = "video_id_1"
|
||||
video_id_2 = "video_id_2"
|
||||
languages = ["de", "en"]
|
||||
|
||||
YouTubeTranscriptApi.get_transcripts([video_id_1, video_id_2], languages=languages)
|
||||
YouTubeTranscriptApi.get_transcripts(
|
||||
[video_id_1, video_id_2], languages=languages
|
||||
)
|
||||
|
||||
mock_get_transcript.assert_any_call(video_id_1, languages, None, None, False)
|
||||
mock_get_transcript.assert_any_call(video_id_2, languages, None, None, False)
|
||||
self.assertEqual(mock_get_transcript.call_count, 2)
|
||||
|
||||
@patch('youtube_transcript_api.YouTubeTranscriptApi.get_transcript', side_effect=Exception('Error'))
|
||||
@patch(
|
||||
"youtube_transcript_api.YouTubeTranscriptApi.get_transcript",
|
||||
side_effect=Exception("Error"),
|
||||
)
|
||||
def test_get_transcripts__stop_on_error(self, mock_get_transcript):
|
||||
with self.assertRaises(Exception):
|
||||
YouTubeTranscriptApi.get_transcripts(['video_id_1', 'video_id_2'])
|
||||
YouTubeTranscriptApi.get_transcripts(["video_id_1", "video_id_2"])
|
||||
|
||||
@patch('youtube_transcript_api.YouTubeTranscriptApi.get_transcript', side_effect=Exception('Error'))
|
||||
@patch(
|
||||
"youtube_transcript_api.YouTubeTranscriptApi.get_transcript",
|
||||
side_effect=Exception("Error"),
|
||||
)
|
||||
def test_get_transcripts__continue_on_error(self, mock_get_transcript):
|
||||
video_id_1 = 'video_id_1'
|
||||
video_id_2 = 'video_id_2'
|
||||
video_id_1 = "video_id_1"
|
||||
video_id_2 = "video_id_2"
|
||||
|
||||
YouTubeTranscriptApi.get_transcripts(['video_id_1', 'video_id_2'], continue_after_error=True)
|
||||
YouTubeTranscriptApi.get_transcripts(
|
||||
["video_id_1", "video_id_2"], continue_after_error=True
|
||||
)
|
||||
|
||||
mock_get_transcript.assert_any_call(video_id_1, ('en',), None, None, False)
|
||||
mock_get_transcript.assert_any_call(video_id_2, ('en',), None, None, False)
|
||||
mock_get_transcript.assert_any_call(video_id_1, ("en",), None, None, False)
|
||||
mock_get_transcript.assert_any_call(video_id_2, ("en",), None, None, False)
|
||||
|
||||
@patch('youtube_transcript_api.YouTubeTranscriptApi.get_transcript')
|
||||
@patch("youtube_transcript_api.YouTubeTranscriptApi.get_transcript")
|
||||
def test_get_transcripts__with_cookies(self, mock_get_transcript):
|
||||
cookies = '/example_cookies.txt'
|
||||
YouTubeTranscriptApi.get_transcripts(['GJLlxj_dtq8'], cookies=cookies)
|
||||
mock_get_transcript.assert_any_call('GJLlxj_dtq8', ('en',), None, cookies, False)
|
||||
cookies = "/example_cookies.txt"
|
||||
YouTubeTranscriptApi.get_transcripts(["GJLlxj_dtq8"], cookies=cookies)
|
||||
mock_get_transcript.assert_any_call(
|
||||
"GJLlxj_dtq8", ("en",), None, cookies, False
|
||||
)
|
||||
|
||||
@patch('youtube_transcript_api.YouTubeTranscriptApi.get_transcript')
|
||||
@patch("youtube_transcript_api.YouTubeTranscriptApi.get_transcript")
|
||||
def test_get_transcripts__with_proxies(self, mock_get_transcript):
|
||||
proxies = {'http': '', 'https:': ''}
|
||||
YouTubeTranscriptApi.get_transcripts(['GJLlxj_dtq8'], proxies=proxies)
|
||||
mock_get_transcript.assert_any_call('GJLlxj_dtq8', ('en',), proxies, None, False)
|
||||
proxies = {"http": "", "https:": ""}
|
||||
YouTubeTranscriptApi.get_transcripts(["GJLlxj_dtq8"], proxies=proxies)
|
||||
mock_get_transcript.assert_any_call(
|
||||
"GJLlxj_dtq8", ("en",), proxies, None, False
|
||||
)
|
||||
|
||||
def test_load_cookies(self):
|
||||
dirname, filename = os.path.split(os.path.abspath(__file__))
|
||||
cookies = dirname + '/example_cookies.txt'
|
||||
session_cookies = YouTubeTranscriptApi._load_cookies(cookies, 'GJLlxj_dtq8')
|
||||
self.assertEqual({'TEST_FIELD': 'TEST_VALUE'}, requests.utils.dict_from_cookiejar(session_cookies))
|
||||
cookies = dirname + "/example_cookies.txt"
|
||||
session_cookies = YouTubeTranscriptApi._load_cookies(cookies, "GJLlxj_dtq8")
|
||||
self.assertEqual(
|
||||
{"TEST_FIELD": "TEST_VALUE"},
|
||||
requests.utils.dict_from_cookiejar(session_cookies),
|
||||
)
|
||||
|
||||
def test_load_cookies__bad_file_path(self):
|
||||
bad_cookies = 'nonexistent_cookies.txt'
|
||||
bad_cookies = "nonexistent_cookies.txt"
|
||||
with self.assertRaises(CookiePathInvalid):
|
||||
YouTubeTranscriptApi._load_cookies(bad_cookies, 'GJLlxj_dtq8')
|
||||
YouTubeTranscriptApi._load_cookies(bad_cookies, "GJLlxj_dtq8")
|
||||
|
||||
def test_load_cookies__no_valid_cookies(self):
|
||||
dirname, filename = os.path.split(os.path.abspath(__file__))
|
||||
expired_cookies = dirname + '/expired_example_cookies.txt'
|
||||
expired_cookies = dirname + "/expired_example_cookies.txt"
|
||||
with self.assertRaises(CookiesInvalid):
|
||||
YouTubeTranscriptApi._load_cookies(expired_cookies, 'GJLlxj_dtq8')
|
||||
YouTubeTranscriptApi._load_cookies(expired_cookies, "GJLlxj_dtq8")
|
||||
|
|
|
@ -10,211 +10,269 @@ from youtube_transcript_api._cli import YouTubeTranscriptCli
|
|||
class TestYouTubeTranscriptCli(TestCase):
|
||||
def setUp(self):
|
||||
self.transcript_mock = MagicMock()
|
||||
self.transcript_mock.fetch = MagicMock(return_value=[
|
||||
{'text': 'Hey, this is just a test', 'start': 0.0, 'duration': 1.54},
|
||||
{'text': 'this is <i>not</i> the original transcript', 'start': 1.54, 'duration': 4.16},
|
||||
{'text': 'just something shorter, I made up for testing', 'start': 5.7, 'duration': 3.239}
|
||||
])
|
||||
self.transcript_mock.fetch = MagicMock(
|
||||
return_value=[
|
||||
{"text": "Hey, this is just a test", "start": 0.0, "duration": 1.54},
|
||||
{
|
||||
"text": "this is <i>not</i> the original transcript",
|
||||
"start": 1.54,
|
||||
"duration": 4.16,
|
||||
},
|
||||
{
|
||||
"text": "just something shorter, I made up for testing",
|
||||
"start": 5.7,
|
||||
"duration": 3.239,
|
||||
},
|
||||
]
|
||||
)
|
||||
self.transcript_mock.translate = MagicMock(return_value=self.transcript_mock)
|
||||
|
||||
self.transcript_list_mock = MagicMock()
|
||||
self.transcript_list_mock.find_generated_transcript = MagicMock(return_value=self.transcript_mock)
|
||||
self.transcript_list_mock.find_manually_created_transcript = MagicMock(return_value=self.transcript_mock)
|
||||
self.transcript_list_mock.find_transcript = MagicMock(return_value=self.transcript_mock)
|
||||
self.transcript_list_mock.find_generated_transcript = MagicMock(
|
||||
return_value=self.transcript_mock
|
||||
)
|
||||
self.transcript_list_mock.find_manually_created_transcript = MagicMock(
|
||||
return_value=self.transcript_mock
|
||||
)
|
||||
self.transcript_list_mock.find_transcript = MagicMock(
|
||||
return_value=self.transcript_mock
|
||||
)
|
||||
|
||||
YouTubeTranscriptApi.list_transcripts = MagicMock(return_value=self.transcript_list_mock)
|
||||
YouTubeTranscriptApi.list_transcripts = MagicMock(
|
||||
return_value=self.transcript_list_mock
|
||||
)
|
||||
|
||||
def test_argument_parsing(self):
|
||||
parsed_args = YouTubeTranscriptCli('v1 v2 --format json --languages de en'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
self.assertEqual(parsed_args.format, 'json')
|
||||
self.assertEqual(parsed_args.languages, ['de', 'en'])
|
||||
self.assertEqual(parsed_args.http_proxy, '')
|
||||
self.assertEqual(parsed_args.https_proxy, '')
|
||||
|
||||
parsed_args = YouTubeTranscriptCli('v1 v2 --languages de en --format json'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
self.assertEqual(parsed_args.format, 'json')
|
||||
self.assertEqual(parsed_args.languages, ['de', 'en'])
|
||||
self.assertEqual(parsed_args.http_proxy, '')
|
||||
self.assertEqual(parsed_args.https_proxy, '')
|
||||
|
||||
parsed_args = YouTubeTranscriptCli(' --format json v1 v2 --languages de en'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
self.assertEqual(parsed_args.format, 'json')
|
||||
self.assertEqual(parsed_args.languages, ['de', 'en'])
|
||||
self.assertEqual(parsed_args.http_proxy, '')
|
||||
self.assertEqual(parsed_args.https_proxy, '')
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
"v1 v2 --format json --languages de en".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertEqual(parsed_args.format, "json")
|
||||
self.assertEqual(parsed_args.languages, ["de", "en"])
|
||||
self.assertEqual(parsed_args.http_proxy, "")
|
||||
self.assertEqual(parsed_args.https_proxy, "")
|
||||
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
'v1 v2 --languages de en --format json '
|
||||
'--http-proxy http://user:pass@domain:port '
|
||||
'--https-proxy https://user:pass@domain:port'.split()
|
||||
"v1 v2 --languages de en --format json".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
self.assertEqual(parsed_args.format, 'json')
|
||||
self.assertEqual(parsed_args.languages, ['de', 'en'])
|
||||
self.assertEqual(parsed_args.http_proxy, 'http://user:pass@domain:port')
|
||||
self.assertEqual(parsed_args.https_proxy, 'https://user:pass@domain:port')
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertEqual(parsed_args.format, "json")
|
||||
self.assertEqual(parsed_args.languages, ["de", "en"])
|
||||
self.assertEqual(parsed_args.http_proxy, "")
|
||||
self.assertEqual(parsed_args.https_proxy, "")
|
||||
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
'v1 v2 --languages de en --format json --http-proxy http://user:pass@domain:port'.split()
|
||||
" --format json v1 v2 --languages de en".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
self.assertEqual(parsed_args.format, 'json')
|
||||
self.assertEqual(parsed_args.languages, ['de', 'en'])
|
||||
self.assertEqual(parsed_args.http_proxy, 'http://user:pass@domain:port')
|
||||
self.assertEqual(parsed_args.https_proxy, '')
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertEqual(parsed_args.format, "json")
|
||||
self.assertEqual(parsed_args.languages, ["de", "en"])
|
||||
self.assertEqual(parsed_args.http_proxy, "")
|
||||
self.assertEqual(parsed_args.https_proxy, "")
|
||||
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
'v1 v2 --languages de en --format json --https-proxy https://user:pass@domain:port'.split()
|
||||
"v1 v2 --languages de en --format json "
|
||||
"--http-proxy http://user:pass@domain:port "
|
||||
"--https-proxy https://user:pass@domain:port".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
self.assertEqual(parsed_args.format, 'json')
|
||||
self.assertEqual(parsed_args.languages, ['de', 'en'])
|
||||
self.assertEqual(parsed_args.https_proxy, 'https://user:pass@domain:port')
|
||||
self.assertEqual(parsed_args.http_proxy, '')
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertEqual(parsed_args.format, "json")
|
||||
self.assertEqual(parsed_args.languages, ["de", "en"])
|
||||
self.assertEqual(parsed_args.http_proxy, "http://user:pass@domain:port")
|
||||
self.assertEqual(parsed_args.https_proxy, "https://user:pass@domain:port")
|
||||
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
"v1 v2 --languages de en --format json --http-proxy http://user:pass@domain:port".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertEqual(parsed_args.format, "json")
|
||||
self.assertEqual(parsed_args.languages, ["de", "en"])
|
||||
self.assertEqual(parsed_args.http_proxy, "http://user:pass@domain:port")
|
||||
self.assertEqual(parsed_args.https_proxy, "")
|
||||
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
"v1 v2 --languages de en --format json --https-proxy https://user:pass@domain:port".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertEqual(parsed_args.format, "json")
|
||||
self.assertEqual(parsed_args.languages, ["de", "en"])
|
||||
self.assertEqual(parsed_args.https_proxy, "https://user:pass@domain:port")
|
||||
self.assertEqual(parsed_args.http_proxy, "")
|
||||
|
||||
def test_argument_parsing__only_video_ids(self):
|
||||
parsed_args = YouTubeTranscriptCli('v1 v2'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
self.assertEqual(parsed_args.format, 'pretty')
|
||||
self.assertEqual(parsed_args.languages, ['en'])
|
||||
parsed_args = YouTubeTranscriptCli("v1 v2".split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertEqual(parsed_args.format, "pretty")
|
||||
self.assertEqual(parsed_args.languages, ["en"])
|
||||
|
||||
def test_argument_parsing__video_ids_starting_with_dash(self):
|
||||
parsed_args = YouTubeTranscriptCli('\-v1 \-\-v2 \--v3'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['-v1', '--v2', '--v3'])
|
||||
self.assertEqual(parsed_args.format, 'pretty')
|
||||
self.assertEqual(parsed_args.languages, ['en'])
|
||||
parsed_args = YouTubeTranscriptCli("\-v1 \-\-v2 \--v3".split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["-v1", "--v2", "--v3"])
|
||||
self.assertEqual(parsed_args.format, "pretty")
|
||||
self.assertEqual(parsed_args.languages, ["en"])
|
||||
|
||||
def test_argument_parsing__fail_without_video_ids(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
YouTubeTranscriptCli('--format json'.split())._parse_args()
|
||||
YouTubeTranscriptCli("--format json".split())._parse_args()
|
||||
|
||||
def test_argument_parsing__json(self):
|
||||
parsed_args = YouTubeTranscriptCli('v1 v2 --format json'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
self.assertEqual(parsed_args.format, 'json')
|
||||
self.assertEqual(parsed_args.languages, ['en'])
|
||||
parsed_args = YouTubeTranscriptCli("v1 v2 --format json".split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertEqual(parsed_args.format, "json")
|
||||
self.assertEqual(parsed_args.languages, ["en"])
|
||||
|
||||
parsed_args = YouTubeTranscriptCli('--format json v1 v2'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
self.assertEqual(parsed_args.format, 'json')
|
||||
self.assertEqual(parsed_args.languages, ['en'])
|
||||
parsed_args = YouTubeTranscriptCli("--format json v1 v2".split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertEqual(parsed_args.format, "json")
|
||||
self.assertEqual(parsed_args.languages, ["en"])
|
||||
|
||||
def test_argument_parsing__languages(self):
|
||||
parsed_args = YouTubeTranscriptCli('v1 v2 --languages de en'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
self.assertEqual(parsed_args.format, 'pretty')
|
||||
self.assertEqual(parsed_args.languages, ['de', 'en'])
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
"v1 v2 --languages de en".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertEqual(parsed_args.format, "pretty")
|
||||
self.assertEqual(parsed_args.languages, ["de", "en"])
|
||||
|
||||
def test_argument_parsing__proxies(self):
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
'v1 v2 --http-proxy http://user:pass@domain:port'.split()
|
||||
"v1 v2 --http-proxy http://user:pass@domain:port".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.http_proxy, 'http://user:pass@domain:port')
|
||||
self.assertEqual(parsed_args.http_proxy, "http://user:pass@domain:port")
|
||||
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
'v1 v2 --https-proxy https://user:pass@domain:port'.split()
|
||||
"v1 v2 --https-proxy https://user:pass@domain:port".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.https_proxy, 'https://user:pass@domain:port')
|
||||
self.assertEqual(parsed_args.https_proxy, "https://user:pass@domain:port")
|
||||
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
'v1 v2 --http-proxy http://user:pass@domain:port --https-proxy https://user:pass@domain:port'.split()
|
||||
"v1 v2 --http-proxy http://user:pass@domain:port --https-proxy https://user:pass@domain:port".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.http_proxy, 'http://user:pass@domain:port')
|
||||
self.assertEqual(parsed_args.https_proxy, 'https://user:pass@domain:port')
|
||||
self.assertEqual(parsed_args.http_proxy, "http://user:pass@domain:port")
|
||||
self.assertEqual(parsed_args.https_proxy, "https://user:pass@domain:port")
|
||||
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
'v1 v2'.split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.http_proxy, '')
|
||||
self.assertEqual(parsed_args.https_proxy, '')
|
||||
parsed_args = YouTubeTranscriptCli("v1 v2".split())._parse_args()
|
||||
self.assertEqual(parsed_args.http_proxy, "")
|
||||
self.assertEqual(parsed_args.https_proxy, "")
|
||||
|
||||
def test_argument_parsing__list_transcripts(self):
|
||||
parsed_args = YouTubeTranscriptCli('--list-transcripts v1 v2'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
"--list-transcripts v1 v2".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertTrue(parsed_args.list_transcripts)
|
||||
|
||||
parsed_args = YouTubeTranscriptCli('v1 v2 --list-transcripts'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
"v1 v2 --list-transcripts".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertTrue(parsed_args.list_transcripts)
|
||||
|
||||
def test_argument_parsing__translate(self):
|
||||
parsed_args = YouTubeTranscriptCli('v1 v2 --languages de en --translate cz'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
self.assertEqual(parsed_args.format, 'pretty')
|
||||
self.assertEqual(parsed_args.languages, ['de', 'en'])
|
||||
self.assertEqual(parsed_args.translate, 'cz')
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
"v1 v2 --languages de en --translate cz".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertEqual(parsed_args.format, "pretty")
|
||||
self.assertEqual(parsed_args.languages, ["de", "en"])
|
||||
self.assertEqual(parsed_args.translate, "cz")
|
||||
|
||||
parsed_args = YouTubeTranscriptCli('v1 v2 --translate cz --languages de en'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
self.assertEqual(parsed_args.format, 'pretty')
|
||||
self.assertEqual(parsed_args.languages, ['de', 'en'])
|
||||
self.assertEqual(parsed_args.translate, 'cz')
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
"v1 v2 --translate cz --languages de en".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertEqual(parsed_args.format, "pretty")
|
||||
self.assertEqual(parsed_args.languages, ["de", "en"])
|
||||
self.assertEqual(parsed_args.translate, "cz")
|
||||
|
||||
def test_argument_parsing__manually_or_generated(self):
|
||||
parsed_args = YouTubeTranscriptCli('v1 v2 --exclude-manually-created'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
"v1 v2 --exclude-manually-created".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertTrue(parsed_args.exclude_manually_created)
|
||||
self.assertFalse(parsed_args.exclude_generated)
|
||||
|
||||
parsed_args = YouTubeTranscriptCli('v1 v2 --exclude-generated'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
"v1 v2 --exclude-generated".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertFalse(parsed_args.exclude_manually_created)
|
||||
self.assertTrue(parsed_args.exclude_generated)
|
||||
|
||||
parsed_args = YouTubeTranscriptCli('v1 v2 --exclude-manually-created --exclude-generated'.split())._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ['v1', 'v2'])
|
||||
parsed_args = YouTubeTranscriptCli(
|
||||
"v1 v2 --exclude-manually-created --exclude-generated".split()
|
||||
)._parse_args()
|
||||
self.assertEqual(parsed_args.video_ids, ["v1", "v2"])
|
||||
self.assertTrue(parsed_args.exclude_manually_created)
|
||||
self.assertTrue(parsed_args.exclude_generated)
|
||||
|
||||
def test_run(self):
|
||||
YouTubeTranscriptCli('v1 v2 --languages de en'.split()).run()
|
||||
YouTubeTranscriptCli("v1 v2 --languages de en".split()).run()
|
||||
|
||||
YouTubeTranscriptApi.list_transcripts.assert_any_call('v1', proxies=None, cookies=None)
|
||||
YouTubeTranscriptApi.list_transcripts.assert_any_call('v2', proxies=None, cookies=None)
|
||||
YouTubeTranscriptApi.list_transcripts.assert_any_call(
|
||||
"v1", proxies=None, cookies=None
|
||||
)
|
||||
YouTubeTranscriptApi.list_transcripts.assert_any_call(
|
||||
"v2", proxies=None, cookies=None
|
||||
)
|
||||
|
||||
self.transcript_list_mock.find_transcript.assert_any_call(['de', 'en'])
|
||||
self.transcript_list_mock.find_transcript.assert_any_call(["de", "en"])
|
||||
|
||||
def test_run__failing_transcripts(self):
|
||||
YouTubeTranscriptApi.list_transcripts = MagicMock(side_effect=VideoUnavailable('video_id'))
|
||||
YouTubeTranscriptApi.list_transcripts = MagicMock(
|
||||
side_effect=VideoUnavailable("video_id")
|
||||
)
|
||||
|
||||
output = YouTubeTranscriptCli('v1 --languages de en'.split()).run()
|
||||
output = YouTubeTranscriptCli("v1 --languages de en".split()).run()
|
||||
|
||||
self.assertEqual(output, str(VideoUnavailable('video_id')))
|
||||
self.assertEqual(output, str(VideoUnavailable("video_id")))
|
||||
|
||||
def test_run__exclude_generated(self):
|
||||
YouTubeTranscriptCli('v1 v2 --languages de en --exclude-generated'.split()).run()
|
||||
YouTubeTranscriptCli(
|
||||
"v1 v2 --languages de en --exclude-generated".split()
|
||||
).run()
|
||||
|
||||
self.transcript_list_mock.find_manually_created_transcript.assert_any_call(['de', 'en'])
|
||||
self.transcript_list_mock.find_manually_created_transcript.assert_any_call(
|
||||
["de", "en"]
|
||||
)
|
||||
|
||||
def test_run__exclude_manually_created(self):
|
||||
YouTubeTranscriptCli('v1 v2 --languages de en --exclude-manually-created'.split()).run()
|
||||
YouTubeTranscriptCli(
|
||||
"v1 v2 --languages de en --exclude-manually-created".split()
|
||||
).run()
|
||||
|
||||
self.transcript_list_mock.find_generated_transcript.assert_any_call(['de', 'en'])
|
||||
self.transcript_list_mock.find_generated_transcript.assert_any_call(
|
||||
["de", "en"]
|
||||
)
|
||||
|
||||
def test_run__exclude_manually_created_and_generated(self):
|
||||
self.assertEqual(
|
||||
YouTubeTranscriptCli(
|
||||
'v1 v2 --languages de en --exclude-manually-created --exclude-generated'.split()
|
||||
"v1 v2 --languages de en --exclude-manually-created --exclude-generated".split()
|
||||
).run(),
|
||||
''
|
||||
"",
|
||||
)
|
||||
|
||||
def test_run__translate(self):
|
||||
YouTubeTranscriptCli('v1 v2 --languages de en --translate cz'.split()).run(),
|
||||
(YouTubeTranscriptCli("v1 v2 --languages de en --translate cz".split()).run(),)
|
||||
|
||||
self.transcript_mock.translate.assert_any_call('cz')
|
||||
self.transcript_mock.translate.assert_any_call("cz")
|
||||
|
||||
def test_run__list_transcripts(self):
|
||||
YouTubeTranscriptCli('--list-transcripts v1 v2'.split()).run()
|
||||
YouTubeTranscriptCli("--list-transcripts v1 v2".split()).run()
|
||||
|
||||
YouTubeTranscriptApi.list_transcripts.assert_any_call('v1', proxies=None, cookies=None)
|
||||
YouTubeTranscriptApi.list_transcripts.assert_any_call('v2', proxies=None, cookies=None)
|
||||
YouTubeTranscriptApi.list_transcripts.assert_any_call(
|
||||
"v1", proxies=None, cookies=None
|
||||
)
|
||||
YouTubeTranscriptApi.list_transcripts.assert_any_call(
|
||||
"v2", proxies=None, cookies=None
|
||||
)
|
||||
|
||||
def test_run__json_output(self):
|
||||
output = YouTubeTranscriptCli('v1 v2 --languages de en --format json'.split()).run()
|
||||
output = YouTubeTranscriptCli(
|
||||
"v1 v2 --languages de en --format json".split()
|
||||
).run()
|
||||
|
||||
# will fail if output is not valid json
|
||||
json.loads(output)
|
||||
|
@ -222,31 +280,37 @@ class TestYouTubeTranscriptCli(TestCase):
|
|||
def test_run__proxies(self):
|
||||
YouTubeTranscriptCli(
|
||||
(
|
||||
'v1 v2 --languages de en '
|
||||
'--http-proxy http://user:pass@domain:port '
|
||||
'--https-proxy https://user:pass@domain:port'
|
||||
"v1 v2 --languages de en "
|
||||
"--http-proxy http://user:pass@domain:port "
|
||||
"--https-proxy https://user:pass@domain:port"
|
||||
).split()
|
||||
).run()
|
||||
|
||||
YouTubeTranscriptApi.list_transcripts.assert_any_call(
|
||||
'v1',
|
||||
proxies={'http': 'http://user:pass@domain:port', 'https': 'https://user:pass@domain:port'},
|
||||
cookies= None
|
||||
"v1",
|
||||
proxies={
|
||||
"http": "http://user:pass@domain:port",
|
||||
"https": "https://user:pass@domain:port",
|
||||
},
|
||||
cookies=None,
|
||||
)
|
||||
|
||||
YouTubeTranscriptApi.list_transcripts.assert_any_call(
|
||||
'v2',
|
||||
proxies={'http': 'http://user:pass@domain:port', 'https': 'https://user:pass@domain:port'},
|
||||
cookies=None
|
||||
"v2",
|
||||
proxies={
|
||||
"http": "http://user:pass@domain:port",
|
||||
"https": "https://user:pass@domain:port",
|
||||
},
|
||||
cookies=None,
|
||||
)
|
||||
|
||||
def test_run__cookies(self):
|
||||
YouTubeTranscriptCli(
|
||||
(
|
||||
'v1 v2 --languages de en '
|
||||
'--cookies blahblah.txt'
|
||||
).split()
|
||||
("v1 v2 --languages de en " "--cookies blahblah.txt").split()
|
||||
).run()
|
||||
YouTubeTranscriptApi.list_transcripts.assert_any_call('v1', proxies=None, cookies='blahblah.txt')
|
||||
YouTubeTranscriptApi.list_transcripts.assert_any_call('v2', proxies=None, cookies='blahblah.txt')
|
||||
|
||||
YouTubeTranscriptApi.list_transcripts.assert_any_call(
|
||||
"v1", proxies=None, cookies="blahblah.txt"
|
||||
)
|
||||
YouTubeTranscriptApi.list_transcripts.assert_any_call(
|
||||
"v2", proxies=None, cookies="blahblah.txt"
|
||||
)
|
||||
|
|
|
@ -10,16 +10,17 @@ from youtube_transcript_api.formatters import (
|
|||
TextFormatter,
|
||||
SRTFormatter,
|
||||
WebVTTFormatter,
|
||||
PrettyPrintFormatter, FormatterLoader
|
||||
PrettyPrintFormatter,
|
||||
FormatterLoader,
|
||||
)
|
||||
|
||||
|
||||
class TestFormatters(TestCase):
|
||||
def setUp(self):
|
||||
self.transcript = [
|
||||
{'text': 'Test line 1', 'start': 0.0, 'duration': 1.50},
|
||||
{'text': 'line between', 'start': 1.5, 'duration': 2.0},
|
||||
{'text': 'testing the end line', 'start': 2.5, 'duration': 3.25}
|
||||
{"text": "Test line 1", "start": 0.0, "duration": 1.50},
|
||||
{"text": "line between", "start": 1.5, "duration": 2.0},
|
||||
{"text": "testing the end line", "start": 2.5, "duration": 3.25},
|
||||
]
|
||||
self.transcripts = [self.transcript, self.transcript]
|
||||
|
||||
|
@ -31,7 +32,7 @@ class TestFormatters(TestCase):
|
|||
|
||||
def test_srt_formatter_starting(self):
|
||||
content = SRTFormatter().format_transcript(self.transcript)
|
||||
lines = content.split('\n')
|
||||
lines = content.split("\n")
|
||||
|
||||
# test starting lines
|
||||
self.assertEqual(lines[0], "1")
|
||||
|
@ -39,19 +40,19 @@ class TestFormatters(TestCase):
|
|||
|
||||
def test_srt_formatter_middle(self):
|
||||
content = SRTFormatter().format_transcript(self.transcript)
|
||||
lines = content.split('\n')
|
||||
lines = content.split("\n")
|
||||
|
||||
# test middle lines
|
||||
self.assertEqual(lines[4], "2")
|
||||
self.assertEqual(lines[5], "00:00:01,500 --> 00:00:02,500")
|
||||
self.assertEqual(lines[6], self.transcript[1]['text'])
|
||||
self.assertEqual(lines[6], self.transcript[1]["text"])
|
||||
|
||||
def test_srt_formatter_ending(self):
|
||||
content = SRTFormatter().format_transcript(self.transcript)
|
||||
lines = content.split('\n')
|
||||
lines = content.split("\n")
|
||||
|
||||
# test ending lines
|
||||
self.assertEqual(lines[-2], self.transcript[-1]['text'])
|
||||
self.assertEqual(lines[-2], self.transcript[-1]["text"])
|
||||
self.assertEqual(lines[-1], "")
|
||||
|
||||
def test_srt_formatter_many(self):
|
||||
|
@ -59,11 +60,14 @@ class TestFormatters(TestCase):
|
|||
content = formatter.format_transcripts(self.transcripts)
|
||||
formatted_single_transcript = formatter.format_transcript(self.transcript)
|
||||
|
||||
self.assertEqual(content, formatted_single_transcript + '\n\n\n' + formatted_single_transcript)
|
||||
self.assertEqual(
|
||||
content,
|
||||
formatted_single_transcript + "\n\n\n" + formatted_single_transcript,
|
||||
)
|
||||
|
||||
def test_webvtt_formatter_starting(self):
|
||||
content = WebVTTFormatter().format_transcript(self.transcript)
|
||||
lines = content.split('\n')
|
||||
lines = content.split("\n")
|
||||
|
||||
# test starting lines
|
||||
self.assertEqual(lines[0], "WEBVTT")
|
||||
|
@ -71,10 +75,10 @@ class TestFormatters(TestCase):
|
|||
|
||||
def test_webvtt_formatter_ending(self):
|
||||
content = WebVTTFormatter().format_transcript(self.transcript)
|
||||
lines = content.split('\n')
|
||||
lines = content.split("\n")
|
||||
|
||||
# test ending lines
|
||||
self.assertEqual(lines[-2], self.transcript[-1]['text'])
|
||||
self.assertEqual(lines[-2], self.transcript[-1]["text"])
|
||||
self.assertEqual(lines[-1], "")
|
||||
|
||||
def test_webvtt_formatter_many(self):
|
||||
|
@ -82,7 +86,10 @@ class TestFormatters(TestCase):
|
|||
content = formatter.format_transcripts(self.transcripts)
|
||||
formatted_single_transcript = formatter.format_transcript(self.transcript)
|
||||
|
||||
self.assertEqual(content, formatted_single_transcript + '\n\n\n' + formatted_single_transcript)
|
||||
self.assertEqual(
|
||||
content,
|
||||
formatted_single_transcript + "\n\n\n" + formatted_single_transcript,
|
||||
)
|
||||
|
||||
def test_pretty_print_formatter(self):
|
||||
content = PrettyPrintFormatter().format_transcript(self.transcript)
|
||||
|
@ -106,7 +113,7 @@ class TestFormatters(TestCase):
|
|||
|
||||
def test_text_formatter(self):
|
||||
content = TextFormatter().format_transcript(self.transcript)
|
||||
lines = content.split('\n')
|
||||
lines = content.split("\n")
|
||||
|
||||
self.assertEqual(lines[0], self.transcript[0]["text"])
|
||||
self.assertEqual(lines[-1], self.transcript[-1]["text"])
|
||||
|
@ -116,11 +123,14 @@ class TestFormatters(TestCase):
|
|||
content = formatter.format_transcripts(self.transcripts)
|
||||
formatted_single_transcript = formatter.format_transcript(self.transcript)
|
||||
|
||||
self.assertEqual(content, formatted_single_transcript + '\n\n\n' + formatted_single_transcript)
|
||||
self.assertEqual(
|
||||
content,
|
||||
formatted_single_transcript + "\n\n\n" + formatted_single_transcript,
|
||||
)
|
||||
|
||||
def test_formatter_loader(self):
|
||||
loader = FormatterLoader()
|
||||
formatter = loader.load('json')
|
||||
formatter = loader.load("json")
|
||||
|
||||
self.assertTrue(isinstance(formatter, JSONFormatter))
|
||||
|
||||
|
@ -132,4 +142,4 @@ class TestFormatters(TestCase):
|
|||
|
||||
def test_formatter_loader__unknown_format(self):
|
||||
with self.assertRaises(FormatterLoader.UnknownFormatterType):
|
||||
FormatterLoader().load('png')
|
||||
FormatterLoader().load("png")
|
||||
|
|
Loading…
Reference in New Issue