mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-28 09:42:33 +01:00
magenta2: fix playback manager
This commit is contained in:
@@ -37,23 +37,25 @@ load completes) — NOT by the live manifest/pid caches, which
|
||||
without ever updating the station map. Keying on the caches would let
|
||||
a playlist-only population vouch for a map that still holds media pids.
|
||||
|
||||
When neither the map nor a get_channels() refresh yields a vouched pid
|
||||
(rate-limited refresh, stale map entry after a feed change), numeric
|
||||
station ids get one last chance through the entitled-channels feed
|
||||
fallback — ``ChannelManager.resolve_release_pid_via_feed`` (wrapped in
|
||||
``_feed_fallback_pid``). That feed carries both the release pid and the
|
||||
direct streamingUrl, so live manifest, DRM and catchup can be served
|
||||
without any SMIL round-trip; it is the escape hatch for content whose
|
||||
selector is user-agent-gated (observed: ``UserAgentBlocked`` error-SMIL,
|
||||
"User Agent Restriction", for a Sky Sport UHD station).
|
||||
SMIL is the MASTER path. The entitled-channels feed fallback —
|
||||
``ChannelManager.resolve_release_pid_via_feed`` (wrapped in
|
||||
``_feed_fallback_pid``), queried with the GENERIC distribution right so
|
||||
the FULL channel universe is visible — runs only AFTER the SMIL path has
|
||||
failed for the resolved id (error-SMIL, no MPD / no releasePid; observed:
|
||||
``UserAgentBlocked`` / "User Agent Restriction" for a Sky Sport UHD
|
||||
station), never before it. The feed carries both the release pid and the
|
||||
direct CDN streamingUrl, so live manifest, DRM and catchup can be served
|
||||
even without a working SMIL path — as the escape hatch, not the first
|
||||
stop. Catchup never does SMIL; there the fallback fires when no live
|
||||
manifest URL is cached (see ``_build_begin_end_manifest_url``).
|
||||
|
||||
Catchup routing
|
||||
---------------
|
||||
Magenta2 catchup reuses the live DASH manifest URL with ``begin``/``end``
|
||||
query parameters (basic-ISO UTC) bounding the requested programme window. The
|
||||
Broadpeak origin then serves a static, programme-bounded manifest
|
||||
(mediaPresentationDuration == programme length), so no manifest rewriting is
|
||||
needed. ``end`` is clamped to now so a running programme never requests beyond
|
||||
(mediaPresentationDuration == programme length), so no manifest rewriting
|
||||
is needed. ``end`` is clamped to now so a running programme never requests beyond
|
||||
the live edge. Example:
|
||||
|
||||
Live: https://svc45.…/zdf_hd/DASH/index.mpd?AppVersion=…
|
||||
@@ -65,10 +67,11 @@ 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,
|
||||
the live caches are (re)populated before building the URL — on a pid miss
|
||||
this refresh is rate-limited to one attempt per ``_LIVE_CACHE_RETRY_INTERVAL``
|
||||
so an unentitled or stale id cannot storm the feed. If the refresh
|
||||
does not yield a vouched pid either, the entitled-channels feed fallback
|
||||
may rewrite the caches (see ``_prepare_live_pid`` / ``_feed_fallback_pid``).
|
||||
SmilManager is NOT used for catchup on this provider.
|
||||
so an unentitled or stale id cannot storm the feed. If no live URL is
|
||||
cached even after the refresh, the entitled-channels feed fallback may
|
||||
still rewrite the caches (see ``_build_begin_end_manifest_url`` /
|
||||
``_feed_fallback_pid``). SmilManager is NOT used for catchup on this
|
||||
provider.
|
||||
"""
|
||||
import base64
|
||||
import enum
|
||||
@@ -219,12 +222,12 @@ class PlaybackManager:
|
||||
2. **Live** (``content_type == CONTENT_TYPE_LIVE``, no time window):
|
||||
Served directly from the ChannelManager cache when the resolved
|
||||
pid is entitled. On a miss the request degrades to SmilManager
|
||||
with the resolved id; if SMIL also fails, the cache is re-read
|
||||
once (narrow race: a concurrent refresh may have populated it
|
||||
while the SMIL fetch was in flight) and, for numeric station
|
||||
ids, the station is re-resolved once too (a concurrent
|
||||
feed-fallback refresh may have REWRITTEN the station->pid
|
||||
mapping, changing the pid itself).
|
||||
— the MASTER path — with the resolved id; only if SMIL fails
|
||||
too does the entitled-channels feed fallback (generic
|
||||
distribution right, full channel universe) get the last word
|
||||
for numeric station ids: it re-resolves the station to a
|
||||
feed-sourced release pid + direct streamingUrl and serves the
|
||||
cached MPD (which may lack the SMIL path's session binding).
|
||||
3. **VOD / recordings**: Fall through to ``SmilManager.get_manifest``.
|
||||
"""
|
||||
if self._is_catchup_request(kwargs):
|
||||
@@ -234,7 +237,7 @@ class PlaybackManager:
|
||||
f"(start={kwargs['start_time']}, end={kwargs['end_time']})"
|
||||
)
|
||||
return self._build_begin_end_manifest_url(
|
||||
pid, kwargs["start_time"], kwargs["end_time"]
|
||||
pid, kwargs["start_time"], kwargs["end_time"], station_id=content_id
|
||||
)
|
||||
|
||||
lookup_id = content_id
|
||||
@@ -254,28 +257,28 @@ class PlaybackManager:
|
||||
result = self._smil_manager.get_manifest(lookup_id, content_type, **kwargs)
|
||||
|
||||
if result is None and content_type == CONTENT_TYPE_LIVE:
|
||||
# Post-SMIL fallback (narrow race): another thread may have
|
||||
# populated the live cache while the SMIL fetch was in flight.
|
||||
# One dict read — never a re-fetch.
|
||||
fallback_pid = lookup_id
|
||||
mpd = self._channel_manager.get_live_manifest_url(fallback_pid)
|
||||
if not mpd and content_id.isdigit():
|
||||
# A concurrent feed-fallback refresh
|
||||
# (ChannelManager.resolve_release_pid_via_feed) can also
|
||||
# REWRITE the station->pid map — the pid itself may have
|
||||
# changed while the SMIL fetch was in flight. One map read
|
||||
# plus one cache read; still never a re-fetch.
|
||||
re_resolved = self._resolve_station_id(content_id)
|
||||
if re_resolved != lookup_id:
|
||||
fallback_pid = re_resolved
|
||||
mpd = self._channel_manager.get_live_manifest_url(fallback_pid)
|
||||
if mpd:
|
||||
logger.warning(
|
||||
f"get_manifest: SMIL returned no MPD for {lookup_id}; using "
|
||||
f"entitled-feed MPD for {fallback_pid} "
|
||||
"(may lack the SMIL path's session binding)"
|
||||
)
|
||||
return mpd
|
||||
# SMIL — the master path — failed for this id. Last resort:
|
||||
# the entitled-channels feed fallback (generic distribution
|
||||
# right, full channel universe), which carries release pid +
|
||||
# direct streamingUrl for exactly this class of failure
|
||||
# (observed: UserAgentBlocked on the selector). Numeric
|
||||
# station ids only — a pid input cannot be re-resolved by
|
||||
# station. _feed_fallback_pid never raises, and its vouched
|
||||
# fast-path absorbs the concurrent-refresh case (another
|
||||
# thread's successful fallback is immediately visible); a
|
||||
# rate-limited or failed fallback leaves us where SMIL did.
|
||||
fallback_pid = (
|
||||
self._feed_fallback_pid(content_id) if content_id.isdigit() else None
|
||||
)
|
||||
if fallback_pid:
|
||||
mpd = self._channel_manager.get_live_manifest_url(fallback_pid)
|
||||
if mpd:
|
||||
logger.warning(
|
||||
f"get_manifest: SMIL returned no MPD for {lookup_id}; using "
|
||||
f"entitled-feed MPD for {fallback_pid} "
|
||||
"(may lack the SMIL path's session binding)"
|
||||
)
|
||||
return mpd
|
||||
return result
|
||||
|
||||
def get_drm(
|
||||
@@ -295,19 +298,21 @@ class PlaybackManager:
|
||||
For ids whose resolved pid is in the entitled-pid cache, the
|
||||
Widevine licence URL is built directly using lib_theplatform — no
|
||||
SMIL fetch needed. If the pid is NOT in the entitled cache the
|
||||
request degrades to SmilManager. It must NOT direct-build in that
|
||||
case: pid-cache membership is the entitlement verification for the
|
||||
direct licence path, and callers cache non-empty DRM results — an
|
||||
unverified releasePid would turn a self-healing empty result into
|
||||
a cached, persistent wrong-licence config. (The resolved pid
|
||||
reaches this check only after the get_channels() refresh and —
|
||||
for numeric station ids — the entitled-channels feed fallback
|
||||
in ``_prepare_live_pid``; a pid still unvouched after both has
|
||||
genuinely resisted two feed-level rescue attempts.)
|
||||
request degrades to SmilManager — the MASTER path. Only when
|
||||
SMIL fails as well does the entitled-channels feed fallback
|
||||
(generic distribution right) get the last word for numeric
|
||||
station ids: it vouches a feed-sourced release pid, and only a
|
||||
VOUCHEd pid is ever direct-built. Pid-cache membership remains
|
||||
the entitlement verification for the direct licence path —
|
||||
callers cache non-empty DRM results, so an unverified releasePid
|
||||
must never be promoted here.
|
||||
3. **VOD / recordings**: Fall through to ``SmilManager.get_drm``.
|
||||
"""
|
||||
lookup_id = content_id
|
||||
if self._is_catchup_request(kwargs) or content_type == CONTENT_TYPE_LIVE:
|
||||
live_request = (
|
||||
self._is_catchup_request(kwargs) or content_type == CONTENT_TYPE_LIVE
|
||||
)
|
||||
if live_request:
|
||||
lookup_id = self._prepare_live_pid(content_id)
|
||||
if self._channel_manager.is_entitled_live_pid(lookup_id):
|
||||
if self._is_catchup_request(kwargs):
|
||||
@@ -335,7 +340,28 @@ class PlaybackManager:
|
||||
if not self._smil_manager:
|
||||
raise RuntimeError("SmilManager not available")
|
||||
self._inject_smil_base_url(lookup_id, kwargs)
|
||||
return self._smil_manager.get_drm(lookup_id, content_type, **kwargs)
|
||||
try:
|
||||
drm_configs = self._smil_manager.get_drm(lookup_id, content_type, **kwargs)
|
||||
except Exception:
|
||||
# SMIL (master path) raised — last resort for numeric station
|
||||
# ids on live/catchup requests; otherwise re-raise untouched so
|
||||
# error semantics are exactly what they were without the patch.
|
||||
fallback = (
|
||||
self._post_smil_feed_fallback_drm(content_id, lookup_id)
|
||||
if live_request
|
||||
else None
|
||||
)
|
||||
if fallback:
|
||||
return fallback
|
||||
raise
|
||||
if drm_configs:
|
||||
return drm_configs
|
||||
if live_request:
|
||||
# SMIL (master path) yielded no config — same last resort.
|
||||
fallback = self._post_smil_feed_fallback_drm(content_id, lookup_id)
|
||||
if fallback:
|
||||
return fallback
|
||||
return drm_configs
|
||||
|
||||
def get_catchup_manifest(
|
||||
self, content_id: str, start_time: int, end_time: int, drm_variant: Optional[str] = "auto", **kwargs
|
||||
@@ -361,7 +387,9 @@ class PlaybackManager:
|
||||
Unix timestamp (seconds) of the catchup window end.
|
||||
"""
|
||||
pid = self._prepare_live_pid(content_id)
|
||||
return self._build_begin_end_manifest_url(pid, start_time, end_time)
|
||||
return self._build_begin_end_manifest_url(
|
||||
pid, start_time, end_time, station_id=content_id
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Internal helpers #
|
||||
@@ -379,17 +407,48 @@ class PlaybackManager:
|
||||
)
|
||||
|
||||
def _build_begin_end_manifest_url(
|
||||
self, content_id: str, start_time: int, end_time: int
|
||||
self,
|
||||
content_id: str,
|
||||
start_time: int,
|
||||
end_time: int,
|
||||
station_id: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Retrieve the cached live manifest URL for *content_id* and append the
|
||||
``begin``/``end`` programme window (basic-ISO UTC). ``end`` is clamped to
|
||||
now so a running programme never requests beyond the live edge.
|
||||
|
||||
Returns ``None`` if the channel is not found in the live cache (the
|
||||
caller is responsible for cache population via ``_prepare_live_pid``).
|
||||
*station_id* is the ORIGINAL numeric station id behind *content_id*
|
||||
(they differ when _prepare_live_pid resolved the station to a pid).
|
||||
Catchup never does SMIL, so when no live manifest URL is cached for
|
||||
*content_id*, the entitled-channels feed fallback (generic
|
||||
distribution right) is the only escalation left — it needs the
|
||||
station id, not the pid. Callers without one keep the old
|
||||
behaviour (no fallback).
|
||||
|
||||
Returns ``None`` if no manifest URL can be produced (the caller is
|
||||
responsible for cache population via ``_prepare_live_pid``).
|
||||
"""
|
||||
live_url = self._channel_manager.get_live_manifest_url(content_id)
|
||||
if not live_url:
|
||||
fallback_station = (
|
||||
station_id
|
||||
if station_id and station_id.isdigit()
|
||||
else (content_id if content_id.isdigit() else None)
|
||||
)
|
||||
if fallback_station:
|
||||
# Catchup reuses the live manifest URL and never touches
|
||||
# SMIL, so the feed fallback (generic right) is the only
|
||||
# escalation when nothing is cached for this station.
|
||||
fallback_pid = self._feed_fallback_pid(fallback_station)
|
||||
if fallback_pid:
|
||||
live_url = self._channel_manager.get_live_manifest_url(fallback_pid)
|
||||
if live_url:
|
||||
logger.warning(
|
||||
f"_build_begin_end_manifest_url: no cached live manifest "
|
||||
f"for {content_id}; using entitled-feed manifest for "
|
||||
f"{fallback_pid} (may lack the SMIL path's session binding)"
|
||||
)
|
||||
if not live_url:
|
||||
logger.error(
|
||||
f"_build_begin_end_manifest_url: no live manifest cached for {content_id}; "
|
||||
@@ -431,19 +490,12 @@ class PlaybackManager:
|
||||
filled by populate_streaming_data (channel-playlist endpoint),
|
||||
which never updates the station map.
|
||||
|
||||
If the pid is STILL unvouched after that and *content_id* is a
|
||||
numeric station id, the entitled-channels feed fallback
|
||||
(``_feed_fallback_pid`` → ChannelManager.resolve_release_pid_via_feed)
|
||||
gets the last word: it re-resolves the station from the feed,
|
||||
which carries both the release pid and the direct streamingUrl,
|
||||
so the live fast paths can serve manifest and DRM with no SMIL
|
||||
at all — the escape hatch for user-agent-gated selectors
|
||||
(``UserAgentBlocked`` error SMILs) and stale-map entries alike.
|
||||
Its refetch is rate-limited independently of the get_channels()
|
||||
refresh above (one attempt per ENTITLED_FEED_FALLBACK_MIN_INTERVAL,
|
||||
shared across stations) and it never raises; on failure the
|
||||
possibly-stale pid passes through and the caller degrades to
|
||||
SmilManager.
|
||||
This method deliberately does NOT trigger the entitled-channels
|
||||
feed fallback: SMIL is the master path, so an unvouched pid is
|
||||
returned as-is and the CALLER tries SMIL with it first. Only a
|
||||
failed SMIL round-trip escalates to the feed fallback (the
|
||||
post-SMIL blocks in get_manifest / get_drm, and — for catchup,
|
||||
which never does SMIL — ``_build_begin_end_manifest_url``).
|
||||
|
||||
Never raises: a failed feed fetch degrades to resolving against
|
||||
whatever map state exists, and callers fall through to SmilManager.
|
||||
@@ -469,27 +521,6 @@ class PlaybackManager:
|
||||
"_prepare_live_pid: feed fetch failed — degrading with the "
|
||||
f"current pid ({pid})"
|
||||
)
|
||||
if (
|
||||
not self._channel_manager.is_entitled_live_pid(pid)
|
||||
and content_id.isdigit()
|
||||
):
|
||||
# Entitled-channels-feed fallback: the pid is still not
|
||||
# vouched — a stale map entry, a media pid from a map that
|
||||
# pre-dates any feed load, or a station the (rate-limited)
|
||||
# refresh could not fix. The feed carries release pid AND
|
||||
# direct streamingUrl, so the live fast paths below can
|
||||
# serve manifest/DRM/catchup without SMIL. Vouched fast
|
||||
# path inside; refetch rate-limited independently of the
|
||||
# refresh above; never raises (see _feed_fallback_pid).
|
||||
feed_pid = self._feed_fallback_pid(content_id)
|
||||
if feed_pid:
|
||||
pid = feed_pid
|
||||
else:
|
||||
logger.debug(
|
||||
"_prepare_live_pid: entitled-feed fallback yielded no "
|
||||
f"vouched pid for station {content_id} — degrading to "
|
||||
"SMIL with the current id"
|
||||
)
|
||||
if pid == content_id and content_id.isdigit():
|
||||
# Explicit degraded-path signal: the live fast paths will miss
|
||||
# on a raw numeric station id (their caches are keyed by pids)
|
||||
@@ -577,19 +608,23 @@ class PlaybackManager:
|
||||
|
||||
def _feed_fallback_pid(self, station_id: str) -> Optional[str]:
|
||||
"""
|
||||
Entitled-channels-feed fallback: wrap
|
||||
``ChannelManager.resolve_release_pid_via_feed`` for
|
||||
``_prepare_live_pid``.
|
||||
Entitled-channels-feed fallback (LAST resort, post-SMIL): wrap
|
||||
``ChannelManager.resolve_release_pid_via_feed`` for the callers
|
||||
that have already watched the SMIL master path fail —
|
||||
get_manifest / get_drm (live) and _build_begin_end_manifest_url
|
||||
(catchup).
|
||||
|
||||
The wrapped method re-resolves *station_id* from the
|
||||
entitled-channels feed — served from the
|
||||
``mpx_feed_entitledChannelsFeed`` endpoint (never a hardcoded
|
||||
URL) — rewrites the station->pid map and the live
|
||||
manifest/pid/HLS caches for every feed entry, and returns the
|
||||
vouched release pid. None means the station is not in the feed
|
||||
(not entitled on this account), the feed could not be fetched, or
|
||||
the refetch was rate-limited; the caller then degrades to
|
||||
SmilManager with the current id. Never raises.
|
||||
URL) and queried with the GENERIC distribution right (the full
|
||||
channel universe, NOT the account's applicable rights, so
|
||||
stations outside the entitled subset remain resolvable) —
|
||||
rewrites the station->pid map and the live manifest/pid/HLS
|
||||
caches for every feed entry, and returns the vouched release
|
||||
pid. None means the station is not in the feed, the feed could
|
||||
not be fetched, or the refetch was rate-limited; the caller then
|
||||
keeps the SMIL path's outcome. Never raises.
|
||||
|
||||
The getattr guard keeps this PlaybackManager usable against an
|
||||
un-patched ChannelManager (the fallback patch spans several
|
||||
@@ -618,6 +653,35 @@ class PlaybackManager:
|
||||
)
|
||||
return None
|
||||
|
||||
def _post_smil_feed_fallback_drm(
|
||||
self, content_id: str, lookup_id: str
|
||||
) -> Optional[List[DRMConfig]]:
|
||||
"""
|
||||
Last-resort licence build for get_drm after the SMIL master path
|
||||
failed (raised or returned no config): re-resolve *content_id*
|
||||
(numeric station id only) through the entitled-channels feed
|
||||
fallback (generic distribution right) and direct-build the licence
|
||||
for the vouched release pid.
|
||||
|
||||
Returns None when there is no fallback (non-numeric id, feed
|
||||
miss/failure/rate-limit, or the pid came back unvouched) — the
|
||||
caller then keeps the SMIL path's outcome (re-raise or empty
|
||||
list). Only a VOUCHEd pid is ever built: pid-cache membership is
|
||||
the entitlement verification for the direct licence path, and
|
||||
callers cache non-empty DRM results.
|
||||
"""
|
||||
if not content_id.isdigit():
|
||||
return None
|
||||
fallback_pid = self._feed_fallback_pid(content_id)
|
||||
if not fallback_pid or not self._channel_manager.is_entitled_live_pid(fallback_pid):
|
||||
return None
|
||||
logger.warning(
|
||||
f"get_drm: SMIL (master path) failed for {lookup_id}; building "
|
||||
f"licence directly from entitled-feed pid {fallback_pid} "
|
||||
"(may lack the SMIL path's session binding)"
|
||||
)
|
||||
return self._build_live_drm(fallback_pid)
|
||||
|
||||
def _build_live_drm(self, content_id: str) -> List[DRMConfig]:
|
||||
"""
|
||||
Build a Widevine DRMConfig directly for a live channel, bypassing SMIL.
|
||||
|
||||
Reference in New Issue
Block a user