diff --git a/lib/streaming_providers/providers/hrti/auth.py b/lib/streaming_providers/providers/hrti/auth.py index 8454702..d2ea802 100644 --- a/lib/streaming_providers/providers/hrti/auth.py +++ b/lib/streaming_providers/providers/hrti/auth.py @@ -15,11 +15,11 @@ from .models import HRTiAuthToken, HRTiCredentials class HRTiAuthenticator(BaseAuthenticator): def __init__( - self, - credentials=None, - config_dir=None, - proxy_config: Optional[ProxyConfig] = None, - http_manager=None, + self, + credentials=None, + config_dir=None, + proxy_config: Optional[ProxyConfig] = None, + http_manager=None, ): # Initialize configuration FIRST @@ -80,7 +80,7 @@ class HRTiAuthenticator(BaseAuthenticator): @property def auth_endpoint(self) -> str: """HRTi authentication endpoint""" - return self.config.api_endpoints["grant_access"] + return self.config.api_endpoints.get("grant_access", "") def _get_auth_headers(self) -> Dict[str, str]: """Get headers specifically for authentication endpoint""" @@ -96,7 +96,6 @@ class HRTiAuthenticator(BaseAuthenticator): "Content-Type": "application/json", "deviceid": device_id, "devicetypeid": self.config.device_reference_id, - # REMOVED: 'host': 'hrti.hrt.hr', # Let HTTP library set this automatically "ipaddress": self._ip_address, "operatorreferenceid": self.config.operator_reference_id, "origin": self.config.base_website, @@ -122,7 +121,6 @@ class HRTiAuthenticator(BaseAuthenticator): "Content-Type": "application/json", "deviceid": device_id, "devicetypeid": self.config.device_reference_id, - # REMOVED: 'host': 'hrti.hrt.hr', # Let HTTP library set this automatically "ipaddress": self._ip_address, "operatorreferenceid": self.config.operator_reference_id, "origin": self.config.base_website, @@ -255,7 +253,13 @@ class HRTiAuthenticator(BaseAuthenticator): # Priority 2: Fall back to API fetch if no configured IP try: logger.debug("No configured IP found, fetching from API...") - response = self.http_manager.get(self.config.api_endpoints["get_ip"], operation="api") + get_ip_endpoint = self.config.api_endpoints.get("get_ip", "") + if not get_ip_endpoint: + logger.warning("No get_ip endpoint configured, using fallback") + self._ip_address = "0.0.0.0" + return self._ip_address + + response = self.http_manager.get(get_ip_endpoint, operation="api") response.raise_for_status() self._ip_address = response.text.strip().strip('"') # Remove quotes if present logger.info(f"Retrieved IP address from API: {self._ip_address}") @@ -311,6 +315,8 @@ class HRTiAuthenticator(BaseAuthenticator): except Exception as e: logger.warning(f"Error loading HRTi environment config: {e}") + # Ensure API endpoints are built even if config fetch fails + self.config._build_fallback_endpoints() def _perform_grant_access(self) -> Dict[str, Any]: """Perform grant access authentication with proper headers and debugging""" @@ -340,8 +346,12 @@ class HRTiAuthenticator(BaseAuthenticator): safe_payload["Password"] = "***" if safe_payload["Password"] else "" logger.debug(f"HRTi Auth Payload: {safe_payload}") + auth_endpoint = self.auth_endpoint + if not auth_endpoint: + raise Exception("No grant_access endpoint configured") + response = self.http_manager.post( - self.auth_endpoint, + auth_endpoint, operation="auth", headers=headers, data=json.dumps(payload), @@ -362,8 +372,8 @@ class HRTiAuthenticator(BaseAuthenticator): logger.error(f"HRTi grant access failed: {e}") # Only fallback to anonymous if we're not already using it if ( - not isinstance(self.credentials, HRTiCredentials) - or self.credentials.username != "anonymoushrt" + not isinstance(self.credentials, HRTiCredentials) + or self.credentials.username != "anonymoushrt" ): logger.info("Falling back to anonymous credentials") self.credentials = self.get_fallback_credentials() @@ -402,8 +412,13 @@ class HRTiAuthenticator(BaseAuthenticator): logger.debug(f"HRTi Device Registration Payload: {payload}") + register_endpoint = self.config.api_endpoints.get("register_device", "") + if not register_endpoint: + logger.warning("No register_device endpoint configured") + return + response = self.http_manager.post( - self.config.api_endpoints["register_device"], + register_endpoint, operation="api", headers=headers, data=json.dumps(payload), @@ -428,22 +443,26 @@ class HRTiAuthenticator(BaseAuthenticator): headers["referer"] = f"{self.config.base_website}/" # Get content ratings - content_response = self.http_manager.post( - self.config.api_endpoints["content_ratings"], - operation="api", - headers=headers, - data=json.dumps({}), - ) - content_response.raise_for_status() + content_endpoint = self.config.api_endpoints.get("content_ratings", "") + if content_endpoint: + content_response = self.http_manager.post( + content_endpoint, + operation="api", + headers=headers, + data=json.dumps({}), + ) + content_response.raise_for_status() # Get profiles - profiles_response = self.http_manager.post( - self.config.api_endpoints["profiles"], - operation="api", - headers=headers, - data=json.dumps({}), - ) - profiles_response.raise_for_status() + profiles_endpoint = self.config.api_endpoints.get("profiles", "") + if profiles_endpoint: + profiles_response = self.http_manager.post( + profiles_endpoint, + operation="api", + headers=headers, + data=json.dumps({}), + ) + profiles_response.raise_for_status() logger.debug("HRTi initial data loaded") @@ -503,14 +522,14 @@ class HRTiAuthenticator(BaseAuthenticator): self._user_id = self._user_id or "" def authorize_session( - self, - content_type: str, - content_ref_id: str, - content_drm_id: str = None, - video_store_ids: list = None, - channel_id: str = None, - start_time: str = None, - end_time: str = None, + self, + content_type: str, + content_ref_id: str, + 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: @@ -542,8 +561,13 @@ class HRTiAuthenticator(BaseAuthenticator): f"Authorizing session - type: {content_type}, ref: {content_ref_id}, drm: {content_drm_id}" ) + auth_endpoint = self.config.api_endpoints.get("authorize_session", "") + if not auth_endpoint: + logger.error("No authorize_session endpoint configured") + return None + response = self.http_manager.post( - self.config.api_endpoints["authorize_session"], + auth_endpoint, operation="api", headers=headers, data=json.dumps(payload), @@ -584,8 +608,13 @@ class HRTiAuthenticator(BaseAuthenticator): payload = {"SessionEventId": 1, "SessionId": session_id} # 1 = play start + report_endpoint = self.config.api_endpoints.get("report_session", "") + if not report_endpoint: + logger.warning("No report_session endpoint configured") + return False + response = self.http_manager.post( - self.config.api_endpoints["report_session"], + report_endpoint, operation="api", headers=headers, data=json.dumps(payload), @@ -602,14 +631,17 @@ class HRTiAuthenticator(BaseAuthenticator): def get_license_data(self, session_id: str) -> str: """Generate license data for DRM - returns base64 encoded string""" try: + # Use merchant from config (now dynamically loaded) + merchant = self.config.merchant or "aviion2" + drm_license = { "userId": self._user_id or "", "sessionId": session_id, - "merchant": self.config.merchant, + "merchant": merchant, } logger.debug( - f"Creating license data - userId: {self._user_id}, sessionId: {session_id}, merchant: {self.config.merchant}" + f"Creating license data - userId: {self._user_id}, sessionId: {session_id}, merchant: {merchant}" ) # Encode to JSON then to base64 @@ -652,14 +684,14 @@ class HRTiAuthenticator(BaseAuthenticator): stored_creds = self.settings_manager.get_provider_credentials(self.provider_name) has_user_creds = ( - isinstance(stored_creds, UserPasswordCredentials) and stored_creds.validate() + isinstance(stored_creds, UserPasswordCredentials) and stored_creds.validate() ) # Check current credentials if not has_user_creds: has_user_creds = ( - isinstance(self.credentials, UserPasswordCredentials) - and self.credentials.validate() + isinstance(self.credentials, UserPasswordCredentials) + and self.credentials.validate() ) # If we have user credentials and token has a real user_id, it's user authenticated @@ -672,9 +704,9 @@ class HRTiAuthenticator(BaseAuthenticator): # If we have user credentials, consider it user authenticated if ( - hasattr(self.credentials, "username") - and self.credentials.username - and self.credentials.username != "anonymoushrt" + hasattr(self.credentials, "username") + and self.credentials.username + and self.credentials.username != "anonymoushrt" ): return TokenAuthLevel.USER_AUTHENTICATED @@ -704,13 +736,13 @@ class HRTiAuthenticator(BaseAuthenticator): stored_creds = self.settings_manager.get_provider_credentials(self.provider_name) has_stored_user_creds = ( - isinstance(stored_creds, UserPasswordCredentials) and stored_creds.validate() + isinstance(stored_creds, UserPasswordCredentials) and stored_creds.validate() ) # Check current credentials has_current_user_creds = ( - isinstance(self.credentials, UserPasswordCredentials) - and self.credentials.validate() + isinstance(self.credentials, UserPasswordCredentials) + and self.credentials.validate() ) # Upgrade if we have any user credentials @@ -795,4 +827,4 @@ class HRTiAuthenticator(BaseAuthenticator): return self._perform_authentication() except Exception as e: logger.error(f"HRTi token refresh failed: {e}") - return None + return None \ No newline at end of file diff --git a/lib/streaming_providers/providers/hrti/constants.py b/lib/streaming_providers/providers/hrti/constants.py index cb6c763..d663fc2 100644 --- a/lib/streaming_providers/providers/hrti/constants.py +++ b/lib/streaming_providers/providers/hrti/constants.py @@ -14,36 +14,34 @@ class HRTiDefaults: # Website and base URLs BASE_WEBSITE = "https://hrti.hrt.hr" BASE_URL = "https://hrti.hrt.hr" - HSAPI_BASE_URL = "https://hsapi.aviion.tv/client.svc/json" # Configuration endpoints ENV_ENDPOINT = f"{BASE_URL}/assets/config/env.json" CONFIG_ENDPOINT = f"{BASE_URL}/assets/config/config.production.json" - # API endpoints - API_ENDPOINTS = { - "get_ip": f"{BASE_URL}/api/api/ott/getIPAddress", - "grant_access": f"{BASE_URL}/api/api/ott/GrantAccess", - "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", - # VOD endpoints - "catalogue_structure": f"{BASE_URL}/api/api/ott/GetCatalogueStructure", - "catalogue": f"{BASE_URL}/api/api/ott/GetCatalogue", - "vod_details": f"{BASE_URL}/api/api/ott/GetVodDetails", - "episodes": f"{BASE_URL}/api/api/ott/GetSeries", - "watch_later": f"{BASE_URL}/api/api/ott/GetWatchLater", - "editors_choice": f"{BASE_URL}/api/api/ott/GetEditorsChoice", + # API endpoint path templates (will be combined with dynamic base URLs) + # These are the path parts only - base URLs come from API config + API_ENDPOINT_PATHS = { + "get_ip": "/getIPAddress", + "grant_access": "/GrantAccess", + "channels": "/GetChannels", + "programme": "/GetProgramme", + "authorize_session": "/AuthorizeSession", + "report_session": "/ReportSessionEvent", + "register_device": "/RegisterDevice", + "content_ratings": "/ContentRatingsGet", + "profiles": "/ProfilesGet", + "catalogue_structure": "/GetCatalogueStructure", + "catalogue": "/GetCatalogue", + "vod_details": "/GetVodDetails", + "episodes": "/GetSeries", + "watch_later": "/GetWatchLater", + "editors_choice": "/GetEditorsChoice", } - # Device information + # Device information (static defaults, may be overridden by config) DEVICE_REFERENCE_ID = "6" # String '6' as required by headers OPERATOR_REFERENCE_ID = "hrt" - MERCHANT = "aviion2" CONNECTION_TYPE = "LAN/WiFi" APPLICATION_VERSION = "5.97.6" OS_VERSION = "Linux" @@ -72,12 +70,20 @@ class HRTiConfig: # Website and base URLs self.base_website = config.get("base_website", HRTiDefaults.BASE_WEBSITE) self.base_url = config.get("base_url", HRTiDefaults.BASE_URL) - self.hsapi_base_url = config.get("hsapi_base_url", HRTiDefaults.HSAPI_BASE_URL) + + # Dynamic URLs - will be populated from API + self.hsapi_base_url = config.get("hsapi_base_url", None) + self.web_api_url = config.get("web_api_url", None) + + # Configuration endpoints self.env_endpoint = config.get("env_endpoint", HRTiDefaults.ENV_ENDPOINT) self.config_endpoint = config.get("config_endpoint", HRTiDefaults.CONFIG_ENDPOINT) - # API endpoints configuration - self.api_endpoints = config.get("api_endpoints", HRTiDefaults.API_ENDPOINTS.copy()) + # API endpoints - start with paths only, build full URLs after API call + self.api_endpoint_paths = config.get( + "api_endpoint_paths", HRTiDefaults.API_ENDPOINT_PATHS.copy() + ) + self.api_endpoints = {} # Will be populated in update_from_api # Device configuration self.device_reference_id = config.get( @@ -86,7 +92,8 @@ class HRTiConfig: self.operator_reference_id = config.get( "operator_reference_id", HRTiDefaults.OPERATOR_REFERENCE_ID ) - self.merchant = config.get("merchant", HRTiDefaults.MERCHANT) + self.merchant = config.get("merchant", None) # Will be set from API + self.player_license_key = config.get("player_license_key", None) # Will be set from API self.connection_type = config.get("connection_type", HRTiDefaults.CONNECTION_TYPE) self.application_version = config.get( "application_version", HRTiDefaults.APPLICATION_VERSION @@ -98,12 +105,35 @@ class HRTiConfig: self.user_agent = config.get("user_agent", HRTiDefaults.USER_AGENT) self.timeout = config.get("timeout", HRTiDefaults.DEFAULT_TIMEOUT) - # Web API URL (can be updated from config) - self.web_api_url = config.get("web_api_url", "api/api/ott") - # VOD settings self.vod_items_per_page = config.get("vod_items_per_page", HRTiDefaults.VOD_ITEMS_PER_PAGE) + def _build_api_endpoints(self): + """Build full API endpoints from dynamic base URLs and static paths""" + endpoints = {} + + # Build endpoints using webApiUrl (for OTT endpoints) + if self.web_api_url: + base_api_url = f"{self.base_url}/{self.web_api_url}" + ott_endpoints = [ + "get_ip", "grant_access", "channels", "programme", + "authorize_session", "report_session", "catalogue_structure", + "catalogue", "vod_details", "episodes", "watch_later", + "editors_choice" + ] + for key in ott_endpoints: + if key in self.api_endpoint_paths: + endpoints[key] = f"{base_api_url}{self.api_endpoint_paths[key]}" + + # Build endpoints using hsapi_base_url (for HSAPI endpoints) + if self.hsapi_base_url: + hsapi_endpoints = ["register_device", "content_ratings", "profiles"] + for key in hsapi_endpoints: + if key in self.api_endpoint_paths: + endpoints[key] = f"{self.hsapi_base_url}{self.api_endpoint_paths[key]}" + + return endpoints + def update_from_api(self, env_data: dict, config_data: dict): """Update configuration from API responses""" try: @@ -117,36 +147,48 @@ class HRTiConfig: if "webApiUrl" in config_data: self.web_api_url = config_data["webApiUrl"] - # Update API endpoints with new web API URL - base_api_url = f"{self.base_url}/{self.web_api_url}" - self.api_endpoints.update( - { - "get_ip": f"{base_api_url}/getIPAddress", - "grant_access": f"{base_api_url}/GrantAccess", - "channels": f"{base_api_url}/GetChannels", - "programme": f"{base_api_url}/GetProgramme", - "authorize_session": f"{base_api_url}/AuthorizeSession", - "catalogue_structure": f"{base_api_url}/GetCatalogueStructure", - "catalogue": f"{base_api_url}/GetCatalogue", - "vod_details": f"{base_api_url}/GetVodDetails", - "episodes": f"{base_api_url}/GetSeries", - "watch_later": f"{base_api_url}/GetWatchLater", - "editors_choice": f"{base_api_url}/GetEditorsChoice", - } - ) + # Update operator-specific settings if "operators" in config_data and config_data["operators"]: operator = config_data["operators"][0] if "playerMerchant" in operator: self.merchant = operator["playerMerchant"] + if "playerLicenseKey" in operator: + self.player_license_key = operator["playerLicenseKey"] if "selfcareUrl" in operator: - # Store selfcare URL if needed - pass + self.selfcare_url = operator["selfcareUrl"] + if "homepageUrl" in operator: + self.homepage_url = operator["homepageUrl"] + + # Build API endpoints from dynamic URLs + self.api_endpoints = self._build_api_endpoints() + + # Log what we got from API (debug) + import logging + logging.debug(f"HRTi config updated from API - hsapi_url: {self.hsapi_base_url}, " + f"web_api_url: {self.web_api_url}, merchant: {self.merchant}") except Exception as e: - # Log error but don't raise - use defaults if update fails import logging logging.debug(f"Error updating HRTi config from API: {e}") + # Fallback to build endpoints with defaults if API update fails + self._build_fallback_endpoints() + + def _build_fallback_endpoints(self): + """Build fallback API endpoints if dynamic config fails""" + # Use default hsapi URL if not set + if not self.hsapi_base_url: + self.hsapi_base_url = "https://hsapi.aviion.tv/client.svc/json" + + # Use default web API URL if not set + if not self.web_api_url: + self.web_api_url = "api/api/ott" + + # Use default merchant if not set + if not self.merchant: + self.merchant = "aviion2" + + self.api_endpoints = self._build_api_endpoints() def get_base_headers(self) -> dict: """Get base HTTP headers""" diff --git a/lib/streaming_providers/providers/hrti/provider.py b/lib/streaming_providers/providers/hrti/provider.py index 6828b66..b9eafb7 100644 --- a/lib/streaming_providers/providers/hrti/provider.py +++ b/lib/streaming_providers/providers/hrti/provider.py @@ -287,8 +287,13 @@ class HRTiProvider(StreamingProvider): f"{'Client ...' if 'authorization' in headers else 'NO AUTHORIZATION'}" ) + channels_endpoint = self.hrti_config.api_endpoints.get("channels", "") + if not channels_endpoint: + logger.error("No channels endpoint configured") + return [] + response = self.http_manager.post( - self.hrti_config.api_endpoints["channels"], + channels_endpoint, operation="api", headers=headers, data=json.dumps({}), @@ -539,8 +544,13 @@ class HRTiProvider(StreamingProvider): "EndTime": f"/Date({end_time})/", } + programme_endpoint = self.hrti_config.api_endpoints.get("programme", "") + if not programme_endpoint: + logger.error("No programme endpoint configured") + return [] + response = self.http_manager.post( - self.hrti_config.api_endpoints["programme"], + programme_endpoint, operation="api", headers=headers, data=json.dumps(payload),