mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-05 23:42:57 +02:00
add template provider
This commit is contained in:
@@ -177,41 +177,54 @@ DRM is optional. Providers with no DRM leave `_build_drm()` returning
|
||||
`None` and don't override `get_channel_drm` / `get_vod_drm`. The
|
||||
provider's `get_drm()` returns `[]` and `implements_drm` is `False`.
|
||||
|
||||
Providers with DRM pick ONE of two architectures:
|
||||
Providers with DRM make two independent choices.
|
||||
|
||||
**Architecture 1 — dedicated DRM manager (preferred for new providers)**
|
||||
### Architecture: dedicated manager vs. folded into managers
|
||||
|
||||
Create `drm_manager.py` with a class matching
|
||||
`base.protocols.DrmManagerProtocol`. Wire it in the provider's
|
||||
`_build_drm()` factory. The provider's `get_drm()` delegates to it.
|
||||
Rule: Does the DRM step share state with the manifest step?
|
||||
yes -> fold into the channel/vod managers
|
||||
no -> use a dedicated DRM manager
|
||||
|
||||
When to pick this: DRM is a distinct step with its own data sources
|
||||
(upfront tokens, licence URL construction, session authorization) that
|
||||
does not share significant state with the manifest fetch.
|
||||
Tie-breaker: when both work, prefer the dedicated manager, because it
|
||||
keeps DRM logic in one place. Folded exists for cases where the
|
||||
alternative would be plumbing session state, an upfront token, or a
|
||||
playbackInfo response between two managers that both need it.
|
||||
|
||||
See `drm_manager.py` in this directory for four concrete reference
|
||||
patterns (RTL+ upfront token, Magenta constructed URL, Discovery
|
||||
playbackInfo, HRTi session id). Pick the closest and adapt.
|
||||
"State" means any value the DRM step would otherwise have to receive
|
||||
from the manifest step: a session id, an upfront token, a playbackInfo
|
||||
response, an account/licence structure, and so on.
|
||||
|
||||
**Architecture 2 — folded into channel/vod managers**
|
||||
### Source pattern: how the licence URL is produced
|
||||
|
||||
Override `get_channel_drm()` on your `ChannelManager` and/or
|
||||
`get_vod_drm()` on your `VodManager`. Leave `_build_drm()` returning
|
||||
`None`. The provider's `get_drm()` routes through the manager list, and
|
||||
`implements_drm` is derived from whether either override is present.
|
||||
Four source patterns appear across the existing providers. The source
|
||||
pattern is orthogonal to the architecture -- any source can be wired
|
||||
into either architecture. See `drm_manager.py` in this directory for
|
||||
file-level references and mechanics:
|
||||
|
||||
When to pick this: the DRM call shares state with the manifest fetch
|
||||
(session ids, playbackInfo responses) and a separate manager would have
|
||||
to be handed that state anyway. HRTi and Magenta use this shape.
|
||||
A. Per-content upfront token (RTL+)
|
||||
B. Constructed from token claims + /user/account (Magenta)
|
||||
C. Arrives with the playbackInfo response (Discovery)
|
||||
D. Session id becomes a base64 auth blob (HRTi)
|
||||
|
||||
**The three method names**
|
||||
New-provider guidance: pick the architecture first (state-sharing rule
|
||||
above), then pick the source pattern that most closely matches how the
|
||||
provider's licence URL is produced, and adapt the mechanics.
|
||||
|
||||
The existing providers currently sit in a mix of shapes -- some have DRM
|
||||
logic directly on the provider class, some in a playback manager, some
|
||||
in a VOD manager that predates this template. "Source pattern" describes
|
||||
what their code does, not what class it lives in. Migrating any provider
|
||||
onto the dedicated or folded architecture is optional; when migrated,
|
||||
each will map to whichever architecture the state-sharing rule selects.
|
||||
|
||||
### The three method names
|
||||
|
||||
Three names appear in the DRM path. They are not interchangeable:
|
||||
|
||||
get_drm_configs — the dedicated DrmManager's only method
|
||||
(matches DrmManagerProtocol)
|
||||
get_channel_drm — the folded architecture's live-channel entry point
|
||||
get_vod_drm — the folded architecture's VOD entry point
|
||||
get_drm_configs -- the dedicated DrmManager's only method
|
||||
(matches DrmManagerProtocol)
|
||||
get_channel_drm -- the folded architecture's live-channel entry point
|
||||
get_vod_drm -- the folded architecture's VOD entry point
|
||||
|
||||
New providers using the dedicated-manager architecture implement
|
||||
`get_drm_configs` and leave the other two alone. Providers using the
|
||||
@@ -220,20 +233,28 @@ leave `get_drm_configs` alone.
|
||||
|
||||
`StreamingProvider.get_drm(content_id, content_type=None)` is the public
|
||||
method callers use; it dispatches to whichever architecture the provider
|
||||
chose. `content_type` is a hint — pass it when you already know the
|
||||
content type (e.g. the backend streaming route has already resolved the
|
||||
item). Leave it `None` and the DRM source infers the type from its own
|
||||
`content_id` grammar, which it knows better than the caller.
|
||||
chose.
|
||||
|
||||
**Which architecture to pick — a rule of thumb**
|
||||
### content_type hint semantics
|
||||
|
||||
Does the DRM call share state with the manifest fetch?
|
||||
yes -> folded (Architecture 2)
|
||||
no -> dedicated (Architecture 1)
|
||||
`content_type` is an optional hint. Pass it when you already know the
|
||||
content type (e.g. the backend streaming route, which has already
|
||||
resolved the item). It is a *narrowing* hint, not a required argument.
|
||||
|
||||
Dedicated is preferred when both work, because it keeps the DRM logic in
|
||||
one place. Folded is the right call when the alternative would be passing
|
||||
session or playback state between two managers anyway.
|
||||
On the folded path, only two values narrow the search:
|
||||
|
||||
"live" -> channel manager only
|
||||
"vod" -> VOD manager only
|
||||
|
||||
Any other value -- including None, "event", "catchup", or a typo --
|
||||
tries both. This is deliberate: widening on unknown input is always
|
||||
safe, but a wrong narrowing produces a silent `[]` for protected
|
||||
content, which is the hardest kind of bug to trace.
|
||||
|
||||
On the dedicated-manager path, the hint is passed through to
|
||||
`get_drm_configs`. The manager may honour it, ignore it, or infer the
|
||||
type from `content_id` grammar when `content_type is None`. Managers are
|
||||
encouraged to widen when in doubt, for the same reason as above.
|
||||
|
||||
## Errors
|
||||
|
||||
|
||||
@@ -12,6 +12,29 @@ DrmManagerProtocol). The shape is shared; the implementations vary enough
|
||||
that a shared ABC would need more escape hatches than it saves. This file
|
||||
is a scaffold and a document -- not an abstract class.
|
||||
|
||||
Source patterns vs. architecture
|
||||
--------------------------------
|
||||
"Source pattern" describes HOW a provider obtains DRM material (an
|
||||
upfront token, a constructed URL, a playbackInfo response, a session id).
|
||||
"Architecture" describes WHERE the code lives in the target model (a
|
||||
dedicated DrmManager or folded into the channel/vod managers). The two
|
||||
are orthogonal: any source pattern can be wired into either architecture.
|
||||
|
||||
A third shape is common in the existing providers: DRM logic in the
|
||||
provider itself (methods on the provider class, not on any manager).
|
||||
This is neither of the target architectures. It works, and it is not
|
||||
required to migrate, but new providers should prefer one of the two
|
||||
target architectures for testability.
|
||||
|
||||
The four source patterns described below are references for the
|
||||
*mechanics* of obtaining DRM material, not for the architecture. See the
|
||||
file-level pointers in each pattern for a working example.
|
||||
|
||||
Use the README's "DRM" section to pick the architecture first (the rule
|
||||
is: fold if DRM shares state with the manifest step). Then pick the
|
||||
source pattern below that most closely matches how your provider's
|
||||
license URL is produced, and adapt the mechanics.
|
||||
|
||||
How to structure a DRM manager for a new provider
|
||||
-------------------------------------------------
|
||||
|
||||
@@ -55,16 +78,12 @@ How to structure a DRM manager for a new provider
|
||||
|
||||
The provider's get_drm() delegates to this manager when present.
|
||||
|
||||
3. Reference implementations
|
||||
The four existing patterns differ enough that picking the closest
|
||||
match and adapting it is faster than designing from scratch. Read
|
||||
the referenced files before writing yours; the description below
|
||||
is a summary, the code is the source of truth.
|
||||
3. Source patterns -- read the referenced files before writing yours
|
||||
|
||||
Pattern A -- per-content upfront token
|
||||
Files: providers/rtlplus/provider.py (DRM flow)
|
||||
providers/rtlplus/auth.py (upfront token)
|
||||
providers/lib_drmtoday.py (the shared library)
|
||||
Files: providers/rtlplus/provider.py
|
||||
providers/rtlplus/auth.py
|
||||
providers/lib_drmtoday.py
|
||||
|
||||
Summary: fetch the layout for the content_id, extract the DRM
|
||||
asset config, call auth.get_scoped_token("upfront", content_id,
|
||||
@@ -74,11 +93,11 @@ How to structure a DRM manager for a new provider
|
||||
Requires an authenticated user (profile selected). The upfront
|
||||
token is per-content, not per-session.
|
||||
|
||||
Pattern B -- construct licence URL from token claims
|
||||
Files: providers/magentaeu/vod_manager.py (DRM flow)
|
||||
providers/magentaeu/provider.py (live DRM flow)
|
||||
providers/magentaeu/auth.py (token claims)
|
||||
providers/lib_theplatform.py (URL + config builders)
|
||||
Pattern B -- constructed licence URL from token claims
|
||||
Files: providers/magentaeu/vod_manager.py
|
||||
providers/magentaeu/provider.py
|
||||
providers/magentaeu/auth.py
|
||||
providers/lib_theplatform.py
|
||||
|
||||
Summary: resolve the media item to get release_pid (via the
|
||||
/media endpoint), read persona_jwt from the access token claims,
|
||||
@@ -91,10 +110,8 @@ How to structure a DRM manager for a new provider
|
||||
sources: /media response, token claims, /user/account.
|
||||
|
||||
Pattern C -- DRM arrives with the playback response
|
||||
Files: providers/discovery/playback_manager.py (playbackInfo +
|
||||
DRM extraction in one place)
|
||||
providers/discovery/constants.py (platform_os -> DRM
|
||||
system mapping)
|
||||
Files: providers/discovery/playback_manager.py
|
||||
providers/discovery/constants.py
|
||||
|
||||
Summary: during get_manifest, POST playbackInfo and cache the
|
||||
response. get_drm() is a cache lookup; no separate DRM call.
|
||||
@@ -106,10 +123,9 @@ How to structure a DRM manager for a new provider
|
||||
drm.expirationDate governs cache invalidation.
|
||||
|
||||
Pattern D -- session id becomes a base64 auth blob
|
||||
Files: providers/hrti/provider.py (both live and VOD DRM flows)
|
||||
providers/hrti/auth.py (session authorize + licence
|
||||
data generation)
|
||||
providers/lib_drmtoday.py (the shared library)
|
||||
Files: providers/hrti/provider.py
|
||||
providers/hrti/auth.py
|
||||
providers/lib_drmtoday.py
|
||||
|
||||
Summary: auth.authorize_session(...) returns a session dict
|
||||
carrying "DrmId". auth.get_license_data(DrmId) returns a
|
||||
@@ -124,8 +140,7 @@ How to structure a DRM manager for a new provider
|
||||
4. What to cache and where
|
||||
Every existing provider caches DRM material somewhere:
|
||||
* RTL+ no cache; the upfront token is refetched per playback.
|
||||
* Magenta the account_info lives on the auth token, not the
|
||||
DRM manager.
|
||||
* Magenta the account_info lives on the auth token.
|
||||
* Discovery the whole playbackInfo response lives in the
|
||||
playback_cache, keyed by edit_id, TTL from
|
||||
drm.expirationDate.
|
||||
@@ -133,8 +148,8 @@ How to structure a DRM manager for a new provider
|
||||
during the manifest step.
|
||||
|
||||
Pick whichever fits. If DRM shares state with manifest, put the cache
|
||||
on the provider and pass it into both managers, matching the pattern
|
||||
the other managers already use.
|
||||
on the provider and pass it into both managers -- that is exactly the
|
||||
signal that the folded architecture is the right choice.
|
||||
|
||||
5. Testing
|
||||
DRM managers take http_manager and auth by injection, so they are
|
||||
|
||||
@@ -34,6 +34,19 @@ class YourProvider(StreamingProvider):
|
||||
SUPPORTED_AUTH_TYPES: ClassVar[List[str]] = ["user_credentials"]
|
||||
SUPPORTED_COUNTRIES: ClassVar[List[str]] = ["TODO", "country", "codes"]
|
||||
|
||||
# Module-private constants used by get_drm's folded path.
|
||||
#
|
||||
# Only these two values narrow the search when content_type is
|
||||
# provided. Anything else (None, "event", "catchup", or a typo)
|
||||
# tries both domains. This is deliberate: a wrong narrowing produces
|
||||
# a silent [] for protected content, which is the hardest kind of
|
||||
# bug to trace. Widening on unknown input is always safe.
|
||||
#
|
||||
# Adding more narrowing values here is a deliberate act and should
|
||||
# be justified by a caller that reliably knows the content type.
|
||||
_LIVE_ONLY_CONTENT_TYPES = frozenset({"live"})
|
||||
_VOD_ONLY_CONTENT_TYPES = frozenset({"vod"})
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
country: str = "TODO",
|
||||
@@ -47,7 +60,7 @@ class YourProvider(StreamingProvider):
|
||||
config = config or {}
|
||||
self.config = YourConfig(config)
|
||||
|
||||
# 1. HTTP manager.
|
||||
# 1. HTTP manager (existing StreamingProvider helper).
|
||||
self.http_manager = self._setup_http_manager(
|
||||
provider_name="TODO: provider_name",
|
||||
proxy_config=proxy_config,
|
||||
@@ -68,7 +81,9 @@ class YourProvider(StreamingProvider):
|
||||
self.epg = self._build_epg()
|
||||
self.drm = self._build_drm()
|
||||
|
||||
# ----- Factory methods -----
|
||||
# ------------------------------------------------------------------
|
||||
# Factory methods
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_auth(self, settings_manager):
|
||||
return YourProviderAuth(
|
||||
@@ -103,7 +118,8 @@ class YourProvider(StreamingProvider):
|
||||
"""
|
||||
Return a dedicated DRM manager, or None.
|
||||
|
||||
Two supported architectures:
|
||||
Two supported architectures (see the README's "DRM" section for
|
||||
the state-sharing rule that picks between them):
|
||||
|
||||
* Dedicated manager: return a class matching DrmManagerProtocol
|
||||
here; the provider's get_drm() delegates to it.
|
||||
@@ -113,13 +129,10 @@ class YourProvider(StreamingProvider):
|
||||
get_vod_drm() on your VodManager. The provider's get_drm()
|
||||
falls back to routing to those.
|
||||
|
||||
New providers should prefer the dedicated manager (see the README
|
||||
section "DRM"). The folded style exists for providers whose DRM
|
||||
call shares significant state with the manifest step.
|
||||
|
||||
See providers/_template/drm_manager.py for the four existing
|
||||
patterns (RTL+ upfront token, Magenta constructed URL, Discovery
|
||||
playbackInfo, HRTi session id).
|
||||
New providers should prefer the dedicated manager unless the DRM
|
||||
step shares state with the manifest step. See
|
||||
providers/_template/drm_manager.py for the four existing source
|
||||
patterns.
|
||||
"""
|
||||
# TODO: return YourDrmManager(
|
||||
# http_manager=self.http_manager,
|
||||
@@ -130,7 +143,9 @@ class YourProvider(StreamingProvider):
|
||||
# )
|
||||
return None
|
||||
|
||||
# ----- Capability flags (derived from manager presence) -----
|
||||
# ------------------------------------------------------------------
|
||||
# Capability flags (derived from manager presence)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def implements_channels(self) -> bool:
|
||||
@@ -173,7 +188,9 @@ class YourProvider(StreamingProvider):
|
||||
)
|
||||
return folded_channels or folded_vod
|
||||
|
||||
# ----- Router -----
|
||||
# ------------------------------------------------------------------
|
||||
# Router
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _route(self, content_id: str, attempts: List[Tuple[Any, Callable]]):
|
||||
"""
|
||||
@@ -188,6 +205,11 @@ class YourProvider(StreamingProvider):
|
||||
If nobody resolved and a NotFoundError was seen, re-raise it --
|
||||
that's "the content existed in some manager's domain but is gone",
|
||||
distinct from "nobody handles this id at all" (which returns None).
|
||||
|
||||
BadRequestError and other errors are NOT caught here. Providers
|
||||
that need endpoint-fallback behavior (Magenta's page-vs-component
|
||||
dispatch) should override handles_content_id() instead, so the
|
||||
router never has to guess.
|
||||
"""
|
||||
last_not_found: Optional[NotFoundError] = None
|
||||
for manager, call in attempts:
|
||||
@@ -204,7 +226,9 @@ class YourProvider(StreamingProvider):
|
||||
raise last_not_found
|
||||
return None
|
||||
|
||||
# ----- Public delegations -----
|
||||
# ------------------------------------------------------------------
|
||||
# Public delegations
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def get_channels(self, **kw):
|
||||
if self.channels is None:
|
||||
@@ -212,6 +236,13 @@ class YourProvider(StreamingProvider):
|
||||
return self.channels.get_channels(**kw)
|
||||
|
||||
def get_manifest(self, content_id: str, **kw) -> Optional[str]:
|
||||
"""
|
||||
Return the manifest URL for the given content, routing by manager.
|
||||
|
||||
Providers with events, catchup, or other content types extend
|
||||
this method to add their branches. Each branch is a
|
||||
(manager, call) tuple in the attempts list.
|
||||
"""
|
||||
return self._route(content_id, [
|
||||
(self.channels, lambda m: m.get_channel_manifest(content_id, **kw)),
|
||||
(self.vod, lambda m: m.get_vod_manifest(content_id, **kw)),
|
||||
@@ -226,16 +257,16 @@ class YourProvider(StreamingProvider):
|
||||
"""
|
||||
Return DRM configuration(s) for the given content.
|
||||
|
||||
content_type is an optional hint. When None (the default), the
|
||||
DRM source infers the type from its own content_id grammar --
|
||||
which is the preferred mode, since the source knows its own
|
||||
grammar better than the caller does. Callers that already know
|
||||
the type (e.g. the backend's streaming route) should pass it
|
||||
explicitly.
|
||||
content_type is an optional hint. Only "live" and "vod" narrow
|
||||
the search on the folded path; any other value (including None,
|
||||
"event", "catchup", or an unrecognized string) tries both
|
||||
domains. This is deliberate: a wrong narrowing produces a silent
|
||||
[] for protected content, which is the hardest kind of bug to
|
||||
trace. Widening on unknown input is always safe.
|
||||
|
||||
If a dedicated DRM manager is configured, delegate to it. Otherwise
|
||||
fall back to per-manager DRM (channel manager's get_channel_drm,
|
||||
VOD manager's get_vod_drm) via the router.
|
||||
If a dedicated DRM manager is configured, the hint is passed
|
||||
through unchanged and the manager decides what to do with it.
|
||||
Otherwise the folded path narrows the search as described above.
|
||||
"""
|
||||
if self.drm is not None:
|
||||
return self.drm.get_drm_configs(
|
||||
@@ -243,7 +274,16 @@ class YourProvider(StreamingProvider):
|
||||
content_type=content_type,
|
||||
**kw,
|
||||
)
|
||||
return self._route(content_id, [
|
||||
(self.channels, lambda m: m.get_channel_drm(content_id, **kw)),
|
||||
(self.vod, lambda m: m.get_vod_drm(content_id, **kw)),
|
||||
]) or []
|
||||
|
||||
# Folded path: invert the check so only recognized values narrow.
|
||||
# Unknown values (including typos) fall through to "try both".
|
||||
attempts: List[Tuple[Any, Callable]] = []
|
||||
if content_type not in self._VOD_ONLY_CONTENT_TYPES:
|
||||
attempts.append(
|
||||
(self.channels, lambda m: m.get_channel_drm(content_id, **kw))
|
||||
)
|
||||
if content_type not in self._LIVE_ONLY_CONTENT_TYPES:
|
||||
attempts.append(
|
||||
(self.vod, lambda m: m.get_vod_drm(content_id, **kw))
|
||||
)
|
||||
return self._route(content_id, attempts) or []
|
||||
Reference in New Issue
Block a user