mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-18 23:22:38 +02:00
magenta2 catchup
This commit is contained in:
@@ -9,15 +9,37 @@ Responsibilities
|
||||
(avoiding a SMIL round-trip for channels already fetched by get_channels).
|
||||
- Build Widevine licence URLs directly for live channels using lib_theplatform.
|
||||
- Delegate VOD and recording manifest/DRM requests to SmilManager.
|
||||
- Serve catchup manifests by appending ``dvr_window_length`` to the cached
|
||||
live manifest URL — no separate SMIL fetch required.
|
||||
- Inject ``smil_base_url`` from the recording-URL cache so callers (e.g.
|
||||
DRMOperations) only need to pass a ``content_id``.
|
||||
- Provide ``get_catchup_manifest`` via SmilManager.
|
||||
|
||||
The class holds NO state of its own beyond references to the managers and
|
||||
callbacks passed at construction time. All caches belong to ChannelManager.
|
||||
|
||||
Catchup routing
|
||||
---------------
|
||||
Magenta2 catchup is DVR-based: the live DASH manifest URL is reused with a
|
||||
``dvr_window_length`` query parameter (in seconds) that tells the CDN how far
|
||||
back the sliding window should reach. Example:
|
||||
|
||||
Live: https://svc45.…/zdf_hd/DASH/index.mpd?AppVersion=…
|
||||
Catchup: https://svc45.…/zdf_hd/DASH/index.mpd?AppVersion=…&dvr_window_length=14400
|
||||
|
||||
The window length is derived from the provider's ``catchup_window`` property
|
||||
(hours) and is passed in as ``dvr_window_seconds`` from the caller, or falls
|
||||
back to ``DVR_WINDOW_SECONDS_DEFAULT`` (4 h = 14 400 s).
|
||||
|
||||
DRM for catchup is identical to live — the same Widevine licence URL applies —
|
||||
so ``get_drm`` routes catchup requests through the same fast-path as live.
|
||||
|
||||
If the live manifest URL is not yet cached when a catchup request arrives,
|
||||
``_ensure_live_cache()`` is called to populate it before building the URL.
|
||||
SmilManager is NOT used for catchup on this provider.
|
||||
"""
|
||||
import base64
|
||||
from typing import Callable, Dict, List, Optional
|
||||
from urllib.parse import urlencode, urlparse, urlunparse, parse_qs
|
||||
|
||||
from ...base.models import DRMConfig
|
||||
from ...base.utils.logger import logger
|
||||
@@ -32,6 +54,10 @@ from ..lib_theplatform import (
|
||||
build_widevine_drm_config,
|
||||
)
|
||||
|
||||
# Default DVR window: 4 hours in seconds. Matches provider.catchup_window = 4.
|
||||
DVR_WINDOW_SECONDS_DEFAULT: int = 4 * 3600 # 14 400
|
||||
DVR_WINDOW_PARAM: str = "dvr_window_length"
|
||||
|
||||
|
||||
class PlaybackManager:
|
||||
"""
|
||||
@@ -84,10 +110,18 @@ class PlaybackManager:
|
||||
"""
|
||||
Return the MPD manifest URL for *content_id*.
|
||||
|
||||
Live channels whose manifest was already fetched by get_channels() are
|
||||
served directly from the ChannelManager cache — no SMIL round-trip.
|
||||
VOD and recordings fall through to SmilManager.
|
||||
Routing logic
|
||||
~~~~~~~~~~~~~
|
||||
1. **Catchup** (``start_time`` + ``end_time`` present in kwargs):
|
||||
Appends ``dvr_window_length`` to the cached live manifest URL.
|
||||
No SMIL round-trip. The live cache is populated on demand if empty.
|
||||
2. **Live** (``content_type == CONTENT_TYPE_LIVE``, no time window):
|
||||
Served directly from the ChannelManager cache when available.
|
||||
3. **VOD / recordings**: Fall through to ``SmilManager.get_manifest``.
|
||||
"""
|
||||
if self._is_catchup_request(kwargs):
|
||||
return self._get_catchup_manifest(content_id, kwargs)
|
||||
|
||||
if content_type == CONTENT_TYPE_LIVE:
|
||||
self._ensure_live_cache()
|
||||
if content_id in self._channel_manager._live_manifest_cache:
|
||||
@@ -105,10 +139,35 @@ class PlaybackManager:
|
||||
"""
|
||||
Return DRM configuration for *content_id*.
|
||||
|
||||
For live channels whose releasePid is cached, the Widevine licence URL
|
||||
is built directly using lib_theplatform — no SMIL fetch needed.
|
||||
VOD and recordings fall through to SmilManager.
|
||||
Routing logic
|
||||
~~~~~~~~~~~~~
|
||||
1. **Catchup** (``start_time`` + ``end_time`` present in kwargs):
|
||||
DRM is identical to live — same Widevine licence URL applies to the
|
||||
DVR window. Routes through the live fast-path using the pid cache.
|
||||
Falls back to SmilManager if the pid is not cached.
|
||||
2. **Live** (``content_type == CONTENT_TYPE_LIVE``, no time window):
|
||||
For live channels whose releasePid is cached, the Widevine licence
|
||||
URL is built directly using lib_theplatform — no SMIL fetch needed.
|
||||
3. **VOD / recordings**: Fall through to ``SmilManager.get_drm``.
|
||||
"""
|
||||
if self._is_catchup_request(kwargs):
|
||||
# Catchup uses the same Widevine licence as live — reuse the fast-path.
|
||||
self._ensure_live_cache()
|
||||
if content_id in self._channel_manager._live_pid_cache:
|
||||
logger.debug(
|
||||
f"get_drm: catchup request for {content_id} — reusing live DRM fast-path"
|
||||
)
|
||||
return self._build_live_drm(content_id)
|
||||
# pid not cached yet — fall through to SmilManager as a best-effort.
|
||||
logger.warning(
|
||||
f"get_drm: catchup for {content_id} but pid not in live cache; "
|
||||
"falling back to SmilManager"
|
||||
)
|
||||
if not self._smil_manager:
|
||||
raise RuntimeError("SmilManager not available")
|
||||
self._inject_smil_base_url(content_id, kwargs)
|
||||
return self._smil_manager.get_drm(content_id, content_type, **kwargs)
|
||||
|
||||
if content_type == CONTENT_TYPE_LIVE:
|
||||
self._ensure_live_cache()
|
||||
if content_id in self._channel_manager._live_pid_cache:
|
||||
@@ -122,17 +181,120 @@ class PlaybackManager:
|
||||
def get_catchup_manifest(
|
||||
self, channel_id: str, start_time: int, end_time: int, **kwargs
|
||||
) -> Optional[str]:
|
||||
"""Return a catchup manifest URL via SmilManager."""
|
||||
if not self._smil_manager:
|
||||
raise RuntimeError("SmilManager not available")
|
||||
return self._smil_manager.get_catchup_manifest(
|
||||
channel_id, start_time, end_time, **kwargs
|
||||
)
|
||||
"""
|
||||
Return a catchup manifest URL for *channel_id*.
|
||||
|
||||
Builds the DVR URL from the cached live manifest by appending
|
||||
``dvr_window_length``. ``start_time`` and ``end_time`` are accepted
|
||||
for interface compatibility but are not used — Magenta2 catchup is a
|
||||
sliding DVR window, not a fixed time-range VOD asset.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
channel_id:
|
||||
The live channel content_id (same key used in the live cache).
|
||||
start_time:
|
||||
Unix timestamp (seconds) of the catchup window start. Accepted
|
||||
for API compatibility; not forwarded to the CDN.
|
||||
end_time:
|
||||
Unix timestamp (seconds) of the catchup window end. Accepted
|
||||
for API compatibility; not forwarded to the CDN.
|
||||
dvr_window_seconds:
|
||||
Optional override (via kwargs) for the DVR window length in
|
||||
seconds. Defaults to ``DVR_WINDOW_SECONDS_DEFAULT`` (14 400).
|
||||
"""
|
||||
dvr_seconds: int = kwargs.get("dvr_window_seconds", DVR_WINDOW_SECONDS_DEFAULT)
|
||||
return self._build_dvr_manifest_url(channel_id, dvr_seconds)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Internal helpers #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@staticmethod
|
||||
def _is_catchup_request(kwargs: Dict) -> bool:
|
||||
"""
|
||||
Return True when *kwargs* carries both ``start_time`` and ``end_time``,
|
||||
indicating a time-shifted / catchup playback request.
|
||||
"""
|
||||
return (
|
||||
kwargs.get("start_time") is not None
|
||||
and kwargs.get("end_time") is not None
|
||||
)
|
||||
|
||||
def _get_catchup_manifest(self, content_id: str, kwargs: Dict) -> Optional[str]:
|
||||
"""
|
||||
Build a DVR manifest URL for a catchup request arriving via
|
||||
``get_manifest``.
|
||||
|
||||
Extracts an optional ``dvr_window_seconds`` override from kwargs;
|
||||
otherwise derives the window from ``end_time - start_time`` when both
|
||||
are present, capped at ``DVR_WINDOW_SECONDS_DEFAULT``.
|
||||
"""
|
||||
# Prefer an explicit override; otherwise derive from the requested
|
||||
# time window so the DVR slider covers at least the requested range.
|
||||
if "dvr_window_seconds" in kwargs:
|
||||
dvr_seconds: int = kwargs["dvr_window_seconds"]
|
||||
else:
|
||||
start_time: int = kwargs["start_time"]
|
||||
end_time: int = kwargs["end_time"]
|
||||
requested_window = end_time - start_time
|
||||
# Never request a window smaller than the requested range, but
|
||||
# cap at the provider default to avoid oversized requests.
|
||||
dvr_seconds = max(
|
||||
min(requested_window, DVR_WINDOW_SECONDS_DEFAULT),
|
||||
DVR_WINDOW_SECONDS_DEFAULT,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"get_manifest: catchup request for {content_id} "
|
||||
f"(start={kwargs['start_time']}, end={kwargs['end_time']}, "
|
||||
f"dvr_window_seconds={dvr_seconds})"
|
||||
)
|
||||
return self._build_dvr_manifest_url(content_id, dvr_seconds)
|
||||
|
||||
def _build_dvr_manifest_url(self, content_id: str, dvr_seconds: int) -> Optional[str]:
|
||||
"""
|
||||
Retrieve the cached live manifest URL for *content_id* and append
|
||||
(or replace) the ``dvr_window_length`` query parameter.
|
||||
|
||||
Returns ``None`` if the channel is not found in the live cache even
|
||||
after attempting to populate it.
|
||||
"""
|
||||
self._ensure_live_cache()
|
||||
|
||||
live_url = self._channel_manager._live_manifest_cache.get(content_id)
|
||||
if not live_url:
|
||||
logger.error(
|
||||
f"_build_dvr_manifest_url: no live manifest cached for {content_id}; "
|
||||
"cannot build catchup URL"
|
||||
)
|
||||
return None
|
||||
|
||||
dvr_url = self._append_dvr_param(live_url, dvr_seconds)
|
||||
logger.debug(
|
||||
f"_build_dvr_manifest_url: {content_id} → {dvr_url}"
|
||||
)
|
||||
return dvr_url
|
||||
|
||||
@staticmethod
|
||||
def _append_dvr_param(url: str, dvr_seconds: int) -> str:
|
||||
"""
|
||||
Return *url* with ``dvr_window_length=<dvr_seconds>`` set in the query
|
||||
string. Any pre-existing ``dvr_window_length`` value is replaced so
|
||||
that repeated calls are idempotent.
|
||||
"""
|
||||
parsed = urlparse(url)
|
||||
# Parse existing query parameters, preserving all existing keys.
|
||||
# parse_qs returns lists; rebuild as a flat dict for urlencode.
|
||||
existing: Dict[str, List[str]] = parse_qs(parsed.query, keep_blank_values=True)
|
||||
# Replace (or add) the DVR param — overwrite any existing value.
|
||||
existing[DVR_WINDOW_PARAM] = [str(dvr_seconds)]
|
||||
new_query = urlencode(
|
||||
{k: v[0] for k, v in existing.items()},
|
||||
safe="",
|
||||
)
|
||||
return urlunparse(parsed._replace(query=new_query))
|
||||
|
||||
def _ensure_live_cache(self) -> None:
|
||||
"""
|
||||
Bootstrap the live-manifest / live-pid caches on demand by calling
|
||||
|
||||
Reference in New Issue
Block a user