mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 03:55:05 +00:00
Renames 314 defs and 41 module/class-level constants. Keeps 11 names underscored where the stripped form collides in scope, plus 5.5.0 shims for the three symbols external service repos call.
209 lines
6.7 KiB
Python
209 lines
6.7 KiB
Python
"""CDM detection and load-time DRM stamping for local and remote CDMs.
|
|
|
|
Covers the predicates in ``unshackle.core.cdm.detect`` (playready/widevine, local/remote,
|
|
and the remote combinations) and the loader stamping that sets a remote CDM's DRM type at
|
|
load time instead of guessing it on each call. Services such as AMZN route to a device
|
|
profile off that type, so a wrong classification here is what produces a Downgrade.Hd
|
|
license denial.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from unshackle.core.cdm import loader as loader_mod
|
|
from unshackle.core.cdm.detect import (
|
|
cdm_location,
|
|
is_local_cdm,
|
|
is_playready_cdm,
|
|
is_remote_cdm,
|
|
is_remote_playready_cdm,
|
|
is_remote_widevine_cdm,
|
|
is_widevine_cdm,
|
|
)
|
|
from unshackle.core.cdm.loader import stamp_remote
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
PRD_DIR = REPO / "unshackle" / "PRDs"
|
|
|
|
|
|
def test_stamped_remote_playready():
|
|
cdm = SimpleNamespace(drm="playready", is_remote_cdm=True)
|
|
assert is_playready_cdm(cdm)
|
|
assert not is_widevine_cdm(cdm)
|
|
assert is_remote_cdm(cdm)
|
|
assert is_remote_playready_cdm(cdm)
|
|
assert not is_remote_widevine_cdm(cdm)
|
|
assert cdm_location(cdm) == "remote"
|
|
|
|
|
|
def test_stamped_remote_widevine():
|
|
cdm = SimpleNamespace(drm="widevine", is_remote_cdm=True)
|
|
assert is_widevine_cdm(cdm)
|
|
assert not is_playready_cdm(cdm)
|
|
assert is_remote_widevine_cdm(cdm)
|
|
assert not is_remote_playready_cdm(cdm)
|
|
assert cdm_location(cdm) == "remote"
|
|
|
|
|
|
def test_drm_stamp_beats_conflicting_is_playready_attr():
|
|
# The stamped drm takes priority over the wrapper's own is_playready guess.
|
|
cdm = SimpleNamespace(drm="playready", is_playready=False, is_remote_cdm=True)
|
|
assert is_playready_cdm(cdm)
|
|
assert not is_widevine_cdm(cdm)
|
|
|
|
|
|
def test_drm_stamp_case_insensitive():
|
|
cdm = SimpleNamespace(drm="PlayReady", is_remote_cdm=True)
|
|
assert is_playready_cdm(cdm)
|
|
|
|
|
|
def test_none_classifies_as_nothing():
|
|
for fn in (
|
|
is_playready_cdm,
|
|
is_widevine_cdm,
|
|
is_remote_cdm,
|
|
is_local_cdm,
|
|
is_remote_playready_cdm,
|
|
is_remote_widevine_cdm,
|
|
):
|
|
assert fn(None) is False
|
|
assert cdm_location(None) == "unknown"
|
|
|
|
|
|
@pytest.mark.parametrize("device_name", ["SL2", "SL3", "SL2000", "SL3000", "sl3000"])
|
|
def test_decryptlabs_sl_names_are_playready(device_name):
|
|
from unshackle.core.cdm.decrypt_labs_remote_cdm import DecryptLabsRemoteCDM
|
|
|
|
cdm = DecryptLabsRemoteCDM(secret="x", device_name=device_name)
|
|
assert cdm.is_playready
|
|
assert is_playready_cdm(cdm)
|
|
assert is_remote_cdm(cdm)
|
|
assert is_remote_playready_cdm(cdm)
|
|
assert not is_local_cdm(cdm)
|
|
|
|
|
|
@pytest.mark.parametrize("device_name", ["ChromeCDM", "L1", "L2"])
|
|
def test_decryptlabs_widevine_names(device_name):
|
|
from unshackle.core.cdm.decrypt_labs_remote_cdm import DecryptLabsRemoteCDM
|
|
|
|
cdm = DecryptLabsRemoteCDM(secret="x", device_name=device_name)
|
|
assert not cdm.is_playready
|
|
assert is_widevine_cdm(cdm)
|
|
assert is_remote_widevine_cdm(cdm)
|
|
|
|
|
|
def test_decryptlabs_device_type_forces_playready():
|
|
from unshackle.core.cdm.decrypt_labs_remote_cdm import DecryptLabsRemoteCDM
|
|
|
|
cdm = DecryptLabsRemoteCDM(secret="x", device_name="ChromeCDM", device_type="PLAYREADY")
|
|
assert cdm.is_playready
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("device", "expect_pr"),
|
|
[
|
|
({"name": "SL3000"}, True),
|
|
({"name": "SL2"}, True),
|
|
({"name": "ChromeCDM"}, False),
|
|
({"type": "PLAYREADY", "name": "ChromeCDM"}, True),
|
|
],
|
|
)
|
|
def test_custom_remote_cdm_classification(device, expect_pr):
|
|
from unshackle.core.cdm.custom_remote_cdm import CustomRemoteCDM
|
|
|
|
cdm = CustomRemoteCDM(host="http://example.invalid", device=device)
|
|
assert cdm.is_playready is expect_pr
|
|
assert is_playready_cdm(cdm) is expect_pr
|
|
assert is_remote_cdm(cdm)
|
|
|
|
|
|
def test_stamp_remote_sets_attrs_and_returns_same_object():
|
|
obj = SimpleNamespace()
|
|
out = stamp_remote(obj, "playready")
|
|
assert out is obj
|
|
assert obj.drm == "playready"
|
|
assert obj.is_remote_cdm is True
|
|
|
|
|
|
def test_stamp_remote_is_best_effort_on_unsettable_object():
|
|
class Slotted:
|
|
__slots__ = ()
|
|
|
|
obj = Slotted()
|
|
out = stamp_remote(obj, "widevine") # must not raise
|
|
assert out is obj
|
|
assert not hasattr(obj, "drm")
|
|
|
|
|
|
def test_load_remote_decryptlabs_playready_is_stamped():
|
|
cdm_api = {"name": "dl-pr", "type": "decrypt_labs", "secret": "x", "device_name": "SL3000"}
|
|
cdm = loader_mod.load_remote_cdm(dict(cdm_api), "dl-pr", "AMZN", None)
|
|
assert cdm.drm == "playready"
|
|
assert cdm.is_remote_cdm is True
|
|
assert is_remote_playready_cdm(cdm)
|
|
|
|
|
|
def test_load_remote_decryptlabs_widevine_is_stamped():
|
|
cdm_api = {"name": "dl-wv", "type": "decrypt_labs", "secret": "x", "device_name": "ChromeCDM"}
|
|
cdm = loader_mod.load_remote_cdm(dict(cdm_api), "dl-wv", "AMZN", None)
|
|
assert cdm.drm == "widevine"
|
|
assert is_remote_widevine_cdm(cdm)
|
|
|
|
|
|
def test_load_remote_native_playready_is_stamped(monkeypatch):
|
|
import pyplayready.remote.remotecdm as prmod
|
|
|
|
class FakePlayReadyRemote:
|
|
def __init__(self, **kwargs):
|
|
self.kwargs = kwargs
|
|
|
|
monkeypatch.setattr(prmod, "RemoteCdm", FakePlayReadyRemote)
|
|
cdm_api = {"name": "pr", "Device Type": "PLAYREADY", "host": "h", "secret": "s", "device_name": "d"}
|
|
cdm = loader_mod.load_remote_cdm(dict(cdm_api), "pr", "AMZN", None)
|
|
assert isinstance(cdm, FakePlayReadyRemote)
|
|
assert cdm.drm == "playready"
|
|
assert cdm.is_remote_cdm is True
|
|
assert is_remote_playready_cdm(cdm)
|
|
|
|
|
|
def test_load_remote_native_widevine_is_stamped(monkeypatch):
|
|
import pywidevine.remotecdm as wvmod
|
|
|
|
class FakeWidevineRemote:
|
|
def __init__(self, **kwargs):
|
|
self.kwargs = kwargs
|
|
|
|
monkeypatch.setattr(wvmod, "RemoteCdm", FakeWidevineRemote)
|
|
cdm_api = {"name": "wv", "Device Type": "ANDROID", "host": "h", "secret": "s", "device_name": "d"}
|
|
cdm = loader_mod.load_remote_cdm(dict(cdm_api), "wv", "AMZN", None)
|
|
assert isinstance(cdm, FakeWidevineRemote)
|
|
assert cdm.drm == "widevine"
|
|
assert is_remote_widevine_cdm(cdm)
|
|
|
|
|
|
@pytest.fixture
|
|
def local_playready_cdm():
|
|
prds = sorted(PRD_DIR.glob("*.prd"))
|
|
if not prds:
|
|
pytest.skip(f"no local .prd present in {PRD_DIR}")
|
|
from pyplayready.cdm import Cdm
|
|
from pyplayready.device import Device
|
|
|
|
return Cdm.from_device(Device.load(str(prds[0])))
|
|
|
|
|
|
def test_local_playready_is_playready_and_local(local_playready_cdm):
|
|
cdm = local_playready_cdm
|
|
assert is_playready_cdm(cdm)
|
|
assert not is_widevine_cdm(cdm)
|
|
assert is_local_cdm(cdm)
|
|
assert not is_remote_cdm(cdm)
|
|
assert not is_remote_playready_cdm(cdm)
|
|
assert cdm_location(cdm) == "local"
|