hrti: dynamic endpoint config

This commit is contained in:
Nirvana
2026-06-05 12:39:47 +02:00
parent c93f2c8fe2
commit b16c839370
3 changed files with 183 additions and 99 deletions
+81 -49
View File
@@ -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 "<empty>"
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
@@ -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"""
@@ -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),