From a2e3dede2812019b95f85ae4e1433baf8c1417e2 Mon Sep 17 00:00:00 2001 From: Nirvana Date: Sat, 29 Nov 2025 20:27:42 +0100 Subject: [PATCH] Add hrti --- .../providers/hrti/auth.py | 71 ++++++++++++-- .../providers/hrti/constants.py | 1 + .../providers/hrti/provider.py | 96 +++++++++++++++---- 3 files changed, 141 insertions(+), 27 deletions(-) diff --git a/lib/streaming_providers/providers/hrti/auth.py b/lib/streaming_providers/providers/hrti/auth.py index 9e40ff9..1bf9198 100644 --- a/lib/streaming_providers/providers/hrti/auth.py +++ b/lib/streaming_providers/providers/hrti/auth.py @@ -464,22 +464,39 @@ class HRTiAuthenticator(BaseAuthenticator): self._user_id = self._user_id or '' def authorize_session(self, content_type: str, content_ref_id: str, - channel_id: str = None, **kwargs) -> Optional[Dict[str, Any]]: + content_drm_id: str = None, + video_store_ids: list = None, + channel_id: str = None, + start_time: str = None, + end_time: str = None) -> Optional[Dict[str, Any]]: """Authorize a playback session""" try: bearer_token = self._current_token.access_token if self._current_token else '' headers = self._get_api_headers(bearer_token) + # Set referer based on content type + if channel_id is None: + headers['referer'] = f'{self.config.base_website}/videostore' + else: + if content_type == "tlive": + headers['referer'] = f'{self.config.base_website}/live/tv?channel={channel_id}' + elif content_type == "rlive": + headers['referer'] = f'{self.config.base_website}/live/radio' + else: + headers['referer'] = f'{self.config.base_website}/live/' + payload = { "ContentType": content_type, "ContentReferenceId": content_ref_id, - "ContentDrmId": f"{content_ref_id}_drm", - "VideostoreReferenceIds": kwargs.get('video_store_ids', []), + "ContentDrmId": content_drm_id or f"{content_ref_id}_drm", + "VideostoreReferenceIds": video_store_ids, "ChannelReferenceId": channel_id, - "StartTime": kwargs.get('start_time'), - "EndTime": kwargs.get('end_time') + "StartTime": start_time, + "EndTime": end_time } + logger.debug(f"Authorizing session - type: {content_type}, ref: {content_ref_id}, drm: {content_drm_id}") + response = self.http_manager.post( self.config.api_endpoints['authorize_session'], operation='api', @@ -490,7 +507,9 @@ class HRTiAuthenticator(BaseAuthenticator): result = response.json() if 'Result' in result: - logger.debug("HRTi session authorization successful") + authorized = result['Result'].get('Authorized', False) + session_id = result['Result'].get('SessionId') or result['Result'].get('DrmId') + logger.debug(f"Session authorization result - authorized: {authorized}, session_id: {session_id}") return result['Result'] else: logger.warning("No result in session authorization response") @@ -500,8 +519,40 @@ class HRTiAuthenticator(BaseAuthenticator): logger.error(f"HRTi session authorization failed: {e}") return None + def report_session_event(self, session_id: str, channel_id: str = None) -> bool: + """Report session event (like play start)""" + try: + bearer_token = self._current_token.access_token if self._current_token else '' + headers = self._get_api_headers(bearer_token) + + # Set referer based on channel + if channel_id is None: + headers['referer'] = f'{self.config.base_website}/videostore' + else: + headers['referer'] = f'{self.config.base_website}/live/tv?channel={channel_id}' + + payload = { + "SessionEventId": 1, # 1 = play start + "SessionId": session_id + } + + response = self.http_manager.post( + self.config.api_endpoints['report_session'], + operation='api', + headers=headers, + data=json.dumps(payload) + ) + response.raise_for_status() + + logger.debug(f"Session event reported for session {session_id}") + return True + + except Exception as e: + logger.warning(f"Session event reporting failed: {e}") + return False + def get_license_data(self, session_id: str) -> str: - """Generate license data for DRM""" + """Generate license data for DRM - returns base64 encoded string""" try: drm_license = { 'userId': self._user_id or '', @@ -509,8 +560,14 @@ class HRTiAuthenticator(BaseAuthenticator): 'merchant': self.config.merchant } + logger.debug( + f"Creating license data - userId: {self._user_id}, sessionId: {session_id}, merchant: {self.config.merchant}") + + # Encode to JSON then to base64 license_bytes = json.dumps(drm_license).encode('utf-8') license_b64 = base64.b64encode(license_bytes).decode('utf-8') + + logger.debug(f"License data (base64, first 30 chars): {license_b64[:30]}...") return license_b64 except Exception as e: diff --git a/lib/streaming_providers/providers/hrti/constants.py b/lib/streaming_providers/providers/hrti/constants.py index e6cef00..cd967ff 100644 --- a/lib/streaming_providers/providers/hrti/constants.py +++ b/lib/streaming_providers/providers/hrti/constants.py @@ -29,6 +29,7 @@ class HRTiDefaults: 'channels': f'{BASE_URL}/api/api/ott/GetChannels', 'programme': f'{BASE_URL}/api/api/ott/GetProgramme', 'authorize_session': f'{BASE_URL}/api/api/ott/AuthorizeSession', + 'report_session': f'{BASE_URL}/api/api/ott/ReportSessionEvent', 'register_device': f'{HSAPI_BASE_URL}/RegisterDevice', 'content_ratings': f'{HSAPI_BASE_URL}/ContentRatingsGet', 'profiles': f'{HSAPI_BASE_URL}/ProfilesGet' diff --git a/lib/streaming_providers/providers/hrti/provider.py b/lib/streaming_providers/providers/hrti/provider.py index 2dbcab9..24cc81c 100644 --- a/lib/streaming_providers/providers/hrti/provider.py +++ b/lib/streaming_providers/providers/hrti/provider.py @@ -217,34 +217,90 @@ class HRTiProvider(StreamingProvider): def enrich_channel_data(self, channel: StreamingChannel, **kwargs) -> Optional[StreamingChannel]: """ - Enrich channel with manifest URL and additional data + Enrich channel with manifest URL and DRM configuration """ try: - # For HRTi, we need to authorize a session to get the manifest - manifest_url = self.get_manifest(channel.channel_id, **kwargs) + logger.debug(f"Enriching channel: {channel.name} ({channel.channel_id})") + # For live channels, we need to authorize a session first + content_type = "rlive" if channel.content_type == "AUDIO" else "tlive" + + # Parse the streaming URL to get content DRM ID + from urllib.parse import urlparse + parts = urlparse(channel.manifest_script) + path_parts = parts.path.strip('/').split('/') + + # Content DRM ID format: directory1_directory2 + # Example: /cdn1oiv/hrtliveorigin/... -> cdn1oiv_hrtliveorigin + content_drm_id = None + if len(path_parts) >= 2: + content_drm_id = f"{path_parts[0]}_{path_parts[1]}" + + logger.debug(f"Content DRM ID for {channel.name}: {content_drm_id}") + + # Authorize session + session_data = self.auth.authorize_session( + content_type=content_type, + content_ref_id=channel.channel_id, + content_drm_id=content_drm_id, + video_store_ids=None, + channel_id=channel.channel_id, + start_time=None, + end_time=None + ) + + if not session_data: + logger.warning(f"Failed to authorize session for channel {channel.name}") + return channel + + # Check if authorized + if not session_data.get('Authorized', False): + logger.warning(f"Session not authorized for channel {channel.name}") + return channel + + logger.debug(f"Session authorized for {channel.name}") + + # Report session event (play start) + session_id = session_data.get('SessionId') or session_data.get('DrmId') + if session_id: + self.auth.report_session_event(session_id, channel.channel_id) + + # Set the manifest URL - use the streaming URL from channel data + manifest_url = channel.manifest_script if manifest_url: - # HRTi manifests are dynamic and session-based channel.set_dynamic_manifest(manifest_url) + logger.debug(f"Set manifest for {channel.name}: {manifest_url}") - # Set DRM configuration - drm_configs = self.get_drm(channel.channel_id, **kwargs) - if drm_configs: - channel.use_cdm = True - channel.cdm_type = "widevine" - # Set license URL from first Widevine config - for config in drm_configs: - if config.system == DRMSystem.WIDEVINE: - channel.license_url = config.license.server_url - break - else: - channel.use_cdm = False - channel.cdm_type = None + # Set DRM configuration with session data + drm_configs = self.get_drm(channel.channel_id, session_data, **kwargs) + if drm_configs: + channel.use_cdm = True + channel.cdm_type = "widevine" - return channel + # Set license URL from the first Widevine config + for config in drm_configs: + if config.system == DRMSystem.WIDEVINE: + channel.license_url = config.license.server_url + + # Build the complete license key for inputstream.adaptive + # Format: server_url|req_headers|req_data|response_format + license_key_parts = [ + config.license.server_url, + config.license.req_headers, + 'R{SSM}', # Placeholder - inputstream will replace with actual challenge + 'JBlicense' # Response is JSON, extract 'license' field + ] + channel.license_key = '|'.join(license_key_parts) + + logger.debug(f"Set DRM config for {channel.name}") + logger.debug(f"License URL: {config.license.server_url}") + break else: - logger.warning(f"Could not fetch manifest for channel {channel.name} ({channel.channel_id})") - return channel + logger.warning(f"No DRM config for {channel.name}") + channel.use_cdm = False + channel.cdm_type = None + + return channel except Exception as e: logger.error(f"Error enriching channel data for {channel.name}: {e}")