diff --git a/lib/streaming_providers/providers/hrti/provider.py b/lib/streaming_providers/providers/hrti/provider.py index 142d4aa..6828b66 100644 --- a/lib/streaming_providers/providers/hrti/provider.py +++ b/lib/streaming_providers/providers/hrti/provider.py @@ -3,7 +3,7 @@ import json import datetime import traceback from urllib.parse import urlparse -from typing import ClassVar, Dict, List, Optional, Union, Tuple +from typing import ClassVar, Dict, List, Optional, Union import requests @@ -15,14 +15,13 @@ from ..lib_drmtoday import create_drmtoday_widevine_config from .auth import HRTiAuthenticator from .constants import HRTiConfig, HRTiDefaults from .vod_manager import HRTiVodManager -from .epg_manager import HRTiEPGManager # Navigation-only prefixes — these content_ids are category/series containers. # They are never directly playable and must never be sent to get_manifest/get_drm. -_NAV_PREFIXES = ("catalogue:", "series:", "special:") +_NAV_PREFIXES = ("catalogue_", "series_", "special_") -# Playable VOD prefix — "details:{ref_id}" is an explicit single-item request. -_DETAILS_PREFIX = "details:" +# Playable VOD prefix — "details_{ref_id}" is an explicit single-item request. +_DETAILS_PREFIX = "details_" class HRTiProvider(StreamingProvider): @@ -78,9 +77,6 @@ class HRTiProvider(StreamingProvider): # Initialize VOD manager self._vod_manager = HRTiVodManager(self) - # Initialize EPG manager - self._epg_manager = HRTiEPGManager(self) - try: self.authenticator.get_bearer_token() logger.debug("HRTi authentication successful during initialization") @@ -159,7 +155,7 @@ class HRTiProvider(StreamingProvider): """ if any(content_id.startswith(p) for p in _NAV_PREFIXES): return False - # "details:" prefix is an explicit VOD request + # "details_" prefix is an explicit VOD request if content_id.startswith(_DETAILS_PREFIX): return True # Bare id — could be VOD or live; live ids are short numerics. @@ -421,7 +417,7 @@ class HRTiProvider(StreamingProvider): Routing: - Navigation nodes (catalogue:/series:/season:/special:) → error, not playable - - details:{ref} or bare non-numeric id → VOD + - details_{ref} or bare non-numeric id → VOD - bare numeric id → live channel """ if self._is_nav_node(content_id): @@ -431,7 +427,7 @@ class HRTiProvider(StreamingProvider): ) return None - # Strip "details:" prefix before passing to VOD manager + # Strip "details_" prefix before passing to VOD manager vod_ref = content_id[len(_DETAILS_PREFIX):] if content_id.startswith(_DETAILS_PREFIX) else content_id if self._is_playable_vod(content_id): @@ -531,19 +527,50 @@ class HRTiProvider(StreamingProvider): # ============================================================================ def get_epg(self, channel_id: str, **kwargs) -> List[Dict]: - """ - Get EPG data for a channel. + """Get EPG data for a channel.""" + try: + headers = self._get_hrti_authenticated_headers() + start_time = self.authenticator.get_time_offset(-4) + end_time = self.authenticator.get_time_offset(4) - Returns list of dictionaries for backward compatibility. - """ - entries = self._epg_manager.get_epg_for_channel(channel_id) - # Convert EPGEntry objects to dict for backward compatibility - return [entry.to_dict() for entry in entries] + payload = { + "ChannelReferenceIds": [channel_id], + "StartTime": f"/Date({start_time})/", + "EndTime": f"/Date({end_time})/", + } - @property - def epg_window(self) -> Tuple[int, int]: - """Return EPG window as (past_days, future_days).""" - return self._epg_manager.get_epg_window() + response = self.http_manager.post( + self.hrti_config.api_endpoints["programme"], + operation="api", + headers=headers, + data=json.dumps(payload), + ) + response.raise_for_status() + + epg_data = response.json() + if "Result" in epg_data: + return [ + { + "title": entry.get("Title", ""), + "description": entry.get("Description", ""), + "start": entry.get("StartTime", ""), + "end": entry.get("EndTime", ""), + "genre": entry.get("Genre", ""), + } + for entry in epg_data["Result"] + ] + return [] + + except Exception as e: + logger.error(f"Error getting EPG data for channel {channel_id}: {e}") + return [] + + def get_license_url(self, channel: StreamingChannel, **kwargs) -> Optional[str]: + """Return the Widevine license server URL for *channel*.""" + drm_configs = self.get_drm(channel.channel_id, **kwargs) + if drm_configs: + return drm_configs[0].license.server_url + return None @property def catchup_window(self) -> int: diff --git a/lib/streaming_providers/providers/hrti/vod_manager.py b/lib/streaming_providers/providers/hrti/vod_manager.py index 924eab7..e136ce8 100644 --- a/lib/streaming_providers/providers/hrti/vod_manager.py +++ b/lib/streaming_providers/providers/hrti/vod_manager.py @@ -18,19 +18,19 @@ Navigation model Every navigable node at every level uses ``catalogue:{ReferenceId}``. There is no separate seasons API — seasons appear as Children of a series node inside GetCatalogueStructure and are therefore already - handled by the catalogue: routing. + handled by the catalogue_ routing. Type-3 series items in GetCatalogue listings link to GetSeries via: - content_id = "series:{SeriesReferenceId}:{SeasonReferenceId}" + content_id = "series_{SeriesReferenceId}--{SeasonReferenceId}" The default SeasonReferenceId is "{SeriesReferenceId}_S01". Content ID scheme ----------------- "" root (GetCatalogueStructure) - "catalogue:{ref_id}" items (GetCatalogue, paginated) - "series:{series_ref}:{season_ref}" episodes (GetSeries) - "special:watch_later" GetWatchLater - "special:editors_choice" GetEditorsChoice + "catalogue_{ref_id}" items (GetCatalogue, paginated) + "series_{series_ref}--{season_ref}" episodes (GetSeries) + "special_watch_later" GetWatchLater + "special_editors_choice" GetEditorsChoice Field mapping (verified from real API responses) ------------------------------------------------- @@ -106,29 +106,29 @@ class HRTiVodManager: Routing by content_id prefix: "" → GetCatalogueStructure (root, top-level categories) - "catalogue:{ref_id}" → GetCatalogue items (paginated) - "series:{sid}:{ssid}" → GetSeries episodes - "special:*" → special collections + "catalogue_{ref_id}" → GetCatalogue items (paginated) + "series_{sid}--{ssid}" → GetSeries episodes + "special_*" → special collections """ if not content_id: return self._fetch_catalogue_structure() - if content_id.startswith("catalogue:"): - ref_id = content_id[len("catalogue:"):] + if content_id.startswith("catalogue_"): + ref_id = content_id[len("catalogue_"):] return self._fetch_catalogue_items(ref_id, cursor, page_size) - if content_id.startswith("series:"): + if content_id.startswith("series_"): # Format: series:{series_ref_id}:{season_ref_id} - parts = content_id[len("series:"):].split(":", 1) + parts = content_id[len("series_"):].split("--", 1) if len(parts) == 2: return self._fetch_series_episodes(parts[0], parts[1]) - logger.warning(f"Malformed series content_id (expected series:SID:SSID): {content_id}") + logger.warning(f"Malformed series content_id (expected series_SID--SSID): {content_id}") return [] - if content_id == "special:watch_later": + if content_id == "special_watch_later": return self._fetch_watch_later() - if content_id == "special:editors_choice": + if content_id == "special_editors_choice": return self._fetch_editors_choice() logger.warning(f"Unknown VOD content_id format: {content_id}") @@ -249,7 +249,7 @@ class HRTiVodManager: children = node.get("Children") or [] return VodCategory( name=name, - content_id=f"catalogue:{ref_id}", + content_id=f"catalogue_{ref_id}", provider=self.provider.provider_name, logo_url=node.get("PosterLandscape"), description=None, @@ -343,7 +343,7 @@ class HRTiVodManager: return None return VodCategory( name=name, - content_id=f"catalogue:{ref_id}", + content_id=f"catalogue_{ref_id}", provider=self.provider.provider_name, logo_url=item.get("PosterLandscape") or item.get("PosterPortrait"), description=None, @@ -371,7 +371,7 @@ class HRTiVodManager: return VodCategory( name=name, - content_id=f"series:{series_ref_id}:{default_season_ref}", + content_id=f"series_{series_ref_id}--{default_season_ref}", provider=self.provider.provider_name, logo_url=item.get("PosterLandscape") or item.get("PosterPortrait"), description=None,