mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-14 04:55:07 +00:00
feat(remote): server vault lookups, service CDM mapping, key display, and service param forwarding
This commit is contained in:
@@ -2656,6 +2656,11 @@ class dl:
|
||||
|
||||
for kid in all_kids:
|
||||
if kid in drm.content_keys:
|
||||
is_track_kid = ["", "*"][kid == track_kid]
|
||||
key = drm.content_keys[kid]
|
||||
label = f"[text2]{kid.hex}:{key}{is_track_kid}"
|
||||
if not any(f"{kid.hex}:{key}" in x.label for x in cek_tree.children):
|
||||
cek_tree.add(label)
|
||||
continue
|
||||
|
||||
is_track_kid = ["", "*"][kid == track_kid]
|
||||
@@ -2846,6 +2851,11 @@ class dl:
|
||||
|
||||
for kid in all_kids:
|
||||
if kid in drm.content_keys:
|
||||
is_track_kid = ["", "*"][kid == track_kid]
|
||||
key = drm.content_keys[kid]
|
||||
label = f"[text2]{kid.hex}:{key}{is_track_kid}"
|
||||
if not any(f"{kid.hex}:{key}" in x.label for x in cek_tree.children):
|
||||
cek_tree.add(label)
|
||||
continue
|
||||
|
||||
is_track_kid = ["", "*"][kid == track_kid]
|
||||
|
||||
@@ -1241,23 +1241,15 @@ def _create_service_instance(
|
||||
|
||||
service_kwargs: Dict[str, Any] = {"title": title_id}
|
||||
|
||||
for key, value in data.items():
|
||||
if key not in [
|
||||
"service",
|
||||
"title_id",
|
||||
"profile",
|
||||
"season",
|
||||
"episode",
|
||||
"wanted",
|
||||
"proxy",
|
||||
"no_proxy",
|
||||
"credentials",
|
||||
"cookies",
|
||||
]:
|
||||
service_kwargs[key] = value
|
||||
transport_keys = {"service", "title_id", "season", "episode", "wanted", "proxy", "no_proxy", "credentials", "cookies",
|
||||
"cache", "client_region", "no_proxy"}
|
||||
|
||||
service_init_params = inspect.signature(service_module.__init__).parameters
|
||||
|
||||
for key, value in data.items():
|
||||
if key not in transport_keys and key in service_init_params:
|
||||
service_kwargs[key] = value
|
||||
|
||||
if hasattr(service_module, "cli") and hasattr(service_module.cli, "params"):
|
||||
for param in service_module.cli.params:
|
||||
if hasattr(param, "name") and param.name not in service_kwargs:
|
||||
@@ -1271,6 +1263,8 @@ def _create_service_instance(
|
||||
service_kwargs[param_name] = None
|
||||
elif param_name == "movie":
|
||||
service_kwargs[param_name] = False
|
||||
elif param_name == "profile":
|
||||
service_kwargs[param_name] = profile
|
||||
else:
|
||||
log.warning(f"Unknown required parameter '{param_name}' for service {normalized_service}")
|
||||
|
||||
@@ -1771,8 +1765,23 @@ def _ensure_track_drm(track: Any) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _resolve_device_name(user_config: dict, drm_type: str) -> str:
|
||||
"""Get the configured device name for a DRM type from user config."""
|
||||
def _resolve_device_name(user_config: dict, drm_type: str, service_tag: str = "") -> str:
|
||||
"""Get the CDM device name, checking service-specific config.cdm first.
|
||||
|
||||
Resolution order:
|
||||
1. config.cdm[service_tag] (service-specific CDM mapping)
|
||||
2. serve.users.{key}.devices / playready_devices (user device list)
|
||||
"""
|
||||
from unshackle.core.config import config as app_config
|
||||
|
||||
cdm_name = app_config.cdm.get(service_tag) if service_tag else None
|
||||
if isinstance(cdm_name, dict):
|
||||
drm_key = {"widevine": "widevine", "playready": "playready"}.get(drm_type)
|
||||
lower_keys = {k.lower(): v for k, v in cdm_name.items()}
|
||||
cdm_name = lower_keys.get(drm_key) or lower_keys.get("default") or app_config.cdm.get("default")
|
||||
if cdm_name and isinstance(cdm_name, str):
|
||||
return cdm_name
|
||||
|
||||
if drm_type == "playready":
|
||||
device_name = (user_config.get("playready_devices") or [None])[0]
|
||||
if not device_name:
|
||||
@@ -1784,6 +1793,49 @@ def _resolve_device_name(user_config: dict, drm_type: str) -> str:
|
||||
return device_name
|
||||
|
||||
|
||||
def _check_vaults(kids: list, service_name: str) -> Optional[Dict[str, str]]:
|
||||
"""Check server vaults for existing keys matching all KIDs.
|
||||
|
||||
Returns a KID:KEY dict if ALL KIDs are found, None otherwise.
|
||||
"""
|
||||
from uuid import UUID
|
||||
|
||||
from unshackle.core.vaults import Vaults
|
||||
|
||||
try:
|
||||
vaults = Vaults(service_name)
|
||||
keys: Dict[str, str] = {}
|
||||
for kid in kids:
|
||||
kid_uuid = kid if isinstance(kid, UUID) else UUID(hex=str(kid))
|
||||
content_key, vault_used = vaults.get_key(kid_uuid)
|
||||
if content_key:
|
||||
keys[kid_uuid.hex] = content_key
|
||||
else:
|
||||
return None
|
||||
if keys:
|
||||
log.info(f"Vault hit: {len(keys)} key(s) from server vaults, skipping CDM")
|
||||
return keys
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _cache_to_vaults(keys: Dict[str, str], service_name: str) -> None:
|
||||
"""Cache newly obtained keys to server vaults."""
|
||||
from uuid import UUID
|
||||
|
||||
from unshackle.core.vaults import Vaults
|
||||
|
||||
try:
|
||||
vaults = Vaults(service_name)
|
||||
key_map = {UUID(hex=kid): key for kid, key in keys.items()}
|
||||
cached = vaults.add_keys(key_map)
|
||||
if cached:
|
||||
log.info(f"Cached {cached} key(s) to server vaults")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _handle_single_server_cdm(
|
||||
service: Any,
|
||||
title: Any,
|
||||
@@ -1807,8 +1859,6 @@ def _handle_single_server_cdm(
|
||||
|
||||
api_key = request.headers.get("X-Secret-Key", "anonymous") if request else "anonymous"
|
||||
user_config = app_config.serve.get("users", {}).get(api_key, {})
|
||||
device_name = _resolve_device_name(user_config, drm_type)
|
||||
cdm = load_cdm(device_name, service_name=service.__class__.__name__)
|
||||
|
||||
if drm_type == "playready":
|
||||
from pyplayready.system.pssh import PSSH as PlayReadyPSSH
|
||||
@@ -1817,6 +1867,13 @@ def _handle_single_server_cdm(
|
||||
|
||||
pr_pssh = PlayReadyPSSH(base64.b64decode(pssh_b64))
|
||||
pr_drm = PlayReady(pssh=pr_pssh, pssh_b64=pssh_b64)
|
||||
|
||||
vault_keys = _check_vaults(pr_drm.kids, service.__class__.__name__)
|
||||
if vault_keys:
|
||||
return vault_keys
|
||||
|
||||
device_name = _resolve_device_name(user_config, drm_type, service.__class__.__name__)
|
||||
cdm = load_cdm(device_name, service_name=service.__class__.__name__)
|
||||
pr_drm.get_content_keys(
|
||||
cdm=cdm,
|
||||
certificate=lambda challenge, **_: None,
|
||||
@@ -1830,7 +1887,15 @@ def _handle_single_server_cdm(
|
||||
|
||||
from unshackle.core.drm import Widevine
|
||||
|
||||
wv_drm = Widevine(pssh=WvPSSH(pssh_b64))
|
||||
wv_pssh = WvPSSH(pssh_b64)
|
||||
wv_drm = Widevine(pssh=wv_pssh)
|
||||
|
||||
vault_keys = _check_vaults(wv_drm.kids, service.__class__.__name__)
|
||||
if vault_keys:
|
||||
return vault_keys
|
||||
|
||||
device_name = _resolve_device_name(user_config, drm_type, service.__class__.__name__)
|
||||
cdm = load_cdm(device_name, service_name=service.__class__.__name__)
|
||||
wv_drm.get_content_keys(
|
||||
cdm=cdm,
|
||||
certificate=lambda challenge, **_: service.get_widevine_service_certificate(
|
||||
@@ -1850,6 +1915,7 @@ def _handle_single_server_cdm(
|
||||
if not keys:
|
||||
raise APIError(APIErrorCode.NO_CONTENT, "Server CDM returned no content keys")
|
||||
|
||||
_cache_to_vaults(keys, service.__class__.__name__)
|
||||
return keys
|
||||
|
||||
|
||||
@@ -1956,6 +2022,8 @@ async def session_license_handler(
|
||||
keys = _handle_single_server_cdm(service, title, track, pssh_str, track_drm_type, request)
|
||||
if keys:
|
||||
all_keys[tid] = keys
|
||||
except SystemExit:
|
||||
log.warning(f"Service exited while resolving keys for track {tid[:12]}, skipping")
|
||||
except Exception as e:
|
||||
log.warning(f"Failed to resolve keys for track {tid[:12]}: {e}")
|
||||
|
||||
@@ -2007,6 +2075,8 @@ async def session_license_handler(
|
||||
|
||||
except APIError:
|
||||
raise
|
||||
except SystemExit:
|
||||
raise APIError(APIErrorCode.SERVICE_ERROR, "Service exited during license request")
|
||||
except Exception as e:
|
||||
log.exception(f"Error proxying license for track {track_id}")
|
||||
debug_mode = request.app.get("debug_api", False) if request else False
|
||||
|
||||
@@ -375,6 +375,7 @@ class RemoteService:
|
||||
server_url: str,
|
||||
api_key: str,
|
||||
services_config: dict,
|
||||
service_params: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
self.__class__.__name__ = service_tag
|
||||
console.print(Padding(Rule(f"[rule.text]Service: {service_tag} (Remote)"), (1, 2)))
|
||||
@@ -383,6 +384,7 @@ class RemoteService:
|
||||
self.title_id = title_id
|
||||
self.client = RemoteClient(server_url, api_key)
|
||||
self.ctx = ctx
|
||||
self._service_params = service_params or {}
|
||||
self.log = logging.getLogger(service_tag)
|
||||
self.credential: Optional[Credential] = None
|
||||
self.current_region: Optional[str] = None
|
||||
@@ -482,6 +484,8 @@ class RemoteService:
|
||||
create_data["profile"] = profile
|
||||
if no_proxy:
|
||||
create_data["no_proxy"] = True
|
||||
if self._service_params:
|
||||
create_data.update(self._service_params)
|
||||
|
||||
cache_data = self._load_cache_files()
|
||||
if cache_data:
|
||||
@@ -569,15 +573,16 @@ class RemoteService:
|
||||
|
||||
from unshackle.core.drm import Widevine
|
||||
|
||||
WIDEVINE_SYSTEM_ID = UUID("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed")
|
||||
first_kid = next(iter(track_keys))
|
||||
dummy_pssh = WvPSSH.new(key_ids=[UUID(hex=first_kid)])
|
||||
dummy_pssh = WvPSSH.new(system_id=WIDEVINE_SYSTEM_ID, key_ids=[UUID(hex=first_kid)])
|
||||
drm_obj = Widevine(pssh=dummy_pssh, kid=first_kid)
|
||||
for kid_hex, key_hex in track_keys.items():
|
||||
drm_obj.content_keys[UUID(hex=kid_hex)] = key_hex
|
||||
track.drm = [drm_obj]
|
||||
key_count = sum(len(v) for v in keys_by_track.values())
|
||||
if key_count:
|
||||
self.log.info(f"Server CDM resolved {key_count} key(s) for {len(keys_by_track)} track(s)")
|
||||
self.log.debug(f"Server CDM resolved {key_count} key(s) for {len(keys_by_track)} track(s)")
|
||||
except Exception as e:
|
||||
self.log.warning("Failed to resolve server CDM keys: %s", e)
|
||||
|
||||
|
||||
@@ -103,7 +103,8 @@ class Services(click.MultiCommand):
|
||||
|
||||
server_name = ctx.parent.params.get("server") if ctx.parent else None
|
||||
server_url, api_key, services_config = resolve_server(server_name)
|
||||
return RemoteService(ctx, tag, title, server_url, api_key, services_config)
|
||||
service_params = {k: v for k, v in kwargs.items() if v is not None and v is not False}
|
||||
return RemoteService(ctx, tag, title, server_url, api_key, services_config, service_params=service_params)
|
||||
|
||||
if cli_params:
|
||||
for param in cli_params:
|
||||
|
||||
Reference in New Issue
Block a user