Compare commits

..

14 Commits

Author SHA1 Message Date
rlaphoenix
4298db7546 Improve Installation/Usage in README, update badges for uv 2025-10-27 13:20:50 +00:00
rlaphoenix
0f2f34c83f Migrate from poetry to uv/hatchling 2025-10-27 13:20:50 +00:00
rlaphoenix
b30171302f Bump to v1.9.0 2025-10-27 09:11:08 +00:00
rlaphoenix
8c310ad5e3 Remove DeepSource config & badge 2025-10-27 09:03:33 +00:00
rlaphoenix
d58db89baf Use official config repos for ruff and mypy, fix poetry dependency resolution 2025-10-27 08:58:15 +00:00
rlaphoenix
8cd623a217 Update pre-commit config 2025-10-27 08:35:08 +00:00
rlaphoenix
72f84249bd Update packages overall to get latest wheels for 3.12 and newer 2025-10-27 08:28:11 +00:00
rlaphoenix
b624355764 Update versions in GitHub Workflows
(I bloody hate github workflows, annoying ahh hell to maintain)
2025-10-27 08:20:36 +00:00
rlaphoenix
c32dd6a757 Drop support for 3.8 from readme badge, update copyright year 2025-10-27 08:14:56 +00:00
rlaphoenix
e75bde1de2 Update dev dependencies, support 3.14 + its been a while
Note that isort isn't the latest version as support for 3.9 was dropped. I feel like it's a tad bit early and a bit sudden to drop support for both 3.8 and 3.9 in the span of a few commits so I will stick to 6.1.0 for now.
2025-10-27 08:13:56 +00:00
rlaphoenix
1bb0b246f4 Updated requests, PyYAML, & aiohttp for full Python 3.14 support 2025-10-27 08:07:18 +00:00
rlaphoenix
9ec88ef1a8 Update pycryptodome package to take advantage of Windows ARM wheels 2025-10-27 07:54:33 +00:00
rlaphoenix
7c2caa2e7e
Merge pull request #56 from UltimaHoarder/master
Support for Python 3.14
2025-10-27 07:48:29 +00:00
UltimaHoarder
fe033c9647 Update Python minimum to 3.9 and protobuf to ^6.33.0 for Python 3.14 2025-10-27 03:04:05 +00:00
10 changed files with 1700 additions and 1247 deletions

View File

@ -1,17 +0,0 @@
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

View File

@ -9,33 +9,25 @@ jobs:
tagged-release:
name: Tagged Release
runs-on: ubuntu-latest
environment:
name: pypi
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install Poetry
uses: abatilo/actions-poetry@v2
python-version: "3.14"
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
poetry-version: 1.6.1
- name: Install project
run: poetry install --only main
version: "0.9.5"
enable-cache: true
- name: Install the project
run: uv sync --locked
- name: Build project
run: poetry build
- name: Upload wheel
uses: actions/upload-artifact@v3
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
run: uv build
- name: Publish to PyPI
env:
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
run: poetry publish
run: uv publish

View File

@ -10,35 +10,37 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install poetry
uses: abatilo/actions-poetry@v2
python-version: "3.14"
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
poetry-version: 1.6.1
- name: Install project
run: poetry install --all-extras
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: poetry run pre-commit run --all-files --show-diff-on-failure
run: uv run pre-commit run --all-files --show-diff-on-failure
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install poetry
uses: abatilo/actions-poetry@v2
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
poetry-version: 1.6.1
- name: Install project
run: poetry install --all-extras --only main
version: "0.9.5"
enable-cache: true
- name: Install the project
run: uv sync --locked --all-extras
- name: Build project
run: poetry build
run: uv build

View File

@ -3,17 +3,27 @@
exclude: '_pb2.pyi?$'
repos:
- repo: https://github.com/mtkennerly/pre-commit-hooks
rev: v0.3.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.2
hooks:
- id: poetry-ruff
- id: poetry-mypy
- 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: 5.11.5
rev: 6.1.0
hooks:
- id: isort
language: system
types: [ python ]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v6.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace

View File

@ -5,6 +5,24 @@ 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/),
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

136
README.md
View File

@ -5,22 +5,20 @@
</p>
<p align="center">
<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 href="https://github.com/devine-dl/pywidevine/blob/master/LICENSE">
<img src="https://img.shields.io/:license-GPL%203.0-blue.svg" alt="License">
</a>
<a href="https://pypi.org/project/pywidevine">
<img src="https://img.shields.io/badge/python-3.8%2B-informational" alt="Python version">
<img src="https://img.shields.io/badge/python-3.9%2B-informational" alt="Python version">
</a>
<a href="https://deepsource.io/gh/devine-dl/pywidevine">
<img src="https://deepsource.io/gh/devine-dl/pywidevine.svg/?label=active+issues" alt="DeepSource">
<a href="https://github.com/astral-sh/uv">
<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">
</a>
</p>
<p align="center">
<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://python-poetry.org">
<img src="https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json" alt="Dependency management: Poetry">
<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>
</p>
@ -37,22 +35,31 @@
## Installation
```shell
$ pip install pywidevine
```
### With pip
> **Note**
If pip gives you a warning about a path not being in your PATH environment variable then promptly add that path then
close all open command prompt/terminal windows, or `pywidevine` CLI won't work as it will not be found.
> Since *pip* is pre-installed with Python, it is the most straight forward way to install pywidevine.
Voilà 🎉 — You now have the `pywidevine` package installed!
You can now import pywidevine in scripts ([see below](#usage)).
A command-line interface is also available, try `pywidevine --help`.
Simply run `pip install pywidevine` and it will be ready to use from the CLI or within scripts in a minute.
### With uv
> This is recommended for those who wish to install from the source code, are working on changes in the source code, or
just simply prefer it's many handy features.
Go to to the official website and [get uv installed](https://docs.astral.sh/uv/getting-started/installation/). Download
or clone this repository, go inside it, and run `uv run pywidevine --version`. To run scripts, like a `license.py` that
is importing pywidevine, do `uv run license.py`. Effectively, put `uv run` before calling whatever is using pywidevine.
For other ways to run pywidevine with uv, see [Running commands](https://docs.astral.sh/uv/guides/projects/#running-commands).
## Usage
The following is a minimal example of using pywidevine in a script to get a License for Bitmovin's
Art of Motion Demo.
There are two ways to use pywidevine, through scripts, or the CLI (command-line interface).
Most people would be using it through scripts due to complexities working with license server APIs.
### Scripts
The following is a minimal example of using pywidevine in a script to get a License for Bitmovin's Art of Motion Demo.
This demo can be found on [Bitmovin's DRM Stream Test demo page](https://bitmovin.com/demos/drm/).
```py
from pywidevine.cdm import Cdm
@ -61,50 +68,93 @@ from pywidevine.pssh import PSSH
import requests
# prepare pssh
# prepare pssh (usually inside the MPD/M3U8, an API response, the player page, or inside the pssh mp4 box)
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa"
"7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
# load device
# load device from a WVD file (your provision)
device = Device.load("C:/Path/To/A/Provision.wvd")
# load cdm
# load cdm (creating a CDM instance using that device)
cdm = Cdm.from_device(device)
# open cdm session
# open cdm session (note that any one device should have a practical limit to amount of sessions open at any one time)
session_id = cdm.open()
# get license challenge
# get license challenge (generate a license request message, signed using the device with the pssh)
challenge = cdm.get_license_challenge(session_id, pssh)
# send license challenge (assuming a generic license server SDK with no API front)
licence = requests.post("https://...", data=challenge)
# send license challenge to bitmovin's license server (which has no auth and asks simply for the license challenge as-is)
# another license server may require authentication and ask for it as JSON or form data instead
# 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()
# parse license challenge
# parse the license response message received from the license server API
cdm.parse_license(session_id, licence.content)
# print keys
for key in cdm.get_keys(session_id):
print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")
# close session, disposes of session data
# finished, close the session, disposing of all keys and other related data
cdm.close(session_id)
```
> **Note**
> There are various features not shown in this specific example like:
>
> - Privacy Mode
> - Setting Service Certificates
> - Remote CDMs and Serving
> - Choosing a License Type to request
> - Creating WVD files
> - and much more!
>
> Take a look at the methods available in the [Cdm class](/pywidevine/cdm.py) and their doc-strings for
> further information. For more examples see the [CLI functions](/pywidevine/main.py) which uses a lot
> of previously mentioned features.
There are other features not shown in this small example like:
- Privacy Mode
- Setting Service Certificates
- Remote CDMs and Serving
- Choosing a License Type
- Creating WVD files
- and much more!
> [!TIP]
> 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
The CLI can be useful to do simple license calls, migrate WVD files, and test provisions.
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
@ -163,4 +213,4 @@ You can find a copy of the license in the LICENSE file in the root folder.
* * *
© rlaphoenix 2022-2023
© rlaphoenix 2022-2025

1051
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,87 +1,97 @@
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "pywidevine"
version = "1.8.0"
description = "Widevine CDM (Content Decryption Module) implementation in Python."
license = "GPL-3.0-only"
authors = ["rlaphoenix <rlaphoenix@pm.me>"]
readme = "README.md"
repository = "https://github.com/devine-dl/pywidevine"
keywords = ["python", "drm", "widevine", "google"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
"Natural Language :: English",
"Operating System :: OS Independent",
"Topic :: Multimedia :: Video",
"Topic :: Security :: Cryptography",
"Topic :: Software Development :: Libraries :: Python Modules"
]
include = [
{ path = "CHANGELOG.md", format = "sdist" },
{ path = "README.md", format = "sdist" },
{ path = "LICENSE", format = "sdist" },
]
[tool.poetry.urls]
"Issues" = "https://github.com/devine-dl/pywidevine/issues"
"Discussions" = "https://github.com/devine-dl/pywidevine/discussions"
"Changelog" = "https://github.com/devine-dl/pywidevine/blob/master/CHANGELOG.md"
[tool.poetry.dependencies]
python = ">=3.8,<4.0"
protobuf = "^4.25.1"
pymp4 = "^1.4.0"
pycryptodome = "^3.19.0"
click = "^8.1.7"
requests = "^2.31.0"
Unidecode = "^1.3.7"
PyYAML = "^6.0.1"
aiohttp = {version = "^3.9.1", optional = true}
[tool.poetry.group.dev.dependencies]
pre-commit = "^3.5.0"
mypy = "^1.7.1"
mypy-protobuf = "^3.5.0"
types-protobuf = "^4.24.0.4"
types-requests = "^2.31.0.10"
types-PyYAML = "^6.0.12.12"
isort = "^5.12.0"
ruff = "~0.1.7"
[tool.poetry.extras]
serve = ["aiohttp"]
[tool.poetry.scripts]
pywidevine = "pywidevine.main:main"
[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
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "pywidevine"
version = "1.9.0"
description = "Widevine CDM (Content Decryption Module) implementation in Python."
authors = [{ name = "rlaphoenix", email = "rlaphoenix@pm.me" }]
requires-python = ">=3.9"
readme = "README.md"
license = "GPL-3.0-only"
keywords = [
"python",
"drm",
"widevine",
"google",
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
"Natural Language :: English",
"Operating System :: OS Independent",
"Topic :: Multimedia :: Video",
"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]
serve = ["aiohttp~=3.13.1"]
[project.urls]
Repository = "https://github.com/devine-dl/pywidevine"
Issues = "https://github.com/devine-dl/pywidevine/issues"
Discussions = "https://github.com/devine-dl/pywidevine/discussions"
Changelog = "https://github.com/devine-dl/pywidevine/blob/master/CHANGELOG.md"
[project.scripts]
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

View File

@ -5,4 +5,4 @@ from .pssh import *
from .remotecdm import *
from .session import *
__version__ = "1.8.0"
__version__ = "1.9.0"

1439
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff