mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-28 20:12:19 +02:00
Add HRTi EPG
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user