mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-23 17:42:19 +02:00
Add livgolf
This commit is contained in:
@@ -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"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
+57
-57
@@ -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})"
|
||||
return f"LIV Golf – {kind_label} Feed ({raw_name})"
|
||||
@@ -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
|
||||
return PROVIDER_LABEL
|
||||
|
||||
Reference in New Issue
Block a user