mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-15 05:25:12 +00:00
refactor(api): dedup handler serializers, validation, and service setup
Collapse repeated serializer/validation/CDM/list-setup logic in handlers.py behind small helpers; output behaviour unchanged.
This commit is contained in:
+136
-265
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import enum
|
||||
import logging
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from aiohttp import web
|
||||
@@ -200,6 +201,44 @@ def instantiate_service(
|
||||
return parent_ctx.invoke(service_module.cli, title=title, **extras)
|
||||
|
||||
|
||||
def setup_list_service(data: Dict[str, Any], normalized_service: str, profile: Optional[str], title_id: str) -> Any:
|
||||
"""Build and authenticate a service instance for list_titles / list_tracks.
|
||||
|
||||
Runs the shared preamble: load yaml → resolve proxy → load CDM → build ctx →
|
||||
instantiate → authenticate. Raises APIError on proxy failure.
|
||||
"""
|
||||
from unshackle.commands.dl import dl
|
||||
|
||||
service_config = load_service_yaml(normalized_service)
|
||||
|
||||
proxy_param = data.get("proxy")
|
||||
no_proxy = data.get("no_proxy", False)
|
||||
proxy_providers = []
|
||||
|
||||
if not no_proxy:
|
||||
proxy_providers = initialize_proxy_providers()
|
||||
|
||||
if proxy_param and not no_proxy:
|
||||
try:
|
||||
proxy_param = resolve_proxy(proxy_param, proxy_providers)
|
||||
except ValueError as e:
|
||||
raise APIError(
|
||||
APIErrorCode.INVALID_PROXY,
|
||||
f"Proxy error: {e}",
|
||||
details={"proxy": proxy_param, "service": normalized_service},
|
||||
)
|
||||
|
||||
cdm = load_full_cdm(normalized_service, profile, data.get("cdm_type"))
|
||||
parent_ctx = build_parent_ctx(profile, cdm, proxy_param, no_proxy, proxy_providers, service_config)
|
||||
service_module = Services.load(normalized_service)
|
||||
service_instance = instantiate_service(parent_ctx, service_module, title_id, data, LIST_HANDLER_TRANSPORT_KEYS)
|
||||
|
||||
cookies = dl.get_cookie_jar(normalized_service, profile)
|
||||
credential = dl.get_credentials(normalized_service, profile)
|
||||
service_instance.authenticate(cookies, credential)
|
||||
return service_instance
|
||||
|
||||
|
||||
def get_allowed_services(request: Optional[web.Request] = None) -> Optional[List[str]]:
|
||||
"""Get effective service allowlist considering global + per-key config.
|
||||
|
||||
@@ -247,6 +286,17 @@ def validate_service(service_tag: str, request: Optional[web.Request] = None) ->
|
||||
return None
|
||||
|
||||
|
||||
def require_fields(data: Dict[str, Any], *names: str) -> None:
|
||||
"""Raise INVALID_INPUT for the first missing/falsy required field."""
|
||||
for name in names:
|
||||
if not data.get(name):
|
||||
raise APIError(
|
||||
APIErrorCode.INVALID_INPUT,
|
||||
f"Missing required parameter: {name}",
|
||||
details={"missing_parameter": name},
|
||||
)
|
||||
|
||||
|
||||
def serialize_title(title: Title_T) -> Dict[str, Any]:
|
||||
"""Convert a title object to JSON-serializable dict."""
|
||||
title_language = str(title.language) if hasattr(title, "language") and title.language else None
|
||||
@@ -258,42 +308,28 @@ def serialize_title(title: Title_T) -> Dict[str, Any]:
|
||||
date = _data.get("date") if isinstance(_data, dict) else None
|
||||
cover_url = _data.get("cover_url") if isinstance(_data, dict) else None
|
||||
|
||||
if isinstance(title, Episode):
|
||||
episode_name = title.name if title.name else f"Episode {title.number:02d}"
|
||||
result = {
|
||||
"type": "episode",
|
||||
"name": episode_name,
|
||||
"series_title": str(title.title),
|
||||
"season": title.season,
|
||||
"number": title.number,
|
||||
"year": title.year,
|
||||
"id": str(title.id) if hasattr(title, "id") else None,
|
||||
"language": title_language,
|
||||
"description": description,
|
||||
"date": date,
|
||||
"cover_url": cover_url,
|
||||
}
|
||||
elif isinstance(title, Movie):
|
||||
result = {
|
||||
"type": "movie",
|
||||
"name": str(title.name) if hasattr(title, "name") else str(title),
|
||||
"year": title.year,
|
||||
"id": str(title.id) if hasattr(title, "id") else None,
|
||||
"language": title_language,
|
||||
"description": description,
|
||||
"date": date,
|
||||
"cover_url": cover_url,
|
||||
}
|
||||
is_episode = isinstance(title, Episode)
|
||||
if is_episode:
|
||||
name = title.name if title.name else f"Episode {title.number:02d}"
|
||||
else:
|
||||
result = {
|
||||
"type": "other",
|
||||
"name": str(title.name) if hasattr(title, "name") else str(title),
|
||||
"id": str(title.id) if hasattr(title, "id") else None,
|
||||
"language": title_language,
|
||||
"description": description,
|
||||
"date": date,
|
||||
"cover_url": cover_url,
|
||||
}
|
||||
name = str(title.name) if hasattr(title, "name") else str(title)
|
||||
|
||||
result = {
|
||||
"type": "episode" if is_episode else "movie" if isinstance(title, Movie) else "other",
|
||||
"name": name,
|
||||
"id": str(title.id) if hasattr(title, "id") else None,
|
||||
"language": title_language,
|
||||
"description": description,
|
||||
"date": date,
|
||||
"cover_url": cover_url,
|
||||
}
|
||||
# "other" titles carry no year; only Episode/Movie do.
|
||||
if isinstance(title, (Episode, Movie)):
|
||||
result["year"] = title.year
|
||||
if is_episode:
|
||||
result["series_title"] = str(title.title)
|
||||
result["season"] = title.season
|
||||
result["number"] = title.number
|
||||
|
||||
return result
|
||||
|
||||
@@ -362,46 +398,19 @@ def serialize_drm(drm_list) -> Optional[List[Dict[str, Any]]]:
|
||||
drm_class = drm.__class__.__name__
|
||||
drm_info["type"] = drm_class.lower()
|
||||
|
||||
# Get PSSH - handle both Widevine and PlayReady
|
||||
if hasattr(drm, "_pssh") and drm._pssh:
|
||||
pssh_obj = None
|
||||
# PSSH: pywidevine exposes dumps(); pyplayready's PSSH has no base64 method
|
||||
# here, so PlayReady omits the field (unchanged from prior behaviour).
|
||||
pssh_obj = getattr(drm, "_pssh", None)
|
||||
if pssh_obj is not None and hasattr(pssh_obj, "dumps"):
|
||||
try:
|
||||
pssh_obj = drm._pssh
|
||||
# Try to get base64 representation
|
||||
if hasattr(pssh_obj, "dumps"):
|
||||
# pywidevine PSSH has dumps() method
|
||||
drm_info["pssh"] = pssh_obj.dumps()
|
||||
elif hasattr(pssh_obj, "__bytes__"):
|
||||
# Convert to base64
|
||||
import base64
|
||||
|
||||
drm_info["pssh"] = base64.b64encode(bytes(pssh_obj)).decode()
|
||||
elif hasattr(pssh_obj, "to_base64"):
|
||||
drm_info["pssh"] = pssh_obj.to_base64()
|
||||
else:
|
||||
# Fallback - str() works for pywidevine PSSH
|
||||
pssh_str = str(pssh_obj)
|
||||
# Check if it's already base64-like or an object repr
|
||||
if not pssh_str.startswith("<"):
|
||||
drm_info["pssh"] = pssh_str
|
||||
drm_info["pssh"] = pssh_obj.dumps()
|
||||
except (ValueError, TypeError, KeyError):
|
||||
# Some PSSH implementations can fail to parse/serialize; log and continue.
|
||||
pssh_type = type(pssh_obj).__name__ if pssh_obj is not None else None
|
||||
log.warning(
|
||||
"Failed to extract/serialize PSSH for DRM type=%s pssh_type=%s",
|
||||
"Failed to serialize PSSH for DRM type=%s pssh_type=%s",
|
||||
drm_class,
|
||||
pssh_type,
|
||||
type(pssh_obj).__name__,
|
||||
exc_info=True,
|
||||
)
|
||||
except Exception:
|
||||
# Don't silently swallow unexpected failures; make them visible and propagate.
|
||||
pssh_type = type(pssh_obj).__name__ if pssh_obj is not None else None
|
||||
log.exception(
|
||||
"Unexpected error while extracting/serializing PSSH for DRM type=%s pssh_type=%s",
|
||||
drm_class,
|
||||
pssh_type,
|
||||
)
|
||||
raise
|
||||
|
||||
# Get KIDs
|
||||
if hasattr(drm, "kids") and drm.kids:
|
||||
@@ -422,15 +431,21 @@ def serialize_drm(drm_list) -> Optional[List[Dict[str, Any]]]:
|
||||
return result if result else None
|
||||
|
||||
|
||||
def enum_name(value: Any) -> str:
|
||||
"""Return an enum-like value's .name, falling back to str()."""
|
||||
return value.name if hasattr(value, "name") else str(value)
|
||||
|
||||
|
||||
def descriptor_name(track: Any) -> Optional[str]:
|
||||
"""Manifest descriptor (HLS/DASH/URL) name for a track, or None."""
|
||||
descriptor = getattr(track, "descriptor", None)
|
||||
return enum_name(descriptor) if descriptor else None
|
||||
|
||||
|
||||
def serialize_video_track(track: Video, include_url: bool = False) -> Dict[str, Any]:
|
||||
"""Convert video track to JSON-serializable dict."""
|
||||
codec_name = track.codec.name if hasattr(track.codec, "name") else str(track.codec)
|
||||
range_name = track.range.name if hasattr(track.range, "name") else str(track.range)
|
||||
|
||||
# Serialize the manifest descriptor (HLS, DASH, URL, etc.)
|
||||
descriptor_name = None
|
||||
if hasattr(track, "descriptor") and track.descriptor:
|
||||
descriptor_name = track.descriptor.name if hasattr(track.descriptor, "name") else str(track.descriptor)
|
||||
codec_name = enum_name(track.codec)
|
||||
range_name = enum_name(track.range)
|
||||
|
||||
result = {
|
||||
"id": str(track.id),
|
||||
@@ -445,7 +460,7 @@ def serialize_video_track(track: Video, include_url: bool = False) -> Dict[str,
|
||||
"range_display": DYNAMIC_RANGE_MAP.get(range_name, range_name),
|
||||
"language": str(track.language) if track.language else None,
|
||||
"drm": serialize_drm(track.drm) if hasattr(track, "drm") and track.drm else None,
|
||||
"descriptor": descriptor_name,
|
||||
"descriptor": descriptor_name(track),
|
||||
}
|
||||
if include_url and hasattr(track, "url") and track.url:
|
||||
result["url"] = str(track.url)
|
||||
@@ -454,12 +469,7 @@ def serialize_video_track(track: Video, include_url: bool = False) -> Dict[str,
|
||||
|
||||
def serialize_audio_track(track: Audio, include_url: bool = False) -> Dict[str, Any]:
|
||||
"""Convert audio track to JSON-serializable dict."""
|
||||
codec_name = track.codec.name if hasattr(track.codec, "name") else str(track.codec)
|
||||
|
||||
# Serialize the manifest descriptor (HLS, DASH, URL, etc.)
|
||||
descriptor_name = None
|
||||
if hasattr(track, "descriptor") and track.descriptor:
|
||||
descriptor_name = track.descriptor.name if hasattr(track.descriptor, "name") else str(track.descriptor)
|
||||
codec_name = enum_name(track.codec)
|
||||
|
||||
result = {
|
||||
"id": str(track.id),
|
||||
@@ -471,7 +481,7 @@ def serialize_audio_track(track: Audio, include_url: bool = False) -> Dict[str,
|
||||
"atmos": track.atmos if hasattr(track, "atmos") else False,
|
||||
"descriptive": track.descriptive if hasattr(track, "descriptive") else False,
|
||||
"drm": serialize_drm(track.drm) if hasattr(track, "drm") and track.drm else None,
|
||||
"descriptor": descriptor_name,
|
||||
"descriptor": descriptor_name(track),
|
||||
}
|
||||
if include_url and hasattr(track, "url") and track.url:
|
||||
result["url"] = str(track.url)
|
||||
@@ -480,19 +490,14 @@ def serialize_audio_track(track: Audio, include_url: bool = False) -> Dict[str,
|
||||
|
||||
def serialize_subtitle_track(track: Subtitle, include_url: bool = False) -> Dict[str, Any]:
|
||||
"""Convert subtitle track to JSON-serializable dict."""
|
||||
# Get descriptor for compatibility
|
||||
descriptor_name = None
|
||||
if hasattr(track, "descriptor") and track.descriptor:
|
||||
descriptor_name = track.descriptor.name if hasattr(track.descriptor, "name") else str(track.descriptor)
|
||||
|
||||
result = {
|
||||
"id": str(track.id),
|
||||
"codec": track.codec.name if hasattr(track.codec, "name") else str(track.codec),
|
||||
"codec": enum_name(track.codec),
|
||||
"language": str(track.language) if track.language else None,
|
||||
"forced": track.forced if hasattr(track, "forced") else False,
|
||||
"sdh": track.sdh if hasattr(track, "sdh") else False,
|
||||
"cc": track.cc if hasattr(track, "cc") else False,
|
||||
"descriptor": descriptor_name,
|
||||
"descriptor": descriptor_name(track),
|
||||
}
|
||||
if include_url and hasattr(track, "url") and track.url:
|
||||
result["url"] = str(track.url)
|
||||
@@ -591,24 +596,11 @@ async def search_handler(data: Dict[str, Any], request: Optional[web.Request] =
|
||||
|
||||
async def list_titles_handler(data: Dict[str, Any], request: Optional[web.Request] = None) -> web.Response:
|
||||
"""Handle list-titles request."""
|
||||
require_fields(data, "service", "title_id")
|
||||
service_tag = data.get("service")
|
||||
title_id = data.get("title_id")
|
||||
profile = data.get("profile")
|
||||
|
||||
if not service_tag:
|
||||
raise APIError(
|
||||
APIErrorCode.INVALID_INPUT,
|
||||
"Missing required parameter: service",
|
||||
details={"missing_parameter": "service"},
|
||||
)
|
||||
|
||||
if not title_id:
|
||||
raise APIError(
|
||||
APIErrorCode.INVALID_INPUT,
|
||||
"Missing required parameter: title_id",
|
||||
details={"missing_parameter": "title_id"},
|
||||
)
|
||||
|
||||
normalized_service = validate_service(service_tag, request)
|
||||
if not normalized_service:
|
||||
raise APIError(
|
||||
@@ -618,37 +610,7 @@ async def list_titles_handler(data: Dict[str, Any], request: Optional[web.Reques
|
||||
)
|
||||
|
||||
try:
|
||||
from unshackle.commands.dl import dl
|
||||
|
||||
service_config = load_service_yaml(normalized_service)
|
||||
|
||||
proxy_param = data.get("proxy")
|
||||
no_proxy = data.get("no_proxy", False)
|
||||
proxy_providers = []
|
||||
|
||||
if not no_proxy:
|
||||
proxy_providers = initialize_proxy_providers()
|
||||
|
||||
if proxy_param and not no_proxy:
|
||||
try:
|
||||
resolved_proxy = resolve_proxy(proxy_param, proxy_providers)
|
||||
proxy_param = resolved_proxy
|
||||
except ValueError as e:
|
||||
raise APIError(
|
||||
APIErrorCode.INVALID_PROXY,
|
||||
f"Proxy error: {e}",
|
||||
details={"proxy": proxy_param, "service": normalized_service},
|
||||
)
|
||||
|
||||
cdm = load_full_cdm(normalized_service, profile, data.get("cdm_type"))
|
||||
parent_ctx = build_parent_ctx(profile, cdm, proxy_param, no_proxy, proxy_providers, service_config)
|
||||
service_module = Services.load(normalized_service)
|
||||
service_instance = instantiate_service(parent_ctx, service_module, title_id, data, LIST_HANDLER_TRANSPORT_KEYS)
|
||||
|
||||
cookies = dl.get_cookie_jar(normalized_service, profile)
|
||||
credential = dl.get_credentials(normalized_service, profile)
|
||||
service_instance.authenticate(cookies, credential)
|
||||
|
||||
service_instance = setup_list_service(data, normalized_service, profile, title_id)
|
||||
titles = service_instance.get_titles()
|
||||
|
||||
if hasattr(titles, "__iter__") and not isinstance(titles, str):
|
||||
@@ -672,24 +634,11 @@ async def list_titles_handler(data: Dict[str, Any], request: Optional[web.Reques
|
||||
|
||||
async def list_tracks_handler(data: Dict[str, Any], request: Optional[web.Request] = None) -> web.Response:
|
||||
"""Handle list-tracks request."""
|
||||
require_fields(data, "service", "title_id")
|
||||
service_tag = data.get("service")
|
||||
title_id = data.get("title_id")
|
||||
profile = data.get("profile")
|
||||
|
||||
if not service_tag:
|
||||
raise APIError(
|
||||
APIErrorCode.INVALID_INPUT,
|
||||
"Missing required parameter: service",
|
||||
details={"missing_parameter": "service"},
|
||||
)
|
||||
|
||||
if not title_id:
|
||||
raise APIError(
|
||||
APIErrorCode.INVALID_INPUT,
|
||||
"Missing required parameter: title_id",
|
||||
details={"missing_parameter": "title_id"},
|
||||
)
|
||||
|
||||
normalized_service = validate_service(service_tag, request)
|
||||
if not normalized_service:
|
||||
raise APIError(
|
||||
@@ -699,37 +648,7 @@ async def list_tracks_handler(data: Dict[str, Any], request: Optional[web.Reques
|
||||
)
|
||||
|
||||
try:
|
||||
from unshackle.commands.dl import dl
|
||||
|
||||
service_config = load_service_yaml(normalized_service)
|
||||
|
||||
proxy_param = data.get("proxy")
|
||||
no_proxy = data.get("no_proxy", False)
|
||||
proxy_providers = []
|
||||
|
||||
if not no_proxy:
|
||||
proxy_providers = initialize_proxy_providers()
|
||||
|
||||
if proxy_param and not no_proxy:
|
||||
try:
|
||||
resolved_proxy = resolve_proxy(proxy_param, proxy_providers)
|
||||
proxy_param = resolved_proxy
|
||||
except ValueError as e:
|
||||
raise APIError(
|
||||
APIErrorCode.INVALID_PROXY,
|
||||
f"Proxy error: {e}",
|
||||
details={"proxy": proxy_param, "service": normalized_service},
|
||||
)
|
||||
|
||||
cdm = load_full_cdm(normalized_service, profile, data.get("cdm_type"))
|
||||
parent_ctx = build_parent_ctx(profile, cdm, proxy_param, no_proxy, proxy_providers, service_config)
|
||||
service_module = Services.load(normalized_service)
|
||||
service_instance = instantiate_service(parent_ctx, service_module, title_id, data, LIST_HANDLER_TRANSPORT_KEYS)
|
||||
|
||||
cookies = dl.get_cookie_jar(normalized_service, profile)
|
||||
credential = dl.get_credentials(normalized_service, profile)
|
||||
service_instance.authenticate(cookies, credential)
|
||||
|
||||
service_instance = setup_list_service(data, normalized_service, profile, title_id)
|
||||
titles = service_instance.get_titles()
|
||||
|
||||
wanted_param = data.get("wanted")
|
||||
@@ -871,6 +790,25 @@ async def list_tracks_handler(data: Dict[str, Any], request: Optional[web.Reques
|
||||
)
|
||||
|
||||
|
||||
VALID_VCODECS = ["H264", "H265", "H.264", "H.265", "AVC", "HEVC", "VC1", "VC-1", "VP8", "VP9", "AV1"]
|
||||
VALID_ACODECS = ["AAC", "AC3", "EC3", "EAC3", "DD", "DD+", "AC4", "OPUS", "FLAC", "ALAC", "VORBIS", "OGG", "DTS"]
|
||||
|
||||
|
||||
def check_codec(value: Any, allowed: List[str], name: str) -> Optional[str]:
|
||||
"""Validate a comma-string or list of codec tokens against `allowed` (case-insensitive)."""
|
||||
if isinstance(value, str):
|
||||
tokens = [v.strip() for v in value.split(",") if v.strip()]
|
||||
elif isinstance(value, list):
|
||||
tokens = [str(v).strip() for v in value if str(v).strip()]
|
||||
else:
|
||||
return f"{name} must be a string or list"
|
||||
|
||||
invalid = [token for token in tokens if token.upper() not in allowed]
|
||||
if invalid:
|
||||
return f"Invalid {name}: {', '.join(invalid)}. Must be one of: {', '.join(allowed)}"
|
||||
return None
|
||||
|
||||
|
||||
def validate_download_parameters(data: Dict[str, Any]) -> Optional[str]:
|
||||
"""
|
||||
Validate download parameters and return error message if invalid.
|
||||
@@ -879,44 +817,14 @@ def validate_download_parameters(data: Dict[str, Any]) -> Optional[str]:
|
||||
None if valid, error message string if invalid
|
||||
"""
|
||||
if "vcodec" in data and data["vcodec"]:
|
||||
valid_vcodecs = ["H264", "H265", "H.264", "H.265", "AVC", "HEVC", "VC1", "VC-1", "VP8", "VP9", "AV1"]
|
||||
if isinstance(data["vcodec"], str):
|
||||
vcodec_values = [v.strip() for v in data["vcodec"].split(",") if v.strip()]
|
||||
elif isinstance(data["vcodec"], list):
|
||||
vcodec_values = [str(v).strip() for v in data["vcodec"] if str(v).strip()]
|
||||
else:
|
||||
return "vcodec must be a string or list"
|
||||
|
||||
invalid = [value for value in vcodec_values if value.upper() not in valid_vcodecs]
|
||||
if invalid:
|
||||
return f"Invalid vcodec: {', '.join(invalid)}. Must be one of: {', '.join(valid_vcodecs)}"
|
||||
err = check_codec(data["vcodec"], VALID_VCODECS, "vcodec")
|
||||
if err:
|
||||
return err
|
||||
|
||||
if "acodec" in data and data["acodec"]:
|
||||
valid_acodecs = [
|
||||
"AAC",
|
||||
"AC3",
|
||||
"EC3",
|
||||
"EAC3",
|
||||
"DD",
|
||||
"DD+",
|
||||
"AC4",
|
||||
"OPUS",
|
||||
"FLAC",
|
||||
"ALAC",
|
||||
"VORBIS",
|
||||
"OGG",
|
||||
"DTS",
|
||||
]
|
||||
if isinstance(data["acodec"], str):
|
||||
acodec_values = [v.strip() for v in data["acodec"].split(",") if v.strip()]
|
||||
elif isinstance(data["acodec"], list):
|
||||
acodec_values = [str(v).strip() for v in data["acodec"] if str(v).strip()]
|
||||
else:
|
||||
return "acodec must be a string or list"
|
||||
|
||||
invalid = [value for value in acodec_values if value.upper() not in valid_acodecs]
|
||||
if invalid:
|
||||
return f"Invalid acodec: {', '.join(invalid)}. Must be one of: {', '.join(valid_acodecs)}"
|
||||
err = check_codec(data["acodec"], VALID_ACODECS, "acodec")
|
||||
if err:
|
||||
return err
|
||||
|
||||
if "sub_format" in data and data["sub_format"]:
|
||||
valid_sub_formats = ["SRT", "VTT", "ASS", "SSA", "TTML", "STPP", "WVTT", "SMI", "SUB", "MPL2", "TMP"]
|
||||
@@ -988,23 +896,10 @@ async def download_handler(data: Dict[str, Any], request: Optional[web.Request]
|
||||
"""Handle download request - create and queue a download job."""
|
||||
from unshackle.core.api.download_manager import get_download_manager
|
||||
|
||||
require_fields(data, "service", "title_id")
|
||||
service_tag = data.get("service")
|
||||
title_id = data.get("title_id")
|
||||
|
||||
if not service_tag:
|
||||
raise APIError(
|
||||
APIErrorCode.INVALID_INPUT,
|
||||
"Missing required parameter: service",
|
||||
details={"missing_parameter": "service"},
|
||||
)
|
||||
|
||||
if not title_id:
|
||||
raise APIError(
|
||||
APIErrorCode.INVALID_INPUT,
|
||||
"Missing required parameter: title_id",
|
||||
details={"missing_parameter": "title_id"},
|
||||
)
|
||||
|
||||
normalized_service = validate_service(service_tag, request)
|
||||
if not normalized_service:
|
||||
raise APIError(
|
||||
@@ -1686,31 +1581,12 @@ async def session_segments_handler(
|
||||
else:
|
||||
track_info["cookies"] = {}
|
||||
|
||||
# Include manifest-specific data for segment resolution
|
||||
# Include manifest-specific data for segment resolution. Round-trip through
|
||||
# JSON so any non-serializable value becomes its str() (default=str).
|
||||
if hasattr(track, "data") and track.data:
|
||||
track_data = {}
|
||||
for key, val in track.data.items():
|
||||
if isinstance(val, dict):
|
||||
# Convert non-serializable values
|
||||
serializable = {}
|
||||
for k, v in val.items():
|
||||
try:
|
||||
import json
|
||||
import json
|
||||
|
||||
json.dumps(v)
|
||||
serializable[k] = v
|
||||
except (TypeError, ValueError):
|
||||
serializable[k] = str(v)
|
||||
track_data[key] = serializable
|
||||
else:
|
||||
try:
|
||||
import json
|
||||
|
||||
json.dumps(val)
|
||||
track_data[key] = val
|
||||
except (TypeError, ValueError):
|
||||
track_data[key] = str(val)
|
||||
track_info["data"] = track_data
|
||||
track_info["data"] = json.loads(json.dumps(track.data, default=str))
|
||||
else:
|
||||
track_info["data"] = {}
|
||||
|
||||
@@ -1730,15 +1606,10 @@ async def session_segments_handler(
|
||||
)
|
||||
|
||||
|
||||
class _CdmTypeStub:
|
||||
"""Lightweight CDM stub so ``is_playready_cdm()`` can detect CDM type.
|
||||
|
||||
Used on the server when the client sends ``cdm_type`` but the server
|
||||
does not need a full CDM (e.g. for cache key / device selection only).
|
||||
"""
|
||||
|
||||
def __init__(self, cdm_type: str) -> None:
|
||||
self.is_playready = cdm_type == "playready"
|
||||
def _cdm_type_stub(cdm_type: str) -> SimpleNamespace:
|
||||
"""Lightweight CDM stand-in so ``is_playready_cdm()`` (reads ``.is_playready``)
|
||||
can detect the type without loading a full CDM."""
|
||||
return SimpleNamespace(is_playready=cdm_type == "playready")
|
||||
|
||||
|
||||
def _resolve_server_cdm(service: str, profile: Optional[str], cdm_type: Optional[str]) -> Optional[Any]:
|
||||
@@ -1766,10 +1637,10 @@ def _resolve_server_cdm(service: str, profile: Optional[str], cdm_type: Optional
|
||||
if cdm_name and isinstance(cdm_name, str):
|
||||
detected_type = _detect_cdm_type(cdm_name, app_config)
|
||||
if detected_type:
|
||||
return _CdmTypeStub(detected_type)
|
||||
return _cdm_type_stub(detected_type)
|
||||
|
||||
if cdm_type:
|
||||
return _CdmTypeStub(cdm_type)
|
||||
return _cdm_type_stub(cdm_type)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user