From e8ecaf8b8e6f6b161a6062448b09d7511d08e2d2 Mon Sep 17 00:00:00 2001 From: Nirvana Date: Mon, 20 Apr 2026 18:42:19 +0200 Subject: [PATCH] Add livgolf --- .../providers/livgolf/__init__.py | 6 +- .../{event_manager.py => channel_manager.py} | 114 +++++++++--------- .../providers/livgolf/provider.py | 62 ++++++---- 3 files changed, 96 insertions(+), 86 deletions(-) rename lib/streaming_providers/providers/livgolf/{event_manager.py => channel_manager.py} (76%) diff --git a/lib/streaming_providers/providers/livgolf/__init__.py b/lib/streaming_providers/providers/livgolf/__init__.py index 038206b..14c05b5 100644 --- a/lib/streaming_providers/providers/livgolf/__init__.py +++ b/lib/streaming_providers/providers/livgolf/__init__.py @@ -7,14 +7,14 @@ from .constants import ( PROVIDER_LOGO, PROVIDER_NAME, ) -from .event_manager import LivGolfEventManager +from .channel_manager import LivGolfChannelManager from .provider import LivGolfProvider __all__ = [ "LivGolfProvider", "LivGolfAuthenticator", "LivGolfAuthToken", - "LivGolfEventManager", + "LivGolfChannelManager", "PROVIDER_NAME", "PROVIDER_LABEL", "PROVIDER_LOGO", @@ -22,4 +22,4 @@ __all__ = [ "DEFAULT_CHAMPION_ID", ] -__version__ = "1.0.0" \ No newline at end of file +__version__ = "1.0.0" diff --git a/lib/streaming_providers/providers/livgolf/event_manager.py b/lib/streaming_providers/providers/livgolf/channel_manager.py similarity index 76% rename from lib/streaming_providers/providers/livgolf/event_manager.py rename to lib/streaming_providers/providers/livgolf/channel_manager.py index 5295327..79ef03d 100644 --- a/lib/streaming_providers/providers/livgolf/event_manager.py +++ b/lib/streaming_providers/providers/livgolf/channel_manager.py @@ -1,22 +1,22 @@ -# streaming_providers/providers/livgolf/event_manager.py +# streaming_providers/providers/livgolf/channel_manager.py # -*- coding: utf-8 -*- """ -Event manager for the LIV Golf provider. +Channel manager for the LIV Golf provider. Responsibilities ---------------- * Discover the best CDN edge region from /mobii/regions. * Fetch team-camera and group-camera stream lists for a champion (tournament). * Rewrite manifest URLs to use the preferred regional CDN. -* Return normalised ``Event`` objects ready for the provider. +* Return normalised ``StreamingChannel`` objects ready for the provider. Design notes ------------ * Region selection is cached for the lifetime of the process — CDN topology does not change during a session. -* Stream lists are fetched fresh on every ``get_events()`` call so that live +* Stream lists are fetched fresh on every ``get_channels()`` call so that live tournament URLs (which rotate) are always current. -* No EPG, no channels — this provider is events-only. +* No EPG support — this provider is channels-only. """ from __future__ import annotations @@ -24,7 +24,7 @@ from __future__ import annotations import re from typing import Any, Dict, List, Optional -from ...base.models import Event +from ...base.models import StreamingChannel from ...base.utils.logger import logger from .constants import ( API_ENDPOINTS, @@ -61,12 +61,12 @@ def _rewrite_cdn(url: str, preferred_base: str) -> str: # --------------------------------------------------------------------------- -# LivGolfEventManager +# LivGolfChannelManager # --------------------------------------------------------------------------- -class LivGolfEventManager: +class LivGolfChannelManager: """ - Fetches LIV Golf live event streams and normalises them into ``Event`` objects. + Fetches LIV Golf live camera streams and normalises them into ``StreamingChannel`` objects. Parameters ---------- @@ -83,21 +83,21 @@ class LivGolfEventManager: # Cached preferred CDN base URL (None = not yet resolved) self._preferred_cdn: Optional[str] = None - # Cache for events keyed by content_id (video_id) - self._events_cache: Dict[str, Event] = {} + # Cache for channels keyed by content_id (video_id) + self._channels_cache: Dict[str, StreamingChannel] = {} - logger.info("[LivGolfEventManager] Initialised") + logger.info("[LivGolfChannelManager] Initialised") # ------------------------------------------------------------------ # Public API # ------------------------------------------------------------------ - def get_events(self, champion_id: str = DEFAULT_CHAMPION_ID) -> List[Event]: + def get_channels(self, champion_id: str = DEFAULT_CHAMPION_ID) -> List[StreamingChannel]: """ - Return all available live streams for *champion_id* as ``Event`` objects. + Return all available live streams for *champion_id* as ``StreamingChannel`` objects. Both team-camera and group-camera feeds are fetched and merged. - Each stream becomes one Event with a DASH manifest URL rewritten to + Each stream becomes one Channel with a DASH manifest URL rewritten to the closest CDN region. Parameters @@ -107,7 +107,7 @@ class LivGolfEventManager: """ authorization = self._auth.get_authorization_header() if not authorization: - logger.error("[LivGolfEventManager] No authorization token available") + logger.error("[LivGolfChannelManager] No authorization token available") return [] preferred_cdn = self._get_preferred_cdn(authorization) @@ -124,50 +124,50 @@ class LivGolfEventManager: stream_kind="group", ) - events: List[Event] = [] - new_cache: Dict[str, Event] = {} + channels: List[StreamingChannel] = [] + new_cache: Dict[str, StreamingChannel] = {} for stream in team_streams: - event = self._build_event(stream, preferred_cdn, stream_kind="team") - if event: - events.append(event) - new_cache[event.content_id] = event + channel = self._build_channel(stream, preferred_cdn, stream_kind="team") + if channel: + channels.append(channel) + new_cache[channel.content_id] = channel for stream in group_streams: - event = self._build_event(stream, preferred_cdn, stream_kind="group") - if event: - events.append(event) - new_cache[event.content_id] = event + channel = self._build_channel(stream, preferred_cdn, stream_kind="group") + if channel: + channels.append(channel) + new_cache[channel.content_id] = channel # Update cache with fresh results - self._events_cache = new_cache + self._channels_cache = new_cache logger.info( - f"[LivGolfEventManager] champion={champion_id}: " + f"[LivGolfChannelManager] champion={champion_id}: " f"{len(team_streams)} team + {len(group_streams)} group streams → " - f"{len(events)} events" + f"{len(channels)} channels" ) - return events + return channels - def get_event( + def get_channel( self, content_id: str, champion_id: str = DEFAULT_CHAMPION_ID - ) -> Optional[Event]: + ) -> Optional[StreamingChannel]: """ - Return a single Event by its content_id. + Return a single StreamingChannel by its content_id. - If the event is not in the cache, it triggers a fresh get_events() call + If the channel is not in the cache, it triggers a fresh get_channels() call to discover it. """ - event = self._events_cache.get(content_id) - if event: - return event + channel = self._channels_cache.get(content_id) + if channel: + return channel logger.info( - f"[LivGolfEventManager] Event '{content_id}' not in cache — " + f"[LivGolfChannelManager] Channel '{content_id}' not in cache — " "triggering fresh fetch" ) - self.get_events(champion_id=champion_id) - return self._events_cache.get(content_id) + self.get_channels(champion_id=champion_id) + return self._channels_cache.get(content_id) # ------------------------------------------------------------------ # Region / CDN resolution @@ -185,7 +185,7 @@ class LivGolfEventManager: return self._preferred_cdn self._preferred_cdn = self._resolve_preferred_cdn(authorization) - logger.info(f"[LivGolfEventManager] Preferred CDN: {self._preferred_cdn}") + logger.info(f"[LivGolfChannelManager] Preferred CDN: {self._preferred_cdn}") return self._preferred_cdn def _resolve_preferred_cdn(self, authorization: str) -> str: @@ -205,7 +205,7 @@ class LivGolfEventManager: data = response.json() except Exception as exc: logger.warning( - f"[LivGolfEventManager] Regions fetch failed, using fallback: {exc}" + f"[LivGolfChannelManager] Regions fetch failed, using fallback: {exc}" ) return FALLBACK_CDN_BASE @@ -221,7 +221,7 @@ class LivGolfEventManager: break if not region_map: - logger.warning("[LivGolfEventManager] Empty region map, using fallback CDN") + logger.warning("[LivGolfChannelManager] Empty region map, using fallback CDN") return FALLBACK_CDN_BASE # Walk the preference list and return the first available region @@ -232,7 +232,7 @@ class LivGolfEventManager: # Fall back to the first returned region first_base = next(iter(region_map.values())) logger.warning( - f"[LivGolfEventManager] No preferred region matched — " + f"[LivGolfChannelManager] No preferred region matched — " f"using first available: {first_base}" ) return first_base @@ -264,27 +264,27 @@ class LivGolfEventManager: data = response.json() streams = data.get("streams", []) logger.debug( - f"[LivGolfEventManager] Fetched {len(streams)} {stream_kind} streams" + f"[LivGolfChannelManager] Fetched {len(streams)} {stream_kind} streams" ) return streams except Exception as exc: logger.warning( - f"[LivGolfEventManager] Failed to fetch {stream_kind} streams: {exc}" + f"[LivGolfChannelManager] Failed to fetch {stream_kind} streams: {exc}" ) return [] # ------------------------------------------------------------------ - # Event construction + # Channel construction # ------------------------------------------------------------------ - def _build_event( + def _build_channel( self, stream: Dict[str, Any], preferred_cdn: str, stream_kind: str, - ) -> Optional[Event]: + ) -> Optional[StreamingChannel]: """ - Convert a single stream dict into an ``Event``. + Convert a single stream dict into a ``StreamingChannel``. Stream dicts have at least: ``id``, ``name``, ``dashUrl``, ``hlsUrl``. Team streams additionally carry ``teamId`` and ``livTeamId``. @@ -294,14 +294,14 @@ class LivGolfEventManager: if not video_id or not raw_name: logger.debug( - f"[LivGolfEventManager] Skipping stream with missing id or name: {stream}" + f"[LivGolfChannelManager] Skipping stream with missing id or name: {stream}" ) return None dash_url = stream.get("dashUrl", "") if not dash_url: logger.debug( - f"[LivGolfEventManager] Skipping stream '{raw_name}' — no DASH URL" + f"[LivGolfChannelManager] Skipping stream '{raw_name}' — no DASH URL" ) return None @@ -329,7 +329,7 @@ class LivGolfEventManager: manifest_script = " ".join(meta_parts) try: - event = Event( + channel = StreamingChannel( name=label, content_id=content_id, provider=PROVIDER_NAME, @@ -347,10 +347,10 @@ class LivGolfEventManager: video="best", on_demand=False, ) - return event + return channel except Exception as exc: logger.warning( - f"[LivGolfEventManager] Failed to construct Event for '{raw_name}': {exc}" + f"[LivGolfChannelManager] Failed to construct Channel for '{raw_name}': {exc}" ) return None @@ -367,7 +367,7 @@ class LivGolfEventManager: """ Turn a raw API name like ``"Team_06"`` or ``"Group_03"`` into a presentable label like ``"LIV Golf – Team Feed 6"`` or - ``"LIV Golf – Group Feed 3"``. + Found Presentable Label ``"LIV Golf – Group Feed 3"``. For team streams the team number is replaced by the teamId where available, since the numeric suffix is just an ordering index. @@ -388,4 +388,4 @@ class LivGolfEventManager: if index is not None: return f"LIV Golf – {kind_label} Feed {index}" - return f"LIV Golf – {kind_label} Feed ({raw_name})" \ No newline at end of file + return f"LIV Golf – {kind_label} Feed ({raw_name})" diff --git a/lib/streaming_providers/providers/livgolf/provider.py b/lib/streaming_providers/providers/livgolf/provider.py index 007b9be..633842c 100644 --- a/lib/streaming_providers/providers/livgolf/provider.py +++ b/lib/streaming_providers/providers/livgolf/provider.py @@ -5,8 +5,8 @@ LIV Golf streaming provider. Supported features ------------------ -* Events (live team and group camera feeds) — no authentication required. -* No channels, no EPG, no catch-up. +* Channels (live team and group camera feeds) — no authentication required. +* No EPG, no events, no catch-up. Authentication -------------- @@ -15,7 +15,7 @@ long-lived (~1 year) and is persisted between sessions by the base authenticator's settings_manager. """ -from typing import ClassVar, Dict, List, Optional +from typing import ClassVar, Dict, List, Optional, Tuple from ...base.models import Event, StreamingChannel from ...base.models.proxy_models import ProxyConfig @@ -31,14 +31,14 @@ from .constants import ( PROVIDER_NAME, USER_AGENT, ) -from .event_manager import LivGolfEventManager +from .channel_manager import LivGolfChannelManager class LivGolfProvider(StreamingProvider): """ StreamingProvider implementation for LIV Golf. - Only ``get_events()`` is meaningful — ``get_channels()``, ``get_epg()``, + Only ``get_channels()`` is meaningful — ``get_events()``, ``get_epg()``, ``get_manifest()``, ``get_catchup_manifest()``, and ``get_drm()`` all return empty / None, consistent with the base contract. """ @@ -78,7 +78,7 @@ class LivGolfProvider(StreamingProvider): proxy_config=self.http_manager.config.proxy_config, ) - self.event_manager = LivGolfEventManager( + self.channel_manager = LivGolfChannelManager( http_manager=self.http_manager, authenticator=self.authenticator, ) @@ -106,6 +106,11 @@ class LivGolfProvider(StreamingProvider): # Manifest URLs are stable for the duration of a tournament round. return False + @property + def epg_window(self) -> Tuple[int, int]: + # No EPG support. + return 0, 0 + @property def catchup_window(self) -> int: return 0 @@ -133,12 +138,12 @@ class LivGolfProvider(StreamingProvider): return self.authenticate(force_refresh=True) # ------------------------------------------------------------------ - # Events — the sole data surface of this provider + # Channels — the sole data surface of this provider # ------------------------------------------------------------------ - def get_events(self, **kwargs) -> List[Event]: + def get_channels(self, **kwargs) -> List[StreamingChannel]: """ - Return all live LIV Golf camera feeds as ``Event`` objects. + Return all live LIV Golf camera feeds as ``StreamingChannel`` objects. Both team-camera streams and group-camera streams are included. @@ -149,28 +154,28 @@ class LivGolfProvider(StreamingProvider): itself defaulting to ``DEFAULT_CHAMPION_ID``). """ champion_id = kwargs.get("champion_id", self._champion_id) - logger.info(f"[LivGolfProvider] get_events(champion_id={champion_id})") + logger.info(f"[LivGolfProvider] get_channels(champion_id={champion_id})") try: # Ensure we have a valid anonymous token before delegating. if self.authenticator.is_token_expired(): - logger.info("[LivGolfProvider] Token expired — refreshing before get_events") + logger.info("[LivGolfProvider] Token expired — refreshing before get_channels") self.authenticate(force_refresh=True) - events = self.event_manager.get_events(champion_id=champion_id) - logger.info(f"[LivGolfProvider] Returning {len(events)} events") - return events + channels = self.channel_manager.get_channels(champion_id=champion_id) + logger.info(f"[LivGolfProvider] Returning {len(channels)} channels") + return channels except Exception as exc: - logger.error(f"[LivGolfProvider] get_events failed: {exc}") + logger.error(f"[LivGolfProvider] get_channels failed: {exc}") raise # ------------------------------------------------------------------ - # Channels / EPG / manifest — not supported; satisfy base contract + # EPG / Events / manifest — not supported or delegated # ------------------------------------------------------------------ - def get_channels(self, **kwargs) -> List[StreamingChannel]: - """LIV Golf has no linear channels.""" + def get_events(self, **kwargs) -> List[Event]: + """LIV Golf has no one-time events; streams are linear channels.""" return [] def get_epg(self, channel_id: str, **kwargs) -> List[Dict]: @@ -179,23 +184,23 @@ class LivGolfProvider(StreamingProvider): def get_manifest(self, content_id: str, **kwargs) -> Optional[str]: """ - Retrieve the manifest URL for a specific event ID. + Retrieve the manifest URL for a specific channel ID. - If the event is not in the event manager's cache, it triggers a fresh - fetch. LIV Golf events embed the manifest URL directly. + If the channel is not in the channel manager's cache, it triggers a fresh + fetch. LIV Golf channels embed the manifest URL directly. """ champion_id = kwargs.get("champion_id", self._champion_id) logger.info(f"[LivGolfProvider] get_manifest(content_id={content_id})") try: - event = self.event_manager.get_event( + channel = self.channel_manager.get_channel( content_id, champion_id=champion_id ) - if event: + if channel: logger.info(f"[LivGolfProvider] Found manifest for '{content_id}'") - return event.manifest + return channel.manifest - logger.warning(f"[LivGolfProvider] Event '{content_id}' not found") + logger.warning(f"[LivGolfProvider] Channel '{content_id}' not found") return None except Exception as exc: @@ -217,6 +222,11 @@ class LivGolfProvider(StreamingProvider): ) -> Optional[str]: return None + def enrich_channel_data( + self, channel: StreamingChannel, **kwargs + ) -> Optional[StreamingChannel]: + return None + @staticmethod def validate_credentials() -> bool: """ @@ -235,4 +245,4 @@ class LivGolfProvider(StreamingProvider): @classmethod def get_static_label(cls, country: str = None) -> str: - return PROVIDER_LABEL \ No newline at end of file + return PROVIDER_LABEL