mirror of
https://github.com/devine-dl/pywidevine.git
synced 2025-11-25 16:36:17 +00:00
Compare commits
No commits in common. "master" and "v1.6.0" have entirely different histories.
17
.deepsource.toml
Normal file
17
.deepsource.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
version = 1
|
||||||
|
|
||||||
|
exclude_patterns = [
|
||||||
|
"pywidevine/license_protocol_pb2.py"
|
||||||
|
]
|
||||||
|
|
||||||
|
[[analyzers]]
|
||||||
|
name = "python"
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[analyzers.meta]
|
||||||
|
runtime_version = "3.x.x"
|
||||||
|
max_line_length = 120
|
||||||
|
|
||||||
|
[[analyzers]]
|
||||||
|
name = "secrets"
|
||||||
|
enabled = false
|
||||||
@ -1,15 +0,0 @@
|
|||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
end_of_line = lf
|
|
||||||
charset = utf-8
|
|
||||||
insert_final_newline = true
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
|
|
||||||
[*.{feature,json,md,yaml,yml,toml}]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.md]
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1 +0,0 @@
|
|||||||
* text=auto eol=lf
|
|
||||||
46
.github/workflows/cd.yml
vendored
46
.github/workflows/cd.yml
vendored
@ -9,25 +9,37 @@ jobs:
|
|||||||
tagged-release:
|
tagged-release:
|
||||||
name: Tagged Release
|
name: Tagged Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment:
|
|
||||||
name: pypi
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: "3.14"
|
python-version: '3.11.x'
|
||||||
- name: Install uv
|
- name: Install Poetry
|
||||||
uses: astral-sh/setup-uv@v6
|
uses: abatilo/actions-poetry@v2.1.0
|
||||||
with:
|
with:
|
||||||
version: "0.9.5"
|
poetry-version: '1.3.2'
|
||||||
enable-cache: true
|
- name: Configure poetry
|
||||||
- name: Install the project
|
run: poetry config virtualenvs.in-project true
|
||||||
run: uv sync --locked
|
- name: Install dependencies
|
||||||
- name: Build project
|
run: |
|
||||||
run: uv build
|
python -m pip install --upgrade pip wheel
|
||||||
|
poetry install
|
||||||
|
- name: Build a wheel
|
||||||
|
run: poetry build
|
||||||
|
- name: Upload wheel
|
||||||
|
uses: actions/upload-artifact@v2.2.4
|
||||||
|
with:
|
||||||
|
name: Python Wheel
|
||||||
|
path: "dist/*.whl"
|
||||||
|
- name: Deploy release
|
||||||
|
uses: marvinpinto/action-automatic-releases@latest
|
||||||
|
with:
|
||||||
|
prerelease: false
|
||||||
|
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
files: |
|
||||||
|
dist/*.whl
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
run: uv publish
|
env:
|
||||||
|
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
|
||||||
|
run: poetry publish
|
||||||
|
|||||||
53
.github/workflows/ci.yml
vendored
53
.github/workflows/ci.yml
vendored
@ -7,40 +7,39 @@ on:
|
|||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: "3.14"
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v6
|
|
||||||
with:
|
|
||||||
version: "0.9.5"
|
|
||||||
enable-cache: true
|
|
||||||
- name: Install the project
|
|
||||||
run: uv sync --locked --all-extras --dev
|
|
||||||
- name: Run pre-commit which does various checks
|
|
||||||
run: uv run pre-commit run --all-files --show-diff-on-failure
|
|
||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
|
||||||
|
poetry-version: [1.3.2]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install uv
|
- name: Install poetry
|
||||||
uses: astral-sh/setup-uv@v6
|
uses: abatilo/actions-poetry@v2.1.0
|
||||||
with:
|
with:
|
||||||
version: "0.9.5"
|
poetry-version: ${{ matrix.poetry-version }}
|
||||||
enable-cache: true
|
- name: Install project
|
||||||
- name: Install the project
|
run: |
|
||||||
run: uv sync --locked --all-extras
|
poetry install --no-dev
|
||||||
|
python -m pip install flake8 pytest
|
||||||
|
|
||||||
|
- name: Lint with flake8
|
||||||
|
run: |
|
||||||
|
# stop the build if there are Python syntax errors or undefined names
|
||||||
|
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||||
|
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||||
|
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||||
|
# - name: Test with pytest
|
||||||
|
# run: |
|
||||||
|
# pytest
|
||||||
|
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: uv build
|
run: poetry build
|
||||||
|
|||||||
40
.gitignore
vendored
40
.gitignore
vendored
@ -23,6 +23,7 @@ parts/
|
|||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
wheels/
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
share/python-wheels/
|
share/python-wheels/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
@ -52,7 +53,6 @@ coverage.xml
|
|||||||
*.py,cover
|
*.py,cover
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
*.mo
|
||||||
@ -75,7 +75,6 @@ instance/
|
|||||||
docs/_build/
|
docs/_build/
|
||||||
|
|
||||||
# PyBuilder
|
# PyBuilder
|
||||||
.pybuilder/
|
|
||||||
target/
|
target/
|
||||||
|
|
||||||
# Jupyter Notebook
|
# Jupyter Notebook
|
||||||
@ -86,9 +85,7 @@ profile_default/
|
|||||||
ipython_config.py
|
ipython_config.py
|
||||||
|
|
||||||
# pyenv
|
# pyenv
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
.python-version
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
# pipenv
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
@ -97,22 +94,7 @@ ipython_config.py
|
|||||||
# install all needed dependencies.
|
# install all needed dependencies.
|
||||||
#Pipfile.lock
|
#Pipfile.lock
|
||||||
|
|
||||||
# poetry
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/#use-with-ide
|
|
||||||
.pdm.toml
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
__pypackages__/
|
||||||
|
|
||||||
# Celery stuff
|
# Celery stuff
|
||||||
@ -138,6 +120,9 @@ venv.bak/
|
|||||||
# Rope project settings
|
# Rope project settings
|
||||||
.ropeproject
|
.ropeproject
|
||||||
|
|
||||||
|
# Jetbrains project settings
|
||||||
|
.idea
|
||||||
|
|
||||||
# mkdocs documentation
|
# mkdocs documentation
|
||||||
/site
|
/site
|
||||||
|
|
||||||
@ -148,16 +133,3 @@ dmypy.json
|
|||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
.idea/
|
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
# See https://pre-commit.com for more information
|
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
|
||||||
|
|
||||||
exclude: '_pb2.pyi?$'
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
||||||
rev: v0.14.2
|
|
||||||
hooks:
|
|
||||||
- id: ruff-check
|
|
||||||
language: system
|
|
||||||
types: [ python ]
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
||||||
rev: v1.18.2
|
|
||||||
hooks:
|
|
||||||
- id: mypy
|
|
||||||
language: system
|
|
||||||
types: [ python ]
|
|
||||||
args: [] # override defaults to empty
|
|
||||||
- repo: https://github.com/pycqa/isort
|
|
||||||
rev: 6.1.0
|
|
||||||
hooks:
|
|
||||||
- id: isort
|
|
||||||
language: system
|
|
||||||
types: [ python ]
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v6.0.0
|
|
||||||
hooks:
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: trailing-whitespace
|
|
||||||
args: [--markdown-linebreak-ext=md]
|
|
||||||
12
.vscode/extensions.json
vendored
12
.vscode/extensions.json
vendored
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"EditorConfig.EditorConfig",
|
|
||||||
"streetsidesoftware.code-spell-checker",
|
|
||||||
"ms-python.python",
|
|
||||||
"ms-python.vscode-pylance",
|
|
||||||
"charliermarsh.ruff",
|
|
||||||
"ms-python.isort",
|
|
||||||
"ms-python.mypy-type-checker",
|
|
||||||
"redhat.vscode-yaml"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
576
CHANGELOG.md
576
CHANGELOG.md
@ -5,131 +5,47 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [1.9.0] - 2025-12-22
|
|
||||||
|
|
||||||
Just a small update to update imports, support the latest python version, and fix configs.
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Support for Python 3.14 (and retroactively 3.13).
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Dropped support for Python 3.8.
|
|
||||||
- DeepSource config and badge.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fixed pre-commit config, now using all official repos while still utilizing poetry dependencies.
|
|
||||||
- Fixed the GitHub workflows for CI/CD, now using the latest action versions.
|
|
||||||
|
|
||||||
## [1.8.0] - 2023-12-22
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Added `py.typed` file to support PEP561 and silence Mypy.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Dropped support for Python 3.7.
|
|
||||||
- Recompiled protobuffers for version 4.25.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Missing `yaml` dependency as it was only installed alongside the `serve` extras group.
|
|
||||||
- Duplicate Concatenated SignedMessages no longer throw a verification failure in `Cdm.set_service_certificate()`.
|
|
||||||
To ensure security of the messages, verification will still fail if any of the SignedMessages do not match each other.
|
|
||||||
|
|
||||||
### New Contributors
|
|
||||||
|
|
||||||
- [sr0lle](https://github.com/sr0lle)
|
|
||||||
|
|
||||||
## [1.7.0] - 2023-11-21
|
|
||||||
|
|
||||||
- Supported Serve API: `v1.4.3` or newer
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Ability to specify output filename by specifying a full path or a relative file name in CLI command `create-device`.
|
|
||||||
- Add the staging privacy certificate (`staging.google.com`) to `Cdm.staging_privacy_cert`.
|
|
||||||
- Similar to `common_privacy_cert` which would be used on Google's production license server,
|
|
||||||
- Though this one is used on Google's staging license server (a production-ready testing server).
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Raise an error if a file already exists at the output path in CLI command `create-device`.
|
|
||||||
- Use std-lib xml instead of lxml to reduce dependencies and support ARM (#35).
|
|
||||||
- Lessen restriction on Python version to any Python version `>=3.7`, but `<4.0`.
|
|
||||||
- I was hoping to do `^3.7`, but some dependencies also require `<4.0` therefore I cannot, for now.
|
|
||||||
- Move Key ID parsing to static `PSSH.parse_key_ids()` method.
|
|
||||||
- The `shaka-packager` subprocess call's return code is now returned from `Cdm.decrypt()`.
|
|
||||||
- The flags variable of a `Device` now defaults to a dict, even if not set.
|
|
||||||
- Heavily improve initializing of protobuf objects, improving readability, typing, and linting quite a bit.
|
|
||||||
- Renamed Device's `_Types` enum class to `DeviceTypes`.
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Removed `Device.Types` class variable alias to `_Types` enum class as a static linter cannot recognize a class
|
|
||||||
variable as a type. Instead, the actual `_Types` (now named `DeviceTypes`) enum should be imported and used instead.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Ensure output directory exists before creating new `.wvd` files in CLI command `create-device`.
|
|
||||||
- Ignore empty Key ID values in v4.0.0.0 PlayReadyHeaders.
|
|
||||||
- Remove `Cdm.system_id` class variable as it conflicted with the `cdm.system_id` class instance variable of the same
|
|
||||||
name. It's also generally not needed. The same data can be gotten via `Cdm.uuid.bytes`.
|
|
||||||
- Casting of `type_` when passed a non-int value in `Cdm.get_license_challenge()`.
|
|
||||||
- Pass a PSSH object in `test` CLI command instead of a string.
|
|
||||||
- Lower-case and setup `__all__` correctly, add missing `__all__` in some of the modules.
|
|
||||||
- For the longest time I thought it was `__ALL__` and an iterable of objects/variables.
|
|
||||||
- However, its actually `__all__` and explicitly a list of Strings...
|
|
||||||
|
|
||||||
### New Contributors
|
|
||||||
|
|
||||||
- [mediaminister](https://github.com/mediaminister)
|
|
||||||
|
|
||||||
## [1.6.0] - 2023-02-03
|
## [1.6.0] - 2023-02-03
|
||||||
|
|
||||||
- Supported Serve API: `v1.4.3` or newer
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Support Python 3.11.
|
- Added full support for Python 3.11.
|
||||||
- New CLI command `export-device` to export WVD files back as files. I.e., a private key and client ID blob file.
|
- Added new `export-device` command-line function to export WVD files back as files. I.e., a private key and client ID
|
||||||
|
blob file.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The PyYAML dependency is now required even if you do not install Pywidevine with the `serve` extra dependencies.
|
||||||
|
- This was required for exporting WVD metadata in the new `export-device` command-line function.
|
||||||
|
|
||||||
## [1.5.3] - 2022-12-27
|
## [1.5.3] - 2022-12-27
|
||||||
|
|
||||||
- Supported Serve API: `v1.4.3` or newer
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- New utility `load_xml()` to parse XML data with lxml ignoring Namespaces.
|
- Added a new utility `load_xml()` to parse XML data with lxml ignoring Namespaces.
|
||||||
- PSSH class now have `__str__` and `__repr__` methods to print the object in more Human-friendly ways.
|
- PSSH class now has a `__str__` and `__repr__` representation to print the object in more Human-friendly and
|
||||||
- `str(pssh)` is now identical to `pssh.dumps()`.
|
useful ways. `str(pssh)` is now identical to `pssh.dumps()` and `repr(pssh)` or just `pssh` in some cases will
|
||||||
- `repr(pssh)` or just `pssh` in some cases will result in a nice overview of the PSSHs contents.
|
result in a nice overview of the PSSHs contents.
|
||||||
- New `to_playready()` method to convert Widevine PSSH Data to PlayReady PSSH Data. Please note that the
|
- Added new `to_playready()` method to convert Widevine PSSH Data to PlayReady PSSH Data. Please note that the
|
||||||
Checksums for AES-CTR and COCKTAIL KIDs cannot be calculated as the Content Encryption Key would be needed.
|
Checksums for AES-CTR and COCKTAIL KIDs cannot be calculated as the Content Encryption Key would be needed.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- The System ID must now be explicitly specified when creating a new PSSH box in `PSSH.new()`.
|
- You must now explicitly specify the System ID to use when creating a new PSSH box.
|
||||||
- This allows you to now create PlayReady PSSH boxes.
|
This allows you to now create PlayReady PSSH boxes.
|
||||||
- The `playready_to_widevine()` method has been renamed to just `to_widevine()`.
|
- The `playready_to_widevine()` method has been renamed to just `to_widevine()`.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Correct capitalization of the `key_IDs` field when making the new box in `PSSH.new()`.
|
- Fix the capitalization of the `key_IDs` field, and it's value when creating a new PSSH box.
|
||||||
- Correct the value type of `key_IDs` value when creating a new box in `PSSH.new()`.
|
- Fix the ability to create v0 PSSH boxes by only setting the `key_IDs` field when the version is set to `1`.
|
||||||
- Ensure Key IDs are list of UUIDs instead of bytes in `PSSH.new()`.
|
- Fix parsing of Key IDs within PlayReadyHeaders by using the new `load_xml()` utility to ignore namespaces so
|
||||||
- Create v0 PSSH boxes by only setting the `key_IDs` field when the version is set to `1` in `PSSH.new()`.
|
that `xpath` can correctly locate any and all KID tags.
|
||||||
- Fix loading of PlayReadyHeaders (and PlayReadyObjects) as PSSH boxes. It would previously load it under the
|
- Fix loading of PlayReadyHeaders (and PlayReadyObjects) as PSSH boxes. It would previously load it under the
|
||||||
Widevine SystemID breaking all PlayReady-specific code after construction.
|
Widevine SystemID breaking all PlayReady-specific code after construction.
|
||||||
- Parse Key IDs within PlayReadyHeaders by using the new `load_xml()` utility to ignore namespaces so that `xpath` can
|
- Fix support for loading PlayReadyObjects with more than one PlayReadyHeader (more than one record).
|
||||||
correctly locate any and all KID tags.
|
|
||||||
- Support parsing PlayReadyObjects with more than one PlayReadyHeader (more than one record).
|
|
||||||
## [1.5.2] - 2022-10-11
|
|
||||||
|
|
||||||
- Supported Serve API: `v1.4.3` or newer
|
## [1.5.2] - 2022-10-11
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
@ -138,397 +54,339 @@ Just a small update to update imports, support the latest python version, and fi
|
|||||||
|
|
||||||
## [1.5.1] - 2022-10-23
|
## [1.5.1] - 2022-10-23
|
||||||
|
|
||||||
- Supported Serve API: `v1.4.3` or newer
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Support for big-int Key IDs in `PSSH`. All integer values are converted to a UUID and are loaded big-endian.
|
- Added import path shortcuts in the `__init__.py` package constructor to all the user classes. Now you can do e.g.,
|
||||||
- Import path shortcuts in the `__init__.py` package constructor to all the user classes.
|
`from pywidevine import PSSH` instead of `from pywidevine.pssh import PSSH`. You can still do it both ways.
|
||||||
- Now you can do e.g., `from pywidevine import PSSH` instead of `from pywidevine.pssh import PSSH`.
|
- Improved error handling and sanitization checks when parsing some Service Certificates in `set_service_certificate()`.
|
||||||
- You can still do it the full direct way if you want.
|
|
||||||
- Parsing check to the raw DrmCertificate in `Cdm.set_service_certificate()`.
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Service Certificates are now stored in the session as a `SignedDrmCertificate`.
|
- Maximum concurrent Cdm sessions are now set to 16 as it seems tto be a more common limit on more up-to-date CDMs,
|
||||||
- This is to keep the signature with the Certificate, without wrapping it in a SignedMessage unnecessarily.
|
including Android's OEMCrypto Library. This also helps encourage people to close their sessions when they are no
|
||||||
- Reduced the maximum concurrent Cdm sessions from 50 to 16 as it seems to be a more common limit on more up-to-date
|
longer required.
|
||||||
devices and versions of OEMCrypto. This also helps encourage people to close their sessions when they are no longer
|
- Service Certificates are now stored in the session as a `SignedDrmCertificate`. This is to keep the signature with
|
||||||
required.
|
the stored Certificate for use by the user if necessary. It also reduces code repetition relating to the usage of the
|
||||||
|
signature.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Acquisition of the Certificate's provider_id in `Cdm.set_service_certificate()` in some edge cases, but also when you
|
- Improved reliability of computing License Signatures. Some license messages when parsed would be slightly different
|
||||||
try to remove the certificate by setting it to `None`.
|
when re-serialized with `SerializeToString()`, therefore the computed signature would have always mismatched.
|
||||||
- When exporting a PSSH object it will now do so in the same version it was initially loaded or created in. Previously
|
- Added support for Key IDs that are integer values. Effectively all values are now considered to be a UUID as 16 bytes
|
||||||
it would always dump as a v1 PSSH box due to a cascading check in pymp4. It now also honors the currently set version
|
(in hex or bytes) or an integer value with support for up to 16 bytes. All integer values are converted to a UUID and
|
||||||
in the case it gets overridden.
|
are loaded big-endian.
|
||||||
- Improved reliability of computing License Signatures by verifying the signature against the original raw License
|
- Fixed acquisition of the Certificate's provider_id within `set_service_certificate()` in some edge cases, but also
|
||||||
message instead of the re-serialized version of the message.
|
when you try to remove the certificate by setting it to `None`.
|
||||||
- Some license messages when parsed would be slightly different when re-serialized against my protobuf, therefore the
|
- PSSH now dumps in the same version the PSSH was loaded or created in. Previously it would always dump as a v1 PSSH
|
||||||
computed signature would have always mismatched.
|
box due to a cascading check in pymp4. It now also honors the currently set version in the case it gets overridden.
|
||||||
|
|
||||||
## [1.5.0] - 2022-09-24
|
## [1.5.0] - 2022-09-24
|
||||||
|
|
||||||
- Supported Serve API: `v1.4.3` or newer
|
With just one change this brings along a reduced dependency tree, smoother experience across different platforms, and
|
||||||
|
speed improvements (especially on larger input messages).
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Updated `protobuf` dependency to `v4.x` branch with recompiled proto-buffers, specifically `v4.21.6`.
|
- Updated protobuf dependency to v4.x branch with recompiled proto-buffers. They now also have python stub files.
|
||||||
|
|
||||||
## [1.4.4] - 2022-09-24
|
## [1.4.4] - 2022-09-24
|
||||||
|
|
||||||
- Supported Serve API: `v1.4.3` or newer
|
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Updated `protobuf` dependency to `3.19.5` due to the Security Advisory [GHSA-8gq9-2x98-w8hf].
|
- Updated `protobuf` dependency to v3.19.5 due to the Security Advisory [GHSA-8gq9-2x98-w8hf].
|
||||||
|
|
||||||
[GHSA-8gq9-2x98-w8hf]: <https://github.com/protocolbuffers/protobuf/security/advisories/GHSA-8gq9-2x98-w8hf>
|
[GHSA-8gq9-2x98-w8hf]: <https://github.com/protocolbuffers/protobuf/security/advisories/GHSA-8gq9-2x98-w8hf>
|
||||||
|
|
||||||
## [1.4.3] - 2022-09-10
|
## [1.4.3] - 2022-09-10
|
||||||
|
|
||||||
- Supported Serve API: `v1.4.3` or newer
|
RemoteCdm minimum supported Serve API version is now v1.4.3.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Serve's `/get_license_challenge` endpoint can now disable privacy mode per-request, even if a service certificate is
|
- Cdm now has a `get_service_certificate()` endpoint to get the currently set service certificate of a Session.
|
||||||
set, as long as privacy mode is not enforced in the Serve API config.
|
RemoteCdm and Serve also has support for these endpoints.
|
||||||
- New Cdm method `get_service_certificate()` to get the currently set service certificate of a Session.
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- All f-string formatting in log statements have been replaced with logging formatting to save performance when that
|
- Added installation instructions, troubleshooting steps, a minimal example, and a list of features to the README.
|
||||||
log wouldn't have been printed.
|
- The minimum version for lxml has been upped to >=4.9.1. This is due to some vulnerabilities present in all older
|
||||||
- The Serve APIs `/open` endpoint's function has been renamed from `open()` to `open_()` to prevent shadowing the
|
versions.
|
||||||
built-in `open`.
|
- All f-string formatting in log statements have been replaced with logging formatting to improve performance when
|
||||||
|
logging is disabled.
|
||||||
### Security
|
|
||||||
|
|
||||||
- Updated `lxml` dependency to `>=4.9.1` due to the Security Advisory [GHSA-wrxv-2j5q-m38w].
|
|
||||||
|
|
||||||
[GHSA-wrxv-2j5q-m38w]: <https://github.com/advisories/GHSA-wrxv-2j5q-m38w>
|
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- The Protocol image has been removed from the README as it is too broad to Browser scenarios and some stuff on it
|
- The Protocol image has been removed from the README as it is too broad to Browser scenarios and some stuff on it
|
||||||
is too broad. If the viewer is really interested they can Google it to get a much better view into the Protocol.
|
is too broad. If the viewer is really interested they can Google it to get a much better view into the Protocol.
|
||||||
|
|
||||||
## [1.4.2] - 2022-09-05
|
### Fixed
|
||||||
|
|
||||||
- Supported Serve API: `v1.4.0` to `v1.4.2`
|
- Serve's get_license_challenge can now disable privacy mode even if a service certificate is set, as long as privacy
|
||||||
|
mode is not enforced in settings.
|
||||||
|
|
||||||
|
## [1.4.2] - 2022-09-05
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Sessions in `Cdm.open()` are now initialized with a unique session number.
|
- Device's constructor no longer throws `ValueError` exceptions if it fails to parse the provided Client ID or it's
|
||||||
- Android Cdm Devices now use a Request ID formula similar to OEMCrypto library when generating a Challenge.
|
VMP data if any. It will now raise a `DecodeError`.
|
||||||
This formula has yet to be fully confirmed and ironed out, but it is closer than the Chrome Cdm formula.
|
|
||||||
- `Device` no longer throws `ValueError` exceptions on `DecodeErrors` if it fails to parse the provided Client ID, or
|
|
||||||
it's VMP data if any. It will now re-raise `DecodeError`.
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Parsed Proto Messages now go through an elaborate yet efficient verification, it must parse and serialize back to it's
|
- Android Cdm Devices now use a Request ID formula similar to OEMCrypto library when generating a Challenge.
|
||||||
received form, byte-for-byte, or it will be rejected.
|
This formula has yet to be fully confirmed and ironed out, but it is better than the Chrome Cdm formula.
|
||||||
- This prevents protobuf from parsing a message that could be a different message depending on the starting bytes.
|
- Various Proto Message Parsing now has full verification and expects the parsed response to be the same length
|
||||||
- It was possible to bypass some minor checks by providing specially crafted messages that parsed as other messages.
|
as the serialized input, or it will throw an error. For example, this prevents vague errors to happen when you
|
||||||
However, I haven't noticed any way where this would lead to a vulnerability or anything bad. It mostly just lead to
|
provide a bad License to `Cdm.parse_license`. It also prevents possibilities of it going past various other checks
|
||||||
Serve API crashes or just rejected messages down the chain as they wouldn't have the right data within them.
|
depending on the first few bytes provided.
|
||||||
|
|
||||||
## [1.4.1] - 2022-08-17
|
## [1.4.1] - 2022-08-17
|
||||||
|
|
||||||
- Supported Serve API: `v1.4.0` to `v1.4.2`
|
Small patch release for some fixes to the PSSH classes recent face-lift.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Rework `PSSH.overwrite_key_ids()` as an instance method now named `PSSH.set_key_ids()`.
|
- `PSSH.overwrite_key_ids` static method is now an instance method named `set_key_ids` and works on the current
|
||||||
- Rework `PSSH.get_key_ids()` as a property method named `PSSH.key_ids`. This allows swift access to all the Key IDs of
|
instance instead of making and returning a new one.
|
||||||
the current PSSH object data.
|
- `PSSH.get_key_ids` static method is now a property method named `key_ids`. This allows swift access to all the
|
||||||
- Rework `PSSH.from_playready_pssh()` as an instance method now named `PSSH.playready_to_widevine()` that now converts
|
Key IDs of the current access.
|
||||||
the current instances values directly. This allows you to more easily instance as any PSSH, then convert after wards
|
- `PSSH.from_playready_pssh` class method is now an instance method named `playready_to_widevine` and now converts
|
||||||
and only if wanted and when needed.
|
the current instances values directly. This allows you to more easily instance as any PSSH, then convert afterwards.
|
||||||
|
|
||||||
## [1.4.0] - 2022-08-06
|
## [1.4.0] - 2022-08-06
|
||||||
|
|
||||||
- Supported Serve API: `v1.4.0` to `v1.4.2`
|
This release is a face-lift for the PSSH class with a moderate amount of Cdm and Serve interface changes.
|
||||||
|
You will likely need to make a moderate amount of changes in your client code, please study the changelog.
|
||||||
|
|
||||||
|
Please note that while it was always privatized as `_sessions`, accessing the Session directly for any purpose was
|
||||||
|
never recommended or supported. With v1.4.0, there will be drastic problems if you continue to do so. One of the
|
||||||
|
few reasons to do that was to get the license keys which is no longer required with CDMs new `get_keys()` method.
|
||||||
|
|
||||||
|
RemoteCdm minimum supported Serve API version is now v1.4.0.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- New PSSH boxes can now be manually crafted with `PSSH.new()`.
|
- The PSSH class now has a `new()` method to craft a new PSSH box. The box can be crafted from arbitrary init_data
|
||||||
- The box can be crafted from arbitrary init_data and/or key_ids.
|
and/or key_ids. If only key_ids is supplied a new Widevine Cenc Header will be created and the key IDs will be put
|
||||||
- If only key_ids is supplied a new Widevine CENC Header will be created and the key IDs will be put into it.
|
into it. This allows you to make compliant v0 or v1 boxes with as little data as just a Key ID.
|
||||||
- This allows you to make compliant v0 or v1 boxes with as little data as just a Key ID.
|
- The PSSH class now has `dump()` and `dumps()` methods to serialize the data as binary or base64 respectively. It will
|
||||||
- PSSH boxes can now be exported as MP4 Box objects using pymp4 with `PSSH.dump()`.
|
be serialized as a pymp4 PSSH box, ready to be used in an MP4 file.
|
||||||
- PSSH boxes can now also be exported as Base64 strings with `PSSH.dumps()`.
|
- Cdm now has a method `get_keys()` to get the keys of the loaded license. This is the alternative to manually
|
||||||
- License Keys can now be obtained from a Cdm session with a parsed license using `Cdm.get_keys()`.
|
accessing the keys by navigating the `_sessions` class instance variable.
|
||||||
- This is the alternative to manually accessing the keys from the `Cdm._sessions` object.
|
- Serve API now also has a `/get_keys` endpoint to call the `get_keys()` method of the underlying Cdm session.
|
||||||
- It is also available on the Serve API through the new `/get_keys` endpoint.
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- `PSSH.get_as_box()` has been merged into the PSSH constructor, simplifying usage of the PSSH class.
|
- Cdm and RemoteCdm now expect a PSSH object as the `init_data` param for `get_license_challenge`. You can no longer
|
||||||
- `PSSH.from_playready_pssh()` is now a class method and returns as a PSSH object.
|
provide it anything else, that includes base64 or bytes form. It must be a PSSH object.
|
||||||
- Only PSSH objects are now accepted by `Cdm.get_license_challenge()`.
|
- Serve no longer returns license keys in the response of the `/keys` endpoint.
|
||||||
- You can no longer provide it anything else, that includes base64 or bytes form.
|
- Serve has changed the endpoint `/challenge` to `/get_license_challenge` and `/keys` to `/parse_license`. This is to
|
||||||
- You should first parse or make a new PSSH with the PSSH class, and then pass that object.
|
be consistent with the method names of the underlying Cdm class.
|
||||||
- This is to simplify typing and repetition across the codebase.
|
- The PSSH class has been reworked from being a static helper class to a proper PSSH class.
|
||||||
- Serve's `/challenge` endpoint has been changed to `/get_license_challenge`, and `/keys` to `/parse_license`.
|
- PSSH.from_playready_pssh is now a class method and returns as a PSSH object.
|
||||||
- This is to be consistent with the method names of the underlying Cdm class.
|
|
||||||
- Serve now passes the license type value as-is (as a string) instead of parsing it to an integer.
|
|
||||||
- Serve now passes the key type value as-is (as a string) instead of parsing it to an integer.
|
|
||||||
- Serve no longer returns license keys in the response of the `/parse_license` endpoint.
|
|
||||||
- Once parsed, the `/get_keys` endpoint should be used to retrieve keys.
|
|
||||||
- Privatized the `Cdm._sessions` class instance variable even more to `Cdm.__sessions`.
|
|
||||||
- If you still need something from it, while not advised, you can call it via `cdm._Cdm__sessions`.
|
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- `PSSH.from_key_ids()` has been removed entirely, you should now use `PSSH.new(key_ids=...)` instead.
|
- PSSH.get_as_box has been removed and merged into the PSSH constructor.
|
||||||
- Unnecessary parsing of the license message received by RemoteCdm is now skipped. Parsing should be done by the Serve
|
- PSSH.from_key_ids has been removed entirely, you should now use `PSSH.new(key_ids=...)` instead.
|
||||||
API as it will be able to actually decrypt and verify the message.
|
- All uses of a local Session() object has been removed from RemoteCdm. The session is now fully controlled by the
|
||||||
- All uses of a local `Session` object has been removed from `RemoteCdm`. The session is now fully controlled by the
|
|
||||||
remote API and de-synchronization by external alteration or unexpected exceptions is no longer a possibility.
|
remote API and de-synchronization by external alteration or unexpected exceptions is no longer a possibility.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Correct the WidevinePsshData proto field name from `key_id` to `key_ids` in the PSSH class.
|
- Various uses of the `key_ids` field of WidevinePsshData proto has been fixed in the PSSH class.
|
||||||
- Handle `DecodeError` and `SignatureMismatch` exceptions in the Serve `/set_service_certificate` endpoint.
|
- Fixed a few Serve API crashes in edge cases with improved error handling on Cdm method calls.
|
||||||
- Handle `InvalidInitData` and `InvalidLicenseType` exceptions in the Serve `/get_license_challenge` endpoint.
|
|
||||||
- Handle various exceptions in the Serve `/parse_license` endpoint.
|
|
||||||
- Handle various client-side runtime errors in `RemoteCdm` with improved error handling.
|
|
||||||
|
|
||||||
## [1.3.1] - 2022-08-04
|
## [1.3.1] - 2022-08-04
|
||||||
|
|
||||||
- Supported Serve API: `v1.3.0` to `v1.3.1`
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- String value support to the `device_type` parameter in `Cdm`s constructor.
|
- Cdm and RemoteCdm can now be supplied a string value for `device_type` for scenarios where providing it as a string
|
||||||
|
is more convenient (e.g., from Config files).
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Serve no longer requires `force_privacy_mode` to be defined in the config file. It now assumes a default of false.
|
|
||||||
- Serve now uses `pywidevine serve ...` instead of the full project url in the Server header.
|
|
||||||
- `RemoteCdm`s Server version check is now case-insensitive.
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- `RemoteCdm`s Server version check now ignores other Server/Proxy names prepended or appended to the Server header.
|
- The `force_privacy_mode` key no longer needs to be defined at all in the configuration file. This was previously
|
||||||
- For example, if reverse-proxied through Caddy it may have prepended "Caddy" to the Server header.
|
crashing serve APIs if it wasn't set before starting.
|
||||||
|
- RemoteCdm's Server version check will no longer fail under certain serving conditions e.g., Caddy prepending `Caddy`
|
||||||
|
to the Server header value. It also fixes case sensitivity and removed the full url from the header.
|
||||||
|
|
||||||
## [1.3.0] - 2022-08-04
|
## [1.3.0] - 2022-08-04
|
||||||
|
|
||||||
- Supported Serve API: `v1.3.0` to `v1.3.1`
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- New Client for using the Serve API; `RemoteCdm` class. It has an identical interface as the original `Cdm` class.
|
- New RemoteCdm class to be used as Client code for the `serve` Remote CDM API server. The RemoteCdm should be used
|
||||||
- However, the constructor is different. Instead of passing a Widevine device object, you need to pass information
|
entirely separately from the normal Cdm class. All serve APIs must update to v1.3.0 to be compatible. The RemoteCdm
|
||||||
about the API like its host (including port if not on a reverse-proxy), and info about the device like its name and
|
verifies the server version to ensure compatibility. Changes to the serve API schema will be immediately reflected in
|
||||||
security level.
|
the RemoteCdm code in the future.
|
||||||
- Other than that, once the RemoteCdm object is created, you use it exactly the same. Magic!
|
- Implemented `/set_service_certificate` endpoint in serve schema as an improved way of setting the service certificate
|
||||||
- Any time there's a change or fix to `Cdm` in this update or any in the future, will also be done to RemoteCdm.
|
than passing it to `/challenge`.
|
||||||
- New Serve endpoint `/set_service_certificate` as an improved way of setting (or unsetting) the service certificate.
|
- You can now unset the service certificate by providing an empty service certificate value (or None or null). This
|
||||||
|
includes support for doing so even in serve API and the new RemoteCdm.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- `Cdm`s constructor now uses more direct values, so you don't have to use the Device class or `.wvd` files.
|
- The Construction of the Cdm object has changed. You can now initialize it with more direct values if you don't want
|
||||||
- To continue using `.wvd` files you must now use `Cdm.from_device()` instead.
|
to use the Device class or don't want to use `.wvd` files. To use Device classes, you must now use the
|
||||||
- You can now unset the Service certificate by providing `None` to `Cdm.set_service_certificate().
|
`Cdm.from_device()` class method.
|
||||||
|
- The ability to pass the certificate to `/challenge` has been removed. Please use the new `/set_service_certificate`
|
||||||
### Removed
|
endpoint before calling `/challenge`. You do not need to set it every time. Once per session is enough unless you
|
||||||
|
now want to use a different certificate.
|
||||||
- Serve's `/challenge` endpoint no longer accepts a `service_certificate` item in the JSON payload.
|
|
||||||
- Instead, use the new `/set_service_certificate` endpoint before calling `/challenge`.
|
|
||||||
- You do not need to set it every time. Once per session is enough unless you now want to use a different certificate.
|
|
||||||
|
|
||||||
## [1.2.1] - 2022-08-02
|
## [1.2.1] - 2022-08-02
|
||||||
|
|
||||||
|
This release is primarily a maintenance release for `serve` functionality but some Cdm fixes are also present.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Support `SignedDrmCertificate` and `SignedMessages` messages in `Cdm.encrypt_client_id()`. This is mainly as a
|
- You can now return all License Keys from Serve's `/keys` endpoint by supplying `ALL` as the key type.
|
||||||
convenience for any scripts wanting to encrypt their Client ID with a service certificate manually.
|
This adds support for Exchange Systems like Netflix's WidevineExchange MSL scheme. I recommend using `ALL` unless
|
||||||
- All License Keys from Serve's `/keys` endpoint can now be received by providing `ALL` as the key type.
|
you only want `CONTENT` keys and will not be using any other type of keys including `SIGNING` and `OPERATOR_SESSION`.
|
||||||
- This adds support for systems needing more than two types of keys from the license, e.g., Netflix MSL.
|
- Serve now has a `/close` endpoint to close a session. The Cdm has a limit of 50 sessions per user.
|
||||||
- For faster response times it is best to still ask for only `CONTENT` keys if that's all you need.
|
- Serve now responds with a `Server` header denoting that pywidevine serve is being used, also specifying the version.
|
||||||
- Serve now has a `/close` endpoint to close a session. All clients should close the session once they are finished
|
This allows Clients to selectively support APIs based on version, and also verify the API as being supported at all.
|
||||||
with it or the user will eventually hit a limit of 50 sessions per user and the server will hog memory til it
|
- Serve now verifies that all Devices in config actually exist before letting you start serving.
|
||||||
restarts.
|
|
||||||
- Serve now verifies that all Devices in config actually exist before starting the server.
|
|
||||||
- Serve now responds with a `Server` header denoting that pywidevine serve is being used, and it's version.
|
|
||||||
- This allows Clients to selectively support APIs based on version; verify the API as being supported.
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Lessened version pin on `lxml` from `^4.9.1` to `>=4.8.0` to support projects using pycaption.
|
- Downgraded lxml to >=4.8.0 to support projects using pycaption, which is likely considering the project's topic.
|
||||||
- Service Certificate is now saved in the session as a `SignedMessage` with a `SignedDrmCertificate` instead of the raw
|
|
||||||
`DrmCertificate`. The `SignedMessage` is unsigned as the `SignedDrmCertificate` within it, is signed. This is so
|
|
||||||
anything inheriting or using the Cdm (e.g., `serve`) can verify the certificate down the chain and keep it signed.
|
|
||||||
- Serve now constructs one Cdm object for each user+device combination so one user cannot fill or overuse the CDM
|
|
||||||
session limit.
|
|
||||||
- All of Serve's endpoints now have a `/{device}` prefix. E.g., instead of `/challenge/STREAMING`, it's now
|
- All of Serve's endpoints now have a `/{device}` prefix. E.g., instead of `/challenge/STREAMING`, it's now
|
||||||
`/device_name/challenge/STREAMING`. This is to support the previous change.
|
`/device_name/challenge/STREAMING`. This is to support a multi-device per-user Cdm setup, see Fixed below regarding
|
||||||
|
Serve's Cdm objects.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Handle server crash when the session limit is reached in Serve's `/open` endpoint by returning a 400 error.
|
- Fixed support for Raw PSSH values, e.g., Netflix's WidevineExchange MSL Scheme arbitrary init_data value.
|
||||||
- Serve now correctly updates (or rather now makes a new Cdm object) if a user switches from one Device to another.
|
- The Service Certificate is now saved to the Session in full SignedMessage form instead of just the underlying
|
||||||
- Previously it would reuse an existing Cdm object, but would forget to switch device if they changed.
|
DrmCertificate. This is so any class inheriting the Cdm (e.g., for Remote capabilities) can sufficiently use
|
||||||
- Note: It does still leave the previous Cdm with the older Device in memory.
|
and supply the service certificate while being signed.
|
||||||
- Handle IOError when parsing bytes as MP4 Box to allow arbitrary data to be made as new boxes in `PSSH.get_as_box()`.
|
- Serve's /open endpoint will now return a 400 error if there's too many sessions opened.
|
||||||
|
- Serve's Cdm objects with Device initialized are now stored per-user and device name. This fixes the issue where the
|
||||||
|
entire user base has only 50 sessions available to be used. Effectively rate limiting to only 50 users at a time.
|
||||||
|
Since /close endpoint was not implemented yet, there was no way to even close effectively meaning only 50 uses could
|
||||||
|
be done.
|
||||||
|
|
||||||
## [1.2.0] - 2022-07-30
|
## [1.2.0] - 2022-07-30
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- New CLI command `serve` that hosts a CDM API that can be externally accessed with authentication. This can be used to
|
- New CLI command `serve` to serve local WVD devices and CDM sessions remotely as a JSON API.
|
||||||
access and/or share your CDM without exposing your Widevine device private key, or even it's identity by enforcing
|
- The CLI command `migrate` can now accept a folder path to batch migrate WVD files.
|
||||||
Privacy Mode.
|
- The Cdm now uses custom exceptions where the use case is justified. All custom exceptions are under a parent custom
|
||||||
- Requires installing with the `serve` extras, i.e., `pip install pywidevine[serve]`.
|
exception to allow catching of any Pywidevine exception.
|
||||||
- The default host of `127.0.0.1` blocks access outside your network, even if port-forwarded. Use
|
|
||||||
`-h 0.0.0.0` to allow remote access.
|
|
||||||
- Setup requires the use of a config file for configuring the CDM and authentication. An example config file named
|
|
||||||
`serve.example.yml` in the project root folder has verbose documentation on available options.
|
|
||||||
- Batch migration of WVD files by passing a folder as the path to the CLI command `migrate`.
|
|
||||||
- Strict mode to `PSSH.get_as_box()` to raise an Exception if passed data is not already a box, as it has been improved
|
|
||||||
to create a new box if not detected as a box already.
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Elevated the Development Status Classifier from 4 (Beta) to 5 (Production/Stable).
|
- The Cdm has been reworked as a session-based Cdm. You now initialize the Cdm with just the device you wish to use,
|
||||||
- License messages passed to `Cdm.parse_license()` are now rejected if they are not of `LICENSE` type.
|
and now you open sessions with `Cdm.open()` to get a session ID. For usage example see `license` CLI command in
|
||||||
- Service Certificates passed to `Cdm.set_service_certificate()` are now verified. This patches a trivial "exploit"
|
`main.py`.
|
||||||
that allows an attacker to recover the plaintext Client ID from a license under Privacy Mode. See
|
- The Cdm no longer requires you to specify `raw` bool parameter. It now supports arbitrary and valid Widevine Cenc
|
||||||
<https://gist.github.com/rlaphoenix/74acabdd7269a21845e18b621c5860ef>.
|
Header Data without needing to explicitly specify which it is.
|
||||||
- Data passed to `PSSH.get_as_box()` now supports arbitrary and box data automatically as it tries to detect if it is a
|
- The Cdm `pssh` param has been renamed as `init_data`. Doc-strings have been changed to prioritize explanation of it
|
||||||
valid box, otherwise makes a new box.
|
referring to Widevine Cenc Header rather than PSSH Boxes. This is to show that the Cdm more-so wants Init Data than
|
||||||
- Renamed the `Cdm` constructor's parameter `pssh` to `init_data`, as that's what the Cdm actually wants and uses,
|
a PSSH box. The full PSSH is never kept nor ever used, only it's init data is. It still supports PSSH box data.
|
||||||
whereas a `PSSH` is an `mp4` atom (aka box) containing `init_data` (a Widevine CENC Header). The full PSSH is never
|
- Cdm `set_service_certificate()` now returns the provider ID string rather than the underlying (and now verified)
|
||||||
kept nor ever used. It still accepts PSSH box data.
|
DrmCertificate. This is because the DrmCertificate is not likely useful and would still be possible to obtain in full
|
||||||
- Service Certificate's Provider ID is now returned by `Cdm.set_service_certificate()` instead of the passed
|
but quick access to the Provider ID may be more useful.
|
||||||
certificate, of which they would already have.
|
- License responses can now be only be parsed once by `Cdm.parse_license()`. Any further attempts will raise an
|
||||||
- The Cdm class now works more closely to the official CDM model. Instead of using one Cdm object per-request having to
|
InvalidContext exception. This is because context data is now cleared for it's respective License Request once it's
|
||||||
provide device information each time,
|
parsed to reduce data lingering in memory.
|
||||||
- You now initialize the Cdm with the Widevine device you wish to use and then open sessions with `Cdm.open()`.
|
- Trove Classifier for Development Status is now 5 (Production/Stable).
|
||||||
- You will receive a session ID that are then passed to other methods of the same Cdm object.
|
|
||||||
- The PSSH/init_data that used to be passed to the constructor is now passed to `Cdm.get_license_challenge()`.
|
|
||||||
- This allows initializing one Cdm object with up to 50 sessions open at the same time.
|
|
||||||
Session limits seem to fluctuate between libraries and devices. 50 seems like a permissive value.
|
|
||||||
- Once you are finished with DRM operations, discard all session (and key) data by calling `Cdm.close(session_id)`.
|
|
||||||
- License Keys are no longer returned by `Cdm.parse_license()` and now must be obtained directly from `cdm._sessions`.
|
|
||||||
- For example, `for key in cdm._sessions[session_id].keys: print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")`.
|
|
||||||
- This is to detach the action of parsing a license as just for getting keys, as it isn't. It can be and should be
|
|
||||||
used for a lot more data like security requirements like HDCP, expiration, and more.
|
|
||||||
- It is also to detour users from directly using the keys over the `Cdm.decrypt()` method.
|
|
||||||
- Various std-lib exceptions have been replaced with custom exceptions under `pywidevine.exceptions`.
|
|
||||||
- License responses can now only be parsed once by `Cdm.parse_license()`. Any further attempts will raise an
|
|
||||||
`InvalidContext` exception.
|
|
||||||
- This is as license context data is cleared once used to reduce data lingering in memory, otherwise the more license
|
|
||||||
requests you make without closing the session, the more and more memory is taken up.
|
|
||||||
- Open multiple sessions in the same Cdm object if you need to request and parse multiple licenses on the same device.
|
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- Direct `DrmCertificate`s are no longer supported by `Cdm.set_service_certificate()` as they have no signature.
|
- You can no longer provide a direct `DrmCertificate` to `Cdm.set_service_certificate()` for security reasons.
|
||||||
See the 3rd Change above. Provide either a `SignedDrmCertificate` or a `SignedMessage` containing a
|
You must provide either a `SignedDrmCertificate` or a `SignedMessage` containing a `SignedDrmCertificate`.
|
||||||
`SignedDrmCertificate`. A `SignedMessage` containing a `DrmCertificate` will also be rejected.
|
- PSSH `from_init_data()` has been removed. It was unused and is unnecessary with improvements to `get_as_box()`.
|
||||||
- `PSSH.from_init_data()`, use `PSSH.get_as_box()`.
|
|
||||||
- `raw` parameter of `Cdm` constructor, as well as CLI commands as it is now handled upstream by the `PSSH` creation.
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Detection of Widevine CENC Header data encoded as bytes in `PSSH.get_as_box()`.
|
- Cdm `set_service_certificate()` now verifies the signature of the provided Certificate. This patches a trivial
|
||||||
- Custom ValueError on missing contexts instead of the generic KeyError in `Cdm.parse_license()`.
|
exploit/workaround that allows an attacker to recover the plaintext Client ID from an encrypted Client ID.
|
||||||
- Typing of `type_` parameter in `Cdm.get_license_challenge()`.
|
- Cdm `parse_license()` now verifies the input message type as a `LICENSE` message.
|
||||||
- Value of `type_` parameter if is a string in `Cdm.get_license_challenge()`.
|
- Cdm `parse_license()` now clears context for the License Request once it's License Response message has been parsed.
|
||||||
|
This reduces data lingering in the `context` dictionary when it may only be needed once.
|
||||||
|
- The Context Availability error handler in Cdm `parse_license()` has been fixed.
|
||||||
|
- Typing of `type_` param of `Cdm.get_license_challenge()` has been fixed.
|
||||||
|
|
||||||
## [1.1.1] - 2022-07-22
|
## [1.1.1] - 2022-07-22
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- The `-v/--vmp` parameter of the `test` CLI command is now optional.
|
- The --vmp argument of the create-device command is now optional.
|
||||||
|
|
||||||
## [1.1.0] - 2022-07-21
|
## [1.1.0] - 2022-07-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- WVD (Widevine Device file) Version 2 bringing reduced file sizes by up to 30%~.
|
- Added support for setting a Service Certificate in SignedDrmCertificate form as well as raw DrmCertificate form.
|
||||||
- New CLI command `create-device` to create `.wvd` files (Widevine Device files) from RSA PEM/DER Private Keys and
|
However, It's unlikely for the service to provide the certificate in raw DrmCertificate form without a signature.
|
||||||
|
- Added a CLI command `create-device` to create Widevine Device (`.wvd`) files from RSA PEM/DER Private Keys and
|
||||||
Client ID blobs. You can also provide VMP (FileHashes) data which will be merged into the Client ID blob.
|
Client ID blobs. You can also provide VMP (FileHashes) data which will be merged into the Client ID blob.
|
||||||
- New CLI command `migrate` that uses `Device.migrate()` and `dump()` to migrate older v1 Widevine Device files to v2.
|
- Added a CLI command `migrate` that uses `Device.migrate()` and `dump()` to migrate older v1 Widevine Device files
|
||||||
- New `Device` method `migrate()` to load an older Widevine Device file format. It is recommended to then use the
|
to v2.
|
||||||
`dumps()` method to save it as a new v2 Widevine Device file, which can then be loaded normally.
|
- Added the v1 Structure of Widevine Devices for migration use.
|
||||||
- Support `SignedDrmCertificate` and `DrmCertificate` messages in `Cdm.set_service_certificate()`. Services can provide
|
- Added `Device.migrate()` class method that effectively loads older format WVD data. You can then use `dumps()` to
|
||||||
the certificate as a `SignedMessage`, `SignedDrmCertificate`, or a `DrmCertificate`. Only `SignedMessage` and
|
get back the WVD data in the latest supported format.
|
||||||
`SignedDrmCertificate` are signed.
|
- Added ability to use Privacy mode on the test command.
|
||||||
- Privacy Mode can now be used in the `test` CLI command with the `-p/--privacy` flag.
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Moved all `.wvd` Widevine Device file structures from `Device` to a `_Structures` class in `device.py`. The
|
- Set Service Certificates are now stored as the raw underlying DrmCertificate as the signature data is unused by
|
||||||
`_Structures` class can be imported and used directly, or via `Device.structures`.
|
the CDM.
|
||||||
- Moved the majority of Widevine Device file migration code from the CLI command `migrate` to `Device.migrate()`. The
|
- Moved all Widevine Device structures under a Structures class.
|
||||||
CLI command `migrate` now internally uses `Device.migrate()`.
|
- I removed the `send_key_control_nonce` flag from all Structures even though it was technically used.
|
||||||
- Set Service Certificates are now stored as `DrmCertificate`s instead of a `SignedMessage` as the signature and other
|
This is because the flag was never used as of this project, and I do not want to take up the flag slot.
|
||||||
data in the message is unused and unneeded.
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Unused Widevine Device file flag `send_key_control_nonce` from v1 and v2 Structures as it was only used before initial
|
|
||||||
release, and isn't a necessary nor useful flag.
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Correct the type argument name from `type` to `type_` in `Device.dump()`.
|
- Devices `dump()` function now uses the correct `type_` parameter when building the struct.
|
||||||
|
- Fixed release date year of v1.0.0 and v1.0.1 in the changelog.
|
||||||
### Security
|
|
||||||
|
|
||||||
- Even though support for more kinds of Service Certificate Signatures were added, they are still unverified as the
|
|
||||||
signing public key is Unknown.
|
|
||||||
|
|
||||||
## [1.0.1] - 2022-07-21
|
## [1.0.1] - 2022-07-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- More information to the PyPI meta information, e.g., classifiers, readme, some URLs.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Moved the License Type parameter from the `Cdm` constructor to it's `get_license_challenge()` method.
|
- Moved the License Type parameter from the Cdm constructor to `get_license_challenge()`.
|
||||||
- Every License request now uses a unique random value instead of the CDM Session ID.
|
- The Session ID is no longer used as the Request ID which could help with blocks or replay checks due
|
||||||
- Only the Context Data of License requests are now stored in the Session instead of the full message.
|
to it being the same Session ID for each request. It's now a random 16 byte value each time.
|
||||||
- Session ID formula now uses a random 16-byte value for both Chrome and Android provisions.
|
- Only the Context Data of each license request is now stored instead of the full message.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- Unused and unnecessary `Cdm.raw` class instance variable.
|
- Removed unnecessary and unused `raw` Cdm class instance variable.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Re-raise DecodeErrors instead of a new ValueError on DecodeErrors in `Cdm.set_service_certificate()`.
|
- CDMs `set_service_certificate()` now correctly raises a DecodeError on Decode Error instead of a ValueError.
|
||||||
- Creating a new License request no longer overwrites the context data of the previous challenge.
|
- Context Data will now always match to their corresponding License Responses. This fixes an issue where creating
|
||||||
|
a second challenge would overwrite the context data of the first challenge. Parsing the first challenge after
|
||||||
|
would result in either a key decrypt error, or garbage key data.
|
||||||
|
|
||||||
## [1.0.0] - 2022-07-20
|
## [1.0.0] - 2022-07-20
|
||||||
|
|
||||||
Initial Release.
|
Initial Release.
|
||||||
|
|
||||||
### Security
|
[1.6.0]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.6.0
|
||||||
|
[1.5.3]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.5.3
|
||||||
- Service Certificate Signatures are unverified as the signing public key is Unknown.
|
[1.5.2]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.5.2
|
||||||
|
[1.5.1]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.5.1
|
||||||
[1.8.0]: https://github.com/devine-dl/pywidevine/releases/tag/v1.8.0
|
[1.5.0]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.5.0
|
||||||
[1.7.0]: https://github.com/devine-dl/pywidevine/releases/tag/v1.7.0
|
[1.4.4]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.4.4
|
||||||
[1.6.0]: https://github.com/devine-dl/pywidevine/releases/tag/v1.6.0
|
[1.4.3]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.4.3
|
||||||
[1.5.3]: https://github.com/devine-dl/pywidevine/releases/tag/v1.5.3
|
[1.4.2]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.4.2
|
||||||
[1.5.2]: https://github.com/devine-dl/pywidevine/releases/tag/v1.5.2
|
[1.4.1]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.4.1
|
||||||
[1.5.1]: https://github.com/devine-dl/pywidevine/releases/tag/v1.5.1
|
[1.4.0]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.4.0
|
||||||
[1.5.0]: https://github.com/devine-dl/pywidevine/releases/tag/v1.5.0
|
[1.3.1]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.3.1
|
||||||
[1.4.4]: https://github.com/devine-dl/pywidevine/releases/tag/v1.4.4
|
[1.3.0]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.3.0
|
||||||
[1.4.3]: https://github.com/devine-dl/pywidevine/releases/tag/v1.4.3
|
[1.2.1]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.2.1
|
||||||
[1.4.2]: https://github.com/devine-dl/pywidevine/releases/tag/v1.4.2
|
[1.2.0]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.2.0
|
||||||
[1.4.1]: https://github.com/devine-dl/pywidevine/releases/tag/v1.4.1
|
[1.1.1]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.1.1
|
||||||
[1.4.0]: https://github.com/devine-dl/pywidevine/releases/tag/v1.4.0
|
[1.1.0]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.1.0
|
||||||
[1.3.1]: https://github.com/devine-dl/pywidevine/releases/tag/v1.3.1
|
[1.0.1]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.0.1
|
||||||
[1.3.0]: https://github.com/devine-dl/pywidevine/releases/tag/v1.3.0
|
[1.0.0]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.0.0
|
||||||
[1.2.1]: https://github.com/devine-dl/pywidevine/releases/tag/v1.2.1
|
|
||||||
[1.2.0]: https://github.com/devine-dl/pywidevine/releases/tag/v1.2.0
|
|
||||||
[1.1.1]: https://github.com/devine-dl/pywidevine/releases/tag/v1.1.1
|
|
||||||
[1.1.0]: https://github.com/devine-dl/pywidevine/releases/tag/v1.1.0
|
|
||||||
[1.0.1]: https://github.com/devine-dl/pywidevine/releases/tag/v1.0.1
|
|
||||||
[1.0.0]: https://github.com/devine-dl/pywidevine/releases/tag/v1.0.0
|
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
# Development
|
|
||||||
|
|
||||||
This project is managed using [Poetry](https://python-poetry.org), a fantastic Python packaging and dependency manager.
|
|
||||||
Install the latest version of Poetry before continuing. Development currently requires Python 3.8+.
|
|
||||||
|
|
||||||
## Set up
|
|
||||||
|
|
||||||
Starting from Zero? Not sure where to begin? Here's steps on setting up this Python project using Poetry. Note that
|
|
||||||
Poetry installation instructions should be followed from the Poetry Docs: https://python-poetry.org/docs/#installation
|
|
||||||
|
|
||||||
1. While optional, It's recommended to configure Poetry to install Virtual environments within project folders:
|
|
||||||
```shell
|
|
||||||
poetry config virtualenvs.in-project true
|
|
||||||
```
|
|
||||||
This makes it easier for Visual Studio Code to detect the Virtual Environment, as well as other IDEs and systems.
|
|
||||||
I've also had issues with Poetry creating duplicate Virtual environments in the default folder for an unknown
|
|
||||||
reason which quickly filled up my System storage.
|
|
||||||
2. Clone the Repository:
|
|
||||||
```shell
|
|
||||||
git clone https://github.com/devine-dl/pywidevine
|
|
||||||
cd pywidevine
|
|
||||||
```
|
|
||||||
3. Install the Project with Poetry:
|
|
||||||
```shell
|
|
||||||
poetry install
|
|
||||||
```
|
|
||||||
This creates a Virtual environment and then installs all project dependencies and executables into the Virtual
|
|
||||||
environment. Your System Python environment is not affected at all.
|
|
||||||
4. Now activate the Virtual environment:
|
|
||||||
```shell
|
|
||||||
poetry shell
|
|
||||||
```
|
|
||||||
Note:
|
|
||||||
- You can alternatively just prefix `poetry run` to any command you wish to run under the Virtual environment.
|
|
||||||
- I recommend entering the Virtual environment and all further instructions will have assumed you did.
|
|
||||||
- JetBrains PyCharm has integrated support for Poetry and automatically enters Poetry Virtual environments, assuming
|
|
||||||
the Python Interpreter on the bottom right is set up correctly.
|
|
||||||
- For more information, see: https://python-poetry.org/docs/basic-usage/#using-your-virtual-environment
|
|
||||||
5. Install Pre-commit tooling to ensure safe and quality commits:
|
|
||||||
```shell
|
|
||||||
pre-commit install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building Source and Wheel distributions
|
|
||||||
|
|
||||||
poetry build
|
|
||||||
|
|
||||||
You can optionally specify `-f` to build `sdist` or `wheel` only.
|
|
||||||
Built files can be found in the `/dist` directory.
|
|
||||||
173
README.md
173
README.md
@ -1,65 +1,74 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/images/widevine_icon_24.png"> <a href="https://github.com/devine-dl/pywidevine">pywidevine</a>
|
<img src="docs/images/widevine_icon_24.png"> <a href="https://github.com/rlaphoenix/pywidevine">pywidevine</a>
|
||||||
<br/>
|
<br/>
|
||||||
<sup><em>Python Widevine CDM implementation</em></sup>
|
<sup><em>Python Widevine CDM implementation.</em></sup>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/devine-dl/pywidevine/blob/master/LICENSE">
|
<a href="https://github.com/rlaphoenix/pywidevine/actions/workflows/ci.yml">
|
||||||
<img src="https://img.shields.io/:license-GPL%203.0-blue.svg" alt="License">
|
<img src="https://github.com/rlaphoenix/pywidevine/actions/workflows/ci.yml/badge.svg" alt="Build status">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://pypi.org/project/pywidevine">
|
<a href="https://pypi.org/project/pywidevine">
|
||||||
<img src="https://img.shields.io/badge/python-3.9%2B-informational" alt="Python version">
|
<img src="https://img.shields.io/badge/python-3.7%2B-informational" alt="Python version">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/astral-sh/uv">
|
<a href="https://deepsource.io/gh/rlaphoenix/pywidevine">
|
||||||
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Onyx-Nostalgia/uv/refs/heads/fix/logo-badge/assets/badge/v0.json" alt="Manager: uv">
|
<img src="https://deepsource.io/gh/rlaphoenix/pywidevine.svg/?label=active+issues" alt="DeepSource">
|
||||||
</a>
|
|
||||||
<a href="https://github.com/astral-sh/ruff">
|
|
||||||
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Linter: Ruff">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/devine-dl/pywidevine/actions/workflows/ci.yml">
|
|
||||||
<img src="https://github.com/devine-dl/pywidevine/actions/workflows/ci.yml/badge.svg" alt="Build status">
|
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🚀 Seamless Installation via [pip](#installation)
|
- 🛡️ Security-first approach; All user input has Signatures verified
|
||||||
- 🛡️ Robust Security with message signature verification
|
- 👥 Remotely accessible Server/Client CDM code
|
||||||
- 🙈 Privacy Mode with Service Certificates
|
- 📦 Supports parsing and serialization of WVD (v2) provisions
|
||||||
- 🌐 Servable CDM API Server and Client with Authentication
|
- 🛠️ Class for creation, parsing, and conversion of PSSH data
|
||||||
- 📦 Custom provision serialization format (WVD v2)
|
- 🧩 Plug-and-play installation via PIP/PyPI
|
||||||
- 🧰 Create, parse, or convert PSSH headers with ease
|
- 🗃️ YAML configuration files
|
||||||
- 🗃️ User-friendly YAML configuration
|
|
||||||
- ❤️ Forever FOSS!
|
- ❤️ Forever FOSS!
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### With pip
|
*Note: Requires [Python] 3.7.0 or newer with PIP installed.*
|
||||||
|
|
||||||
> Since *pip* is pre-installed with Python, it is the most straight forward way to install pywidevine.
|
```shell
|
||||||
|
$ pip install pywidevine
|
||||||
|
```
|
||||||
|
|
||||||
Simply run `pip install pywidevine` and it will be ready to use from the CLI or within scripts in a minute.
|
You now have the `pywidevine` package installed and a `pywidevine` executable is now available.
|
||||||
|
Check it out with `pywidevine --help` - Voilà 🎉!
|
||||||
|
|
||||||
### With uv
|
### From Source Code
|
||||||
|
|
||||||
> This is recommended for those who wish to install from the source code, are working on changes in the source code, or
|
The following steps are instructions on download, preparing, and running the code under a Poetry environment.
|
||||||
just simply prefer it's many handy features.
|
You can skip steps 3-5 with a simple `pip install .` call instead, but you miss out on a wide array of benefits.
|
||||||
|
|
||||||
Go to to the official website and [get uv installed](https://docs.astral.sh/uv/getting-started/installation/). Download
|
1. `git clone https://github.com/rlaphoenix/pywidevine`
|
||||||
or clone this repository, go inside it, and run `uv run pywidevine --version`. To run scripts, like a `license.py` that
|
2. `cd pywidevine`
|
||||||
is importing pywidevine, do `uv run license.py`. Effectively, put `uv run` before calling whatever is using pywidevine.
|
3. (optional) `poetry config virtualenvs.in-project true`
|
||||||
For other ways to run pywidevine with uv, see [Running commands](https://docs.astral.sh/uv/guides/projects/#running-commands).
|
4. `poetry install`
|
||||||
|
5. `poetry run pywidevine --help`
|
||||||
|
|
||||||
|
As seen in Step 5, running the `pywidevine` executable is somewhat different to a normal PIP installation.
|
||||||
|
See [Poetry's Docs] on various ways of making calls under the virtual-environment.
|
||||||
|
|
||||||
|
[Python]: <https://python.org>
|
||||||
|
[Poetry]: <https://python-poetry.org>
|
||||||
|
[Poetry's Docs]: <https://python-poetry.org/docs/basic-usage/#using-your-virtual-environment>
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
There are two ways to use pywidevine, through scripts, or the CLI (command-line interface).
|
The following is a minimal example of using pywidevine in a script. It gets a License for Bitmovin's
|
||||||
Most people would be using it through scripts due to complexities working with license server APIs.
|
Art of Motion Demo. There's various stuff not shown in this specific example like:
|
||||||
|
|
||||||
### Scripts
|
- Privacy Mode
|
||||||
|
- Setting Service Certificates
|
||||||
|
- Remote CDMs and Serving
|
||||||
|
- Choosing a License Type to request
|
||||||
|
- Creating WVD files
|
||||||
|
- and much more!
|
||||||
|
|
||||||
The following is a minimal example of using pywidevine in a script to get a License for Bitmovin's Art of Motion Demo.
|
Just take a look around the Cdm code to see what stuff does. Everything is documented quite well.
|
||||||
This demo can be found on [Bitmovin's DRM Stream Test demo page](https://bitmovin.com/demos/drm/).
|
There's also various functions in `main.py` that showcases a lot of features.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from pywidevine.cdm import Cdm
|
from pywidevine.cdm import Cdm
|
||||||
@ -68,93 +77,46 @@ from pywidevine.pssh import PSSH
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
# prepare pssh (usually inside the MPD/M3U8, an API response, the player page, or inside the pssh mp4 box)
|
# prepare pssh
|
||||||
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa"
|
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa"
|
||||||
"7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
|
"7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
|
||||||
|
|
||||||
# load device from a WVD file (your provision)
|
# load device
|
||||||
device = Device.load("C:/Path/To/A/Provision.wvd")
|
device = Device.load("C:/Path/To/A/Provision.wvd")
|
||||||
|
|
||||||
# load cdm (creating a CDM instance using that device)
|
# load cdm
|
||||||
cdm = Cdm.from_device(device)
|
cdm = Cdm.from_device(device)
|
||||||
|
|
||||||
# open cdm session (note that any one device should have a practical limit to amount of sessions open at any one time)
|
# open cdm session
|
||||||
session_id = cdm.open()
|
session_id = cdm.open()
|
||||||
|
|
||||||
# get license challenge (generate a license request message, signed using the device with the pssh)
|
# get license challenge
|
||||||
challenge = cdm.get_license_challenge(session_id, pssh)
|
challenge = cdm.get_license_challenge(session_id, pssh)
|
||||||
|
|
||||||
# send license challenge to bitmovin's license server (which has no auth and asks simply for the license challenge as-is)
|
# send license challenge (assuming a generic license server SDK with no API front)
|
||||||
# another license server may require authentication and ask for it as JSON or form data instead
|
licence = requests.post("https://...", data=challenge)
|
||||||
# you may also be required to use privacy mode, where you use their service certificate when creating the challenge
|
|
||||||
licence = requests.post("https://cwip-shaka-proxy.appspot.com/no_auth", data=challenge)
|
|
||||||
licence.raise_for_status()
|
licence.raise_for_status()
|
||||||
|
|
||||||
# parse the license response message received from the license server API
|
# parse license challenge
|
||||||
cdm.parse_license(session_id, licence.content)
|
cdm.parse_license(session_id, licence.content)
|
||||||
|
|
||||||
# print keys
|
# print keys
|
||||||
for key in cdm.get_keys(session_id):
|
for key in cdm.get_keys(session_id):
|
||||||
print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")
|
print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")
|
||||||
|
|
||||||
# finished, close the session, disposing of all keys and other related data
|
# close session, disposes of session data
|
||||||
cdm.close(session_id)
|
cdm.close(session_id)
|
||||||
```
|
```
|
||||||
|
|
||||||
There are other features not shown in this small example like:
|
## Troubleshooting
|
||||||
|
|
||||||
- Privacy Mode
|
### Executable `pywidevine` was not found
|
||||||
- Setting Service Certificates
|
|
||||||
- Remote CDMs and Serving
|
|
||||||
- Choosing a License Type
|
|
||||||
- Creating WVD files
|
|
||||||
- and much more!
|
|
||||||
|
|
||||||
> [!TIP]
|
Make sure the Python installation's Scripts directory is added to your Path Environment Variable.
|
||||||
> For examples, take a look at the methods available in the [Cdm class](/pywidevine/cdm.py) and read their doc-strings
|
|
||||||
> for further information.
|
|
||||||
|
|
||||||
### Command-line Interface
|
If this happened under a Poetry environment, make sure you use the appropriate Poetry-specific way of calling
|
||||||
|
the executable. You may make this executable available globally by adding the .venv's Scripts folder to your
|
||||||
The CLI can be useful to do simple license calls, migrate WVD files, and test provisions.
|
Path Environment Variable.
|
||||||
Take a look at `pywidevine --help` to see a list of commands available.
|
|
||||||
|
|
||||||
```plain
|
|
||||||
Usage: pywidevine [OPTIONS] COMMAND [ARGS]...
|
|
||||||
|
|
||||||
pywidevine—Python Widevine CDM implementation.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-v, --version Print version information.
|
|
||||||
-d, --debug Enable DEBUG level logs.
|
|
||||||
--help Show this message and exit.
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
create-device Create a Widevine Device (.wvd) file from an RSA Private...
|
|
||||||
export-device Export a Widevine Device (.wvd) file to an RSA Private...
|
|
||||||
license Make a License Request for PSSH to SERVER using DEVICE.
|
|
||||||
migrate Upgrade from earlier versions of the Widevine Device...
|
|
||||||
serve Serve your local CDM and Widevine Devices Remotely.
|
|
||||||
test Test the CDM code by getting Content Keys for Bitmovin's...
|
|
||||||
```
|
|
||||||
|
|
||||||
Every command has further help information, simply type `pywidevine <command> --help`.
|
|
||||||
For example, `pywidevine test --help`:
|
|
||||||
|
|
||||||
```plain
|
|
||||||
Usage: pywidevine test [OPTIONS] DEVICE
|
|
||||||
|
|
||||||
Test the CDM code by getting Content Keys for Bitmovin's Art of Motion
|
|
||||||
example. https://bitmovin.com/demos/drm
|
|
||||||
https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/mpds/11331.mpd
|
|
||||||
|
|
||||||
The device argument is a Path to a Widevine Device (.wvd) file which
|
|
||||||
contains the device private key among other required information.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-p, --privacy Use Privacy Mode, off by default.
|
|
||||||
--help Show this message and exit.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
@ -197,20 +159,11 @@ been improving its security using math and obscurity for years. It's getting har
|
|||||||
versions only being beaten by Brute-force style methods. However, they have a huge team of very skilled workers, and
|
versions only being beaten by Brute-force style methods. However, they have a huge team of very skilled workers, and
|
||||||
making a CDM in C++ has immediate security benefits and a lot of methods to obscure and obfuscate the code.
|
making a CDM in C++ has immediate security benefits and a lot of methods to obscure and obfuscate the code.
|
||||||
|
|
||||||
## Contributors
|
## Credit
|
||||||
|
|
||||||
<a href="https://github.com/rlaphoenix"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/17136956?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt=""/></a>
|
|
||||||
<a href="https://github.com/mediaminister"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45148099?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt=""/></a>
|
|
||||||
<a href="https://github.com/sr0lle"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/111277375?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt=""/></a>
|
|
||||||
|
|
||||||
## Licensing
|
|
||||||
|
|
||||||
This software is licensed under the terms of [GNU General Public License, Version 3.0](LICENSE).
|
|
||||||
You can find a copy of the license in the LICENSE file in the root folder.
|
|
||||||
|
|
||||||
- Widevine Icon © Google.
|
- Widevine Icon © Google.
|
||||||
- Props to the awesome community for their shared research and insight into the Widevine Protocol and Key Derivation.
|
- The awesome community for their shared research and insight into the Widevine Protocol and Key Derivation.
|
||||||
|
|
||||||
* * *
|
## License
|
||||||
|
|
||||||
© rlaphoenix 2022-2025
|
[GNU General Public License, Version 3.0](LICENSE)
|
||||||
|
|||||||
792
poetry.lock
generated
Normal file
792
poetry.lock
generated
Normal file
@ -0,0 +1,792 @@
|
|||||||
|
# This file is automatically @generated by Poetry and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiohttp"
|
||||||
|
version = "3.8.1"
|
||||||
|
description = "Async http client/server framework (asyncio)"
|
||||||
|
category = "main"
|
||||||
|
optional = true
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"},
|
||||||
|
{file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
aiosignal = ">=1.1.2"
|
||||||
|
async-timeout = ">=4.0.0a3,<5.0"
|
||||||
|
asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""}
|
||||||
|
attrs = ">=17.3.0"
|
||||||
|
charset-normalizer = ">=2.0,<3.0"
|
||||||
|
frozenlist = ">=1.1.1"
|
||||||
|
multidict = ">=4.5,<7.0"
|
||||||
|
typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
|
||||||
|
yarl = ">=1.0,<2.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
speedups = ["Brotli", "aiodns", "cchardet"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiosignal"
|
||||||
|
version = "1.2.0"
|
||||||
|
description = "aiosignal: a list of registered asynchronous callbacks"
|
||||||
|
category = "main"
|
||||||
|
optional = true
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
|
||||||
|
{file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
frozenlist = ">=1.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-timeout"
|
||||||
|
version = "4.0.2"
|
||||||
|
description = "Timeout context manager for asyncio programs"
|
||||||
|
category = "main"
|
||||||
|
optional = true
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
|
||||||
|
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asynctest"
|
||||||
|
version = "0.13.0"
|
||||||
|
description = "Enhance the standard unittest package with features for testing asyncio libraries"
|
||||||
|
category = "main"
|
||||||
|
optional = true
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
files = [
|
||||||
|
{file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
|
||||||
|
{file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attrs"
|
||||||
|
version = "21.4.0"
|
||||||
|
description = "Classes Without Boilerplate"
|
||||||
|
category = "main"
|
||||||
|
optional = true
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
files = [
|
||||||
|
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
|
||||||
|
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||||
|
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||||
|
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"]
|
||||||
|
tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2022.12.7"
|
||||||
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
|
||||||
|
{file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "2.1.0"
|
||||||
|
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.0"
|
||||||
|
files = [
|
||||||
|
{file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"},
|
||||||
|
{file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
unicode-backport = ["unicodedata2"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.1.3"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||||
|
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.5"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
files = [
|
||||||
|
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
||||||
|
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "construct"
|
||||||
|
version = "2.8.8"
|
||||||
|
description = "A powerful declarative parser/builder for binary data"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "construct-2.8.8.tar.gz", hash = "sha256:1b84b8147f6fd15bcf64b737c3e8ac5100811ad80c830cb4b2545140511c4157"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "frozenlist"
|
||||||
|
version = "1.3.0"
|
||||||
|
description = "A list-like structure which implements collections.abc.MutableSequence"
|
||||||
|
category = "main"
|
||||||
|
optional = true
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"},
|
||||||
|
{file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.3"
|
||||||
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
files = [
|
||||||
|
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
||||||
|
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "importlib-metadata"
|
||||||
|
version = "4.12.0"
|
||||||
|
description = "Read metadata from Python packages"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"},
|
||||||
|
{file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||||
|
zipp = ">=0.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"]
|
||||||
|
perf = ["ipython"]
|
||||||
|
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lxml"
|
||||||
|
version = "4.9.2"
|
||||||
|
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
|
||||||
|
files = [
|
||||||
|
{file = "lxml-4.9.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2"},
|
||||||
|
{file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892"},
|
||||||
|
{file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a"},
|
||||||
|
{file = "lxml-4.9.2-cp27-cp27m-win32.whl", hash = "sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de"},
|
||||||
|
{file = "lxml-4.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3"},
|
||||||
|
{file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50"},
|
||||||
|
{file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975"},
|
||||||
|
{file = "lxml-4.9.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c"},
|
||||||
|
{file = "lxml-4.9.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a"},
|
||||||
|
{file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4"},
|
||||||
|
{file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4"},
|
||||||
|
{file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7"},
|
||||||
|
{file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184"},
|
||||||
|
{file = "lxml-4.9.2-cp310-cp310-win32.whl", hash = "sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda"},
|
||||||
|
{file = "lxml-4.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab"},
|
||||||
|
{file = "lxml-4.9.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9"},
|
||||||
|
{file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf"},
|
||||||
|
{file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380"},
|
||||||
|
{file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92"},
|
||||||
|
{file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1"},
|
||||||
|
{file = "lxml-4.9.2-cp311-cp311-win32.whl", hash = "sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33"},
|
||||||
|
{file = "lxml-4.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd"},
|
||||||
|
{file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0"},
|
||||||
|
{file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e"},
|
||||||
|
{file = "lxml-4.9.2-cp35-cp35m-win32.whl", hash = "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df"},
|
||||||
|
{file = "lxml-4.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5"},
|
||||||
|
{file = "lxml-4.9.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53"},
|
||||||
|
{file = "lxml-4.9.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7"},
|
||||||
|
{file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe"},
|
||||||
|
{file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c"},
|
||||||
|
{file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1"},
|
||||||
|
{file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e"},
|
||||||
|
{file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74"},
|
||||||
|
{file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38"},
|
||||||
|
{file = "lxml-4.9.2-cp36-cp36m-win32.whl", hash = "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5"},
|
||||||
|
{file = "lxml-4.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3"},
|
||||||
|
{file = "lxml-4.9.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03"},
|
||||||
|
{file = "lxml-4.9.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941"},
|
||||||
|
{file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726"},
|
||||||
|
{file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b"},
|
||||||
|
{file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894"},
|
||||||
|
{file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45"},
|
||||||
|
{file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e"},
|
||||||
|
{file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b"},
|
||||||
|
{file = "lxml-4.9.2-cp37-cp37m-win32.whl", hash = "sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe"},
|
||||||
|
{file = "lxml-4.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9"},
|
||||||
|
{file = "lxml-4.9.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8"},
|
||||||
|
{file = "lxml-4.9.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24"},
|
||||||
|
{file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889"},
|
||||||
|
{file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f"},
|
||||||
|
{file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03"},
|
||||||
|
{file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c"},
|
||||||
|
{file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f"},
|
||||||
|
{file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457"},
|
||||||
|
{file = "lxml-4.9.2-cp38-cp38-win32.whl", hash = "sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b"},
|
||||||
|
{file = "lxml-4.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7"},
|
||||||
|
{file = "lxml-4.9.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1"},
|
||||||
|
{file = "lxml-4.9.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140"},
|
||||||
|
{file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4"},
|
||||||
|
{file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf"},
|
||||||
|
{file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947"},
|
||||||
|
{file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5"},
|
||||||
|
{file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5"},
|
||||||
|
{file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2"},
|
||||||
|
{file = "lxml-4.9.2-cp39-cp39-win32.whl", hash = "sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1"},
|
||||||
|
{file = "lxml-4.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f"},
|
||||||
|
{file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c"},
|
||||||
|
{file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a"},
|
||||||
|
{file = "lxml-4.9.2-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419"},
|
||||||
|
{file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05"},
|
||||||
|
{file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f"},
|
||||||
|
{file = "lxml-4.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9"},
|
||||||
|
{file = "lxml-4.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5"},
|
||||||
|
{file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746"},
|
||||||
|
{file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7"},
|
||||||
|
{file = "lxml-4.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409"},
|
||||||
|
{file = "lxml-4.9.2.tar.gz", hash = "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cssselect = ["cssselect (>=0.7)"]
|
||||||
|
html5 = ["html5lib"]
|
||||||
|
htmlsoup = ["BeautifulSoup4"]
|
||||||
|
source = ["Cython (>=0.29.7)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "multidict"
|
||||||
|
version = "6.0.2"
|
||||||
|
description = "multidict implementation"
|
||||||
|
category = "main"
|
||||||
|
optional = true
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"},
|
||||||
|
{file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protobuf"
|
||||||
|
version = "4.21.6"
|
||||||
|
description = ""
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "protobuf-4.21.6-cp310-abi3-win32.whl", hash = "sha256:49f88d56a9180dbb7f6199c920f5bb5c1dd0172f672983bb281298d57c2ac8eb"},
|
||||||
|
{file = "protobuf-4.21.6-cp310-abi3-win_amd64.whl", hash = "sha256:7a6cc8842257265bdfd6b74d088b829e44bcac3cca234c5fdd6052730017b9ea"},
|
||||||
|
{file = "protobuf-4.21.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ba596b9ffb85c909fcfe1b1a23136224ed678af3faf9912d3fa483d5f9813c4e"},
|
||||||
|
{file = "protobuf-4.21.6-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:4143513c766db85b9d7c18dbf8339673c8a290131b2a0fe73855ab20770f72b0"},
|
||||||
|
{file = "protobuf-4.21.6-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:b6cea204865595a92a7b240e4b65bcaaca3ad5d2ce25d9db3756eba06041138e"},
|
||||||
|
{file = "protobuf-4.21.6-cp37-cp37m-win32.whl", hash = "sha256:9666da97129138585b26afcb63ad4887f602e169cafe754a8258541c553b8b5d"},
|
||||||
|
{file = "protobuf-4.21.6-cp37-cp37m-win_amd64.whl", hash = "sha256:308173d3e5a3528787bb8c93abea81d5a950bdce62840d9760effc84127fb39c"},
|
||||||
|
{file = "protobuf-4.21.6-cp38-cp38-win32.whl", hash = "sha256:aa29113ec901281f29d9d27b01193407a98aa9658b8a777b0325e6d97149f5ce"},
|
||||||
|
{file = "protobuf-4.21.6-cp38-cp38-win_amd64.whl", hash = "sha256:8f9e60f7d44592c66e7b332b6a7b4b6e8d8b889393c79dbc3a91f815118f8eac"},
|
||||||
|
{file = "protobuf-4.21.6-cp39-cp39-win32.whl", hash = "sha256:80e6540381080715fddac12690ee42d087d0d17395f8d0078dfd6f1181e7be4c"},
|
||||||
|
{file = "protobuf-4.21.6-cp39-cp39-win_amd64.whl", hash = "sha256:77b355c8604fe285536155286b28b0c4cbc57cf81b08d8357bf34829ea982860"},
|
||||||
|
{file = "protobuf-4.21.6-py2.py3-none-any.whl", hash = "sha256:07a0bb9cc6114f16a39c866dc28b6e3d96fa4ffb9cc1033057412547e6e75cb9"},
|
||||||
|
{file = "protobuf-4.21.6-py3-none-any.whl", hash = "sha256:c7c864148a237f058c739ae7a05a2b403c0dfa4ce7d1f3e5213f352ad52d57c6"},
|
||||||
|
{file = "protobuf-4.21.6.tar.gz", hash = "sha256:6b1040a5661cd5f6e610cbca9cfaa2a17d60e2bb545309bc1b278bb05be44bdd"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycryptodome"
|
||||||
|
version = "3.15.0"
|
||||||
|
description = "Cryptographic library for Python"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
files = [
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff7ae90e36c1715a54446e7872b76102baa5c63aa980917f4aa45e8c78d1a3ec"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2ffd8b31561455453ca9f62cb4c24e6b8d119d6d531087af5f14b64bee2c23e6"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2ea63d46157386c5053cfebcdd9bd8e0c8b7b0ac4a0507a027f5174929403884"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:7c9ed8aa31c146bef65d89a1b655f5f4eab5e1120f55fc297713c89c9e56ff0b"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:5099c9ca345b2f252f0c28e96904643153bae9258647585e5e6f649bb7a1844a"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:2ec709b0a58b539a4f9d33fb8508264c3678d7edb33a68b8906ba914f71e8c13"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:2ae53125de5b0d2c95194d957db9bb2681da8c24d0fb0fe3b056de2bcaf5d837"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27m-win32.whl", hash = "sha256:fd2184aae6ee2a944aaa49113e6f5787cdc5e4db1eb8edb1aea914bd75f33a0c"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:7e3a8f6ee405b3bd1c4da371b93c31f7027944b2bcce0697022801db93120d83"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:b9c5b1a1977491533dfd31e01550ee36ae0249d78aae7f632590db833a5012b8"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0926f7cc3735033061ef3cf27ed16faad6544b14666410727b31fea85a5b16eb"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:2aa55aae81f935a08d5a3c2042eb81741a43e044bd8a81ea7239448ad751f763"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:c3640deff4197fa064295aaac10ab49a0d55ef3d6a54ae1499c40d646655c89f"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:045d75527241d17e6ef13636d845a12e54660aa82e823b3b3341bcf5af03fa79"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:eb6fce570869e70cc8ebe68eaa1c26bed56d40ad0f93431ee61d400525433c54"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9ee40e2168f1348ae476676a2e938ca80a2f57b14a249d8fe0d3cdf803e5a676"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:4c3ccad74eeb7b001f3538643c4225eac398c77d617ebb3e57571a897943c667"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:1b22bcd9ec55e9c74927f6b1f69843cb256fb5a465088ce62837f793d9ffea88"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:57f565acd2f0cf6fb3e1ba553d0cb1f33405ec1f9c5ded9b9a0a5320f2c0bd3d"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:4b52cb18b0ad46087caeb37a15e08040f3b4c2d444d58371b6f5d786d95534c2"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:092a26e78b73f2530b8bd6b3898e7453ab2f36e42fd85097d705d6aba2ec3e5e"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:50ca7e587b8e541eb6c192acf92449d95377d1f88908c0a32ac5ac2703ebe28b"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp35-abi3-win32.whl", hash = "sha256:e244ab85c422260de91cda6379e8e986405b4f13dc97d2876497178707f87fc1"},
|
||||||
|
{file = "pycryptodome-3.15.0-cp35-abi3-win_amd64.whl", hash = "sha256:c77126899c4b9c9827ddf50565e93955cb3996813c18900c16b2ea0474e130e9"},
|
||||||
|
{file = "pycryptodome-3.15.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:9eaadc058106344a566dc51d3d3a758ab07f8edde013712bc8d22032a86b264f"},
|
||||||
|
{file = "pycryptodome-3.15.0-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:ff287bcba9fbeb4f1cccc1f2e90a08d691480735a611ee83c80a7d74ad72b9d9"},
|
||||||
|
{file = "pycryptodome-3.15.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:60b4faae330c3624cc5a546ba9cfd7b8273995a15de94ee4538130d74953ec2e"},
|
||||||
|
{file = "pycryptodome-3.15.0-pp27-pypy_73-win32.whl", hash = "sha256:a8f06611e691c2ce45ca09bbf983e2ff2f8f4f87313609d80c125aff9fad6e7f"},
|
||||||
|
{file = "pycryptodome-3.15.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b9cc96e274b253e47ad33ae1fccc36ea386f5251a823ccb50593a935db47fdd2"},
|
||||||
|
{file = "pycryptodome-3.15.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:ecaaef2d21b365d9c5ca8427ffc10cebed9d9102749fd502218c23cb9a05feb5"},
|
||||||
|
{file = "pycryptodome-3.15.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:d2a39a66057ab191e5c27211a7daf8f0737f23acbf6b3562b25a62df65ffcb7b"},
|
||||||
|
{file = "pycryptodome-3.15.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:9c772c485b27967514d0df1458b56875f4b6d025566bf27399d0c239ff1b369f"},
|
||||||
|
{file = "pycryptodome-3.15.0.tar.gz", hash = "sha256:9135dddad504592bcc18b0d2d95ce86c3a5ea87ec6447ef25cfedea12d6018b8"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pymp4"
|
||||||
|
version = "1.2.0"
|
||||||
|
description = "A Python parser for MP4 boxes"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "pymp4-1.2.0.tar.gz", hash = "sha256:4a3d2e0838cfe28cd3dc64f45379e16d91b0212192f87a3e28f3804372727456"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
construct = "2.8.8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0"
|
||||||
|
description = "YAML parser and emitter for Python"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
|
||||||
|
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.28.1"
|
||||||
|
description = "Python HTTP for Humans."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7, <4"
|
||||||
|
files = [
|
||||||
|
{file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
|
||||||
|
{file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
certifi = ">=2017.4.17"
|
||||||
|
charset-normalizer = ">=2,<3"
|
||||||
|
idna = ">=2.5,<4"
|
||||||
|
urllib3 = ">=1.21.1,<1.27"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||||
|
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.3.0"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
|
||||||
|
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unidecode"
|
||||||
|
version = "1.3.4"
|
||||||
|
description = "ASCII transliterations of Unicode text"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
files = [
|
||||||
|
{file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"},
|
||||||
|
{file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "1.26.10"
|
||||||
|
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
|
||||||
|
files = [
|
||||||
|
{file = "urllib3-1.26.10-py2.py3-none-any.whl", hash = "sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec"},
|
||||||
|
{file = "urllib3-1.26.10.tar.gz", hash = "sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
|
||||||
|
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"]
|
||||||
|
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yarl"
|
||||||
|
version = "1.7.2"
|
||||||
|
description = "Yet another URL library"
|
||||||
|
category = "main"
|
||||||
|
optional = true
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"},
|
||||||
|
{file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
idna = ">=2.0"
|
||||||
|
multidict = ">=4.0"
|
||||||
|
typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zipp"
|
||||||
|
version = "3.8.1"
|
||||||
|
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"},
|
||||||
|
{file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
|
||||||
|
testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
|
||||||
|
|
||||||
|
[extras]
|
||||||
|
serve = ["aiohttp", "PyYAML"]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "2.0"
|
||||||
|
python-versions = ">=3.7,<3.12"
|
||||||
|
content-hash = "c392d2830d8a0614ebdaa8b16fce5ccd0f92020db948270cb57246fe4c7b1372"
|
||||||
120
pyproject.toml
120
pyproject.toml
@ -1,97 +1,45 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
build-backend = "hatchling.build"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
[project]
|
[tool.poetry]
|
||||||
name = "pywidevine"
|
name = "pywidevine"
|
||||||
version = "1.9.0"
|
version = "1.6.0"
|
||||||
description = "Widevine CDM (Content Decryption Module) implementation in Python."
|
description = "Widevine CDM (Content Decryption Module) implementation in Python."
|
||||||
authors = [{ name = "rlaphoenix", email = "rlaphoenix@pm.me" }]
|
authors = ["rlaphoenix <rlaphoenix@pm.me>"]
|
||||||
requires-python = ">=3.9"
|
|
||||||
readme = "README.md"
|
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
keywords = [
|
readme = "README.md"
|
||||||
"python",
|
repository = "https://github.com/rlaphoenix/pywidevine"
|
||||||
"drm",
|
keywords = ["widevine", "drm", "google"]
|
||||||
"widevine",
|
|
||||||
"google",
|
|
||||||
]
|
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Intended Audience :: End Users/Desktop",
|
"Intended Audience :: End Users/Desktop",
|
||||||
"Natural Language :: English",
|
"Natural Language :: English",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Topic :: Multimedia :: Video",
|
"Topic :: Multimedia :: Video",
|
||||||
"Topic :: Security :: Cryptography",
|
"Topic :: Security :: Cryptography"
|
||||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
||||||
]
|
|
||||||
dependencies = [
|
|
||||||
"protobuf~=6.33.0",
|
|
||||||
"pymp4~=1.4.0",
|
|
||||||
"pycryptodome~=3.23.0",
|
|
||||||
"click~=8.1.7",
|
|
||||||
"requests~=2.32.5",
|
|
||||||
"Unidecode~=1.3.7",
|
|
||||||
"PyYAML~=6.0.3",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[tool.poetry.urls]
|
||||||
serve = ["aiohttp~=3.13.1"]
|
"Bug Tracker" = "https://github.com/rlaphoenix/pywidevine/issues"
|
||||||
|
"Forums" = "https://github.com/rlaphoenix/pywidevine/discussions"
|
||||||
|
"Changelog" = "https://github.com/rlaphoenix/pywidevine/blob/master/CHANGELOG.md"
|
||||||
|
|
||||||
[project.urls]
|
[tool.poetry.dependencies]
|
||||||
Repository = "https://github.com/devine-dl/pywidevine"
|
python = ">=3.7,<3.12"
|
||||||
Issues = "https://github.com/devine-dl/pywidevine/issues"
|
protobuf = "4.21.6"
|
||||||
Discussions = "https://github.com/devine-dl/pywidevine/discussions"
|
pymp4 = "^1.2.0"
|
||||||
Changelog = "https://github.com/devine-dl/pywidevine/blob/master/CHANGELOG.md"
|
pycryptodome = "^3.15.0"
|
||||||
|
click = "^8.1.3"
|
||||||
|
requests = "^2.28.1"
|
||||||
|
lxml = ">=4.9.2"
|
||||||
|
Unidecode = "^1.3.4"
|
||||||
|
PyYAML = "^6.0"
|
||||||
|
aiohttp = {version = "^3.8.1", optional = true}
|
||||||
|
|
||||||
[project.scripts]
|
[tool.poetry.extras]
|
||||||
|
serve = ["aiohttp", "PyYAML"]
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
pywidevine = "pywidevine.main:main"
|
pywidevine = "pywidevine.main:main"
|
||||||
|
|
||||||
[dependency-groups]
|
|
||||||
dev = [
|
|
||||||
"pre-commit~=4.3.0",
|
|
||||||
"mypy~=1.18.2",
|
|
||||||
"mypy-protobuf~=3.6.0",
|
|
||||||
"types-protobuf~=6.32.1.20250918",
|
|
||||||
"types-requests~=2.32.4.20250913",
|
|
||||||
"types-PyYAML~=6.0.12.20250915",
|
|
||||||
"isort~=6.1.0",
|
|
||||||
"ruff~=0.14.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.hatch.build.targets.sdist]
|
|
||||||
include = [
|
|
||||||
"pywidevine",
|
|
||||||
"CHANGELOG.md",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
|
||||||
packages = ["pywidevine"]
|
|
||||||
|
|
||||||
[tool.ruff]
|
|
||||||
extend-exclude = [
|
|
||||||
"*_pb2.py",
|
|
||||||
"*.pyi",
|
|
||||||
]
|
|
||||||
force-exclude = true
|
|
||||||
line-length = 120
|
|
||||||
select = ["E4", "E7", "E9", "F", "W"]
|
|
||||||
|
|
||||||
[tool.ruff.extend-per-file-ignores]
|
|
||||||
"pywidevine/__init__.py" = ["F403"]
|
|
||||||
|
|
||||||
[tool.isort]
|
|
||||||
line_length = 118
|
|
||||||
extend_skip_glob = ["*_pb2.py", "*.pyi"]
|
|
||||||
|
|
||||||
[tool.mypy]
|
|
||||||
check_untyped_defs = true
|
|
||||||
disallow_incomplete_defs = true
|
|
||||||
disallow_untyped_defs = true
|
|
||||||
exclude = [
|
|
||||||
'_pb2.pyi?$' # generated protobuffer files
|
|
||||||
]
|
|
||||||
follow_imports = "silent"
|
|
||||||
ignore_missing_imports = true
|
|
||||||
no_implicit_optional = true
|
|
||||||
|
|||||||
@ -5,4 +5,5 @@ from .pssh import *
|
|||||||
from .remotecdm import *
|
from .remotecdm import *
|
||||||
from .session import *
|
from .session import *
|
||||||
|
|
||||||
__version__ = "1.9.0"
|
|
||||||
|
__version__ = "1.5.3"
|
||||||
|
|||||||
@ -7,58 +7,45 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Union
|
from typing import Union, Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from Crypto.Cipher import AES, PKCS1_OAEP
|
from Crypto.Cipher import AES, PKCS1_OAEP
|
||||||
from Crypto.Hash import CMAC, HMAC, SHA1, SHA256
|
from Crypto.Hash import SHA1, HMAC, SHA256, CMAC
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from Crypto.Random import get_random_bytes
|
from Crypto.Random import get_random_bytes
|
||||||
from Crypto.Signature import pss
|
from Crypto.Signature import pss
|
||||||
from Crypto.Util import Padding
|
from Crypto.Util import Padding
|
||||||
from google.protobuf.message import DecodeError
|
from google.protobuf.message import DecodeError
|
||||||
|
|
||||||
from pywidevine.device import Device, DeviceTypes
|
from pywidevine.device import Device
|
||||||
from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType,
|
from pywidevine.exceptions import TooManySessions, InvalidSession, InvalidLicenseType, SignatureMismatch, \
|
||||||
InvalidSession, NoKeysLoaded, SignatureMismatch, TooManySessions)
|
InvalidInitData, InvalidLicenseMessage, NoKeysLoaded, InvalidContext
|
||||||
from pywidevine.key import Key
|
from pywidevine.key import Key
|
||||||
from pywidevine.license_protocol_pb2 import (ClientIdentification, DrmCertificate, EncryptedClientIdentification,
|
from pywidevine.license_protocol_pb2 import DrmCertificate, SignedMessage, SignedDrmCertificate, LicenseType, \
|
||||||
License, LicenseRequest, LicenseType, SignedDrmCertificate,
|
LicenseRequest, ProtocolVersion, ClientIdentification, EncryptedClientIdentification, License
|
||||||
SignedMessage)
|
|
||||||
from pywidevine.pssh import PSSH
|
from pywidevine.pssh import PSSH
|
||||||
from pywidevine.session import Session
|
from pywidevine.session import Session
|
||||||
from pywidevine.utils import get_binary_path
|
from pywidevine.utils import get_binary_path
|
||||||
|
|
||||||
|
|
||||||
class Cdm:
|
class Cdm:
|
||||||
uuid = UUID(bytes=b"\xed\xef\x8b\xa9\x79\xd6\x4a\xce\xa3\xc8\x27\xdc\xd5\x1d\x21\xed")
|
system_id = b"\xed\xef\x8b\xa9\x79\xd6\x4a\xce\xa3\xc8\x27\xdc\xd5\x1d\x21\xed"
|
||||||
|
uuid = UUID(bytes=system_id)
|
||||||
urn = f"urn:uuid:{uuid}"
|
urn = f"urn:uuid:{uuid}"
|
||||||
key_format = urn
|
key_format = urn
|
||||||
service_certificate_challenge = b"\x08\x04"
|
service_certificate_challenge = b"\x08\x04"
|
||||||
common_privacy_cert = (
|
common_privacy_cert = ("CAUSxwUKwQIIAxIQFwW5F8wSBIaLBjM6L3cqjBiCtIKSBSKOAjCCAQoCggEBAJntWzsyfateJO/DtiqVtZhSCtW8y"
|
||||||
# Used by Google's production license server (license.google.com)
|
"zdQPgZFuBTYdrjfQFEEQa2M462xG7iMTnJaXkqeB5UpHVhYQCOn4a8OOKkSeTkwCGELbxWMh4x+Ib/7/up34QGeHl"
|
||||||
# Not publicly accessible directly, but a lot of services have their own gateways to it
|
"eB6KRfRiY9FOYOgFioYHrc4E+shFexN6jWfM3rM3BdmDoh+07svUoQykdJDKR+ql1DghjduvHK3jOS8T1v+2RC/TH"
|
||||||
"CAUSxwUKwQIIAxIQFwW5F8wSBIaLBjM6L3cqjBiCtIKSBSKOAjCCAQoCggEBAJntWzsyfateJO/DtiqVtZhSCtW8yzdQPgZFuBTYdrjfQFEE"
|
"hv0CwxgTRxLpMlSCkv5fuvWCSmvzu9Vu69WTi0Ods18Vcc6CCuZYSC4NZ7c4kcHCCaA1vZ8bYLErF8xNEkKdO7Dev"
|
||||||
"Qa2M462xG7iMTnJaXkqeB5UpHVhYQCOn4a8OOKkSeTkwCGELbxWMh4x+Ib/7/up34QGeHleB6KRfRiY9FOYOgFioYHrc4E+shFexN6jWfM3r"
|
"Sy8BDFnoKEPiWC8La59dsPxebt9k+9MItHEbzxJQAZyfWgkCAwEAAToUbGljZW5zZS53aWRldmluZS5jb20SgAOuN"
|
||||||
"M3BdmDoh+07svUoQykdJDKR+ql1DghjduvHK3jOS8T1v+2RC/THhv0CwxgTRxLpMlSCkv5fuvWCSmvzu9Vu69WTi0Ods18Vcc6CCuZYSC4NZ"
|
"HMUtag1KX8nE4j7e7jLUnfSSYI83dHaMLkzOVEes8y96gS5RLknwSE0bv296snUE5F+bsF2oQQ4RgpQO8GVK5uk5M"
|
||||||
"7c4kcHCCaA1vZ8bYLErF8xNEkKdO7DevSy8BDFnoKEPiWC8La59dsPxebt9k+9MItHEbzxJQAZyfWgkCAwEAAToUbGljZW5zZS53aWRldmlu"
|
"4PxL/CCpgIqq9L/NGcHc/N9XTMrCjRtBBBbPneiAQwHL2zNMr80NQJeEI6ZC5UYT3wr8+WykqSSdhV5Cs6cD7xdn9"
|
||||||
"ZS5jb20SgAOuNHMUtag1KX8nE4j7e7jLUnfSSYI83dHaMLkzOVEes8y96gS5RLknwSE0bv296snUE5F+bsF2oQQ4RgpQO8GVK5uk5M4PxL/C"
|
"qm9Nta/gr52u/DLpP3lnSq8x2/rZCR7hcQx+8pSJmthn8NpeVQ/ypy727+voOGlXnVaPHvOZV+WRvWCq5z3CqCLl5"
|
||||||
"CpgIqq9L/NGcHc/N9XTMrCjRtBBBbPneiAQwHL2zNMr80NQJeEI6ZC5UYT3wr8+WykqSSdhV5Cs6cD7xdn9qm9Nta/gr52u/DLpP3lnSq8x2"
|
"+Gf2Ogsrf9s2LFvE7NVV2FvKqcWTw4PIV9Sdqrd+QLeFHd/SSZiAjjWyWOddeOrAyhb3BHMEwg2T7eTo/xxvF+YkP"
|
||||||
"/rZCR7hcQx+8pSJmthn8NpeVQ/ypy727+voOGlXnVaPHvOZV+WRvWCq5z3CqCLl5+Gf2Ogsrf9s2LFvE7NVV2FvKqcWTw4PIV9Sdqrd+QLeF"
|
"j89qPwXCYcOxF+6gjomPwzvofcJOxkJkoMmMzcFBDopvab5tDQsyN9UPLGhGC98X/8z8QSQ+spbJTYLdgFenFoGq4"
|
||||||
"Hd/SSZiAjjWyWOddeOrAyhb3BHMEwg2T7eTo/xxvF+YkPj89qPwXCYcOxF+6gjomPwzvofcJOxkJkoMmMzcFBDopvab5tDQsyN9UPLGhGC98"
|
"7gLwDS6NWYYQSqzE3Udf2W7pzk4ybyG4PHBYV3s4cyzdq8amvtE/sNSdOKReuHpfQ=")
|
||||||
"X/8z8QSQ+spbJTYLdgFenFoGq47gLwDS6NWYYQSqzE3Udf2W7pzk4ybyG4PHBYV3s4cyzdq8amvtE/sNSdOKReuHpfQ=")
|
|
||||||
staging_privacy_cert = (
|
|
||||||
# Used by Google's staging license server (staging.google.com)
|
|
||||||
# This can be publicly accessed without authentication using https://cwip-shaka-proxy.appspot.com/no_auth
|
|
||||||
"CAUSxQUKvwIIAxIQKHA0VMAI9jYYredEPbbEyBiL5/mQBSKOAjCCAQoCggEBALUhErjQXQI/zF2V4sJRwcZJtBd82NK+7zVbsGdD3mYePSq8"
|
|
||||||
"MYK3mUbVX9wI3+lUB4FemmJ0syKix/XgZ7tfCsB6idRa6pSyUW8HW2bvgR0NJuG5priU8rmFeWKqFxxPZmMNPkxgJxiJf14e+baq9a1Nuip+"
|
|
||||||
"FBdt8TSh0xhbWiGKwFpMQfCB7/+Ao6BAxQsJu8dA7tzY8U1nWpGYD5LKfdxkagatrVEB90oOSYzAHwBTK6wheFC9kF6QkjZWt9/v70JIZ2fz"
|
|
||||||
"PvYoPU9CVKtyWJOQvuVYCPHWaAgNRdiTwryi901goMDQoJk87wFgRwMzTDY4E5SGvJ2vJP1noH+a2UMCAwEAAToSc3RhZ2luZy5nb29nbGUu"
|
|
||||||
"Y29tEoADmD4wNSZ19AunFfwkm9rl1KxySaJmZSHkNlVzlSlyH/iA4KrvxeJ7yYDa6tq/P8OG0ISgLIJTeEjMdT/0l7ARp9qXeIoA4qprhM19"
|
|
||||||
"ccB6SOv2FgLMpaPzIDCnKVww2pFbkdwYubyVk7jei7UPDe3BKTi46eA5zd4Y+oLoG7AyYw/pVdhaVmzhVDAL9tTBvRJpZjVrKH1lexjOY9Dv"
|
|
||||||
"1F/FJp6X6rEctWPlVkOyb/SfEJwhAa/K81uDLyiPDZ1Flg4lnoX7XSTb0s+Cdkxd2b9yfvvpyGH4aTIfat4YkF9Nkvmm2mU224R1hx0WjocL"
|
|
||||||
"sjA89wxul4TJPS3oRa2CYr5+DU4uSgdZzvgtEJ0lksckKfjAF0K64rPeytvDPD5fS69eFuy3Tq26/LfGcF96njtvOUA4P5xRFtICogySKe6W"
|
|
||||||
"nCUZcYMDtQ0BMMM1LgawFNg4VA+KDCJ8ABHg9bOOTimO0sswHrRWSWX1XF15dXolCk65yEqz5lOfa2/fVomeopkU")
|
|
||||||
root_signed_cert = SignedDrmCertificate()
|
root_signed_cert = SignedDrmCertificate()
|
||||||
root_signed_cert.ParseFromString(base64.b64decode(
|
root_signed_cert.ParseFromString(base64.b64decode(
|
||||||
"CpwDCAASAQAY3ZSIiwUijgMwggGKAoIBgQC0/jnDZZAD2zwRlwnoaM3yw16b8udNI7EQ24dl39z7nzWgVwNTTPZtNX2meNuzNtI/nECplSZy"
|
"CpwDCAASAQAY3ZSIiwUijgMwggGKAoIBgQC0/jnDZZAD2zwRlwnoaM3yw16b8udNI7EQ24dl39z7nzWgVwNTTPZtNX2meNuzNtI/nECplSZy"
|
||||||
@ -79,7 +66,7 @@ class Cdm:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
device_type: Union[DeviceTypes, str],
|
device_type: Union[Device.Types, str],
|
||||||
system_id: int,
|
system_id: int,
|
||||||
security_level: int,
|
security_level: int,
|
||||||
client_id: ClientIdentification,
|
client_id: ClientIdentification,
|
||||||
@ -89,9 +76,9 @@ class Cdm:
|
|||||||
if not device_type:
|
if not device_type:
|
||||||
raise ValueError("Device Type must be provided")
|
raise ValueError("Device Type must be provided")
|
||||||
if isinstance(device_type, str):
|
if isinstance(device_type, str):
|
||||||
device_type = DeviceTypes[device_type]
|
device_type = Device.Types[device_type]
|
||||||
if not isinstance(device_type, DeviceTypes):
|
if not isinstance(device_type, Device.Types):
|
||||||
raise TypeError(f"Expected device_type to be a {DeviceTypes!r} not {device_type!r}")
|
raise TypeError(f"Expected device_type to be a {Device.Types!r} not {device_type!r}")
|
||||||
|
|
||||||
if not system_id:
|
if not system_id:
|
||||||
raise ValueError("System ID must be provided")
|
raise ValueError("System ID must be provided")
|
||||||
@ -164,7 +151,7 @@ class Cdm:
|
|||||||
raise InvalidSession(f"Session identifier {session_id!r} is invalid.")
|
raise InvalidSession(f"Session identifier {session_id!r} is invalid.")
|
||||||
del self.__sessions[session_id]
|
del self.__sessions[session_id]
|
||||||
|
|
||||||
def set_service_certificate(self, session_id: bytes, certificate: Optional[Union[bytes, str]]) -> Optional[str]:
|
def set_service_certificate(self, session_id: bytes, certificate: Optional[Union[bytes, str]]) -> str:
|
||||||
"""
|
"""
|
||||||
Set a Service Privacy Certificate for Privacy Mode. (optional but recommended)
|
Set a Service Privacy Certificate for Privacy Mode. (optional but recommended)
|
||||||
|
|
||||||
@ -191,8 +178,7 @@ class Cdm:
|
|||||||
match the underlying DrmCertificate.
|
match the underlying DrmCertificate.
|
||||||
|
|
||||||
Returns the Service Provider ID of the verified DrmCertificate if successful.
|
Returns the Service Provider ID of the verified DrmCertificate if successful.
|
||||||
If certificate is None, it will return the now-unset certificate's Provider ID,
|
If certificate is None, it will return the now unset certificate's Provider ID.
|
||||||
or None if no certificate was set yet.
|
|
||||||
"""
|
"""
|
||||||
session = self.__sessions.get(session_id)
|
session = self.__sessions.get(session_id)
|
||||||
if not session:
|
if not session:
|
||||||
@ -222,11 +208,7 @@ class Cdm:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
signed_message.ParseFromString(certificate)
|
signed_message.ParseFromString(certificate)
|
||||||
if all(
|
if signed_message.SerializeToString() == certificate:
|
||||||
# See https://github.com/devine-dl/pywidevine/issues/41
|
|
||||||
bytes(chunk) == signed_message.SerializeToString()
|
|
||||||
for chunk in zip(*[iter(certificate)] * len(signed_message.SerializeToString()))
|
|
||||||
):
|
|
||||||
signed_drm_certificate.ParseFromString(signed_message.msg)
|
signed_drm_certificate.ParseFromString(signed_message.msg)
|
||||||
else:
|
else:
|
||||||
signed_drm_certificate.ParseFromString(certificate)
|
signed_drm_certificate.ParseFromString(certificate)
|
||||||
@ -280,7 +262,7 @@ class Cdm:
|
|||||||
self,
|
self,
|
||||||
session_id: bytes,
|
session_id: bytes,
|
||||||
pssh: PSSH,
|
pssh: PSSH,
|
||||||
license_type: str = "STREAMING",
|
type_: Union[int, str] = LicenseType.STREAMING,
|
||||||
privacy_mode: bool = True
|
privacy_mode: bool = True
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""
|
"""
|
||||||
@ -289,10 +271,8 @@ class Cdm:
|
|||||||
Parameters:
|
Parameters:
|
||||||
session_id: Session identifier.
|
session_id: Session identifier.
|
||||||
pssh: PSSH Object to get the init data from.
|
pssh: PSSH Object to get the init data from.
|
||||||
license_type: Type of License you wish to exchange, often `STREAMING`.
|
type_: Type of License you wish to exchange, often `STREAMING`. The `OFFLINE`
|
||||||
- "STREAMING": Normal one-time-use license.
|
Licenses are for Offline licensing of Downloaded content.
|
||||||
- "OFFLINE": Offline-use licence, usually for Downloaded content.
|
|
||||||
- "AUTOMATIC": License type decision is left to provider.
|
|
||||||
privacy_mode: Encrypt the Client ID using the Privacy Certificate. If the
|
privacy_mode: Encrypt the Client ID using the Privacy Certificate. If the
|
||||||
privacy certificate is not set yet, this does nothing.
|
privacy certificate is not set yet, this does nothing.
|
||||||
|
|
||||||
@ -315,15 +295,17 @@ class Cdm:
|
|||||||
if not isinstance(pssh, PSSH):
|
if not isinstance(pssh, PSSH):
|
||||||
raise InvalidInitData(f"Expected pssh to be a {PSSH}, not {pssh!r}")
|
raise InvalidInitData(f"Expected pssh to be a {PSSH}, not {pssh!r}")
|
||||||
|
|
||||||
if not isinstance(license_type, str):
|
try:
|
||||||
raise InvalidLicenseType(f"Expected license_type to be a {str}, not {license_type!r}")
|
if isinstance(type_, int):
|
||||||
if license_type not in LicenseType.keys():
|
LicenseType.Name(int(type_))
|
||||||
raise InvalidLicenseType(
|
elif isinstance(type_, str):
|
||||||
f"Invalid license_type value of '{license_type}'. "
|
type_ = LicenseType.Value(type_)
|
||||||
f"Available values: {LicenseType.keys()}"
|
elif not isinstance(type_, LicenseType):
|
||||||
)
|
raise InvalidLicenseType()
|
||||||
|
except ValueError:
|
||||||
|
raise InvalidLicenseType(f"License Type {type_!r} is invalid")
|
||||||
|
|
||||||
if self.device_type == DeviceTypes.ANDROID:
|
if self.device_type == Device.Types.ANDROID:
|
||||||
# OEMCrypto's request_id seems to be in AES CTR Counter block form with no suffix
|
# OEMCrypto's request_id seems to be in AES CTR Counter block form with no suffix
|
||||||
# Bytes 5-8 does not seem random, in real tests they have been consecutive \x00 or \xFF
|
# Bytes 5-8 does not seem random, in real tests they have been consecutive \x00 or \xFF
|
||||||
# Real example: A0DCE548000000000500000000000000
|
# Real example: A0DCE548000000000500000000000000
|
||||||
@ -335,36 +317,35 @@ class Cdm:
|
|||||||
else:
|
else:
|
||||||
request_id = get_random_bytes(16)
|
request_id = get_random_bytes(16)
|
||||||
|
|
||||||
license_request = LicenseRequest(
|
license_request = LicenseRequest()
|
||||||
client_id=(
|
license_request.type = LicenseRequest.RequestType.Value("NEW")
|
||||||
self.__client_id
|
license_request.request_time = int(time.time())
|
||||||
) if not (session.service_certificate and privacy_mode) else None,
|
license_request.protocol_version = ProtocolVersion.Value("VERSION_2_1")
|
||||||
encrypted_client_id=self.encrypt_client_id(
|
license_request.key_control_nonce = random.randrange(1, 2 ** 31)
|
||||||
|
|
||||||
|
# pssh_data may be either a WidevineCencHeader or custom data
|
||||||
|
# we have to assume the pssh.init_data value is valid, we cannot test
|
||||||
|
license_request.content_id.widevine_pssh_data.pssh_data.append(pssh.init_data)
|
||||||
|
license_request.content_id.widevine_pssh_data.license_type = type_
|
||||||
|
license_request.content_id.widevine_pssh_data.request_id = request_id
|
||||||
|
|
||||||
|
if session.service_certificate and privacy_mode:
|
||||||
|
# encrypt the client id for privacy mode
|
||||||
|
license_request.encrypted_client_id.CopyFrom(self.encrypt_client_id(
|
||||||
client_id=self.__client_id,
|
client_id=self.__client_id,
|
||||||
service_certificate=session.service_certificate
|
service_certificate=session.service_certificate
|
||||||
) if session.service_certificate and privacy_mode else None,
|
))
|
||||||
content_id=LicenseRequest.ContentIdentification(
|
else:
|
||||||
widevine_pssh_data=LicenseRequest.ContentIdentification.WidevinePsshData(
|
license_request.client_id.CopyFrom(self.__client_id)
|
||||||
pssh_data=[pssh.init_data], # either a WidevineCencHeader or custom data
|
|
||||||
license_type=license_type,
|
|
||||||
request_id=request_id
|
|
||||||
)
|
|
||||||
),
|
|
||||||
type="NEW",
|
|
||||||
request_time=int(time.time()),
|
|
||||||
protocol_version="VERSION_2_1",
|
|
||||||
key_control_nonce=random.randrange(1, 2 ** 31),
|
|
||||||
).SerializeToString()
|
|
||||||
|
|
||||||
signed_license_request = SignedMessage(
|
license_message = SignedMessage()
|
||||||
type="LICENSE_REQUEST",
|
license_message.type = SignedMessage.MessageType.LICENSE_REQUEST
|
||||||
msg=license_request,
|
license_message.msg = license_request.SerializeToString()
|
||||||
signature=self.__signer.sign(SHA1.new(license_request))
|
license_message.signature = self.__signer.sign(SHA1.new(license_message.msg))
|
||||||
).SerializeToString()
|
|
||||||
|
|
||||||
session.context[request_id] = self.derive_context(license_request)
|
session.context[request_id] = self.derive_context(license_message.msg)
|
||||||
|
|
||||||
return signed_license_request
|
return license_message.SerializeToString()
|
||||||
|
|
||||||
def parse_license(self, session_id: bytes, license_message: Union[SignedMessage, bytes, str]) -> None:
|
def parse_license(self, session_id: bytes, license_message: Union[SignedMessage, bytes, str]) -> None:
|
||||||
"""
|
"""
|
||||||
@ -414,7 +395,7 @@ class Cdm:
|
|||||||
if not isinstance(license_message, SignedMessage):
|
if not isinstance(license_message, SignedMessage):
|
||||||
raise InvalidLicenseMessage(f"Expecting license_response to be a SignedMessage, got {license_message!r}")
|
raise InvalidLicenseMessage(f"Expecting license_response to be a SignedMessage, got {license_message!r}")
|
||||||
|
|
||||||
if license_message.type != SignedMessage.MessageType.Value("LICENSE"):
|
if license_message.type != SignedMessage.MessageType.LICENSE:
|
||||||
raise InvalidLicenseMessage(
|
raise InvalidLicenseMessage(
|
||||||
f"Expecting a LICENSE message, not a "
|
f"Expecting a LICENSE message, not a "
|
||||||
f"'{SignedMessage.MessageType.Name(license_message.type)}' message."
|
f"'{SignedMessage.MessageType.Name(license_message.type)}' message."
|
||||||
@ -493,7 +474,7 @@ class Cdm:
|
|||||||
output_file: Union[Path, str],
|
output_file: Union[Path, str],
|
||||||
temp_dir: Optional[Union[Path, str]] = None,
|
temp_dir: Optional[Union[Path, str]] = None,
|
||||||
exists_ok: bool = False
|
exists_ok: bool = False
|
||||||
) -> int:
|
):
|
||||||
"""
|
"""
|
||||||
Decrypt a Widevine-encrypted file using Shaka-packager.
|
Decrypt a Widevine-encrypted file using Shaka-packager.
|
||||||
Shaka-packager is much more stable than mp4decrypt.
|
Shaka-packager is much more stable than mp4decrypt.
|
||||||
@ -530,7 +511,8 @@ class Cdm:
|
|||||||
|
|
||||||
input_file = Path(input_file)
|
input_file = Path(input_file)
|
||||||
output_file = Path(output_file)
|
output_file = Path(output_file)
|
||||||
temp_dir_ = Path(temp_dir) if temp_dir else None
|
if temp_dir:
|
||||||
|
temp_dir = Path(temp_dir)
|
||||||
|
|
||||||
if not input_file.is_file():
|
if not input_file.is_file():
|
||||||
raise FileNotFoundError(f"Input file does not exist, {input_file}")
|
raise FileNotFoundError(f"Input file does not exist, {input_file}")
|
||||||
@ -564,18 +546,18 @@ class Cdm:
|
|||||||
])
|
])
|
||||||
]
|
]
|
||||||
|
|
||||||
if temp_dir_:
|
if temp_dir:
|
||||||
temp_dir_.mkdir(parents=True, exist_ok=True)
|
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
args.extend(["--temp_dir", str(temp_dir_)])
|
args.extend(["--temp_dir", temp_dir])
|
||||||
|
|
||||||
return subprocess.check_call([executable, *args])
|
subprocess.check_call([executable, *args])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def encrypt_client_id(
|
def encrypt_client_id(
|
||||||
client_id: ClientIdentification,
|
client_id: ClientIdentification,
|
||||||
service_certificate: Union[SignedDrmCertificate, DrmCertificate],
|
service_certificate: Union[SignedDrmCertificate, DrmCertificate],
|
||||||
key: Optional[bytes] = None,
|
key: bytes = None,
|
||||||
iv: Optional[bytes] = None
|
iv: bytes = None
|
||||||
) -> EncryptedClientIdentification:
|
) -> EncryptedClientIdentification:
|
||||||
"""Encrypt the Client ID with the Service's Privacy Certificate."""
|
"""Encrypt the Client ID with the Service's Privacy Certificate."""
|
||||||
privacy_key = key or get_random_bytes(16)
|
privacy_key = key or get_random_bytes(16)
|
||||||
@ -588,19 +570,20 @@ class Cdm:
|
|||||||
if not isinstance(service_certificate, DrmCertificate):
|
if not isinstance(service_certificate, DrmCertificate):
|
||||||
raise ValueError(f"Expecting Service Certificate to be a DrmCertificate, not {service_certificate!r}")
|
raise ValueError(f"Expecting Service Certificate to be a DrmCertificate, not {service_certificate!r}")
|
||||||
|
|
||||||
encrypted_client_id = EncryptedClientIdentification(
|
enc_client_id = EncryptedClientIdentification()
|
||||||
provider_id=service_certificate.provider_id,
|
enc_client_id.provider_id = service_certificate.provider_id
|
||||||
service_certificate_serial_number=service_certificate.serial_number,
|
enc_client_id.service_certificate_serial_number = service_certificate.serial_number
|
||||||
encrypted_client_id=AES.
|
|
||||||
new(privacy_key, AES.MODE_CBC, privacy_iv).
|
|
||||||
encrypt(Padding.pad(client_id.SerializeToString(), 16)),
|
|
||||||
encrypted_client_id_iv=privacy_iv,
|
|
||||||
encrypted_privacy_key=PKCS1_OAEP.
|
|
||||||
new(RSA.importKey(service_certificate.public_key)).
|
|
||||||
encrypt(privacy_key)
|
|
||||||
)
|
|
||||||
|
|
||||||
return encrypted_client_id
|
enc_client_id.encrypted_client_id = AES. \
|
||||||
|
new(privacy_key, AES.MODE_CBC, privacy_iv). \
|
||||||
|
encrypt(Padding.pad(client_id.SerializeToString(), 16))
|
||||||
|
|
||||||
|
enc_client_id.encrypted_privacy_key = PKCS1_OAEP. \
|
||||||
|
new(RSA.importKey(service_certificate.public_key)). \
|
||||||
|
encrypt(privacy_key)
|
||||||
|
enc_client_id.encrypted_client_id_iv = privacy_iv
|
||||||
|
|
||||||
|
return enc_client_id
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def derive_context(message: bytes) -> tuple[bytes, bytes]:
|
def derive_context(message: bytes) -> tuple[bytes, bytes]:
|
||||||
@ -655,4 +638,4 @@ class Cdm:
|
|||||||
return enc_key, mac_key_server, mac_key_client
|
return enc_key, mac_key_server, mac_key_client
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("Cdm",)
|
__ALL__ = (Cdm,)
|
||||||
|
|||||||
@ -14,10 +14,10 @@ from construct import Padded, Padding, Struct, this
|
|||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from google.protobuf.message import DecodeError
|
from google.protobuf.message import DecodeError
|
||||||
|
|
||||||
from pywidevine.license_protocol_pb2 import ClientIdentification, DrmCertificate, FileHashes, SignedDrmCertificate
|
from pywidevine.license_protocol_pb2 import ClientIdentification, FileHashes, SignedDrmCertificate, DrmCertificate
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypes(Enum):
|
class _Types(Enum):
|
||||||
CHROME = 1
|
CHROME = 1
|
||||||
ANDROID = 2
|
ANDROID = 2
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ class _Structures:
|
|||||||
"version" / Const(Int8ub, 2),
|
"version" / Const(Int8ub, 2),
|
||||||
"type_" / CEnum(
|
"type_" / CEnum(
|
||||||
Int8ub,
|
Int8ub,
|
||||||
**{t.name: t.value for t in DeviceTypes}
|
**{t.name: t.value for t in _Types}
|
||||||
),
|
),
|
||||||
"security_level" / Int8ub,
|
"security_level" / Int8ub,
|
||||||
"flags" / Padded(1, COptional(BitStruct(
|
"flags" / Padded(1, COptional(BitStruct(
|
||||||
@ -55,7 +55,7 @@ class _Structures:
|
|||||||
"version" / Const(Int8ub, 1),
|
"version" / Const(Int8ub, 1),
|
||||||
"type_" / CEnum(
|
"type_" / CEnum(
|
||||||
Int8ub,
|
Int8ub,
|
||||||
**{t.name: t.value for t in DeviceTypes}
|
**{t.name: t.value for t in _Types}
|
||||||
),
|
),
|
||||||
"security_level" / Int8ub,
|
"security_level" / Int8ub,
|
||||||
"flags" / Padded(1, COptional(BitStruct(
|
"flags" / Padded(1, COptional(BitStruct(
|
||||||
@ -72,13 +72,14 @@ class _Structures:
|
|||||||
|
|
||||||
|
|
||||||
class Device:
|
class Device:
|
||||||
|
Types = _Types
|
||||||
Structures = _Structures
|
Structures = _Structures
|
||||||
supported_structure = Structures.v2
|
supported_structure = Structures.v2
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*_: Any,
|
*_: Any,
|
||||||
type_: DeviceTypes,
|
type_: Types,
|
||||||
security_level: int,
|
security_level: int,
|
||||||
flags: Optional[dict],
|
flags: Optional[dict],
|
||||||
private_key: Optional[bytes],
|
private_key: Optional[bytes],
|
||||||
@ -102,9 +103,9 @@ class Device:
|
|||||||
if not private_key:
|
if not private_key:
|
||||||
raise ValueError("Private Key is required, the WVD does not contain one or is malformed.")
|
raise ValueError("Private Key is required, the WVD does not contain one or is malformed.")
|
||||||
|
|
||||||
self.type = DeviceTypes[type_] if isinstance(type_, str) else type_
|
self.type = self.Types[type_] if isinstance(type_, str) else type_
|
||||||
self.security_level = security_level
|
self.security_level = security_level
|
||||||
self.flags = flags or {}
|
self.flags = flags
|
||||||
self.private_key = RSA.importKey(private_key)
|
self.private_key = RSA.importKey(private_key)
|
||||||
self.client_id = ClientIdentification()
|
self.client_id = ClientIdentification()
|
||||||
try:
|
try:
|
||||||
@ -198,36 +199,36 @@ class Device:
|
|||||||
raise ValueError("Device Data does not seem to be a WVD file (v0).")
|
raise ValueError("Device Data does not seem to be a WVD file (v0).")
|
||||||
|
|
||||||
if header.version == 1: # v1 to v2
|
if header.version == 1: # v1 to v2
|
||||||
v1_struct = _Structures.v1.parse(data)
|
data = _Structures.v1.parse(data)
|
||||||
v1_struct.version = 2 # update version to 2 to allow loading
|
data.version = 2 # update version to 2 to allow loading
|
||||||
v1_struct.flags = Container() # blank flags that may have been used in v1
|
data.flags = Container() # blank flags that may have been used in v1
|
||||||
|
|
||||||
vmp = FileHashes()
|
vmp = FileHashes()
|
||||||
if v1_struct.vmp:
|
if data.vmp:
|
||||||
try:
|
try:
|
||||||
vmp.ParseFromString(v1_struct.vmp)
|
vmp.ParseFromString(data.vmp)
|
||||||
if vmp.SerializeToString() != v1_struct.vmp:
|
if vmp.SerializeToString() != data.vmp:
|
||||||
raise DecodeError("partial parse")
|
raise DecodeError("partial parse")
|
||||||
except DecodeError as e:
|
except DecodeError as e:
|
||||||
raise DecodeError(f"Failed to parse VMP data as FileHashes, {e}")
|
raise DecodeError(f"Failed to parse VMP data as FileHashes, {e}")
|
||||||
v1_struct.vmp = vmp
|
data.vmp = vmp
|
||||||
|
|
||||||
client_id = ClientIdentification()
|
client_id = ClientIdentification()
|
||||||
try:
|
try:
|
||||||
client_id.ParseFromString(v1_struct.client_id)
|
client_id.ParseFromString(data.client_id)
|
||||||
if client_id.SerializeToString() != v1_struct.client_id:
|
if client_id.SerializeToString() != data.client_id:
|
||||||
raise DecodeError("partial parse")
|
raise DecodeError("partial parse")
|
||||||
except DecodeError as e:
|
except DecodeError as e:
|
||||||
raise DecodeError(f"Failed to parse VMP data as FileHashes, {e}")
|
raise DecodeError(f"Failed to parse VMP data as FileHashes, {e}")
|
||||||
|
|
||||||
new_vmp_data = v1_struct.vmp.SerializeToString()
|
new_vmp_data = data.vmp.SerializeToString()
|
||||||
if client_id.vmp_data and client_id.vmp_data != new_vmp_data:
|
if client_id.vmp_data and client_id.vmp_data != new_vmp_data:
|
||||||
logging.getLogger("migrate").warning("Client ID already has Verified Media Path data")
|
logging.getLogger("migrate").warning("Client ID already has Verified Media Path data")
|
||||||
client_id.vmp_data = new_vmp_data
|
client_id.vmp_data = new_vmp_data
|
||||||
v1_struct.client_id = client_id.SerializeToString()
|
data.client_id = client_id.SerializeToString()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = _Structures.v2.build(v1_struct)
|
data = _Structures.v2.build(data)
|
||||||
except ConstructError as e:
|
except ConstructError as e:
|
||||||
raise ValueError(f"Migration failed, {e}")
|
raise ValueError(f"Migration failed, {e}")
|
||||||
|
|
||||||
@ -237,4 +238,4 @@ class Device:
|
|||||||
raise ValueError(f"Device Data seems to be corrupt or invalid, or migration failed, {e}")
|
raise ValueError(f"Device Data seems to be corrupt or invalid, or migration failed, {e}")
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("Device", "DeviceTypes")
|
__ALL__ = (Device,)
|
||||||
|
|||||||
@ -27,7 +27,7 @@ class Key:
|
|||||||
def from_key_container(cls, key: License.KeyContainer, enc_key: bytes) -> Key:
|
def from_key_container(cls, key: License.KeyContainer, enc_key: bytes) -> Key:
|
||||||
"""Load Key from a KeyContainer object."""
|
"""Load Key from a KeyContainer object."""
|
||||||
permissions = []
|
permissions = []
|
||||||
if key.type == License.KeyContainer.KeyType.Value("OPERATOR_SESSION"):
|
if key.type == License.KeyContainer.KeyType.OPERATOR_SESSION:
|
||||||
for descriptor, value in key.operator_session_key_permissions.ListFields():
|
for descriptor, value in key.operator_session_key_permissions.ListFields():
|
||||||
if value == 1:
|
if value == 1:
|
||||||
permissions.append(descriptor.name)
|
permissions.append(descriptor.name)
|
||||||
@ -61,6 +61,3 @@ class Key:
|
|||||||
kid += b"\x00" * (16 - len(kid))
|
kid += b"\x00" * (16 - len(kid))
|
||||||
|
|
||||||
return UUID(bytes=kid)
|
return UUID(bytes=kid)
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("Key",)
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,5 +1,3 @@
|
|||||||
# mypy: ignore-errors
|
|
||||||
|
|
||||||
from google.protobuf.internal import containers as _containers
|
from google.protobuf.internal import containers as _containers
|
||||||
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||||
from google.protobuf import descriptor as _descriptor
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
|||||||
@ -6,15 +6,15 @@ from zlib import crc32
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
import requests
|
import requests
|
||||||
import yaml
|
|
||||||
from construct import ConstructError
|
from construct import ConstructError
|
||||||
|
from unidecode import unidecode, UnidecodeError
|
||||||
|
import yaml
|
||||||
from google.protobuf.json_format import MessageToDict
|
from google.protobuf.json_format import MessageToDict
|
||||||
from unidecode import UnidecodeError, unidecode
|
|
||||||
|
|
||||||
from pywidevine import __version__
|
from pywidevine import __version__
|
||||||
from pywidevine.cdm import Cdm
|
from pywidevine.cdm import Cdm
|
||||||
from pywidevine.device import Device, DeviceTypes
|
from pywidevine.device import Device
|
||||||
from pywidevine.license_protocol_pb2 import FileHashes, LicenseType
|
from pywidevine.license_protocol_pb2 import LicenseType, FileHashes
|
||||||
from pywidevine.pssh import PSSH
|
from pywidevine.pssh import PSSH
|
||||||
|
|
||||||
|
|
||||||
@ -26,25 +26,27 @@ def main(version: bool, debug: bool) -> None:
|
|||||||
logging.basicConfig(level=logging.DEBUG if debug else logging.INFO)
|
logging.basicConfig(level=logging.DEBUG if debug else logging.INFO)
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
|
copyright_years = 2022
|
||||||
current_year = datetime.now().year
|
current_year = datetime.now().year
|
||||||
copyright_years = f"2022-{current_year}"
|
if copyright_years != current_year:
|
||||||
|
copyright_years = f"{copyright_years}-{current_year}"
|
||||||
|
|
||||||
log.info("pywidevine version %s Copyright (c) %s rlaphoenix", __version__, copyright_years)
|
log.info("pywidevine version %s Copyright (c) %s rlaphoenix", __version__, copyright_years)
|
||||||
log.info("https://github.com/devine-dl/pywidevine")
|
log.info("https://github.com/rlaphoenix/pywidevine")
|
||||||
if version:
|
if version:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@main.command(name="license")
|
@main.command(name="license")
|
||||||
@click.argument("device_path", type=Path)
|
@click.argument("device", type=Path)
|
||||||
@click.argument("pssh", type=PSSH)
|
@click.argument("pssh", type=str)
|
||||||
@click.argument("server", type=str)
|
@click.argument("server", type=str)
|
||||||
@click.option("-t", "--type", "license_type", type=click.Choice(LicenseType.keys(), case_sensitive=False),
|
@click.option("-t", "--type", "type_", type=click.Choice(LicenseType.keys(), case_sensitive=False),
|
||||||
default="STREAMING",
|
default="STREAMING",
|
||||||
help="License Type to Request.")
|
help="License Type to Request.")
|
||||||
@click.option("-p", "--privacy", is_flag=True, default=False,
|
@click.option("-p", "--privacy", is_flag=True, default=False,
|
||||||
help="Use Privacy Mode, off by default.")
|
help="Use Privacy Mode, off by default.")
|
||||||
def license_(device_path: Path, pssh: PSSH, server: str, license_type: str, privacy: bool) -> None:
|
def license_(device: Path, pssh: str, server: str, type_: str, privacy: bool):
|
||||||
"""
|
"""
|
||||||
Make a License Request for PSSH to SERVER using DEVICE.
|
Make a License Request for PSSH to SERVER using DEVICE.
|
||||||
It will return a list of all keys within the returned license.
|
It will return a list of all keys within the returned license.
|
||||||
@ -63,8 +65,11 @@ def license_(device_path: Path, pssh: PSSH, server: str, license_type: str, priv
|
|||||||
"""
|
"""
|
||||||
log = logging.getLogger("license")
|
log = logging.getLogger("license")
|
||||||
|
|
||||||
|
# prepare pssh
|
||||||
|
pssh = PSSH(pssh)
|
||||||
|
|
||||||
# load device
|
# load device
|
||||||
device = Device.load(device_path)
|
device = Device.load(device)
|
||||||
log.info("[+] Loaded Device (%s L%s)", device.system_id, device.security_level)
|
log.info("[+] Loaded Device (%s L%s)", device.system_id, device.security_level)
|
||||||
log.debug(device)
|
log.debug(device)
|
||||||
|
|
||||||
@ -79,36 +84,37 @@ def license_(device_path: Path, pssh: PSSH, server: str, license_type: str, priv
|
|||||||
|
|
||||||
if privacy:
|
if privacy:
|
||||||
# get service cert for license server via cert challenge
|
# get service cert for license server via cert challenge
|
||||||
service_cert_res = requests.post(
|
service_cert = requests.post(
|
||||||
url=server,
|
url=server,
|
||||||
data=cdm.service_certificate_challenge
|
data=cdm.service_certificate_challenge
|
||||||
)
|
)
|
||||||
if service_cert_res.status_code != 200:
|
if service_cert.status_code != 200:
|
||||||
log.error(
|
log.error(
|
||||||
"[-] Failed to get Service Privacy Certificate: [%s] %s",
|
"[-] Failed to get Service Privacy Certificate: [%s] %s",
|
||||||
service_cert_res.status_code,
|
service_cert.status_code,
|
||||||
service_cert_res.text
|
service_cert.text
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
service_cert = service_cert_res.content
|
service_cert = service_cert.content
|
||||||
provider_id = cdm.set_service_certificate(session_id, service_cert)
|
provider_id = cdm.set_service_certificate(session_id, service_cert)
|
||||||
log.info("[+] Set Service Privacy Certificate: %s", provider_id)
|
log.info("[+] Set Service Privacy Certificate: %s", provider_id)
|
||||||
log.debug(service_cert)
|
log.debug(service_cert)
|
||||||
|
|
||||||
# get license challenge
|
# get license challenge
|
||||||
|
license_type = LicenseType.Value(type_)
|
||||||
challenge = cdm.get_license_challenge(session_id, pssh, license_type, privacy_mode=True)
|
challenge = cdm.get_license_challenge(session_id, pssh, license_type, privacy_mode=True)
|
||||||
log.info("[+] Created License Request Message (Challenge)")
|
log.info("[+] Created License Request Message (Challenge)")
|
||||||
log.debug(challenge)
|
log.debug(challenge)
|
||||||
|
|
||||||
# send license challenge
|
# send license challenge
|
||||||
license_res = requests.post(
|
licence = requests.post(
|
||||||
url=server,
|
url=server,
|
||||||
data=challenge
|
data=challenge
|
||||||
)
|
)
|
||||||
if license_res.status_code != 200:
|
if licence.status_code != 200:
|
||||||
log.error("[-] Failed to send challenge: [%s] %s", license_res.status_code, license_res.text)
|
log.error("[-] Failed to send challenge: [%s] %s", licence.status_code, licence.text)
|
||||||
return
|
return
|
||||||
licence = license_res.content
|
licence = licence.content
|
||||||
log.info("[+] Got License Message")
|
log.info("[+] Got License Message")
|
||||||
log.debug(licence)
|
log.debug(licence)
|
||||||
|
|
||||||
@ -129,7 +135,7 @@ def license_(device_path: Path, pssh: PSSH, server: str, license_type: str, priv
|
|||||||
@click.option("-p", "--privacy", is_flag=True, default=False,
|
@click.option("-p", "--privacy", is_flag=True, default=False,
|
||||||
help="Use Privacy Mode, off by default.")
|
help="Use Privacy Mode, off by default.")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def test(ctx: click.Context, device: Path, privacy: bool) -> None:
|
def test(ctx: click.Context, device: Path, privacy: bool):
|
||||||
"""
|
"""
|
||||||
Test the CDM code by getting Content Keys for Bitmovin's Art of Motion example.
|
Test the CDM code by getting Content Keys for Bitmovin's Art of Motion example.
|
||||||
https://bitmovin.com/demos/drm
|
https://bitmovin.com/demos/drm
|
||||||
@ -140,8 +146,8 @@ def test(ctx: click.Context, device: Path, privacy: bool) -> None:
|
|||||||
"""
|
"""
|
||||||
# The PSSH is the same for all tracks both video and audio.
|
# The PSSH is the same for all tracks both video and audio.
|
||||||
# However, this might not be the case for all services/manifests.
|
# However, this might not be the case for all services/manifests.
|
||||||
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa"
|
pssh = "AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa" \
|
||||||
"7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
|
"7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA=="
|
||||||
|
|
||||||
# This License Server requires no authorization at all, no cookies, no credentials
|
# This License Server requires no authorization at all, no cookies, no credentials
|
||||||
# nothing. This is often not the case for real services.
|
# nothing. This is often not the case for real services.
|
||||||
@ -149,28 +155,28 @@ def test(ctx: click.Context, device: Path, privacy: bool) -> None:
|
|||||||
|
|
||||||
# Specify OFFLINE if it's a PSSH for a download/offline mode title, e.g., the
|
# Specify OFFLINE if it's a PSSH for a download/offline mode title, e.g., the
|
||||||
# Download feature on Netflix Apps. Otherwise, use STREAMING or AUTOMATIC.
|
# Download feature on Netflix Apps. Otherwise, use STREAMING or AUTOMATIC.
|
||||||
license_type = "STREAMING"
|
license_type = LicenseType.STREAMING
|
||||||
|
|
||||||
# this runs the `cdm license` CLI-command code with the data we set above
|
# this runs the `cdm license` CLI-command code with the data we set above
|
||||||
# it will print information as it goes to the terminal
|
# it will print information as it goes to the terminal
|
||||||
ctx.invoke(
|
ctx.invoke(
|
||||||
license_,
|
license_,
|
||||||
device_path=device,
|
device=device,
|
||||||
pssh=pssh,
|
pssh=pssh,
|
||||||
server=license_server,
|
server=license_server,
|
||||||
license_type=license_type,
|
type_=LicenseType.Name(license_type),
|
||||||
privacy=privacy
|
privacy=privacy
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
@click.option("-t", "--type", "type_", type=click.Choice([x.name for x in DeviceTypes], case_sensitive=False),
|
@click.option("-t", "--type", "type_", type=click.Choice([x.name for x in Device.Types], case_sensitive=False),
|
||||||
required=True, help="Device Type")
|
required=True, help="Device Type")
|
||||||
@click.option("-l", "--level", type=click.IntRange(1, 3), required=True, help="Device Security Level")
|
@click.option("-l", "--level", type=click.IntRange(1, 3), required=True, help="Device Security Level")
|
||||||
@click.option("-k", "--key", type=Path, required=True, help="Device RSA Private Key in PEM or DER format")
|
@click.option("-k", "--key", type=Path, required=True, help="Device RSA Private Key in PEM or DER format")
|
||||||
@click.option("-c", "--client_id", type=Path, required=True, help="Widevine ClientIdentification Blob file")
|
@click.option("-c", "--client_id", type=Path, required=True, help="Widevine ClientIdentification Blob file")
|
||||||
@click.option("-v", "--vmp", type=Path, default=None, help="Widevine FileHashes Blob file")
|
@click.option("-v", "--vmp", type=Path, default=None, help="Widevine FileHashes Blob file")
|
||||||
@click.option("-o", "--output", type=Path, default=None, help="Output Path or Directory")
|
@click.option("-o", "--output", type=Path, default=None, help="Output Directory")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def create_device(
|
def create_device(
|
||||||
ctx: click.Context,
|
ctx: click.Context,
|
||||||
@ -195,7 +201,7 @@ def create_device(
|
|||||||
log = logging.getLogger("create-device")
|
log = logging.getLogger("create-device")
|
||||||
|
|
||||||
device = Device(
|
device = Device(
|
||||||
type_=DeviceTypes[type_.upper()],
|
type_=Device.Types[type_.upper()],
|
||||||
security_level=level,
|
security_level=level,
|
||||||
flags=None,
|
flags=None,
|
||||||
private_key=key.read_bytes(),
|
private_key=key.read_bytes(),
|
||||||
@ -224,19 +230,7 @@ def create_device(
|
|||||||
except UnidecodeError as e:
|
except UnidecodeError as e:
|
||||||
raise click.ClickException(f"Failed to sanitize name, {e}")
|
raise click.ClickException(f"Failed to sanitize name, {e}")
|
||||||
|
|
||||||
if output and output.suffix:
|
out_path = (output or Path.cwd()) / f"{name}_{device.system_id}_l{device.security_level}.wvd"
|
||||||
if output.suffix.lower() != ".wvd":
|
|
||||||
log.warning(f"Saving WVD with the file extension '{output.suffix}' but '.wvd' is recommended.")
|
|
||||||
out_path = output
|
|
||||||
else:
|
|
||||||
out_dir = output or Path.cwd()
|
|
||||||
out_path = out_dir / f"{name}_{device.system_id}_l{device.security_level}.wvd"
|
|
||||||
|
|
||||||
if out_path.exists():
|
|
||||||
log.error(f"A file already exists at the path '{out_path}', cannot overwrite.")
|
|
||||||
return
|
|
||||||
|
|
||||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
out_path.write_bytes(wvd_bin)
|
out_path.write_bytes(wvd_bin)
|
||||||
|
|
||||||
log.info("Created Widevine Device (.wvd) file, %s", out_path.name)
|
log.info("Created Widevine Device (.wvd) file, %s", out_path.name)
|
||||||
@ -376,10 +370,10 @@ def migrate(ctx: click.Context, path: Path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@main.command("serve", short_help="Serve your local CDM and Widevine Devices Remotely.")
|
@main.command("serve", short_help="Serve your local CDM and Widevine Devices Remotely.")
|
||||||
@click.argument("config_path", type=Path)
|
@click.argument("config", type=Path)
|
||||||
@click.option("-h", "--host", type=str, default="127.0.0.1", help="Host to serve from.")
|
@click.option("-h", "--host", type=str, default="127.0.0.1", help="Host to serve from.")
|
||||||
@click.option("-p", "--port", type=int, default=8786, help="Port to serve from.")
|
@click.option("-p", "--port", type=int, default=8786, help="Port to serve from.")
|
||||||
def serve_(config_path: Path, host: str, port: int) -> None:
|
def serve_(config: Path, host: str, port: int):
|
||||||
"""
|
"""
|
||||||
Serve your local CDM and Widevine Devices Remotely.
|
Serve your local CDM and Widevine Devices Remotely.
|
||||||
|
|
||||||
@ -391,8 +385,8 @@ def serve_(config_path: Path, host: str, port: int) -> None:
|
|||||||
Host as 127.0.0.1 may block remote access even if port-forwarded.
|
Host as 127.0.0.1 may block remote access even if port-forwarded.
|
||||||
Instead, use 0.0.0.0 and ensure the TCP port you choose is forwarded.
|
Instead, use 0.0.0.0 and ensure the TCP port you choose is forwarded.
|
||||||
"""
|
"""
|
||||||
from pywidevine import serve # isort:skip
|
from pywidevine import serve
|
||||||
import yaml # isort:skip
|
import yaml
|
||||||
|
|
||||||
config = yaml.safe_load(config_path.read_text(encoding="utf8"))
|
config = yaml.safe_load(config.read_text(encoding="utf8"))
|
||||||
serve.run(config, host, port)
|
serve.run(config, host, port)
|
||||||
|
|||||||
@ -4,9 +4,8 @@ import base64
|
|||||||
import binascii
|
import binascii
|
||||||
import string
|
import string
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Optional, Union
|
from typing import Union, Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from xml.etree.ElementTree import XML
|
|
||||||
|
|
||||||
import construct
|
import construct
|
||||||
from construct import Container
|
from construct import Container
|
||||||
@ -14,6 +13,7 @@ from google.protobuf.message import DecodeError
|
|||||||
from pymp4.parser import Box
|
from pymp4.parser import Box
|
||||||
|
|
||||||
from pywidevine.license_protocol_pb2 import WidevinePsshData
|
from pywidevine.license_protocol_pb2 import WidevinePsshData
|
||||||
|
from pywidevine.utils import load_xml
|
||||||
|
|
||||||
|
|
||||||
class PSSH:
|
class PSSH:
|
||||||
@ -82,17 +82,17 @@ class PSSH:
|
|||||||
box = Box.parse(data)
|
box = Box.parse(data)
|
||||||
except (IOError, construct.ConstructError): # not a box
|
except (IOError, construct.ConstructError): # not a box
|
||||||
try:
|
try:
|
||||||
widevine_pssh_data = WidevinePsshData()
|
cenc_header = WidevinePsshData()
|
||||||
widevine_pssh_data.ParseFromString(data)
|
cenc_header.ParseFromString(data)
|
||||||
data_serialized = widevine_pssh_data.SerializeToString()
|
cenc_header = cenc_header.SerializeToString()
|
||||||
if data_serialized != data: # not actually a WidevinePsshData
|
if cenc_header != data: # not actually a WidevinePsshData
|
||||||
raise DecodeError()
|
raise DecodeError()
|
||||||
box = Box.parse(Box.build(dict(
|
box = Box.parse(Box.build(dict(
|
||||||
type=b"pssh",
|
type=b"pssh",
|
||||||
version=0,
|
version=0,
|
||||||
flags=0,
|
flags=0,
|
||||||
system_ID=PSSH.SystemId.Widevine,
|
system_ID=PSSH.SystemId.Widevine,
|
||||||
init_data=data_serialized
|
init_data=cenc_header
|
||||||
)))
|
)))
|
||||||
except DecodeError: # not a widevine cenc header
|
except DecodeError: # not a widevine cenc header
|
||||||
if "</WRMHEADER>".encode("utf-16-le") in data:
|
if "</WRMHEADER>".encode("utf-16-le") in data:
|
||||||
@ -170,6 +170,25 @@ class PSSH:
|
|||||||
if init_data is None and key_ids is None:
|
if init_data is None and key_ids is None:
|
||||||
raise ValueError("Version 1 PSSH boxes must use either init_data or key_ids but neither were provided")
|
raise ValueError("Version 1 PSSH boxes must use either init_data or key_ids but neither were provided")
|
||||||
|
|
||||||
|
if key_ids is not None:
|
||||||
|
# ensure key_ids are UUID, supports hex, base64, and bytes
|
||||||
|
if not all(isinstance(x, (UUID, bytes, str)) for x in key_ids):
|
||||||
|
not_bytes = [x for x in key_ids if not isinstance(x, (UUID, bytes, str))]
|
||||||
|
raise TypeError(
|
||||||
|
"Expected all of key_ids to be a UUID, hex, base64, or bytes, but one or more are not, "
|
||||||
|
f"{not_bytes!r}"
|
||||||
|
)
|
||||||
|
key_ids = [
|
||||||
|
UUID(bytes=key_id_b)
|
||||||
|
for key_id in key_ids
|
||||||
|
for key_id_b in [
|
||||||
|
key_id.bytes if isinstance(key_id, UUID) else
|
||||||
|
bytes.fromhex(key_id) if all(c in string.hexdigits for c in key_id) else
|
||||||
|
base64.b64decode(key_id) if isinstance(key_id, str) else
|
||||||
|
key_id
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
if init_data is not None:
|
if init_data is not None:
|
||||||
if isinstance(init_data, WidevinePsshData):
|
if isinstance(init_data, WidevinePsshData):
|
||||||
init_data = init_data.SerializeToString()
|
init_data = init_data.SerializeToString()
|
||||||
@ -247,31 +266,19 @@ class PSSH:
|
|||||||
# TODO: Add support for Embedded License Stores (0x03)
|
# TODO: Add support for Embedded License Stores (0x03)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
wrm_ns = {"wrm": "http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader"}
|
prr_header = load_xml(prr_value.decode("utf-16-le"))
|
||||||
prr_header = XML(prr_value.decode("utf-16-le"))
|
prr_header_version = prr_header.attrib["version"]
|
||||||
prr_header_version = prr_header.get("version")
|
|
||||||
if prr_header_version == "4.0.0.0":
|
if prr_header_version == "4.0.0.0":
|
||||||
key_ids = [
|
key_ids = prr_header.xpath("DATA/KID/text()")
|
||||||
x.text
|
|
||||||
for x in prr_header.findall("./wrm:DATA/wrm:KID", wrm_ns)
|
|
||||||
if x.text
|
|
||||||
]
|
|
||||||
elif prr_header_version == "4.1.0.0":
|
elif prr_header_version == "4.1.0.0":
|
||||||
key_ids = [
|
key_ids = prr_header.xpath("DATA/PROTECTINFO/KID/@VALUE")
|
||||||
x.attrib["VALUE"]
|
|
||||||
for x in prr_header.findall("./wrm:DATA/wrm:PROTECTINFO/wrm:KID", wrm_ns)
|
|
||||||
]
|
|
||||||
elif prr_header_version in ("4.2.0.0", "4.3.0.0"):
|
elif prr_header_version in ("4.2.0.0", "4.3.0.0"):
|
||||||
# TODO: Retain the Encryption Scheme information in v4.3.0.0
|
# TODO: Retain the Encryption Scheme information in v4.3.0.0
|
||||||
# This is because some Key IDs can be AES-CTR while some are AES-CBC.
|
# This is because some Key IDs can be AES-CTR while some are AES-CBC.
|
||||||
# Conversion to WidevineCencHeader could use this information.
|
# Conversion to WidevineCencHeader could use this information.
|
||||||
key_ids = [
|
key_ids = prr_header.xpath("DATA/PROTECTINFO/KIDS/KID/@VALUE")
|
||||||
x.attrib["VALUE"]
|
|
||||||
for x in prr_header.findall("./wrm:DATA/wrm:PROTECTINFO/wrm:KIDS/wrm:KID", wrm_ns)
|
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported PlayReadyHeader version {prr_header_version}")
|
raise ValueError(f"Unsupported PlayReadyHeader version {prr_header_version}")
|
||||||
|
|
||||||
return [
|
return [
|
||||||
UUID(bytes=base64.b64decode(key_id))
|
UUID(bytes=base64.b64decode(key_id))
|
||||||
for key_id in key_ids
|
for key_id in key_ids
|
||||||
@ -307,17 +314,16 @@ class PSSH:
|
|||||||
if self.system_id == PSSH.SystemId.Widevine:
|
if self.system_id == PSSH.SystemId.Widevine:
|
||||||
raise ValueError("This is already a Widevine PSSH")
|
raise ValueError("This is already a Widevine PSSH")
|
||||||
|
|
||||||
widevine_pssh_data = WidevinePsshData(
|
cenc_header = WidevinePsshData()
|
||||||
key_ids=[x.bytes for x in self.key_ids],
|
cenc_header.algorithm = 1 # 0=Clear, 1=AES-CTR
|
||||||
algorithm="AESCTR"
|
cenc_header.key_ids[:] = [x.bytes for x in self.key_ids]
|
||||||
)
|
|
||||||
|
|
||||||
if self.version == 1:
|
if self.version == 1:
|
||||||
# ensure both cenc header and box has same Key IDs
|
# ensure both cenc header and box has same Key IDs
|
||||||
# v1 uses both this and within init data for basically no reason
|
# v1 uses both this and within init data for basically no reason
|
||||||
self.__key_ids = self.key_ids
|
self.__key_ids = self.key_ids
|
||||||
|
|
||||||
self.init_data = widevine_pssh_data.SerializeToString()
|
self.init_data = cenc_header.SerializeToString()
|
||||||
self.system_id = PSSH.SystemId.Widevine
|
self.system_id = PSSH.SystemId.Widevine
|
||||||
|
|
||||||
def to_playready(
|
def to_playready(
|
||||||
@ -368,11 +374,11 @@ class PSSH:
|
|||||||
<PROTECTINFO>
|
<PROTECTINFO>
|
||||||
<KIDS>{key_ids_xml}</KIDS>
|
<KIDS>{key_ids_xml}</KIDS>
|
||||||
</PROTECTINFO>
|
</PROTECTINFO>
|
||||||
{'<LA_URL>%s</LA_URL>' % la_url if la_url else ''}
|
{f'<LA_URL>%s</LA_URL>' % la_url if la_url else ''}
|
||||||
{'<LUI_URL>%s</LUI_URL>' % lui_url if lui_url else ''}
|
{f'<LUI_URL>%s</LUI_URL>' % lui_url if lui_url else ''}
|
||||||
{'<DS_ID>%s</DS_ID>' % base64.b64encode(ds_id).decode() if ds_id else ''}
|
{f'<DS_ID>%s</DS_ID>' % base64.b64encode(ds_id).decode() if ds_id else ''}
|
||||||
{'<DECRYPTORSETUP>%s</DECRYPTORSETUP>' % decryptor_setup if decryptor_setup else ''}
|
{f'<DECRYPTORSETUP>%s</DECRYPTORSETUP>' % decryptor_setup if decryptor_setup else ''}
|
||||||
{'<CUSTOMATTRIBUTES xmlns="">%s</CUSTOMATTRIBUTES>' % custom_data if custom_data else ''}
|
{f'<CUSTOMATTRIBUTES xmlns="">%s</CUSTOMATTRIBUTES>' % custom_data if custom_data else ''}
|
||||||
</DATA>
|
</DATA>
|
||||||
</WRMHEADER>
|
</WRMHEADER>
|
||||||
""".encode("utf-16-le")
|
""".encode("utf-16-le")
|
||||||
@ -386,57 +392,30 @@ class PSSH:
|
|||||||
self.init_data = pro
|
self.init_data = pro
|
||||||
self.system_id = PSSH.SystemId.PlayReady
|
self.system_id = PSSH.SystemId.PlayReady
|
||||||
|
|
||||||
def set_key_ids(self, key_ids: list[Union[UUID, str, bytes]]) -> None:
|
def set_key_ids(self, key_ids: list[UUID]) -> None:
|
||||||
"""Overwrite all Key IDs with the specified Key IDs."""
|
"""Overwrite all Key IDs with the specified Key IDs."""
|
||||||
if self.system_id != PSSH.SystemId.Widevine:
|
if self.system_id != PSSH.SystemId.Widevine:
|
||||||
# TODO: Add support for setting the Key IDs in a PlayReady Header
|
# TODO: Add support for setting the Key IDs in a PlayReady Header
|
||||||
raise ValueError(f"Only Widevine PSSH Boxes are supported, not {self.system_id}.")
|
raise ValueError(f"Only Widevine PSSH Boxes are supported, not {self.system_id}.")
|
||||||
|
|
||||||
key_id_uuids = self.parse_key_ids(key_ids)
|
if not isinstance(key_ids, list):
|
||||||
|
raise TypeError(f"Expecting key_ids to be a list, not {key_ids!r}")
|
||||||
|
|
||||||
|
if not all(isinstance(x, UUID) for x in key_ids):
|
||||||
|
not_uuid = [x for x in key_ids if not isinstance(x, UUID)]
|
||||||
|
raise TypeError(f"All Key IDs in key_ids must be a {UUID}, not {not_uuid}")
|
||||||
|
|
||||||
if self.version == 1 or self.__key_ids:
|
if self.version == 1 or self.__key_ids:
|
||||||
# only use v1 box key_ids if version is 1, or it's already being used
|
# only use v1 box key_ids if version is 1, or it's already being used
|
||||||
# this is in case the service stupidly expects it for version 0
|
# this is in case the service stupidly expects it for version 0
|
||||||
self.__key_ids = key_id_uuids
|
self.__key_ids = key_ids
|
||||||
|
|
||||||
cenc_header = WidevinePsshData()
|
cenc_header = WidevinePsshData()
|
||||||
cenc_header.ParseFromString(self.init_data)
|
cenc_header.ParseFromString(self.init_data)
|
||||||
|
|
||||||
cenc_header.key_ids[:] = [
|
cenc_header.key_ids[:] = [
|
||||||
key_id.bytes
|
key_id.bytes
|
||||||
for key_id in key_id_uuids
|
for key_id in key_ids
|
||||||
]
|
]
|
||||||
|
|
||||||
self.init_data = cenc_header.SerializeToString()
|
self.init_data = cenc_header.SerializeToString()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_key_ids(key_ids: list[Union[UUID, str, bytes]]) -> list[UUID]:
|
|
||||||
"""
|
|
||||||
Parse a list of Key IDs in hex, base64, or bytes to UUIDs.
|
|
||||||
|
|
||||||
Raises TypeError if `key_ids` is not a list, or the list contains one
|
|
||||||
or more items that are not a UUID, str, or bytes object.
|
|
||||||
"""
|
|
||||||
if not isinstance(key_ids, list):
|
|
||||||
raise TypeError(f"Expected key_ids to be a list, not {key_ids!r}")
|
|
||||||
|
|
||||||
if not all(isinstance(x, (UUID, str, bytes)) for x in key_ids):
|
|
||||||
raise TypeError("Some items of key_ids are not a UUID, str, or bytes. Unsure how to continue...")
|
|
||||||
|
|
||||||
uuids = [
|
|
||||||
UUID(bytes=key_id_b)
|
|
||||||
for key_id in key_ids
|
|
||||||
for key_id_b in [
|
|
||||||
key_id.bytes if isinstance(key_id, UUID) else
|
|
||||||
(
|
|
||||||
bytes.fromhex(key_id) if all(c in string.hexdigits for c in key_id) else
|
|
||||||
base64.b64decode(key_id)
|
|
||||||
) if isinstance(key_id, str) else
|
|
||||||
key_id
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
return uuids
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("PSSH",)
|
|
||||||
|
|||||||
@ -3,21 +3,21 @@ from __future__ import annotations
|
|||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import re
|
import re
|
||||||
from typing import Optional, Union
|
from typing import Union, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from Crypto.Hash import SHA1
|
from Crypto.Hash import SHA1
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from Crypto.Signature import pss
|
from Crypto.Signature import pss
|
||||||
from google.protobuf.message import DecodeError
|
from google.protobuf.message import DecodeError
|
||||||
|
|
||||||
from pywidevine.cdm import Cdm
|
from pywidevine.cdm import Cdm
|
||||||
from pywidevine.device import Device, DeviceTypes
|
from pywidevine.device import Device
|
||||||
from pywidevine.exceptions import (DeviceMismatch, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType,
|
from pywidevine.exceptions import InvalidInitData, InvalidLicenseType, InvalidLicenseMessage, DeviceMismatch, \
|
||||||
SignatureMismatch)
|
SignatureMismatch
|
||||||
from pywidevine.key import Key
|
from pywidevine.key import Key
|
||||||
from pywidevine.license_protocol_pb2 import (ClientIdentification, License, LicenseType, SignedDrmCertificate,
|
|
||||||
SignedMessage)
|
from pywidevine.license_protocol_pb2 import LicenseType, SignedMessage, License, ClientIdentification, \
|
||||||
|
SignedDrmCertificate
|
||||||
from pywidevine.pssh import PSSH
|
from pywidevine.pssh import PSSH
|
||||||
|
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ class RemoteCdm(Cdm):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
device_type: Union[DeviceTypes, str],
|
device_type: Union[Device.Types, str],
|
||||||
system_id: int,
|
system_id: int,
|
||||||
security_level: int,
|
security_level: int,
|
||||||
host: str,
|
host: str,
|
||||||
@ -37,9 +37,9 @@ class RemoteCdm(Cdm):
|
|||||||
if not device_type:
|
if not device_type:
|
||||||
raise ValueError("Device Type must be provided")
|
raise ValueError("Device Type must be provided")
|
||||||
if isinstance(device_type, str):
|
if isinstance(device_type, str):
|
||||||
device_type = DeviceTypes[device_type]
|
device_type = Device.Types[device_type]
|
||||||
if not isinstance(device_type, DeviceTypes):
|
if not isinstance(device_type, Device.Types):
|
||||||
raise TypeError(f"Expected device_type to be a {DeviceTypes!r} not {device_type!r}")
|
raise TypeError(f"Expected device_type to be a {Device.Types!r} not {device_type!r}")
|
||||||
|
|
||||||
if not system_id:
|
if not system_id:
|
||||||
raise ValueError("System ID must be provided")
|
raise ValueError("System ID must be provided")
|
||||||
@ -86,10 +86,10 @@ class RemoteCdm(Cdm):
|
|||||||
server = r.headers.get("Server")
|
server = r.headers.get("Server")
|
||||||
if not server or "pywidevine serve" not in server.lower():
|
if not server or "pywidevine serve" not in server.lower():
|
||||||
raise ValueError(f"This Remote CDM API does not seem to be a pywidevine serve API ({server}).")
|
raise ValueError(f"This Remote CDM API does not seem to be a pywidevine serve API ({server}).")
|
||||||
server_version_re = re.search(r"pywidevine serve v([\d.]+)", server, re.IGNORECASE)
|
server_version = re.search(r"pywidevine serve v([\d.]+)", server, re.IGNORECASE)
|
||||||
if not server_version_re:
|
if not server_version:
|
||||||
raise ValueError("The pywidevine server API is not stating the version correctly, cannot continue.")
|
raise ValueError("The pywidevine server API is not stating the version correctly, cannot continue.")
|
||||||
server_version = server_version_re.group(1)
|
server_version = server_version.group(1)
|
||||||
if server_version < "1.4.3":
|
if server_version < "1.4.3":
|
||||||
raise ValueError(f"This pywidevine serve API version ({server_version}) is not supported.")
|
raise ValueError(f"This pywidevine serve API version ({server_version}) is not supported.")
|
||||||
|
|
||||||
@ -185,7 +185,7 @@ class RemoteCdm(Cdm):
|
|||||||
self,
|
self,
|
||||||
session_id: bytes,
|
session_id: bytes,
|
||||||
pssh: PSSH,
|
pssh: PSSH,
|
||||||
license_type: str = "STREAMING",
|
type_: Union[int, str] = LicenseType.STREAMING,
|
||||||
privacy_mode: bool = True
|
privacy_mode: bool = True
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
if not pssh:
|
if not pssh:
|
||||||
@ -193,16 +193,20 @@ class RemoteCdm(Cdm):
|
|||||||
if not isinstance(pssh, PSSH):
|
if not isinstance(pssh, PSSH):
|
||||||
raise InvalidInitData(f"Expected pssh to be a {PSSH}, not {pssh!r}")
|
raise InvalidInitData(f"Expected pssh to be a {PSSH}, not {pssh!r}")
|
||||||
|
|
||||||
if not isinstance(license_type, str):
|
try:
|
||||||
raise InvalidLicenseType(f"Expected license_type to be a {str}, not {license_type!r}")
|
if isinstance(type_, int):
|
||||||
if license_type not in LicenseType.keys():
|
type_ = LicenseType.Name(int(type_))
|
||||||
raise InvalidLicenseType(
|
elif isinstance(type_, str):
|
||||||
f"Invalid license_type value of '{license_type}'. "
|
type_ = LicenseType.Name(LicenseType.Value(type_))
|
||||||
f"Available values: {LicenseType.keys()}"
|
elif isinstance(type_, LicenseType):
|
||||||
)
|
type_ = LicenseType.Name(type_)
|
||||||
|
else:
|
||||||
|
raise InvalidLicenseType()
|
||||||
|
except ValueError:
|
||||||
|
raise InvalidLicenseType(f"License Type {type_!r} is invalid")
|
||||||
|
|
||||||
r = self.__session.post(
|
r = self.__session.post(
|
||||||
url=f"{self.host}/{self.device_name}/get_license_challenge/{license_type}",
|
url=f"{self.host}/{self.device_name}/get_license_challenge/{type_}",
|
||||||
json={
|
json={
|
||||||
"session_id": session_id.hex(),
|
"session_id": session_id.hex(),
|
||||||
"init_data": pssh.dumps(),
|
"init_data": pssh.dumps(),
|
||||||
@ -247,7 +251,7 @@ class RemoteCdm(Cdm):
|
|||||||
if not isinstance(license_message, SignedMessage):
|
if not isinstance(license_message, SignedMessage):
|
||||||
raise InvalidLicenseMessage(f"Expecting license_response to be a SignedMessage, got {license_message!r}")
|
raise InvalidLicenseMessage(f"Expecting license_response to be a SignedMessage, got {license_message!r}")
|
||||||
|
|
||||||
if license_message.type != SignedMessage.MessageType.Value("LICENSE"):
|
if license_message.type != SignedMessage.MessageType.LICENSE:
|
||||||
raise InvalidLicenseMessage(
|
raise InvalidLicenseMessage(
|
||||||
f"Expecting a LICENSE message, not a "
|
f"Expecting a LICENSE message, not a "
|
||||||
f"'{SignedMessage.MessageType.Name(license_message.type)}' message."
|
f"'{SignedMessage.MessageType.Name(license_message.type)}' message."
|
||||||
@ -297,4 +301,4 @@ class RemoteCdm(Cdm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("RemoteCdm",)
|
__ALL__ = (RemoteCdm,)
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import base64
|
import base64
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from aiohttp.typedefs import Handler
|
|
||||||
from google.protobuf.message import DecodeError
|
from google.protobuf.message import DecodeError
|
||||||
|
|
||||||
from pywidevine.pssh import PSSH
|
from pywidevine.pssh import PSSH
|
||||||
@ -21,14 +20,14 @@ except ImportError:
|
|||||||
from pywidevine import __version__
|
from pywidevine import __version__
|
||||||
from pywidevine.cdm import Cdm
|
from pywidevine.cdm import Cdm
|
||||||
from pywidevine.device import Device
|
from pywidevine.device import Device
|
||||||
from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType,
|
from pywidevine.exceptions import TooManySessions, InvalidSession, SignatureMismatch, InvalidInitData, \
|
||||||
InvalidSession, SignatureMismatch, TooManySessions)
|
InvalidLicenseType, InvalidLicenseMessage, InvalidContext
|
||||||
|
|
||||||
routes = web.RouteTableDef()
|
routes = web.RouteTableDef()
|
||||||
|
|
||||||
|
|
||||||
async def _startup(app: web.Application) -> None:
|
async def _startup(app: web.Application):
|
||||||
app["cdms"] = {}
|
app["cdms"]: dict[tuple[str, str], Cdm] = {}
|
||||||
app["config"]["devices"] = {
|
app["config"]["devices"] = {
|
||||||
path.stem: path
|
path.stem: path
|
||||||
for x in app["config"]["devices"]
|
for x in app["config"]["devices"]
|
||||||
@ -39,7 +38,7 @@ async def _startup(app: web.Application) -> None:
|
|||||||
raise FileNotFoundError(f"Device file does not exist: {device}")
|
raise FileNotFoundError(f"Device file does not exist: {device}")
|
||||||
|
|
||||||
|
|
||||||
async def _cleanup(app: web.Application) -> None:
|
async def _cleanup(app: web.Application):
|
||||||
app["cdms"].clear()
|
app["cdms"].clear()
|
||||||
del app["cdms"]
|
del app["cdms"]
|
||||||
app["config"].clear()
|
app["config"].clear()
|
||||||
@ -47,7 +46,7 @@ async def _cleanup(app: web.Application) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@routes.get("/")
|
@routes.get("/")
|
||||||
async def ping(_: Any) -> web.Response:
|
async def ping(_) -> web.Response:
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Pong!"
|
"message": "Pong!"
|
||||||
@ -212,15 +211,13 @@ async def get_service_certificate(request: web.Request) -> web.Response:
|
|||||||
}, status=400)
|
}, status=400)
|
||||||
|
|
||||||
if service_certificate:
|
if service_certificate:
|
||||||
service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode()
|
service_certificate = base64.b64encode(service_certificate.SerializeToString()).decode()
|
||||||
else:
|
|
||||||
service_certificate_b64 = None
|
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Successfully got the Service Certificate.",
|
"message": "Successfully got the Service Certificate.",
|
||||||
"data": {
|
"data": {
|
||||||
"service_certificate": service_certificate_b64
|
"service_certificate": service_certificate
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -270,7 +267,7 @@ async def get_license_challenge(request: web.Request) -> web.Response:
|
|||||||
license_request = cdm.get_license_challenge(
|
license_request = cdm.get_license_challenge(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
pssh=init_data,
|
pssh=init_data,
|
||||||
license_type=license_type,
|
type_=license_type,
|
||||||
privacy_mode=privacy_mode
|
privacy_mode=privacy_mode
|
||||||
)
|
)
|
||||||
except InvalidSession:
|
except InvalidSession:
|
||||||
@ -369,7 +366,7 @@ async def get_keys(request: web.Request) -> web.Response:
|
|||||||
session_id = bytes.fromhex(body["session_id"])
|
session_id = bytes.fromhex(body["session_id"])
|
||||||
|
|
||||||
# get key type
|
# get key type
|
||||||
key_type: Optional[str] = request.match_info["key_type"]
|
key_type = request.match_info["key_type"]
|
||||||
if key_type == "ALL":
|
if key_type == "ALL":
|
||||||
key_type = None
|
key_type = None
|
||||||
|
|
||||||
@ -417,24 +414,26 @@ async def get_keys(request: web.Request) -> web.Response:
|
|||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def authentication(request: web.Request, handler: Handler) -> web.Response:
|
async def authentication(request: web.Request, handler) -> web.Response:
|
||||||
secret_key = request.headers.get("X-Secret-Key")
|
response = None
|
||||||
|
if request.path != "/":
|
||||||
|
secret_key = request.headers.get("X-Secret-Key")
|
||||||
|
if not secret_key:
|
||||||
|
request.app.logger.debug(f"{request.remote} did not provide authorization.")
|
||||||
|
response = web.json_response({
|
||||||
|
"status": "401",
|
||||||
|
"message": "Secret Key is Empty."
|
||||||
|
}, status=401)
|
||||||
|
elif secret_key not in request.app["config"]["users"]:
|
||||||
|
request.app.logger.debug(f"{request.remote} failed authentication with '{secret_key}'.")
|
||||||
|
response = web.json_response({
|
||||||
|
"status": "401",
|
||||||
|
"message": "Secret Key is Invalid, the Key is case-sensitive."
|
||||||
|
}, status=401)
|
||||||
|
|
||||||
if request.path != "/" and not secret_key:
|
if response is None:
|
||||||
request.app.logger.debug(f"{request.remote} did not provide authorization.")
|
|
||||||
response = web.json_response({
|
|
||||||
"status": "401",
|
|
||||||
"message": "Secret Key is Empty."
|
|
||||||
}, status=401)
|
|
||||||
elif request.path != "/" and secret_key not in request.app["config"]["users"]:
|
|
||||||
request.app.logger.debug(f"{request.remote} failed authentication with '{secret_key}'.")
|
|
||||||
response = web.json_response({
|
|
||||||
"status": "401",
|
|
||||||
"message": "Secret Key is Invalid, the Key is case-sensitive."
|
|
||||||
}, status=401)
|
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
response = await handler(request) # type: ignore[assignment]
|
response = await handler(request)
|
||||||
except web.HTTPException as e:
|
except web.HTTPException as e:
|
||||||
request.app.logger.error(f"An unexpected error has occurred, {e}")
|
request.app.logger.error(f"An unexpected error has occurred, {e}")
|
||||||
response = web.json_response({
|
response = web.json_response({
|
||||||
@ -443,13 +442,13 @@ async def authentication(request: web.Request, handler: Handler) -> web.Response
|
|||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
response.headers.update({
|
response.headers.update({
|
||||||
"Server": f"https://github.com/devine-dl/pywidevine serve v{__version__}"
|
"Server": f"https://github.com/rlaphoenix/pywidevine serve v{__version__}"
|
||||||
})
|
})
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def run(config: dict, host: Optional[Union[str, web.HostSequence]] = None, port: Optional[int] = None) -> None:
|
def run(config: dict, host: Optional[Union[str, web.HostSequence]] = None, port: Optional[int] = None):
|
||||||
app = web.Application(middlewares=[authentication])
|
app = web.Application(middlewares=[authentication])
|
||||||
app.on_startup.append(_startup)
|
app.on_startup.append(_startup)
|
||||||
app.on_cleanup.append(_cleanup)
|
app.on_cleanup.append(_cleanup)
|
||||||
|
|||||||
@ -13,6 +13,3 @@ class Session:
|
|||||||
self.service_certificate: Optional[SignedDrmCertificate] = None
|
self.service_certificate: Optional[SignedDrmCertificate] = None
|
||||||
self.context: dict[bytes, tuple[bytes, bytes]] = {}
|
self.context: dict[bytes, tuple[bytes, bytes]] = {}
|
||||||
self.keys: list[Key] = []
|
self.keys: list[Key] = []
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("Session",)
|
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from lxml.etree import ElementTree
|
||||||
|
|
||||||
|
|
||||||
def get_binary_path(*names: str) -> Optional[Path]:
|
def get_binary_path(*names: str) -> Optional[Path]:
|
||||||
@ -10,3 +13,23 @@ def get_binary_path(*names: str) -> Optional[Path]:
|
|||||||
if path:
|
if path:
|
||||||
return Path(path)
|
return Path(path)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_xml(xml: Union[str, bytes]) -> ElementTree:
|
||||||
|
"""Parse XML data to an ElementTree, without namespaces anywhere."""
|
||||||
|
if not isinstance(xml, bytes):
|
||||||
|
xml = xml.encode("utf8")
|
||||||
|
root = etree.fromstring(xml)
|
||||||
|
for elem in root.getiterator():
|
||||||
|
if not hasattr(elem.tag, "find"):
|
||||||
|
# e.g. comment elements
|
||||||
|
continue
|
||||||
|
elem.tag = etree.QName(elem).localname
|
||||||
|
for name, value in elem.attrib.items():
|
||||||
|
local_name = etree.QName(name).localname
|
||||||
|
if local_name == name:
|
||||||
|
continue
|
||||||
|
del elem.attrib[name]
|
||||||
|
elem.attrib[local_name] = value
|
||||||
|
etree.cleanup_namespaces(root)
|
||||||
|
return root
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
# List of Widevine Device (.wvd) file paths to use with serve.
|
# List of Widevine Device (.wvd) file paths to use with serve.
|
||||||
# Note: Each individual user needs explicit permission to use a device listed.
|
# Note: Each individual user needs explicit permission to use a device listed.
|
||||||
devices:
|
devices:
|
||||||
- 'C:\Users\devine-dl\Documents\WVDs\test_device_001.wvd'
|
- 'C:\Users\rlaphoenix\Documents\WVDs\test_device_001.wvd'
|
||||||
|
|
||||||
# List of User's by Secret Key. The Secret Key must be supplied by the User to use the API.
|
# List of User's by Secret Key. The Secret Key must be supplied by the User to use the API.
|
||||||
users:
|
users:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user