mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-17 06:32:30 +02:00
Add discovery
This commit is contained in:
@@ -15,20 +15,49 @@ from .exceptions import LicenseConfigError
|
||||
@dataclass
|
||||
class DRMConfig:
|
||||
"""
|
||||
Complete DRM Configuration.
|
||||
Complete DRM Configuration for a single DRM system.
|
||||
|
||||
Combines DRM system identification with license configuration
|
||||
for a single DRM system.
|
||||
Produces output compatible with the inputstream.adaptive.drm JSON property
|
||||
(Kodi 22+). The to_dict() output can be merged with other DRMConfig dicts
|
||||
and passed directly to json.dumps() — no further transformation needed.
|
||||
|
||||
Top-level ISA parameters (outside "license"):
|
||||
|
||||
Attributes:
|
||||
system: DRM system type
|
||||
priority: Priority for multi-DRM scenarios (higher = preferred)
|
||||
license: License server configuration (optional for unencrypted)
|
||||
system: DRM system type.
|
||||
priority: DRM priority in multi-DRM scenarios. Lower number = higher
|
||||
priority. Must be >= 1; 0 is invalid per ISA spec. Two DRMs must
|
||||
not share the same priority value.
|
||||
license: License server configuration.
|
||||
init_data: Custom initialization data (PSSH box) encoded as base64.
|
||||
Replaces any PSSH provided by the manifest. For Widevine, also
|
||||
accepts raw Widevine PSSH data with optional placeholders:
|
||||
{KID} (KID as bytes), {UUID} (KID as UUID string) — both must
|
||||
be encoded as base64 together with the surrounding data.
|
||||
pre_init_data: Widevine only. Pre-initialize a DRM session for
|
||||
licensed manifests. Format: "PSSH_base64|KID_base64".
|
||||
Requires priority=1 and a proxy server in the add-on.
|
||||
persistent_storage: Enable CDM persistent state (store session data
|
||||
locally). Only enable if the streaming service requires it.
|
||||
secure_decoder: Force-enable (True) or force-disable (False) the
|
||||
secure decoder, overriding the ISA add-on user setting.
|
||||
Omit (None) to leave the user setting untouched.
|
||||
force_single_session: Force a single DRM session for all tracks.
|
||||
Saves license round-trips but may cause playback issues if the
|
||||
backend does not return all keys in one response.
|
||||
optional_key_req_params: CDM-specific key request parameters.
|
||||
PlayReady: {"custom_data": "..."} sets PRCustomData.
|
||||
"""
|
||||
|
||||
system: DRMSystem
|
||||
priority: int = 0
|
||||
priority: int = 1
|
||||
license: Optional[LicenseConfig] = None
|
||||
init_data: Optional[str] = None
|
||||
pre_init_data: Optional[str] = None
|
||||
persistent_storage: Optional[bool] = None
|
||||
secure_decoder: Optional[bool] = None
|
||||
force_single_session: Optional[bool] = None
|
||||
optional_key_req_params: Optional[dict] = None
|
||||
|
||||
def validate(self) -> None:
|
||||
"""
|
||||
@@ -42,34 +71,64 @@ class DRMConfig:
|
||||
f"system must be a DRMSystem enum, got {type(self.system)}"
|
||||
)
|
||||
|
||||
# priority=0 is explicitly invalid per ISA spec
|
||||
if self.priority == 0:
|
||||
raise LicenseConfigError(
|
||||
"priority=0 is invalid per ISA spec. Use priority >= 1 "
|
||||
"(lower number = higher priority)."
|
||||
)
|
||||
|
||||
if self.pre_init_data and self.priority != 1:
|
||||
raise LicenseConfigError(
|
||||
"pre_init_data requires priority=1 per ISA spec."
|
||||
)
|
||||
|
||||
if self.license:
|
||||
self.license.validate()
|
||||
|
||||
def to_dict(self) -> dict[str, dict]:
|
||||
"""
|
||||
Convert to dictionary format expected by players.
|
||||
Convert to the dictionary format expected by inputstream.adaptive.drm.
|
||||
|
||||
Returns:
|
||||
Dictionary with DRM configuration in player-compatible format:
|
||||
{
|
||||
"com.widevine.alpha": {
|
||||
"priority": 1,
|
||||
"license": {...}
|
||||
"license": { ... }, # if set
|
||||
"init_data": "...", # if set
|
||||
"pre_init_data": "...", # if set
|
||||
"persistent_storage": True, # if set
|
||||
"secure_decoder": False, # if set
|
||||
"force_single_session": True, # if set
|
||||
"optional_key_req_params": {...} # if set
|
||||
}
|
||||
}
|
||||
"""
|
||||
result: dict[str, dict] = {
|
||||
self.system.value: {
|
||||
"priority": self.priority
|
||||
}
|
||||
}
|
||||
cfg: dict = {"priority": self.priority}
|
||||
|
||||
if self.license:
|
||||
license_dict = self.license.to_dict()
|
||||
if license_dict:
|
||||
result[self.system.value]["license"] = license_dict
|
||||
cfg["license"] = license_dict
|
||||
|
||||
return result
|
||||
if self.init_data is not None:
|
||||
cfg["init_data"] = self.init_data
|
||||
if self.pre_init_data is not None:
|
||||
cfg["pre_init_data"] = self.pre_init_data
|
||||
if self.persistent_storage is not None:
|
||||
cfg["persistent_storage"] = self.persistent_storage
|
||||
if self.secure_decoder is not None:
|
||||
cfg["secure_decoder"] = self.secure_decoder
|
||||
if self.force_single_session is not None:
|
||||
cfg["force_single_session"] = self.force_single_session
|
||||
if self.optional_key_req_params:
|
||||
cfg["optional_key_req_params"] = self.optional_key_req_params
|
||||
|
||||
return {self.system.value: cfg}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Factory helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def create_widevine(
|
||||
@@ -79,15 +138,13 @@ class DRMConfig:
|
||||
**license_kwargs
|
||||
) -> "DRMConfig":
|
||||
"""
|
||||
Helper to create Widevine DRM configuration.
|
||||
Create a Widevine DRM configuration.
|
||||
|
||||
Args:
|
||||
server_url: Widevine license server URL
|
||||
priority: Priority (default: 1)
|
||||
**license_kwargs: Additional LicenseConfig parameters
|
||||
|
||||
Returns:
|
||||
DRMConfig instance for Widevine
|
||||
server_url: Widevine license server URL. Supports {CHA-B64U},
|
||||
{CHA-MD5} placeholders to inject the challenge in the URL.
|
||||
priority: Priority (default: 1).
|
||||
**license_kwargs: Additional LicenseConfig parameters.
|
||||
"""
|
||||
license_config = LicenseConfig(server_url=server_url, **license_kwargs)
|
||||
return cls(system=DRMSystem.WIDEVINE, priority=priority, license=license_config)
|
||||
@@ -100,15 +157,12 @@ class DRMConfig:
|
||||
**license_kwargs
|
||||
) -> "DRMConfig":
|
||||
"""
|
||||
Helper to create PlayReady DRM configuration.
|
||||
Create a PlayReady DRM configuration.
|
||||
|
||||
Args:
|
||||
server_url: PlayReady license server URL
|
||||
priority: Priority (default: 1)
|
||||
**license_kwargs: Additional LicenseConfig parameters
|
||||
|
||||
Returns:
|
||||
DRMConfig instance for PlayReady
|
||||
server_url: PlayReady license server URL.
|
||||
priority: Priority (default: 1).
|
||||
**license_kwargs: Additional LicenseConfig parameters.
|
||||
"""
|
||||
license_config = LicenseConfig(server_url=server_url, **license_kwargs)
|
||||
return cls(system=DRMSystem.PLAYREADY, priority=priority, license=license_config)
|
||||
@@ -117,19 +171,16 @@ class DRMConfig:
|
||||
def create_clearkey(
|
||||
cls,
|
||||
keyids: dict[str, str],
|
||||
priority: int = 0,
|
||||
priority: int = 1,
|
||||
**license_kwargs
|
||||
) -> "DRMConfig":
|
||||
"""
|
||||
Helper to create ClearKey DRM configuration.
|
||||
Create a ClearKey DRM configuration.
|
||||
|
||||
Args:
|
||||
keyids: Mapping of Key IDs to Keys (hex strings)
|
||||
priority: Priority (default: 0)
|
||||
**license_kwargs: Additional LicenseConfig parameters
|
||||
|
||||
Returns:
|
||||
DRMConfig instance for ClearKey
|
||||
keyids: Mapping of Key IDs to Keys (hex strings, 32 chars each).
|
||||
priority: Priority (default: 1).
|
||||
**license_kwargs: Additional LicenseConfig parameters.
|
||||
"""
|
||||
license_config = LicenseConfig(keyids=keyids, **license_kwargs)
|
||||
return cls(system=DRMSystem.CLEARKEY, priority=priority, license=license_config)
|
||||
@@ -143,16 +194,13 @@ class DRMConfig:
|
||||
**license_kwargs
|
||||
) -> "DRMConfig":
|
||||
"""
|
||||
Helper to create FairPlay DRM configuration.
|
||||
Create a FairPlay DRM configuration.
|
||||
|
||||
Args:
|
||||
server_url: FairPlay license server URL (skd://)
|
||||
server_certificate: Base64-encoded FairPlay certificate
|
||||
priority: Priority (default: 1)
|
||||
**license_kwargs: Additional LicenseConfig parameters
|
||||
|
||||
Returns:
|
||||
DRMConfig instance for FairPlay
|
||||
server_url: FairPlay license server URL (skd://).
|
||||
server_certificate: Base64-encoded FairPlay certificate.
|
||||
priority: Priority (default: 1).
|
||||
**license_kwargs: Additional LicenseConfig parameters.
|
||||
"""
|
||||
license_config = LicenseConfig(
|
||||
server_url=server_url,
|
||||
@@ -162,7 +210,6 @@ class DRMConfig:
|
||||
return cls(system=DRMSystem.FAIRPLAY, priority=priority, license=license_config)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return detailed representation."""
|
||||
has_license = "with license" if self.license else "no license"
|
||||
return (
|
||||
f"<DRMConfig({self.system.name}, priority={self.priority}, {has_license})>"
|
||||
|
||||
@@ -38,7 +38,7 @@ class LicenseUnwrapperParams:
|
||||
non-standard license server responses.
|
||||
|
||||
Attributes:
|
||||
path_data: JSON/XML path to license data (e.g., "license.data")
|
||||
path_data: JSON/XML path to license data (e.g., "licenseresponse/data")
|
||||
path_data_traverse: Whether to traverse nested structures for data
|
||||
path_hdcp_res: JSON/XML path to HDCP resolution restriction
|
||||
path_hdcp_res_traverse: Whether to traverse for HDCP resolution
|
||||
@@ -77,17 +77,37 @@ class LicenseConfig:
|
||||
including server URLs, certificates, request customization, and
|
||||
response processing.
|
||||
|
||||
All fields map directly to ISA (inputstream.adaptive) license parameters.
|
||||
The to_dict() output can be used as-is inside the inputstream.adaptive.drm
|
||||
JSON property — no further transformation is required.
|
||||
|
||||
Attributes:
|
||||
server_url: License server URL
|
||||
server_certificate: Base64-encoded server certificate (for FairPlay, etc.)
|
||||
use_http_get_request: Use GET instead of POST for license requests
|
||||
req_headers: Custom HTTP headers as JSON string
|
||||
req_params: URL query parameters as JSON string
|
||||
req_data: Base64-encoded custom request body data
|
||||
wrapper: Request body wrapper type
|
||||
unwrapper: Response unwrapper type
|
||||
unwrapper_params: Parameters for response unwrapping
|
||||
keyids: ClearKey key mappings (KID -> Key in hex)
|
||||
server_url: License server URL. For Widevine, supports placeholders
|
||||
to inject the DRM challenge: {CHA-B64U}, {CHA-MD5}.
|
||||
For ClearKey, also accepts a URI "data" scheme:
|
||||
"data:application/json;base64,<base64>"
|
||||
server_certificate: Base64-encoded server certificate
|
||||
(Widevine, FairPlay).
|
||||
use_http_get_request: Force HTTP GET for the license request instead
|
||||
of the default POST (Widevine, PlayReady, Wiseplay only).
|
||||
req_headers: Custom HTTP headers as a URL-encoded string.
|
||||
Format: "Header1=Value1&Header2=Value2" where values are
|
||||
URL-encoded (use urllib.parse.urlencode() or quote_plus()).
|
||||
Example: "Content-Type=application%2Foctet-stream&User-Agent=Mozilla%2F5.0"
|
||||
req_params: Path extension or parameters appended to the license URL.
|
||||
Example: "/one/two/three-path"
|
||||
req_data: Base64-encoded custom request body template.
|
||||
Supports ISA placeholders: {CHA-RAW}, {CHA-B64}, {CHA-B64U},
|
||||
{CHA-DEC}, {SID-RAW}, {SID-B64}, {SID-B64U}, {KID-UUID},
|
||||
{KID-HEX}, {PSSH-B64}, {PSSH-B64U}.
|
||||
wrapper: Request body wrapper flags, comma-separated:
|
||||
"base64" | "urlenc" | "none"
|
||||
unwrapper: Response unwrapper flags, comma-separated:
|
||||
"auto" | "base64" | "json" | "xml" | "none"
|
||||
unwrapper_params: Parameters for JSON/XML response unwrapping
|
||||
(required when unwrapper includes "json" or "xml").
|
||||
keyids: ClearKey only. Map of KID -> Key pairs in hex format
|
||||
(32 hex chars each).
|
||||
"""
|
||||
|
||||
server_url: Optional[str] = None
|
||||
@@ -104,7 +124,6 @@ class LicenseConfig:
|
||||
def __post_init__(self):
|
||||
"""Normalize key IDs in keyids mapping."""
|
||||
if self.keyids:
|
||||
# Normalize all KIDs and keys to lowercase hex
|
||||
normalized_keyids = {}
|
||||
for kid, key in self.keyids.items():
|
||||
try:
|
||||
@@ -112,7 +131,6 @@ class LicenseConfig:
|
||||
norm_key = key.lower().replace("-", "")
|
||||
normalized_keyids[norm_kid] = norm_key
|
||||
except Exception:
|
||||
# Skip invalid entries
|
||||
pass
|
||||
self.keyids = normalized_keyids
|
||||
|
||||
@@ -123,7 +141,6 @@ class LicenseConfig:
|
||||
Raises:
|
||||
LicenseConfigError: If configuration is invalid
|
||||
"""
|
||||
# Validate server_certificate if present
|
||||
if self.server_certificate:
|
||||
try:
|
||||
safe_base64_decode(self.server_certificate)
|
||||
@@ -132,7 +149,6 @@ class LicenseConfig:
|
||||
f"server_certificate must be valid base64: {e}"
|
||||
) from e
|
||||
|
||||
# Validate req_data if present
|
||||
if self.req_data:
|
||||
try:
|
||||
safe_base64_decode(self.req_data)
|
||||
@@ -141,7 +157,14 @@ class LicenseConfig:
|
||||
f"req_data must be valid base64: {e}"
|
||||
) from e
|
||||
|
||||
# Validate keyids format (for ClearKey)
|
||||
# req_headers must be a URL-encoded string, NOT a JSON dict
|
||||
if self.req_headers and self.req_headers.strip().startswith("{"):
|
||||
raise LicenseConfigError(
|
||||
"req_headers must be a URL-encoded string "
|
||||
"(e.g. 'Content-Type=application%2Foctet-stream'), not a JSON dict. "
|
||||
"Use urllib.parse.urlencode() to encode headers."
|
||||
)
|
||||
|
||||
if self.keyids:
|
||||
for kid, key in self.keyids.items():
|
||||
if len(kid) != 32:
|
||||
@@ -165,7 +188,8 @@ class LicenseConfig:
|
||||
Helper to create LicenseConfig with base64-encoded req_data.
|
||||
|
||||
Args:
|
||||
req_data_template: Plain text request data template
|
||||
req_data_template: Plain text request data template (may contain
|
||||
ISA placeholders like {CHA-B64}, {SID-RAW}, {KID-HEX}, etc.)
|
||||
**kwargs: Other LicenseConfig parameters
|
||||
|
||||
Returns:
|
||||
@@ -187,10 +211,13 @@ class LicenseConfig:
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
Convert to dictionary, excluding None/empty values.
|
||||
Convert to dictionary ready for the inputstream.adaptive.drm JSON payload.
|
||||
|
||||
Output maps directly to ISA license parameters — no further
|
||||
transformation is needed before json.dumps().
|
||||
|
||||
Returns:
|
||||
Dictionary representation
|
||||
Dictionary with only non-empty/non-False values included.
|
||||
"""
|
||||
result = {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user