Compare commits

...

2 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
6 changed files with 1608 additions and 1730 deletions

View File

@ -9,33 +9,25 @@ 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@v5
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: "3.14" python-version: "3.14"
- name: Install Poetry - name: Install uv
uses: abatilo/actions-poetry@v3 uses: astral-sh/setup-uv@v6
with: with:
poetry-version: 2.1.3 version: "0.9.5"
- name: Install project enable-cache: true
run: poetry install --only main - name: Install the project
run: uv sync --locked
- name: Build project - name: Build project
run: poetry build run: uv build
- name: Upload wheel
uses: actions/upload-artifact@v4
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
env: run: uv publish
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
run: poetry publish

View File

@ -15,14 +15,15 @@ jobs:
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: "3.14" python-version: "3.14"
- name: Install poetry - name: Install uv
uses: abatilo/actions-poetry@v3 uses: astral-sh/setup-uv@v6
with: with:
poetry-version: 2.1.3 version: "0.9.5"
- name: Install project enable-cache: true
run: poetry install --all-extras - name: Install the project
run: uv sync --locked --all-extras --dev
- name: Run pre-commit which does various checks - 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: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@ -34,11 +35,12 @@ jobs:
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install poetry - name: Install uv
uses: abatilo/actions-poetry@v3 uses: astral-sh/setup-uv@v6
with: with:
poetry-version: 2.1.3 version: "0.9.5"
- name: Install project enable-cache: true
run: poetry install --all-extras --only main - name: Install the project
run: uv sync --locked --all-extras
- name: Build project - name: Build project
run: poetry build run: uv build

129
README.md
View File

@ -5,17 +5,20 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/devine-dl/pywidevine/actions/workflows/ci.yml"> <a href="https://github.com/devine-dl/pywidevine/blob/master/LICENSE">
<img src="https://github.com/devine-dl/pywidevine/actions/workflows/ci.yml/badge.svg" alt="Build status"> <img src="https://img.shields.io/:license-GPL%203.0-blue.svg" alt="License">
</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.9%2B-informational" alt="Python version">
</a> </a>
<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>
<a href="https://github.com/astral-sh/ruff"> <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"> <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>
<a href="https://python-poetry.org"> <a href="https://github.com/devine-dl/pywidevine/actions/workflows/ci.yml">
<img src="https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json" alt="Dependency management: Poetry"> <img src="https://github.com/devine-dl/pywidevine/actions/workflows/ci.yml/badge.svg" alt="Build status">
</a> </a>
</p> </p>
@ -32,22 +35,31 @@
## Installation ## Installation
```shell ### With pip
$ pip install pywidevine
```
> **Note** > Since *pip* is pre-installed with Python, it is the most straight forward way to install pywidevine.
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.
Voilà 🎉 — You now have the `pywidevine` package installed! Simply run `pip install pywidevine` and it will be ready to use from the CLI or within scripts in a minute.
You can now import pywidevine in scripts ([see below](#usage)).
A command-line interface is also available, try `pywidevine --help`. ### 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 ## Usage
The following is a minimal example of using pywidevine in a script to get a License for Bitmovin's There are two ways to use pywidevine, through scripts, or the CLI (command-line interface).
Art of Motion Demo. 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 ```py
from pywidevine.cdm import Cdm from pywidevine.cdm import Cdm
@ -56,50 +68,93 @@ from pywidevine.pssh import PSSH
import requests 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" pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa"
"7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==") "7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
# load device # load device from a WVD file (your provision)
device = Device.load("C:/Path/To/A/Provision.wvd") 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) 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() 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) challenge = cdm.get_license_challenge(session_id, pssh)
# send license challenge (assuming a generic license server SDK with no API front) # send license challenge to bitmovin's license server (which has no auth and asks simply for the license challenge as-is)
licence = requests.post("https://...", data=challenge) # 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() 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) 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()}")
# close session, disposes of session data # finished, close the session, disposing of all keys and other related data
cdm.close(session_id) cdm.close(session_id)
``` ```
> **Note** There are other features not shown in this small example like:
> There are various features not shown in this specific example like:
> - Privacy Mode
> - Privacy Mode - Setting Service Certificates
> - Setting Service Certificates - Remote CDMs and Serving
> - Remote CDMs and Serving - Choosing a License Type
> - Choosing a License Type to request - Creating WVD files
> - Creating WVD files - and much more!
> - and much more!
> > [!TIP]
> Take a look at the methods available in the [Cdm class](/pywidevine/cdm.py) and their doc-strings for > For examples, take a look at the methods available in the [Cdm class](/pywidevine/cdm.py) and read their doc-strings
> further information. For more examples see the [CLI functions](/pywidevine/main.py) which uses a lot > for further information.
> of previously mentioned features.
### 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 ## Disclaimer

1620
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,21 @@
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["hatchling"]
build-backend = "poetry.core.masonry.api" build-backend = "hatchling.build"
[tool.poetry] [project]
name = "pywidevine" name = "pywidevine"
version = "1.9.0" version = "1.9.0"
description = "Widevine CDM (Content Decryption Module) implementation in Python." description = "Widevine CDM (Content Decryption Module) implementation in Python."
license = "GPL-3.0-only" authors = [{ name = "rlaphoenix", email = "rlaphoenix@pm.me" }]
authors = ["rlaphoenix <rlaphoenix@pm.me>"] requires-python = ">=3.9"
readme = "README.md" readme = "README.md"
repository = "https://github.com/devine-dl/pywidevine" license = "GPL-3.0-only"
keywords = ["python", "drm", "widevine", "google"] keywords = [
"python",
"drm",
"widevine",
"google",
]
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers", "Intended Audience :: Developers",
@ -19,46 +24,51 @@ classifiers = [
"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" "Topic :: Software Development :: Libraries :: Python Modules",
] ]
include = [ dependencies = [
{ path = "CHANGELOG.md", format = "sdist" }, "protobuf~=6.33.0",
{ path = "README.md", format = "sdist" }, "pymp4~=1.4.0",
{ path = "LICENSE", format = "sdist" }, "pycryptodome~=3.23.0",
"click~=8.1.7",
"requests~=2.32.5",
"Unidecode~=1.3.7",
"PyYAML~=6.0.3",
] ]
[tool.poetry.urls] [project.optional-dependencies]
"Issues" = "https://github.com/devine-dl/pywidevine/issues" serve = ["aiohttp~=3.13.1"]
"Discussions" = "https://github.com/devine-dl/pywidevine/discussions"
"Changelog" = "https://github.com/devine-dl/pywidevine/blob/master/CHANGELOG.md"
[tool.poetry.dependencies] [project.urls]
python = ">=3.9,<4.0" Repository = "https://github.com/devine-dl/pywidevine"
protobuf = "^6.33.0" Issues = "https://github.com/devine-dl/pywidevine/issues"
pymp4 = "^1.4.0" Discussions = "https://github.com/devine-dl/pywidevine/discussions"
pycryptodome = "^3.23.0" Changelog = "https://github.com/devine-dl/pywidevine/blob/master/CHANGELOG.md"
click = "^8.1.7"
requests = "^2.32.5"
Unidecode = "^1.3.7"
PyYAML = "^6.0.3"
aiohttp = {version = "^3.13.1", optional = true}
[tool.poetry.group.dev.dependencies] [project.scripts]
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.poetry.extras]
serve = ["aiohttp"]
[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] [tool.ruff]
extend-exclude = [ extend-exclude = [
"*_pb2.py", "*_pb2.py",

1439
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff