This commit is contained in:
Nirvana
2025-11-27 21:12:38 +01:00
parent c6ac391294
commit 3e25328e28
5 changed files with 1124 additions and 0 deletions
@@ -0,0 +1,10 @@
# [file name]: __init__.py
# [file content begin]
# lib/streaming_providers/providers/hrti/__init__.py
"""
HRTi streaming provider module
"""
from .provider import HRTiProvider
__all__ = ["HRTiProvider"]
# [file content end]
@@ -0,0 +1,406 @@
# [file name]: auth.py
# [file content begin]
# streaming_providers/providers/hrti/auth.py
import json
import base64
import time
from typing import Dict, Any, Optional
from datetime import datetime, timedelta
from ...base.auth.base_auth import BaseAuthenticator, BaseAuthToken, TokenAuthLevel
from ...base.utils.logger import logger
from .models import HRTiCredentials, HRTiAuthToken
from .constants import HRTiConfig
from ...base.models.proxy_models import ProxyConfig
class HRTiAuthenticator(BaseAuthenticator):
def __init__(self, credentials=None, config_dir=None,
proxy_config: Optional[ProxyConfig] = None, http_manager=None):
# Initialize configuration FIRST
self._config = HRTiConfig()
# Get proxy_config if not provided
if proxy_config is None:
from ...base.network import ProxyConfigManager
proxy_mgr = ProxyConfigManager(config_dir)
proxy_config = proxy_mgr.get_proxy_config('hrti')
# Store HTTP manager and proxy config locally (like MagentaEU)
self._http_manager = http_manager
self._proxy_config = proxy_config
# Require http_manager like MagentaEU does
if http_manager is None:
raise ValueError("http_manager is required for HRTiAuthenticator")
# Call parent init WITHOUT proxy_config and http_manager (like MagentaEU)
super().__init__(
provider_name='hrti',
credentials=credentials,
country='HR', # Default country for HRTi
config_dir=config_dir
# NO proxy_config or http_manager passed to parent
)
# Initialize HRTi-specific properties
self._ip_address = None
self._device_id = None
self._user_id = None
# Load or initialize device ID
self._initialize_device()
@property
def config(self):
"""Safe access to config"""
return self._config
@property
def http_manager(self):
"""Safe access to http_manager - required by provider"""
if self._http_manager is None:
# This should never happen since we validate in __init__
raise ValueError("HTTP manager not available - this should have been set during initialization")
return self._http_manager
@http_manager.setter
def http_manager(self, value):
"""Allow setting http_manager"""
self._http_manager = value
@property
def auth_endpoint(self) -> str:
"""HRTi authentication endpoint"""
return self.config.api_endpoints['grant_access']
def _get_auth_headers(self) -> Dict[str, str]:
"""Get headers for authentication requests"""
return self.config.get_base_headers()
def _build_auth_payload(self) -> Dict[str, Any]:
"""Build authentication payload from credentials"""
return self.credentials.to_auth_payload()
def _create_token_from_response(self, response_data: Dict[str, Any]) -> HRTiAuthToken:
"""Create HRTi-specific token from API response"""
result = response_data.get('Result', {})
# Store user ID if available
if 'Customer' in result:
self._user_id = result['Customer'].get('CustomerId', '')
return HRTiAuthToken(
access_token=result.get('Token', ''),
token_type='Client',
expires_in=86400, # 24 hours default
issued_at=time.time(),
user_id=self._user_id,
valid_from=result.get('ValidFrom', ''),
valid_to=result.get('ValidTo', '')
)
def get_fallback_credentials(self):
"""Get fallback credentials (anonymous access)"""
return HRTiCredentials(
username='anonymoushrt',
password='an0nPasshrt'
)
def _perform_authentication(self) -> HRTiAuthToken:
"""Perform HRTi custom authentication flow"""
logger.info("Starting HRTi authentication flow")
# Step 1: Get IP address
self._get_ip_address()
# Step 2: Get environment configuration
self._get_environment_config()
# Step 3: Perform grant access
token_data = self._perform_grant_access()
# Step 4: Register device
self._register_device()
# Step 5: Get content rating and profiles
self._get_initial_data()
token = self._create_token_from_response(token_data)
logger.info("HRTi authentication successful")
return token
def _get_ip_address(self) -> str:
"""Get public IP address"""
if self._ip_address:
return self._ip_address
try:
# Use the http_manager that's guaranteed to be available
response = self.http_manager.get(
self.config.api_endpoints['get_ip'],
operation='api'
)
response.raise_for_status()
self._ip_address = response.text.strip()
logger.debug(f"Retrieved IP address: {self._ip_address}")
return self._ip_address
except Exception as e:
logger.error(f"Error getting IP address: {e}")
self._ip_address = "0.0.0.0" # Fallback
return self._ip_address
def _get_environment_config(self):
"""Get HRTi environment configuration"""
try:
# Ensure we have http_manager
if not self.http_manager:
raise Exception("HTTP manager not available")
# Get env config
env_response = self.http_manager.get(
self.config.env_endpoint,
operation='api'
)
env_response.raise_for_status()
env_data = env_response.json()
# Get main config
config_response = self.http_manager.get(
self.config.config_endpoint,
operation='api'
)
config_response.raise_for_status()
config_data = config_response.json()
# Update config with retrieved values
self.config.update_from_api(env_data, config_data)
logger.debug("HRTi environment configuration loaded")
except Exception as e:
logger.warning(f"Error loading HRTi environment config: {e}")
def _perform_grant_access(self) -> Dict[str, Any]:
"""Perform grant access authentication"""
try:
# Ensure we have http_manager
if not self.http_manager:
raise Exception("HTTP manager not available")
headers = self._get_auth_headers()
payload = self._build_auth_payload()
response = self.http_manager.post(
self.auth_endpoint,
operation='auth',
headers=headers,
data=json.dumps(payload)
)
response.raise_for_status()
result = response.json()
if 'Result' not in result:
raise Exception("No result in grant access response")
return result
except Exception as e:
logger.error(f"HRTi grant access failed: {e}")
# Try fallback credentials
if not isinstance(self.credentials, HRTiCredentials) or self.credentials.username != 'anonymoushrt':
logger.info("Falling back to anonymous credentials")
self.credentials = self.get_fallback_credentials()
return self._perform_grant_access()
else:
raise e
def _register_device(self):
"""Register device with HRTi"""
try:
# Ensure we have http_manager
if not self.http_manager:
raise Exception("HTTP manager not available")
headers = self.config.get_auth_headers(
device_id=self._device_id,
ip_address=self._ip_address,
token=self._current_token.access_token if self._current_token else ''
)
payload = {
"DeviceSerial": self._device_id,
"DeviceReferenceId": self.config.device_reference_id,
"IpAddress": self._ip_address,
"ConnectionType": self.config.connection_type,
"ApplicationVersion": self.config.application_version,
"DrmId": self._device_id,
"OsVersion": self.config.os_version,
"ClientType": self.config.client_type
}
response = self.http_manager.post(
self.config.api_endpoints['register_device'],
operation='api',
headers=headers,
data=json.dumps(payload)
)
response.raise_for_status()
logger.debug("HRTi device registration successful")
except Exception as e:
logger.warning(f"HRTi device registration failed: {e}")
def _get_initial_data(self):
"""Get initial content rating and profiles"""
try:
# Ensure we have http_manager
if not self.http_manager:
raise Exception("HTTP manager not available")
headers = self.config.get_auth_headers(
device_id=self._device_id,
ip_address=self._ip_address,
token=self._current_token.access_token if self._current_token else ''
)
# 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()
# 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()
logger.debug("HRTi initial data loaded")
except Exception as e:
logger.debug(f"Error loading HRTi initial data: {e}")
def _initialize_device(self):
"""Initialize or load device ID"""
try:
self._device_id = self.settings_manager.get_device_id(self.provider_name, self.country)
if not self._device_id:
import uuid
self._device_id = str(uuid.uuid4())
logger.debug(f"Generated new device ID: {self._device_id}")
except Exception as e:
logger.error(f"Error initializing device ID: {e}")
import uuid
self._device_id = str(uuid.uuid4())
def get_device_id(self) -> str:
"""Get device ID"""
return self._device_id
def get_ip_address(self) -> str:
"""Get IP address"""
if not self._ip_address:
self._get_ip_address()
return self._ip_address
def authorize_session(self, content_type: str, content_ref_id: str,
channel_id: str = None, **kwargs) -> Optional[Dict[str, Any]]:
"""Authorize a playback session"""
try:
# Ensure we have http_manager
if not self.http_manager:
raise Exception("HTTP manager not available")
headers = self.config.get_auth_headers(
device_id=self._device_id,
ip_address=self._ip_address,
token=self._current_token.access_token if self._current_token else ''
)
payload = {
"ContentType": content_type,
"ContentReferenceId": content_ref_id,
"ContentDrmId": f"{content_ref_id}_drm",
"VideostoreReferenceIds": kwargs.get('video_store_ids', []),
"ChannelReferenceId": channel_id,
"StartTime": kwargs.get('start_time'),
"EndTime": kwargs.get('end_time')
}
response = self.http_manager.post(
self.config.api_endpoints['authorize_session'],
operation='api',
headers=headers,
data=json.dumps(payload)
)
response.raise_for_status()
result = response.json()
if 'Result' in result:
logger.debug("HRTi session authorization successful")
return result['Result']
else:
logger.warning("No result in session authorization response")
return None
except Exception as e:
logger.error(f"HRTi session authorization failed: {e}")
return None
def get_license_data(self, session_id: str) -> str:
"""Generate license data for DRM"""
try:
drm_license = {
'userId': self._user_id or '',
'sessionId': session_id,
'merchant': self.config.merchant
}
license_bytes = json.dumps(drm_license).encode('utf-8')
license_b64 = base64.b64encode(license_bytes).decode('utf-8')
return license_b64
except Exception as e:
logger.error(f"Error generating license data: {e}")
return ""
@staticmethod
def get_time_offset(hours_offset: int) -> int:
"""Get timestamp with offset in milliseconds"""
target_time = datetime.now() + timedelta(hours=hours_offset)
return int(target_time.timestamp() * 1000)
def _classify_token(self, token: BaseAuthToken) -> TokenAuthLevel:
"""Classify HRTi token authentication level"""
if not token or not token.access_token:
return TokenAuthLevel.ANONYMOUS
# Check if using anonymous credentials
if (hasattr(self.credentials, 'username') and
self.credentials.username == 'anonymoushrt'):
return TokenAuthLevel.ANONYMOUS
# If we have a valid token and user credentials, consider it user authenticated
if (hasattr(self.credentials, 'username') and
self.credentials.username and
self.credentials.username != 'anonymoushrt'):
return TokenAuthLevel.USER_AUTHENTICATED
return TokenAuthLevel.CLIENT_CREDENTIALS
def _refresh_token(self) -> Optional[BaseAuthToken]:
"""Refresh HRTi token - reauthenticate since it's custom auth"""
logger.info("Refreshing HRTi token via reauthentication")
try:
return self._perform_authentication()
except Exception as e:
logger.error(f"HRTi token refresh failed: {e}")
return None
# [file content end]
@@ -0,0 +1,157 @@
# [file name]: constants.py
# [file content begin]
# streaming_providers/providers/hrti/constants.py
"""
HRTi provider constants and default configurations
"""
class HRTiDefaults:
"""Default values for HRTi provider"""
# Provider information
PROVIDER_LOGO = 'https://hrti.hrt.hr/assets/images/logo.png'
PROVIDER_NAME = 'HRTi'
# 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',
'register_device': f'{HSAPI_BASE_URL}/RegisterDevice',
'content_ratings': f'{HSAPI_BASE_URL}/ContentRatingsGet',
'profiles': f'{HSAPI_BASE_URL}/ProfilesGet'
}
# DRM and License endpoints
LICENSE_URL = 'https://lic.drmtoday.com/license-proxy-widevine/cenc/'
# Device information
DEVICE_REFERENCE_ID = '6'
OPERATOR_REFERENCE_ID = 'hrt'
MERCHANT = 'aviion2'
CONNECTION_TYPE = 'LAN/WiFi'
APPLICATION_VERSION = '5.62.5'
OS_VERSION = 'Linux'
CLIENT_TYPE = 'Chrome 96'
# User Agent
USER_AGENT = 'kodi plugin for hrti.hrt.hr (python)'
# HTTP settings
DEFAULT_TIMEOUT = 30
class HRTiConfig:
"""Configuration class for HRTi provider"""
def __init__(self, config_dict: dict = None):
"""Initialize with optional configuration overrides"""
config = config_dict or {}
self.logo = config.get('logo', HRTiDefaults.PROVIDER_LOGO)
# 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)
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())
# DRM and License
self.license_url = config.get('license_url', HRTiDefaults.LICENSE_URL)
# Device configuration
self.device_reference_id = config.get('device_reference_id', HRTiDefaults.DEVICE_REFERENCE_ID)
self.operator_reference_id = config.get('operator_reference_id', HRTiDefaults.OPERATOR_REFERENCE_ID)
self.merchant = config.get('merchant', HRTiDefaults.MERCHANT)
self.connection_type = config.get('connection_type', HRTiDefaults.CONNECTION_TYPE)
self.application_version = config.get('application_version', HRTiDefaults.APPLICATION_VERSION)
self.os_version = config.get('os_version', HRTiDefaults.OS_VERSION)
self.client_type = config.get('client_type', HRTiDefaults.CLIENT_TYPE)
# HTTP settings
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')
def update_from_api(self, env_data: dict, config_data: dict):
"""Update configuration from API responses"""
try:
# Update from env data
if 'applicationVersion' in env_data:
self.application_version = env_data['applicationVersion']
# Update from config data
if 'apiUrl' in config_data:
self.hsapi_base_url = config_data['apiUrl']
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"
})
if 'operators' in config_data and config_data['operators']:
operator = config_data['operators'][0]
if 'playerMerchant' in operator:
self.merchant = operator['playerMerchant']
if 'selfcareUrl' in operator:
# Store selfcare URL if needed
pass
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}")
def get_base_headers(self) -> dict:
"""Get base HTTP headers"""
return {
'User-Agent': self.user_agent,
'Accept': 'application/json',
'Content-Type': 'application/json'
}
def get_auth_headers(self, device_id: str = None, ip_address: str = None, token: str = None) -> dict:
"""Get authenticated headers for API requests"""
headers = self.get_base_headers()
if device_id:
headers['deviceid'] = device_id
if ip_address:
headers['ipaddress'] = ip_address
if token:
headers['authorization'] = f'Client {token}'
headers.update({
'operatorreferenceid': self.operator_reference_id,
'devicetypeid': self.device_reference_id,
'origin': self.base_website,
'referer': self.base_website
})
return headers
# [file content end]
@@ -0,0 +1,225 @@
# [file name]: models.py
# [file content begin]
# streaming_providers/providers/hrti/models.py
from dataclasses import dataclass
from typing import Dict, Any, Optional, List
from datetime import datetime
from ...base.auth.base_auth import BaseAuthToken
from ...base.auth.credentials import UserPasswordCredentials
from .constants import HRTiDefaults
@dataclass
class HRTiCredentials(UserPasswordCredentials):
"""
HRTi specific username/password credentials
"""
def __init__(self, username: str, password: str):
super().__init__(username=username, password=password)
self.credential_type = 'hrti_user'
def to_auth_payload(self) -> Dict[str, Any]:
"""Convert to authentication payload for HRTi"""
return {
"Username": self.username,
"Password": self.password,
"OperatorReferenceId": HRTiDefaults.OPERATOR_REFERENCE_ID
}
def validate(self) -> bool:
"""Validate HRTi credentials"""
return bool(self.username and self.password)
class HRTiAuthToken(BaseAuthToken):
"""
HRTi specific authentication token
"""
def __init__(self, access_token: str, token_type: str, expires_in: int,
issued_at: float, user_id: str = '', valid_from: str = '',
valid_to: str = '', refresh_token: Optional[str] = None):
super().__init__(
access_token=access_token,
token_type=token_type,
expires_in=expires_in,
issued_at=issued_at,
refresh_token=refresh_token
)
# HRTi specific fields
self.user_id = user_id
self.valid_from = valid_from
self.valid_to = valid_to
self.credential_type = 'hrti'
def to_dict(self) -> Dict[str, Any]:
"""Convert token to dictionary"""
base_dict = super().to_dict()
base_dict.update({
'user_id': self.user_id,
'valid_from': self.valid_from,
'valid_to': self.valid_to,
'credential_type': self.credential_type
})
return base_dict
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'HRTiAuthToken':
"""Create token from dictionary"""
return cls(
access_token=data['access_token'],
token_type=data.get('token_type', 'Client'),
expires_in=data.get('expires_in', 86400),
issued_at=data.get('issued_at', 0),
user_id=data.get('user_id', ''),
valid_from=data.get('valid_from', ''),
valid_to=data.get('valid_to', ''),
refresh_token=data.get('refresh_token')
)
def is_valid(self) -> bool:
"""Check if token is still valid"""
import time
if not self.access_token:
return False
# Basic expiration check
current_time = time.time()
buffer_time = 300 # 5 minutes buffer
return current_time < (self.issued_at + self.expires_in - buffer_time)
@dataclass
class HRTiChannel:
"""
HRTi channel information
"""
id: str
name: str
reference_id: str
streaming_url: str
icon_url: str
is_radio: bool = False
description: Optional[str] = None
sort_order: int = 0
def __post_init__(self):
"""Validate channel data after initialization"""
if not self.id or not self.name or not self.reference_id:
raise ValueError("Channel must have id, name, and reference_id")
@classmethod
def from_api_response(cls, data: Dict[str, Any]) -> 'HRTiChannel':
"""Create channel from API response data"""
return cls(
id=data.get('ReferenceId', ''),
name=data.get('Name', ''),
reference_id=data.get('ReferenceId', ''),
streaming_url=data.get('StreamingURL', ''),
icon_url=data.get('Icon', ''),
is_radio=data.get('Radio', False),
description=data.get('Description'),
sort_order=data.get('SortOrder', 0)
)
def to_dict(self) -> Dict[str, Any]:
"""Convert channel to dictionary"""
return {
'id': self.id,
'name': self.name,
'reference_id': self.reference_id,
'streaming_url': self.streaming_url,
'icon_url': self.icon_url,
'is_radio': self.is_radio,
'description': self.description,
'sort_order': self.sort_order
}
@dataclass
class HRTiEpgEntry:
"""
HRTi EPG (Electronic Program Guide) entry
"""
reference_id: str
title: str
description_short: str
description_long: str
start_time: str
end_time: str
image_url: str
channel_reference_id: str
content_rating: Optional[str] = None
episode_number: Optional[str] = None
season_number: Optional[str] = None
@classmethod
def from_api_response(cls, data: Dict[str, Any], channel_id: str) -> 'HRTiEpgEntry':
"""Create EPG entry from API response"""
return cls(
reference_id=data.get('ReferenceId', ''),
title=data.get('Title', ''),
description_short=data.get('DescriptionShort', ''),
description_long=data.get('DescriptionLong', ''),
start_time=data.get('TimeStart', ''),
end_time=data.get('TimeEnd', ''),
image_url=data.get('ImagePath', ''),
channel_reference_id=channel_id,
content_rating=data.get('ContentRating'),
episode_number=data.get('EpisodeNr'),
season_number=data.get('SeasonNr')
)
def to_dict(self) -> Dict[str, Any]:
"""Convert EPG entry to dictionary"""
return {
'reference_id': self.reference_id,
'title': self.title,
'description_short': self.description_short,
'description_long': self.description_long,
'start_time': self.start_time,
'end_time': self.end_time,
'image_url': self.image_url,
'channel_reference_id': self.channel_reference_id,
'content_rating': self.content_rating,
'episode_number': self.episode_number,
'season_number': self.season_number
}
@dataclass
class HRTiSession:
"""
HRTi playback session information
"""
session_id: str
authorized: bool
drm_id: str
channel_reference_id: str
content_reference_id: str
@classmethod
def from_api_response(cls, data: Dict[str, Any]) -> 'HRTiSession':
"""Create session from API response"""
return cls(
session_id=data.get('SessionId', ''),
authorized=data.get('Authorized', False),
drm_id=data.get('DrmId', ''),
channel_reference_id=data.get('ChannelReferenceId', ''),
content_reference_id=data.get('ContentReferenceId', '')
)
def to_dict(self) -> Dict[str, Any]:
"""Convert session to dictionary"""
return {
'session_id': self.session_id,
'authorized': self.authorized,
'drm_id': self.drm_id,
'channel_reference_id': self.channel_reference_id,
'content_reference_id': self.content_reference_id
}
# [file content end]
@@ -0,0 +1,326 @@
# [file name]: provider.py
# [file content begin]
# lib/streaming_providers/providers/hrti/provider.py
import json
import requests
from typing import Dict, List, Optional
from ...base.provider import StreamingProvider
from ...base.models.streaming_channel import StreamingChannel
from ...base.models import DRMConfig, LicenseConfig, DRMSystem
from .auth import HRTiAuthenticator
from .constants import HRTiConfig
from ...base.utils import logger
from ...base.models.proxy_models import ProxyConfig
from ...base.network import HTTPManagerFactory
class HRTiProvider(StreamingProvider):
def __init__(self, country: str = 'HR', config: Optional[Dict] = None, proxy_config: Optional[ProxyConfig] = None):
super().__init__(country)
# Initialize configuration with overrides
self.hrti_config = HRTiConfig(config)
self.channels_cache = None
# Create HTTP manager
if proxy_config is None:
from ...base.network import ProxyConfigManager
proxy_mgr = ProxyConfigManager()
proxy_config = proxy_mgr.get_proxy_config('hrti')
self.http_manager = HTTPManagerFactory.create_for_provider(
'hrti',
proxy_config=proxy_config,
user_agent=self.hrti_config.user_agent,
timeout=self.hrti_config.timeout
)
# Initialize authenticator and share HTTP manager
self.auth = HRTiAuthenticator(
proxy_config=proxy_config,
http_manager=self.http_manager
)
# Share HTTP manager for consistency
self.http_manager = self.auth.http_manager
try:
# Initialize authentication
bearer_token = self.auth.get_bearer_token()
logger.debug(f"HRTi authentication successful during initialization")
except Exception as e:
logger.warning(f"HRTi could not authenticate during initialization: {e}")
@property
def provider_name(self) -> str:
return "hrti"
@property
def provider_label(self) -> str:
return 'HRTi'
@property
def provider_logo(self) -> str:
return self.hrti_config.logo
@property
def uses_dynamic_manifests(self) -> bool:
# HRTi requires session authorization for manifests
return True
def _get_authenticated_headers(self) -> Dict[str, str]:
"""
Get headers with HRTi authentication
"""
bearer_token = self.auth.get_bearer_token()
device_id = self.auth.get_device_id()
ip_address = self.auth.get_ip_address()
return self.hrti_config.get_auth_headers(
device_id=device_id,
ip_address=ip_address,
token=bearer_token
)
def get_channels(self, **kwargs) -> List[StreamingChannel]:
"""
Fetch channels from HRTi API
"""
try:
headers = self._get_authenticated_headers()
response = self.http_manager.post(
self.hrti_config.api_endpoints['channels'],
operation='api',
headers=headers,
data=json.dumps({})
)
response.raise_for_status()
channels_data = response.json()
if 'Result' in channels_data:
channels = []
for channel in channels_data['Result']:
streaming_channel = self._parse_channel_data(channel)
if streaming_channel:
channels.append(streaming_channel)
self.channels = channels
return channels
else:
logger.warning("No channels found in HRTi response")
return []
except requests.RequestException as e:
logger.error(f"Error fetching HRTi channels: {e}")
# Try to refresh auth and retry once
try:
logger.info("Attempting to refresh authentication and retry...")
self.auth.invalidate_token()
headers = self._get_authenticated_headers()
response = self.http_manager.post(
self.hrti_config.api_endpoints['channels'],
operation='api',
headers=headers,
data=json.dumps({})
)
response.raise_for_status()
channels_data = response.json()
if 'Result' in channels_data:
channels = []
for channel in channels_data['Result']:
streaming_channel = self._parse_channel_data(channel)
if streaming_channel:
channels.append(streaming_channel)
self.channels = channels
return channels
except Exception as retry_e:
logger.error(f"Retry failed: {retry_e}")
return []
except Exception as e:
logger.error(f"Error parsing HRTi channels: {e}")
return []
def _parse_channel_data(self, channel_data: Dict) -> Optional[StreamingChannel]:
"""
Parse HRTi channel data to StreamingChannel
"""
try:
name = channel_data.get('Name', '')
channel_id = channel_data.get('ReferenceId', '')
streaming_url = channel_data.get('StreamingURL', '')
is_radio = channel_data.get('Radio', False)
icon_url = channel_data.get('Icon', '')
if not name or not channel_id:
return None
# Create channel object
channel = StreamingChannel(
name=name,
channel_id=channel_id,
provider=self.provider_name,
logo_url=icon_url,
mode="live",
session_manifest=True, # HRTi requires session authorization
manifest=None, # Will be set dynamically
manifest_script=streaming_url, # Store streaming URL for manifest fetching
content_type="AUDIO" if is_radio else "LIVE",
country=self.country,
language="hr" # Croatian
)
# HRTi uses DRM for most content
channel.use_cdm = True
channel.cdm_type = "widevine"
return channel
except Exception as e:
logger.error(f"Error parsing channel {channel_data}: {e}")
return None
def enrich_channel_data(self, channel: StreamingChannel, **kwargs) -> Optional[StreamingChannel]:
"""
Enrich channel with manifest URL and additional data
"""
try:
# For HRTi, we need to authorize a session to get the manifest
manifest_url = self.get_manifest(channel.channel_id, **kwargs)
if manifest_url:
# HRTi manifests are dynamic and session-based
channel.set_dynamic_manifest(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
return channel
else:
logger.warning(f"Could not fetch manifest for channel {channel.name} ({channel.channel_id})")
return channel
except Exception as e:
logger.error(f"Error enriching channel data for {channel.name}: {e}")
return channel
def get_manifest(self, channel_id: str, **kwargs) -> Optional[str]:
"""
Get manifest URL for a channel by authorizing a session
"""
try:
# Authorize session for this channel
session_data = self.auth.authorize_session(
content_type="tlive", # TV live
content_ref_id=channel_id,
channel_id=channel_id
)
if session_data and session_data.get('Authorized', False):
# For live channels, use the streaming URL from channel data
# The actual manifest will be resolved during playback with session authorization
channels = self.get_channels()
for channel in channels:
if channel.channel_id == channel_id:
return channel.manifest_script # This is the streaming URL
logger.warning(f"Session authorization failed for channel {channel_id}")
return None
except Exception as e:
logger.error(f"Error getting manifest for channel {channel_id}: {e}")
return None
# In provider.py - fix the get_drm method:
def get_drm(self, channel_id: str, **kwargs) -> List[DRMConfig]:
"""
Get DRM configurations for a channel
"""
try:
# Use license URL from constants
license_url = self.hrti_config.license_url
drm_config = DRMConfig(
system=DRMSystem.WIDEVINE,
priority=1,
license=LicenseConfig(
server_url=license_url,
req_headers=json.dumps({
'User-Agent': self.hrti_config.user_agent,
'Content-Type': 'text/plain',
'origin': self.hrti_config.base_website,
'referer': self.hrti_config.base_website
}),
req_data="{CHA-RAW}",
use_http_get_request=False
)
)
return [drm_config]
except Exception as e:
logger.error(f"Error getting DRM config for channel {channel_id}: {e}")
return []
def get_epg_data(self, channel_id: str, **kwargs) -> Optional[Dict]:
"""
Get EPG data for a channel
"""
try:
headers = self._get_authenticated_headers()
# Get current time range (4 hours before and after)
start_time = self.auth.get_time_offset(-4)
end_time = self.auth.get_time_offset(4)
payload = {
"ChannelReferenceIds": [channel_id],
"StartTime": f"/Date({start_time})/",
"EndTime": f"/Date({end_time})/"
}
response = self.http_manager.post( # FIXED: Use http_manager
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 epg_data['Result']
return None
except Exception as e:
logger.error(f"Error getting EPG data for channel {channel_id}: {e}")
return None
def get_license_url(self, channel: StreamingChannel, **kwargs) -> Optional[str]:
"""
Get license URL for a DRM-protected channel
"""
drm_configs = self.get_drm(channel.channel_id, **kwargs)
if drm_configs:
return drm_configs[0].license.server_url
return None
# [file content end]