Files
script.service.ultimate/lib/streaming_providers/providers/_template/provider.py
T
2026-10-07 12:12:10 +02:00

208 lines
8.4 KiB
Python

# streaming_providers/providers/_template/provider.py (v2 template)
"""
{TODO: Provider name} orchestrator.
Owns shared resources (http_manager, caches, auth, managers) and declares
which managers exist. Everything generic -- capability flags, content_id
routing, DRM validation, header delegation, EPG delegation -- is inherited
from ManagedProvider (base/managed_provider.py), which itself subclasses
StreamingProvider, so the registry and the backend see an ordinary provider.
Authentication is lazy -- no network I/O in __init__.
What you write
--------------
* provider_name, class metadata (PROVIDER_LABEL, ...)
* __init__: http manager, auth, provider-owned caches, then
``self._init_managers()``
* the _build_*() factories for the capabilities you have (the rest
inherit "return None" = capability absent)
* get_manifest / get_drm ONLY if you have provider-specific prefixes that
need parsed arguments (catchup timestamps, ...); call super() for the rest
What you no longer write (vs. the v1 template)
----------------------------------------------
* the eight implements_* properties
* _route()
* default get_manifest / get_drm routing
* get_channels / get_epg / get_epg_grid / get_program_details delegation
* header delegation to the managers
Plugin name: the registry derives it from the CLASS NAME via
`cls.__name__.lower().replace("provider", "")`. Keep it consistent with
provider_name (see docs/provider-v2/TODO.md, item N-1).
"""
from typing import ClassVar, Dict, List, Optional
from ...base.managed_provider import ManagedProvider
from ...base.managers import ChannelManager, VodManager
from ...base.models.proxy_models import ProxyConfig
from ...base.protocols import DrmManagerProtocol
from ...base.utils.logger import logger
from ...base.vod import VodPage
from .auth import YourProviderAuth
from .channel_manager import YourChannelManager
from .constants import YourConfig, YourDefaults
# from .vod_manager import YourVodManager
# from .epg_manager import YourEpgManager
# from .recordings_manager import YourRecordingsManager
# from .favorites_manager import YourFavoritesManager
# from .bookmarks_manager import YourBookmarksManager
# from .catchup_manager import YourCatchupManager
# from .drm_manager import YourDrmManager
class YourProvider(ManagedProvider):
"""{TODO: provider name} streaming provider."""
# provider_name is ABSTRACT on StreamingProvider. If it is missing the
# registry logs "Can't instantiate abstract class ..." (with traceback
# since the registry fix) and the provider never shows up in the UI.
# Machine identifier: lowercase, no spaces, == plugin directory ==
# PROVIDER_NAME in constants.py.
@property
def provider_name(self) -> str:
return YourDefaults.PROVIDER_NAME
# Class metadata, read by the registry BEFORE any instance exists.
PROVIDER_LABEL: ClassVar[str] = "TODO: display label"
PROVIDER_LOGO: ClassVar[str] = YourDefaults.PROVIDER_LOGO
SUPPORTED_AUTH_TYPES: ClassVar[List[str]] = ["user_credentials"]
# ALWAYS set SUPPORTED_COUNTRIES (never the empty base default):
# ["AT"] single | ["hr", "pl"] multi (one instance per country) | ["*"]
SUPPORTED_COUNTRIES: ClassVar[List[str]] = ["TODO"]
# DRM architecture, declared explicitly (no override sniffing):
# dedicated manager -> return it from _build_drm()
# folded -> set True and override get_channel_drm / get_vod_drm
# Setting both raises ConfigurationError at construction.
DRM_IN_MANAGERS: ClassVar[bool] = False
# True (default): header hooks on the managers are honoured and the
# default is auth.build_headers(). Set False to keep the legacy "{}".
HEADERS_FROM_MANAGERS: ClassVar[bool] = True
def __init__(
self,
country: str = "TODO",
config: Optional[Dict] = None,
proxy_config: Optional[ProxyConfig] = None,
settings_manager=None,
credentials=None,
**kwargs,
):
super().__init__(country)
# Unknown kwargs are tolerated (the registry may pass host-level
# extras) but never silently.
if kwargs:
logger.debug(
f"{self.provider_name}: ignoring unknown kwargs {sorted(kwargs)}"
)
self.config = YourConfig(config or {}, country=self.country)
# 1. HTTP manager.
self.http_manager = self._setup_http_manager(
provider_name=self.provider_name,
proxy_config=proxy_config,
user_agent=self.config.user_agent,
timeout=self.config.timeout,
)
# 2. Auth (lazy). Providers WITHOUT auth: return None or a minimal
# stub (README, "Providers without auth").
self._credentials = credentials
self.auth = self._build_auth(settings_manager)
# 3. Provider-owned caches; managers borrow them by reference.
# (Plain dicts are not thread-safe for check-then-set; see
# TODO item C-3.)
self._channels_cache: Dict = {}
self._playback_cache: Dict = {}
# 4. Build the managers in dependency order (catchup after
# channels/epg). NOTE: this sets self.channels etc. to MANAGERS,
# shadowing the legacy list attribute; ManagedProvider overrides
# to_output_format() for that reason.
self._init_managers()
# ------------------------------------------------------------------
# Factories -- override only the capabilities you have
# ------------------------------------------------------------------
def _build_auth(self, settings_manager):
"""`credentials=` MUST be forwarded (credentials source #1)."""
return YourProviderAuth(
http_manager=self.http_manager,
country=self.country,
settings_manager=settings_manager,
credentials=self._credentials,
config=self.config,
)
def _build_channels(self) -> Optional[ChannelManager]:
return YourChannelManager(
http_manager=self.http_manager,
auth=self.auth,
country=self.country,
config=self.config,
channels_cache=self._channels_cache,
)
def _build_vod(self) -> Optional[VodManager]:
# TODO: return YourVodManager(http_manager=..., auth=..., country=...,
# config=..., playback_cache=...)
return None
# _build_epg / _build_recordings / _build_favorites / _build_bookmarks /
# _build_catchup: inherited "return None". Override to enable, e.g.
#
# def _build_catchup(self):
# return YourCatchupManager(
# http_manager=self.http_manager, auth=self.auth,
# country=self.country, config=self.config,
# channels=self.channels, epg=self.epg, # already built
# )
def _build_drm(self) -> Optional[DrmManagerProtocol]:
"""Dedicated DRM manager, or None (folded / no DRM). See README "DRM"."""
return None
# ------------------------------------------------------------------
# Provider-specific routing (only if you need it)
# ------------------------------------------------------------------
#
# def get_manifest(self, content_id: str, **kw) -> Optional[str]:
# if content_id.startswith("catchup:"):
# parsed = parse_catchup_id(content_id) # raises BadRequestError
# return self.catchup.get_catchup_manifest(
# parsed.content_id, parsed.start_time, parsed.end_time, **kw
# ) if self.catchup else None
# return super().get_manifest(content_id, **kw)
#
# Do NOT fall back to the live manifest when catchup fails.
# ------------------------------------------------------------------
# VOD delegation
# ------------------------------------------------------------------
# ManagedProvider delegates channels, EPG, manifest, headers and DRM.
# VOD navigation is NOT delegated yet because the legacy
# ProviderVodMixin signature has not been verified (TODO item M-2).
# Until then providers with VOD keep this delegation themselves.
def get_vod_category(
self,
content_id: str = "",
cursor: Optional[str] = None,
page_size: int = 24,
**kw,
) -> VodPage:
if self.vod is None:
return VodPage()
return self.vod.get_vod_category(
content_id, cursor=cursor, page_size=page_size, **kw
)