mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-24 18:12:32 +02:00
Add hrti
This commit is contained in:
@@ -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]
|
||||
Reference in New Issue
Block a user