# streaming_providers/providers/_template/catchup_manager.py """ {TODO: Provider name} catchup manager (optional). Include this file only if the provider supports timeshift / restart. Providers without catchup don't create a catchup manager -- the provider's implements_catchup is False and calls to get_catchup_manifest return None. See ../_template/README.md for the contract. Reference implementations ------------------------- MoveTV's provider (providers/movetv/provider.py) implements catchup by resolving an EPG entry, then POSTing to a catchup-source endpoint that returns a URL + a play-auth header. The URL and header are coupled, so the manager needs a reference to the EPG manager (to resolve epg_id) and to the channel manager (for the channel's stream uid). Magenta EU's provider (providers/magentaeu/provider.py) implements catchup by appending start/end query parameters to the live manifest URL via build_catchup_url(). simpliTV's catchup only takes a start bound: it passes end_time=None through and documents that it ignores it. The router parses the "catchup:@" id (an explicit branch above _route) and hands the parsed arguments to this manager. HRTi has no catchup -- its VOD and EPG are separate domains, and authorize_session's session id is not reused for timeshift. State sharing ------------- The catchup step often shares state with the channel manager (the live manifest URL) or the EPG manager (the epg_id for the requested window). Pass those collaborators as explicit keyword-only arguments rather than reaching back to the provider (the provider's _build_catchup does this). Do NOT fall back to the live manifest ------------------------------------- If get_catchup_manifest cannot resolve catchup for the given window, return None. Do not return the live manifest URL as a "catchup" manifest -- the DRM pipeline would extract PSSH from the live stream, which may differ from the catchup stream's encryption context. Callers that want the live manifest on failure call provider.get_manifest(). end_time is Optional[int] ------------------------- If the provider's API takes only a start bound, accept None and document that it is ignored. If the API needs both bounds, raise BadRequestError on None. Never pass a sentinel (0, start_time + 1800) when the ABC accepts None. """ from typing import List, Optional from ...base.managers import CatchupManager from ...base.models import DRMConfig from ...base.utils.logger import logger # from ...base.errors import BadRequestError class YourCatchupManager(CatchupManager): """Catchup for {TODO: provider name}.""" def __init__( self, *, http_manager, auth, country, config, channels=None, epg=None, ): super().__init__( http_manager=http_manager, auth=auth, country=country, config=config, ) # Common collaborators. Catchup often needs one or both. # - channels: for resolving a channel's live manifest URL # (Magenta, simpliTV) or its stream uid (MoveTV). # - epg: for resolving an epg_id from a start_time # (MoveTV). self._channels = channels self._epg = epg # ----- Capability ----- @property def catchup_window_hours(self) -> int: """Return the catchup window in hours. 0 means no catchup.""" return 0 # TODO: e.g. 168 for 7 days # ----- Abstract method ----- def get_catchup_manifest( self, content_id: str, start_time: int, end_time: Optional[int] = None, epg_id: Optional[str] = None, **kw, ) -> Optional[str]: """ Return the catchup manifest URL, or None if not resolvable. start_time / end_time are integer epoch seconds. end_time may be None (see module docstring). Do NOT fall back to the live manifest URL here. """ raise NotImplementedError("YourCatchupManager.get_catchup_manifest") # ----- Concrete methods (override when needed) ----- # def get_catchup_drm( # self, # content_id: str, # start_time: int, # end_time: Optional[int] = None, # epg_id: Optional[str] = None, # **kw, # ) -> List[DRMConfig]: # """ # Override only if catchup uses a different DRM config from live. # The default returns [] -- the caller falls back to live DRM. # """ # return []