diff --git a/README.md b/README.md
index aa49565..9ae4a20 100644
--- a/README.md
+++ b/README.md
@@ -22,13 +22,20 @@ Once configured, your live TV channels from streaming platforms appear directly
## 📺 Supported Providers
Currently supported:
+
- 🇩🇪 **Joyn (DE)**
- 🇦🇹 **Joyn (AT)**
- 🇨🇠**Joyn (CH)**
- 🇩🇪 **RTL+**
+- 🇦🇹 **Magenta TV (AT)**
+- ðŸ‡ðŸ‡· **Max TV (HR)**
+- 🇵🇱 **Magenta TV (PL)**
+- 🇲🇪 **Magenta TV (ME)**
+- ðŸ‡ðŸ‡º **Magenta TV (HU)**
More providers will be added in future versions.
+
---
## ✨ Key Features
diff --git a/lib/streaming_providers/base/auth/base_auth.py b/lib/streaming_providers/base/auth/base_auth.py
index 814bc2a..58fd888 100644
--- a/lib/streaming_providers/base/auth/base_auth.py
+++ b/lib/streaming_providers/base/auth/base_auth.py
@@ -380,17 +380,17 @@ class BaseAuthenticator(ABC):
return self.credentials is not None and self.credentials.validate()
- # Authentication methods remain mostly unchanged
def authenticate(self, force_refresh: bool = False) -> BaseAuthToken:
"""Authenticate and get access token with persistent storage"""
country_str = f" (country: {self.country})" if self.country else ""
logger.debug(f"[{self.provider_name}{country_str}] Starting authentication, force_refresh={force_refresh}")
- # Check current token state for debugging
+ # DEBUG: Log current token state
if self._current_token:
- logger.debug(
- f"[{self.provider_name}{country_str}] Current token - is_expired: {self._current_token.is_expired}, "
- f"has_refresh: {bool(self._current_token.refresh_token)}")
+ logger.debug(f"[{self.provider_name}{country_str}] Current token state - "
+ f"is_expired: {self._current_token.is_expired}, "
+ f"has_refresh: {bool(self._current_token.refresh_token)}, "
+ f"needs_refresh: {self._current_token.needs_refresh()}")
else:
logger.debug(f"[{self.provider_name}{country_str}] No current token")
@@ -399,15 +399,27 @@ class BaseAuthenticator(ABC):
logger.info(f"[{self.provider_name}{country_str}] Using existing valid token")
return self._current_token
- # 2. Try refresh if available
- if (not force_refresh and
+ # 2. ENHANCED REFRESH LOGIC: Attempt refresh if we have a token with refresh capability
+ # This covers both: tokens that need refresh AND expired tokens that can be refreshed
+ should_attempt_refresh = (
+ not force_refresh and
self._current_token and
self._current_token.refresh_token and
- self._current_token.needs_refresh()):
+ (self._current_token.needs_refresh() or self._current_token.is_expired)
+ )
+ logger.debug(f"[{self.provider_name}{country_str}] Refresh decision - "
+ f"should_attempt_refresh: {should_attempt_refresh}, "
+ f"force_refresh: {force_refresh}, "
+ f"has_token: {bool(self._current_token)}, "
+ f"has_refresh_token: {bool(self._current_token.refresh_token if self._current_token else False)}, "
+ f"needs_refresh: {self._current_token.needs_refresh() if self._current_token else False}, "
+ f"is_expired: {self._current_token.is_expired if self._current_token else False}")
+
+ if should_attempt_refresh:
logger.info(f"[{self.provider_name}{country_str}] Attempting token refresh")
try:
- refreshed_token = self._refresh_token()
+ refreshed_token = self._refresh_token() # Provider-specific implementation
logger.debug(f"[{self.provider_name}{country_str}] Refresh result: {refreshed_token is not None}")
if refreshed_token:
@@ -416,11 +428,9 @@ class BaseAuthenticator(ABC):
logger.info(f"[{self.provider_name}{country_str}] Token refresh successful")
return self._current_token
else:
- logger.debug(
- f"[{self.provider_name}{country_str}] Refresh returned None, falling back to full auth")
+ logger.debug(f"[{self.provider_name}{country_str}] Refresh failed, falling back to full auth")
except Exception as e:
- logger.warning(
- f"[{self.provider_name}{country_str}] Token refresh failed: {e}, attempting new authentication")
+ logger.warning(f"[{self.provider_name}{country_str}] Token refresh failed: {e}")
# 3. Ensure we have credentials before attempting full authentication
if not self._ensure_credentials():
diff --git a/lib/streaming_providers/base/auth/session_manager.py b/lib/streaming_providers/base/auth/session_manager.py
index ed5feca..569f1a0 100644
--- a/lib/streaming_providers/base/auth/session_manager.py
+++ b/lib/streaming_providers/base/auth/session_manager.py
@@ -258,16 +258,7 @@ class SessionManager:
return False
def load_token_data(self, provider: str, country: Optional[str] = None) -> Optional[Dict[str, Any]]:
- """
- Load token data for a provider
-
- Args:
- provider: Provider name
- country: Optional country code
-
- Returns:
- Dictionary with token data or None if not found/expired
- """
+ """Load token data for a provider"""
country_str = f" (country: {country})" if country else ""
logger.debug(f"Loading token data for {provider}{country_str}")
@@ -294,10 +285,20 @@ class SessionManager:
f"expires_in={expires_in}, current_time={current_time}, "
f"time_until_expiry={time_until_expiry:.0f}")
- if current_time >= (expires_at - 300): # 5 minute buffer
- logger.info(f"Token expired for {provider}{country_str} "
- f"(expired {abs(time_until_expiry):.0f}s ago)")
- return None
+ is_expired = current_time >= (expires_at - 300) # 5 minute buffer
+ has_refresh_token = bool(session_data.get('refresh_token'))
+
+ logger.debug(f"Token status - is_expired: {is_expired}, has_refresh_token: {has_refresh_token}")
+
+ if is_expired:
+ if has_refresh_token:
+ # Token is expired BUT we have a refresh token - return the data so refresh can be attempted
+ logger.info(
+ f"Token expired for {provider}{country_str} but refresh token available - returning data for refresh")
+ return session_data
+ else:
+ logger.info(f"Token expired for {provider}{country_str} and no refresh token available")
+ return None
logger.info(f"Loaded valid token data for {provider}{country_str} "
f"(expires in {time_until_expiry:.0f}s, "
diff --git a/lib/streaming_providers/base/manager.py b/lib/streaming_providers/base/manager.py
index 35e581d..a6b2621 100644
--- a/lib/streaming_providers/base/manager.py
+++ b/lib/streaming_providers/base/manager.py
@@ -1,7 +1,7 @@
# streaming_providers/base/manager.py
from typing import Dict, List, Optional
from .provider import StreamingProvider
-from .models import StreamingChannel
+from .models import StreamingChannel, DRMSystem
from .drm import DRMPluginManager
from .utils.logger import logger
@@ -339,48 +339,51 @@ class ProviderManager:
return xmltv_data
def get_channel_drm_configs(self, provider_name: str, channel_id: str, **kwargs) -> List:
- """
- Get DRM configurations for a specific channel from a provider.
- DRM configs are processed through registered plugins before being returned.
-
- Args:
- provider_name: Name of the provider
- channel_id: ID of the channel
- **kwargs: Additional arguments (e.g., country)
-
- Returns:
- List of DRM configuration objects (processed by plugins if available)
-
- Raises:
- ValueError: If provider not found
- """
provider = self.get_provider(provider_name)
if not provider:
logger.error(f"ProviderManager: Cannot get DRM configs - provider '{provider_name}' not found")
raise ValueError(f"Provider '{provider_name}' not found")
logger.debug(f"ProviderManager: Getting DRM configs for channel '{channel_id}' from provider '{provider_name}'")
-
+
# Get raw DRM configs from provider
drm_configs = provider.get_drm_configs_by_id(channel_id, **kwargs)
logger.debug(f"ProviderManager: Retrieved {len(drm_configs)} raw DRM configs")
-
- # Get manifest URL for the channel
- manifest_url = provider.get_manifest(channel_id, **kwargs)
-
- # Extract PSSH data from manifest if available
+
+ # Check if we need PSSH data at all
pssh_data_list = []
- if manifest_url:
- logger.debug(f"ProviderManager: Extracting PSSH data from manifest")
- try:
- pssh_data_list = self._extract_pssh_from_manifest(manifest_url)
- logger.debug(f"ProviderManager: Extracted {len(pssh_data_list)} PSSH data entries")
- except Exception as e:
- logger.warning(f"ProviderManager: Could not extract PSSH data from manifest: {e}")
-
+ if drm_configs and self.drm_plugin_manager.plugins:
+ # Get DRM systems from configs
+ config_drm_systems = {config.system for config in drm_configs}
+ # Get DRM systems that have plugins (excluding GENERIC which processes all)
+ plugin_drm_systems = set(self.drm_plugin_manager.plugins.keys())
+
+ # Check if there's overlap OR if there's a GENERIC plugin (which processes everything)
+ needs_pssh = bool(
+ config_drm_systems & plugin_drm_systems or
+ DRMSystem.GENERIC in plugin_drm_systems
+ )
+
+ if needs_pssh:
+ manifest_url = provider.get_manifest(channel_id, **kwargs)
+ if manifest_url:
+ logger.debug(
+ f"ProviderManager: PSSH needed - extracting from manifest (matching systems: {config_drm_systems & plugin_drm_systems})")
+ try:
+ pssh_data_list = self._extract_pssh_from_manifest(manifest_url)
+ logger.debug(f"ProviderManager: Extracted {len(pssh_data_list)} PSSH data entries")
+ except Exception as e:
+ logger.warning(f"ProviderManager: Could not extract PSSH data from manifest: {e}")
+ else:
+ logger.debug(
+ f"ProviderManager: No matching plugins for DRM systems {config_drm_systems}, skipping PSSH extraction")
+ else:
+ logger.debug(f"ProviderManager: No configs or no plugins registered, skipping PSSH extraction")
+
# Process through DRM plugins with PSSH data
processed_configs = self.drm_plugin_manager.process_drm_configs(drm_configs, pssh_data_list, **kwargs)
- logger.info(f"ProviderManager: Processed DRM configs for channel '{channel_id}' - {len(processed_configs)} configs returned")
+ logger.info(
+ f"ProviderManager: Processed DRM configs for channel '{channel_id}' - {len(processed_configs)} configs returned")
return processed_configs
diff --git a/lib/streaming_providers/base/provider.py b/lib/streaming_providers/base/provider.py
index 521759d..9ec1b5c 100644
--- a/lib/streaming_providers/base/provider.py
+++ b/lib/streaming_providers/base/provider.py
@@ -69,18 +69,18 @@ class StreamingProvider(ABC):
"""
pass
- def get_drm_configs(self, channel: StreamingChannel, **kwargs) -> List[DRMConfig]:
- """
- Get all DRM configurations for a channel
+# def get_drm_configs(self, channel: StreamingChannel, **kwargs) -> List[DRMConfig]:
+# """
+# Get all DRM configurations for a channel
- Args:
- channel: Channel to get DRM configs for
- **kwargs: Additional parameters
+# Args:
+ # channel: Channel to get DRM configs for
+ # **kwargs: Additional parameters
- Returns:
- List of DRMConfig objects (can be empty if no DRM is used)
- """
- return []
+ # Returns:
+ # List of DRMConfig objects (can be empty if no DRM is used)
+ # """
+ # return []
def get_drm_configs_by_id(self, channel_id: str, **kwargs) -> List[DRMConfig]:
"""
diff --git a/lib/streaming_providers/providers/joyn/provider.py b/lib/streaming_providers/providers/joyn/provider.py
index 3f960d2..6cfd4d4 100644
--- a/lib/streaming_providers/providers/joyn/provider.py
+++ b/lib/streaming_providers/providers/joyn/provider.py
@@ -8,7 +8,7 @@ import urllib.parse
from datetime import datetime, timedelta
from base64 import b64decode
from json import dumps
-from urllib.parse import urlencode
+#from urllib.parse import urlencode
from ...base.provider import StreamingProvider
from ...base.models import DRMConfig, LicenseConfig, DRMSystem
@@ -550,50 +550,50 @@ class JoynProvider(StreamingProvider):
logger.error(f"Error getting manifest for {channel.name}: {e}")
return None
- def get_drm_configs(self,
- channel: StreamingChannel,
- needs_base64_wrap: bool = False,
- **kwargs) -> List[DRMConfig]:
- """
- Get DRM configuration for Joyn channels (Widevine)
+# def get_drm_configs(self,
+# channel: StreamingChannel,
+# needs_base64_wrap: bool = False,
+# **kwargs) -> List[DRMConfig]:
+# """
+# Get DRM configuration for Joyn channels (Widevine)
- Args:
- channel: StreamingChannel object
- needs_base64_wrap: Whether to wrap the license request in base64
- **kwargs: Additional parameters
+# Args:
+# channel: StreamingChannel object
+# needs_base64_wrap: Whether to wrap the license request in base64
+# **kwargs: Additional parameters
- Returns:
- List of DRMConfig objects (typically contains one Widevine config)
- """
- if not channel.license_url:
- return [] # No DRM if no license URL
+# Returns:
+# List of DRMConfig objects (typically contains one Widevine config)
+# """
+# if not channel.license_url:
+# return [] # No DRM if no license URL
- try:
- # Get fresh auth token if needed
- if not self.authenticator.is_authenticated():
- self.bearer_token = self.authenticator.authenticate()
+# try:
+# # Get fresh auth token if needed
+# if not self.authenticator.is_authenticated():
+# self.bearer_token = self.authenticator.authenticate()
- # Prepare license headers
- license_headers = DRM_REQUEST_HEADERS.copy()
- license_headers['Authorization'] = f"Bearer {self.bearer_token}"
+# # Prepare license headers
+# license_headers = DRM_REQUEST_HEADERS.copy()
+# license_headers['Authorization'] = f"Bearer {self.bearer_token}"
- return [
- DRMConfig(
- system=DRMSystem.WIDEVINE,
- priority=1, # Highest priority for Widevine
- license=LicenseConfig(
- server_url=channel.license_url,
- server_certificate=channel.certificate_url,
- req_headers=urlencode(license_headers),
- use_http_get_request=False,
- wrapper="base64" if needs_base64_wrap else None
- )
- )
- ]
+# return [
+# DRMConfig(
+# system=DRMSystem.WIDEVINE,
+# priority=1, # Highest priority for Widevine
+# license=LicenseConfig(
+# server_url=channel.license_url,
+# server_certificate=channel.certificate_url,
+# req_headers=urlencode(license_headers),
+# use_http_get_request=False,
+# wrapper="base64" if needs_base64_wrap else None
+# )
+# )
+# ]
- except Exception as e:
- logger.error(f"Error generating DRM config for {channel.name}: {e}")
- return []
+# except Exception as e:
+# logger.error(f"Error generating DRM config for {channel.name}: {e}")
+# return []
def get_manifest(self,
channel_id: str,
diff --git a/lib/streaming_providers/providers/magenta_eu/__init__.py b/lib/streaming_providers/providers/magentaeu/__init__.py
similarity index 79%
rename from lib/streaming_providers/providers/magenta_eu/__init__.py
rename to lib/streaming_providers/providers/magentaeu/__init__.py
index 170cb4b..8a2a07a 100644
--- a/lib/streaming_providers/providers/magenta_eu/__init__.py
+++ b/lib/streaming_providers/providers/magentaeu/__init__.py
@@ -1,6 +1,6 @@
# streaming_providers/providers/magenta_eu/__init__.py
from .provider import MagentaProvider
-from .auth import MagentaAuthenticator, MagentaAuthToken, MagentaCredentials
+from .auth import MagentaAuthenticator, MagentaAuthToken
from .constants import (
SUPPORTED_COUNTRIES,
DEFAULT_COUNTRY,
@@ -12,7 +12,6 @@ __all__ = [
'MagentaProvider',
'MagentaAuthenticator',
'MagentaAuthToken',
- 'MagentaCredentials',
'SUPPORTED_COUNTRIES',
'DEFAULT_COUNTRY',
'COUNTRY_CONFIG',
diff --git a/lib/streaming_providers/providers/magenta_eu/auth.py b/lib/streaming_providers/providers/magentaeu/auth.py
similarity index 55%
rename from lib/streaming_providers/providers/magenta_eu/auth.py
rename to lib/streaming_providers/providers/magentaeu/auth.py
index 00c7d20..bec0fc3 100644
--- a/lib/streaming_providers/providers/magenta_eu/auth.py
+++ b/lib/streaming_providers/providers/magentaeu/auth.py
@@ -1,4 +1,4 @@
-# streaming_providers/providers/magenta_eu/auth.py
+# streaming_providers/providers/magentaeu/auth.py
# -*- coding: utf-8 -*-
import uuid
import json
@@ -6,11 +6,17 @@ import base64
import time
from typing import Dict, Optional, Any
from dataclasses import dataclass, field
-from Crypto.Cipher import PKCS1_OAEP
-from Crypto.PublicKey import RSA
+# Updated imports for pycryptodome
+try:
+ # Try pycryptodome first (Kodi script.module.pycryptodome)
+ from Cryptodome.Cipher import PKCS1_OAEP
+ from Cryptodome.PublicKey import RSA
+except ImportError:
+ # Fallback to older pycrypto naming
+ from Crypto.Cipher import PKCS1_OAEP
+ from Crypto.PublicKey import RSA
from ...base.auth.base_auth import BaseAuthenticator, BaseAuthToken, TokenAuthLevel
-from ...base.auth.credentials import UserPasswordCredentials
from ...base.models.proxy_models import ProxyConfig
from ...base.utils.logger import logger
from .constants import (
@@ -36,10 +42,9 @@ from .constants import (
CALL_TYPES,
MANAGE_DEVICE,
BROADCASTING_STREAM_LIMITATION_APPLIES,
- get_base_url,
get_bifrost_url,
- get_natco_key,
- get_app_key,
+ get_base_url,
+ get_base_headers,
get_language
)
@@ -49,10 +54,10 @@ class InvalidTokenError(Exception):
pass
-def base64url_decode(input: str) -> bytes:
+def base64url_decode(input_str: str) -> bytes:
"""Base64 URL decode"""
- padding = '=' * (4 - (len(input) % 4))
- return base64.urlsafe_b64decode(input + padding)
+ padding = '=' * (4 - (len(input_str) % 4))
+ return base64.urlsafe_b64decode(input_str + padding)
def decode_jwt(token: str, verify: bool = True) -> Dict[str, Any]:
@@ -81,21 +86,6 @@ def is_token_valid(token: str) -> bool:
except InvalidTokenError:
return False
-
-@dataclass
-class MagentaCredentials(UserPasswordCredentials):
- """Magenta TV credentials with country support"""
- country: str = DEFAULT_COUNTRY
-
- def validate(self) -> bool:
- """Validate credentials"""
- return bool(self.username and self.password and self.country in SUPPORTED_COUNTRIES)
-
- @property
- def credential_type(self) -> str:
- return "magenta_user_password"
-
-
@dataclass
class MagentaAuthToken(BaseAuthToken):
"""Magenta TV authentication token"""
@@ -150,20 +140,12 @@ class MagentaAuthConfig:
self.x_user_agent = X_USER_AGENT
self.timeout = DEFAULT_REQUEST_TIMEOUT
- def get_base_headers(self) -> Dict[str, str]:
- """Get base headers for requests"""
- return {
- 'User-Agent': self.user_agent,
- 'Accept': 'application/json',
- 'Content-Type': 'application/json',
- }
-
def get_auth_headers(self, call_type: str = CALL_TYPES['GUEST_USER'],
flow: str = AUTH_FLOWS['START_UP'],
step: str = AUTH_STEPS['GET_ACCESS_TOKEN'],
device_id: str = None, session_id: str = None) -> Dict[str, str]:
"""Get authentication headers"""
- headers = self.get_base_headers()
+ headers = get_base_headers()
headers.update({
'X-User-Agent': self.x_user_agent,
'X-Call-Type': call_type,
@@ -184,7 +166,7 @@ class MagentaAuthConfig:
def encrypt_password(self, password: str) -> str:
"""Encrypt password using RSA public key"""
try:
- rsa_key = self.country_config['rsa_key'],
+ rsa_key = self.country_config['rsa_key']
if not rsa_key:
logger.error(f"No RSA public key configured for country: {self.country}")
return password
@@ -206,10 +188,12 @@ class MagentaAuthenticator(BaseAuthenticator):
credentials=None,
config_dir: Optional[str] = None,
http_manager=None,
- proxy_config: Optional[ProxyConfig] = None):
- """
- Initialize Magenta authenticator
- """
+ proxy_config: Optional[ProxyConfig] = None,
+ device_id: Optional[str] = None, # New parameter
+ session_id: Optional[str] = None): # New parameter
+
+ logger.info(f"=== MagentaAuthenticator.__init__ START ===")
+
if country not in SUPPORTED_COUNTRIES:
raise ValueError(f"Unsupported country: {country}. Must be one of: {SUPPORTED_COUNTRIES}")
@@ -223,9 +207,10 @@ class MagentaAuthenticator(BaseAuthenticator):
# Setup config
self._config = MagentaAuthConfig(self.country, self._http_manager)
- # NOW call parent __init__ - this will setup settings_manager and load session
+ # Call parent init (this will load existing session if available)
+ # Call parent init (this will load existing session if available)
super().__init__(
- provider_name='magenta_eu',
+ provider_name='magentaeu',
settings_manager=settings_manager,
credentials=credentials,
country=country,
@@ -233,23 +218,123 @@ class MagentaAuthenticator(BaseAuthenticator):
enable_kodi_integration=True
)
- # After parent init, we can extract session data from the loaded token
- self._extract_session_data_from_token()
+ logger.info(f"=== MagentaAuthenticator.__init__ AFTER super().__init__ ===")
- def _extract_session_data_from_token(self) -> None:
- """Extract session data from the loaded token (if any)"""
+ # CORRECT LOGIC: Use stored IDs first, then cookie-based, then random
+ final_device_id = None
+ final_session_id = None
+
+ # 1. FIRST PRIORITY: Use stored session IDs from loaded token
if self._current_token and isinstance(self._current_token, MagentaAuthToken):
- # Session data is already stored in the token
- logger.debug("Session data extracted from loaded token")
+ stored_device_id = self._current_token.device_id
+ stored_session_id = self._current_token.session_id
+
+ if stored_device_id and stored_session_id:
+ final_device_id = stored_device_id
+ final_session_id = stored_session_id
+ logger.debug(f"Using stored session IDs - device_id: {final_device_id}, session_id: {final_session_id}")
+
+ # 2. SECOND PRIORITY: Only get cookie-based IDs if no stored IDs available
+ if not final_device_id or not final_session_id:
+ # Get cookie-based IDs (second priority)
+ cookie_device_id, cookie_session_id = device_id, session_id
+
+ # If no cookie IDs provided as parameters, initialize guest session
+ if not cookie_device_id or not cookie_session_id:
+ cookie_device_id, cookie_session_id = self._initialize_guest_session()
+ logger.debug(
+ f"Initialized guest session from cookies - device_id: {cookie_device_id}, session_id: {cookie_session_id}")
+
+ if cookie_device_id and cookie_session_id:
+ final_device_id = cookie_device_id
+ final_session_id = cookie_session_id
+ logger.debug(
+ f"Using cookie-based session IDs - device_id: {final_device_id}, session_id: {final_session_id}")
+
+ # 3. LAST RESORT: Generate random IDs (shouldn't happen)
+ if not final_device_id or not final_session_id:
+ final_device_id = str(uuid.uuid4())
+ final_session_id = str(uuid.uuid4())
+ logger.warning(f"CRITICAL: No session IDs available, using random fallback")
+
+ # Ensure current token has the correct IDs
+ if not self._current_token or not isinstance(self._current_token, MagentaAuthToken):
+ self._current_token = MagentaAuthToken(
+ access_token="",
+ refresh_token="",
+ token_type="Bearer",
+ expires_in=0,
+ issued_at=time.time(),
+ device_id=final_device_id,
+ session_id=final_session_id
+ )
else:
- # No token loaded, initialize empty session data
- logger.debug("No token loaded, session data will be initialized on first auth")
+ self._current_token.device_id = final_device_id
+ self._current_token.session_id = final_session_id
+
+ logger.info(f"Final session IDs - device_id: {final_device_id}, session_id: {final_session_id}")
@property
def auth_endpoint(self) -> str:
"""Authentication endpoint - required by BaseAuthenticator"""
return API_ENDPOINTS['LOGIN'].format(natco=self.country)
+ @property
+ def current_token(self):
+ return self._current_token
+
+ @property
+ def channel_map_id(self):
+ if self._current_token and hasattr(self._current_token, 'channel_map_id'):
+ return self._current_token.channel_map_id
+ return ""
+
+ def get_auth_headers(self, call_type: str, flow: str, step: str) -> Dict[str, str]:
+ return self._config.get_auth_headers(call_type, flow, step)
+
+ def get_epg_headers(self) -> Dict[str, str]:
+ return self.get_auth_headers("GUEST_USER", "START_UP", "EPG_CHANNEL")
+
+ @property
+ def http_manager(self):
+ """Public access to HTTP manager"""
+ return self._http_manager
+
+ def _initialize_guest_session(self) -> tuple[str, str]:
+ """Initialize guest session and get device_id/session_id from cookies"""
+ try:
+ startup_url = f"{get_base_url(self.country)}/epg"
+ headers = get_base_headers()
+
+ response = self._http_manager.get(
+ startup_url,
+ operation='session_init',
+ headers=headers,
+ timeout=DEFAULT_REQUEST_TIMEOUT
+ )
+
+ # Extract cookies from response
+ device_id = ""
+ session_id = ""
+
+ if hasattr(response, 'cookies'):
+ cookies = response.cookies.get_dict()
+ device_id = cookies.get("deviceId", "")
+ session_id = cookies.get("sessionId", "")
+
+ # If no cookies found, generate UUIDs
+ if not device_id or not session_id:
+ device_id = str(uuid.uuid4())
+ session_id = str(uuid.uuid4())
+
+ logger.debug(f"Initialized guest session from cookies - device_id: {device_id}, session_id: {session_id}")
+ return device_id, session_id
+
+ except Exception as e:
+ logger.warning(f"Session initialization failed: {e}")
+ # Generate fallback IDs
+ return str(uuid.uuid4()), str(uuid.uuid4())
+
def _get_auth_headers(self) -> Dict[str, str]:
"""Get headers for authentication request - required by BaseAuthenticator"""
device_id = ""
@@ -270,16 +355,23 @@ class MagentaAuthenticator(BaseAuthenticator):
def _build_auth_payload(self) -> Dict[str, Any]:
"""Build authentication payload - required by BaseAuthenticator"""
- if not self.credentials or not isinstance(self.credentials, MagentaCredentials):
- raise Exception("No valid Magenta credentials available")
+ from ...base.auth.credentials import UserPasswordCredentials
+ if not self.credentials or not isinstance(self.credentials, UserPasswordCredentials):
+ raise Exception("No valid credentials available")
+
+ # Enhanced validation
+ if not self.credentials.username or not self.credentials.password:
+ raise Exception("Username and password cannot be empty")
+
+ # Get device_id from current token or expect it to be provided via other means
device_id = ""
if self._current_token and isinstance(self._current_token, MagentaAuthToken):
device_id = self._current_token.device_id or ""
- # If no device_id, initialize session
+ # If no device_id, we need to get it from the provider
if not device_id:
- device_id, _ = self._initialize_session()
+ device_id = str(uuid.uuid4())
encrypted_password = self._config.encrypt_password(self.credentials.password)
@@ -304,33 +396,65 @@ class MagentaAuthenticator(BaseAuthenticator):
"broadcastingStreamLimitationApplies": BROADCASTING_STREAM_LIMITATION_APPLIES
},
"telekomLogin": {
- "username": self.credentials.username,
- "password": encrypted_password
+ "username": self.credentials.username, # Works for both types!
+ "password": encrypted_password # Works for both types!
}
}
def _create_token_from_response(self, response_data: Dict[str, Any]) -> BaseAuthToken:
"""Create token from API response - required by BaseAuthenticator"""
- # Get existing session data from current token
+ # PRESERVE the existing session IDs (which follow the correct priority)
device_id = ""
session_id = ""
channel_map_id = ""
- if self._current_token and isinstance(self._current_token, MagentaAuthToken):
+ # Try to get session IDs from multiple sources in priority order:
+
+ # 1. First from the response_data itself (when loading from stored session)
+ if 'device_id' in response_data:
+ device_id = response_data.get('device_id', '')
+ if 'session_id' in response_data:
+ session_id = response_data.get('session_id', '')
+
+ # 2. Then from current token (for new authentications)
+ if (not device_id or not session_id) and self._current_token and isinstance(self._current_token,
+ MagentaAuthToken):
device_id = self._current_token.device_id or ""
session_id = self._current_token.session_id or ""
channel_map_id = self._current_token.channel_map_id or ""
- # If no device_id/session_id, initialize session
- if not device_id or not session_id:
- device_id, session_id = self._initialize_session()
+ # 3. If we found session IDs, log it
+ if device_id and session_id:
+ logger.debug(f"Creating new token with session IDs - device_id: {device_id}, session_id: {session_id}")
+ else:
+ logger.warning(f"No session IDs found in response_data or current_token during token creation")
+
+ # DUAL KEY SUPPORT: Handle both camelCase (API responses) and snake_case (stored sessions)
+ # Access token
+ access_token = response_data.get('accessToken') or response_data.get('access_token')
+ if not access_token:
+ logger.error(f"CRITICAL: No access token found in response data")
+ logger.error(f"Available keys: {list(response_data.keys())}")
+ raise Exception("No access token found in response data")
+
+ # Refresh token
+ refresh_token = response_data.get('refreshToken') or response_data.get('refresh_token', '')
+
+ # Expires in
+ expires_in = response_data.get('expiresIn') or response_data.get('expires_in', 3600)
+
+ # Token type
+ token_type = response_data.get('tokenType') or response_data.get('token_type', 'Bearer')
+
+ # For stored sessions, issued_at might be in the data, otherwise use current time
+ issued_at = response_data.get('issued_at', time.time())
token = MagentaAuthToken(
- access_token=response_data['accessToken'],
- refresh_token=response_data.get('refreshToken', ''),
- token_type='Bearer',
- expires_in=response_data.get('expiresIn', 3600),
- issued_at=time.time(),
+ access_token=access_token,
+ refresh_token=refresh_token,
+ token_type=token_type,
+ expires_in=expires_in,
+ issued_at=issued_at,
device_id=device_id,
session_id=session_id,
channel_map_id=channel_map_id
@@ -338,75 +462,61 @@ class MagentaAuthenticator(BaseAuthenticator):
# Classify token
token.auth_level = self._classify_token(token)
- logger.debug(f"Token created and classified as: {token.auth_level.value}")
+ logger.debug(f"Token created successfully from {len(response_data)} data fields")
return token
def get_fallback_credentials(self):
"""Get fallback credentials - required by BaseAuthenticator"""
- # Return empty credentials as fallback
- return MagentaCredentials(username="", password="", country=self.country)
-
- def _initialize_session(self) -> tuple:
- """Initialize session by visiting startup page, returns (device_id, session_id)"""
- try:
- startup_url = API_ENDPOINTS['STARTUP_PAGE'].format(
- base_url=self._config.country_config['base_url']
- )
-
- headers = self._config.get_base_headers()
- response = self._http_manager.get(
- startup_url,
- operation='session_init',
- headers=headers,
- timeout=self._config.timeout
- )
-
- # Extract cookies
- device_id = ""
- session_id = ""
- if hasattr(response, 'cookies'):
- cookies = response.cookies.get_dict()
- device_id = cookies.get("deviceId", str(uuid.uuid4()))
- session_id = cookies.get("sessionId", str(uuid.uuid4()))
-
- logger.debug(f"Session initialized - device_id: {device_id}, session_id: {session_id}")
-
- return device_id, session_id
-
- except Exception as e:
- logger.warning(f"Session initialization failed: {e}")
- # Generate fallback IDs
- return str(uuid.uuid4()), str(uuid.uuid4())
+ from ...base.auth.credentials import UserPasswordCredentials
+ return UserPasswordCredentials(username="", password="")
def _perform_authentication(self) -> BaseAuthToken:
"""Perform Magenta TV authentication - required by BaseAuthenticator"""
- if not self.credentials or not isinstance(self.credentials, MagentaCredentials):
- raise Exception("No valid Magenta credentials available")
+ # Enhanced credential validation - FIXED VERSION
+ if not self.credentials:
+ raise Exception("No credentials available for authentication")
+
+ # Accept both MagentaCredentials AND base UserPasswordCredentials
+ from ...base.auth.credentials import UserPasswordCredentials
+ if not isinstance(self.credentials, UserPasswordCredentials):
+ raise Exception(
+ f"Invalid credential type: {type(self.credentials)}. Expected UserPasswordCredentials or MagentaCredentials")
+
+ # Validate credential content
+ if not self.credentials.username or not self.credentials.password:
+ raise Exception("Username and password are required for authentication")
logger.info(f"Performing Magenta TV authentication for country: {self.country}")
- # Perform login
- headers = self._get_auth_headers()
- payload = self._build_auth_payload()
+ try:
+ # Perform login
+ headers = self._get_auth_headers()
+ payload = self._build_auth_payload()
- response = self._http_manager.post(
- self.auth_endpoint,
- operation='auth',
- headers=headers,
- json_data=payload,
- timeout=self._config.timeout
- )
+ logger.debug(f"Authentication payload prepared for user: {self.credentials.username}")
- response.raise_for_status()
- token_data = response.json()
+ response = self._http_manager.post(
+ self.auth_endpoint,
+ operation='auth',
+ headers=headers,
+ json_data=payload,
+ timeout=self._config.timeout
+ )
- # Handle device limit exceeded
- if token_data.get("deviceLimitExceed", False):
- logger.info("Device limit exceeded, attempting token upgrade")
- token_data = self._upgrade_token(token_data['refreshToken'])
+ response.raise_for_status()
+ token_data = response.json()
- return self._create_token_from_response(token_data)
+ # Handle device limit exceeded
+ if token_data.get("deviceLimitExceed", False):
+ logger.info("Device limit exceeded, attempting token upgrade")
+ token_data = self._upgrade_token(token_data['refreshToken'])
+
+ return self._create_token_from_response(token_data)
+
+ except Exception as e:
+ logger.error(f"Authentication failed for user {self.credentials.username}: {e}")
+ raise
def _upgrade_token(self, refresh_token: str) -> Dict[str, Any]:
"""Upgrade token when device limit is exceeded"""
@@ -432,6 +542,16 @@ class MagentaAuthenticator(BaseAuthenticator):
response.raise_for_status()
return response.json()
+ def _get_session_data(self) -> tuple[str, str, str]:
+ """Safely get session data from current token"""
+ if isinstance(self._current_token, MagentaAuthToken):
+ return (
+ self._current_token.device_id or "",
+ self._current_token.session_id or "",
+ self._current_token.channel_map_id or ""
+ )
+ return "", "", ""
+
def _refresh_token(self) -> Optional[BaseAuthToken]:
"""Refresh Magenta TV token - override base method"""
if not self._current_token or not self._current_token.refresh_token:
@@ -443,19 +563,34 @@ class MagentaAuthenticator(BaseAuthenticator):
refresh_url = API_ENDPOINTS['REFRESH_TOKEN'].format(natco=self.country)
+ device_id, session_id, channel_map_id = self._get_session_data()
+
+ # Build headers according to your working example
headers = self._config.get_auth_headers(
call_type=CALL_TYPES['AUTH_USER'],
flow=AUTH_FLOWS['START_UP'],
step=AUTH_STEPS['REFRESH_TOKEN']
)
- headers['Refresh_token'] = self._current_token.refresh_token
+ # Add the specific headers from your working example
+ headers.update({
+ 'Authorization': f'Bearer {self._current_token.access_token}',
+ 'Refresh_token': self._current_token.refresh_token,
+ 'channel': 'Tv',
+ })
+
+ # Build payload matching your working example
payload = {
- "clientVersion": self._config.app_version,
- "concurrencyLimitParam": DEVICE_CONCURRENCY_PARAM,
- "deviceId": self._current_token.device_id or ""
+ "clientVersion": APP_VERSION, # Use current APP_VERSION
+ "deviceId": device_id,
+ "concurrencyLimitParam": DEVICE_CONCURRENCY_PARAM
}
+ logger.debug(f"Refresh request - URL: {refresh_url}")
+ logger.debug(
+ f"Refresh request - Headers: { {k: v for k, v in headers.items() if k not in ['Authorization', 'Refresh_token']} }")
+ logger.debug(f"Refresh request - Payload: {payload}")
+
response = self._http_manager.post(
refresh_url,
operation='auth_refresh',
@@ -467,16 +602,16 @@ class MagentaAuthenticator(BaseAuthenticator):
response.raise_for_status()
token_data = response.json()
- # Create new token but preserve session data
+ # Create new token with updated data but preserve session IDs
new_token = MagentaAuthToken(
access_token=token_data['accessToken'],
- refresh_token=token_data.get('refreshToken', ''),
+ refresh_token=token_data.get('refreshToken', self._current_token.refresh_token),
token_type='Bearer',
expires_in=token_data.get('expiresIn', 3600),
issued_at=time.time(),
- device_id=self._current_token.device_id,
- session_id=self._current_token.session_id,
- channel_map_id=self._current_token.channel_map_id
+ device_id=device_id,
+ session_id=session_id,
+ channel_map_id=channel_map_id
)
# Classify token
@@ -486,6 +621,8 @@ class MagentaAuthenticator(BaseAuthenticator):
except Exception as e:
logger.warning(f"Token refresh failed: {e}")
+ if hasattr(e, 'response') and hasattr(e.response, 'text'):
+ logger.error(f"Refresh response content: {e.response.text}")
return None
def _classify_token(self, token: BaseAuthToken) -> TokenAuthLevel:
@@ -494,7 +631,19 @@ class MagentaAuthenticator(BaseAuthenticator):
if not token or not token.access_token:
return TokenAuthLevel.UNKNOWN
- claims = token.get_jwt_claims()
+ # Only MagentaAuthToken has get_jwt_claims method
+ if isinstance(token, MagentaAuthToken):
+ claims = token.get_jwt_claims()
+ else:
+ # For BaseAuthToken, try to decode JWT manually
+ try:
+ claims = decode_jwt(token.access_token, verify=False)
+ except InvalidTokenError:
+ return TokenAuthLevel.UNKNOWN
+ except (ValueError, json.JSONDecodeError, UnicodeDecodeError) as e:
+ logger.debug(f"Error decoding JWT token: {e}")
+ return TokenAuthLevel.UNKNOWN
+
if not claims:
return TokenAuthLevel.UNKNOWN
diff --git a/lib/streaming_providers/providers/magenta_eu/constants.py b/lib/streaming_providers/providers/magentaeu/constants.py
similarity index 89%
rename from lib/streaming_providers/providers/magenta_eu/constants.py
rename to lib/streaming_providers/providers/magentaeu/constants.py
index 0f9358f..156b03a 100644
--- a/lib/streaming_providers/providers/magenta_eu/constants.py
+++ b/lib/streaming_providers/providers/magentaeu/constants.py
@@ -1,8 +1,10 @@
-# streaming_providers/providers/magenta_eu/constants.py
+# streaming_providers/providers/magentaeu/constants.py
# ============================================================================
# Magenta TV Configuration
# ============================================================================
+from typing import Dict
+
# Supported countries
SUPPORTED_COUNTRIES = ['hr', 'pl', 'me', 'at', 'hu']
@@ -211,4 +213,34 @@ def get_language(country: str) -> str:
def get_rsa_key(country: str) -> str:
"""Get RSA public key for country"""
- return get_country_config(country)['rsa_key']
\ No newline at end of file
+ return get_country_config(country)['rsa_key']
+
+def get_base_headers() -> Dict[str, str]:
+ """Get base headers for requests"""
+ return {
+ 'User-Agent': USER_AGENT,
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ }
+
+def get_guest_headers(country: str, device_id: str, session_id: str) -> Dict[str, str]:
+ """Get headers for guest/unauthenticated requests"""
+ import uuid
+
+ headers = {
+ 'User-Agent': USER_AGENT,
+ 'X-User-Agent': X_USER_AGENT,
+ 'X-Call-Type': 'GUEST_USER',
+ 'X-Tv-Flow': 'START_UP',
+ 'X-Tv-Step': 'EPG_CHANNEL',
+ 'x-request-session-id': session_id,
+ 'x-request-tracking-id': str(uuid.uuid4()),
+ 'Tenant': 'tv',
+ 'Origin': get_base_url(country),
+ 'App_key': get_app_key(country),
+ 'App_version': APP_VERSION,
+ 'Device-Id': device_id,
+ 'Device-Name': DEVICE_NAME,
+ }
+ return headers
+
diff --git a/lib/streaming_providers/providers/magenta_eu/provider.py b/lib/streaming_providers/providers/magentaeu/provider.py
similarity index 57%
rename from lib/streaming_providers/providers/magenta_eu/provider.py
rename to lib/streaming_providers/providers/magentaeu/provider.py
index 734f7ad..d043d03 100644
--- a/lib/streaming_providers/providers/magenta_eu/provider.py
+++ b/lib/streaming_providers/providers/magentaeu/provider.py
@@ -1,17 +1,16 @@
-# streaming_providers/providers/magenta_eu/provider.py
+# streaming_providers/providers/magentaeu/provider.py
# -*- coding: utf-8 -*-
-from typing import Dict, Optional, List
-import json
import time
-from datetime import datetime, timedelta
+from typing import Dict, Optional, List
+from ...base.auth import UserPasswordCredentials
from ...base.provider import StreamingProvider
from ...base.models import DRMConfig, LicenseConfig, DRMSystem
from ...base.models.streaming_channel import StreamingChannel
from ...base.network import HTTPManagerFactory, ProxyConfigManager
from ...base.models.proxy_models import ProxyConfig
from ...base.utils.logger import logger
-from .auth import MagentaAuthenticator, MagentaCredentials
+from .auth import MagentaAuthenticator
from .constants import (
SUPPORTED_COUNTRIES,
DEFAULT_COUNTRY,
@@ -24,10 +23,10 @@ from .constants import (
WV_URL,
CONTENT_TYPE_LIVE,
STREAMING_FORMAT_DASH,
- get_base_url,
get_bifrost_url,
get_natco_key,
- get_app_key,
+ get_guest_headers,
+ get_base_url,
get_language
)
@@ -39,9 +38,8 @@ class MagentaProvider(StreamingProvider):
config_dir: Optional[str] = None,
proxy_config: Optional[ProxyConfig] = None,
proxy_url: Optional[str] = None):
- """
- Initialize Magenta provider
- """
+
+ logger.info(f"=== MagentaProvider.__init__ START for country: {country} ===")
super().__init__(country=country)
if country not in SUPPORTED_COUNTRIES:
@@ -54,47 +52,46 @@ class MagentaProvider(StreamingProvider):
self._load_proxy_from_manager(config_dir)
)
- if self.proxy_config:
- logger.info("Using proxy configuration for Magenta TV")
- else:
- logger.debug("No proxy configuration found for Magenta TV")
-
# Create HTTP manager
self.http_manager = HTTPManagerFactory.create_for_provider(
- provider_name='magenta_eu',
+ provider_name='magentaeu',
proxy_config=self.proxy_config,
user_agent=USER_AGENT,
timeout=DEFAULT_REQUEST_TIMEOUT,
max_retries=DEFAULT_MAX_RETRIES
)
- # Create authenticator
+ # Initialize ALL instance attributes
+ self._device_id = None
+ self._session_id = None
+ self.bearer_token = None
+ self._channels_cache = None
+ self._channels_cache_timestamp = 0
+ self._cache_ttl = 3600 # Cache TTL in seconds
+
+ # Create authenticator - it will handle session initialization internally
self.authenticator = MagentaAuthenticator(
country=country,
config_dir=config_dir,
http_manager=self.http_manager,
proxy_config=self.proxy_config
+ # No need to pass device_id/session_id - authenticator handles this
)
- # Authenticate
- try:
- self.bearer_token = self.authenticator.get_bearer_token()
- except Exception as e:
- logger.warning(f"Could not authenticate during initialization: {e}")
- self.bearer_token = None
+ logger.info(f"=== MagentaProvider.__init__ COMPLETE ===")
def _load_proxy_from_manager(self, config_dir: Optional[str]) -> Optional[ProxyConfig]:
"""Load proxy configuration from ProxyConfigManager"""
try:
proxy_manager = ProxyConfigManager(config_dir)
- return proxy_manager.get_proxy_config('magenta_eu', self.country)
+ return proxy_manager.get_proxy_config('magentaeu', self.country)
except Exception as e:
logger.warning(f"Could not load proxy from ProxyConfigManager: {e}")
return None
@property
def provider_name(self) -> str:
- return 'magenta_eu'
+ return 'magentaeu'
@property
def provider_label(self) -> str:
@@ -105,8 +102,9 @@ class MagentaProvider(StreamingProvider):
return False
def authenticate(self, **kwargs) -> str:
- """Authenticate and return bearer token"""
+ logger.info(f"=== MagentaProvider.authenticate() CALLED with kwargs: {kwargs} ===")
self.bearer_token = self.authenticator.get_bearer_token(force_refresh=kwargs.get('force_refresh', False))
+ logger.info(f"=== MagentaProvider.authenticate() COMPLETE ===")
return self.bearer_token
def get_dynamic_manifest_params(self, channel: StreamingChannel, **kwargs) -> Optional[str]:
@@ -118,35 +116,27 @@ class MagentaProvider(StreamingProvider):
return self.bearer_token
def fetch_channels(self, **kwargs) -> List[StreamingChannel]:
- """Fetch available channels from Magenta TV"""
+ """Fetch available channels from Magenta TV - no authentication required"""
try:
- # Get user account to ensure we have channel map ID
- self.authenticator.get_user_account()
+ # USE AUTHENTICATOR'S SESSION IDs (single source of truth)
+ device_id = self.authenticator.current_token.device_id if self.authenticator.current_token else ""
+ session_id = self.authenticator.current_token.session_id if self.authenticator.current_token else ""
channels_url = API_ENDPOINTS['EPG_CHANNELS'].format(
bifrost_url=get_bifrost_url(self.country)
)
- # Get channel map ID from authenticator
- channel_map_id = ""
- if (self.authenticator._current_token and
- isinstance(self.authenticator._current_token,
- self.authenticator.__class__.__bases__[0].MagentaAuthToken)):
- channel_map_id = self.authenticator._current_token.channel_map_id or ""
+ headers = get_guest_headers(self.country, device_id, session_id)
params = {
- 'channelMap_id': channel_map_id,
+ 'channelMap_id': '',
'includeVirtualChannels': 'true',
'natco_key': get_natco_key(self.country),
'app_language': get_language(self.country),
'natco_code': self.country
}
- headers = self.authenticator._config.get_auth_headers(
- call_type="GUEST_USER",
- flow="START_UP",
- step="EPG_CHANNEL"
- )
+ logger.debug(f"Fetching channels with device_id: {device_id}, session_id: {session_id}")
response = self.http_manager.get(
channels_url,
@@ -160,10 +150,16 @@ class MagentaProvider(StreamingProvider):
channels_data = response.json()
channels = self._process_channels_response(channels_data)
+ self._channels_cache = channels
+ self._channels_cache_timestamp = time.time()
+
logger.info(f"Successfully fetched {len(channels)} channels for country {self.country}")
return channels
except Exception as e:
+ logger.error(f"Error fetching channels from Magenta TV: {e}")
+ if hasattr(e, 'response') and hasattr(e.response, 'text'):
+ logger.error(f"Response content: {e.response.text}")
raise Exception(f"Error fetching channels from Magenta TV: {e}")
def _process_channels_response(self, response_data: Dict) -> List[StreamingChannel]:
@@ -234,7 +230,7 @@ class MagentaProvider(StreamingProvider):
if not channel.manifest:
return None
- # Get DRM config
+ # Get DRM config (this requires authentication)
drm_config = self.get_drm_config(channel)
if drm_config:
channel.drm_config = drm_config
@@ -246,53 +242,141 @@ class MagentaProvider(StreamingProvider):
return None
def get_manifest(self, channel_id: str, **kwargs) -> Optional[str]:
- """
- Get manifest URL for a specific channel by ID
- For Magenta TV, manifests are already provided in channel data
- """
- # Since manifests are provided directly in channel data,
- # this would need to fetch channel data again or use cached data
+ if self._channels_cache:
+ for channel in self._channels_cache:
+ if channel.channel_id == channel_id:
+ return channel.manifest
return None
+ def get_drm_configs_by_id(self, channel_id: str, **kwargs) -> List[DRMConfig]:
+ """Get DRM configurations for channel by ID"""
+ logger.info(f"=== get_drm_configs_by_id CALLED for channel_id: {channel_id} ===")
+
+ # Find channel in cache
+ channel = None
+ if self._channels_cache:
+ for cached_channel in self._channels_cache:
+ if cached_channel.channel_id == channel_id:
+ channel = cached_channel
+ break
+
+ if not channel:
+ logger.warning(f"Channel with ID {channel_id} not found in cache")
+ return []
+
+ # Get DRM config using the existing method
+ drm_config = self.get_drm_config(channel, **kwargs)
+ return [drm_config] if drm_config else []
+
def get_drm_configs(self, channel: StreamingChannel, **kwargs) -> List[DRMConfig]:
"""Get DRM configurations for channel"""
+ logger.info(f"=== get_drm_configs CALLED for channel: {channel.name} ===")
drm_config = self.get_drm_config(channel)
return [drm_config] if drm_config else []
def get_drm_config(self, channel: StreamingChannel, **kwargs) -> Optional[DRMConfig]:
- """Get DRM configuration for channel"""
+ """Get DRM configuration for channel with correct authentication"""
try:
+ import json
+ import base64
+ from .auth import MagentaAuthToken, decode_jwt
+
pid = channel.cdm.replace("pid=", "") if channel.cdm else ""
+ logger.info(f"=== get_drm_config: Extracted PID: {pid} ===")
+
if not pid:
+ logger.debug(f"No PID found for channel {channel.name}")
return None
license_url = f"{WV_URL}{pid}"
- headers = DRM_REQUEST_HEADERS.copy()
- headers.update({
- 'Authorization': f'Bearer {self.bearer_token}',
+ # Get access token (authenticate if needed)
+ if not self.bearer_token:
+ try:
+ self.authenticate()
+ except Exception as e:
+ logger.warning(f"Authentication failed for DRM config: {e}")
+ return None
+
+ access_token = self.bearer_token
+ if not access_token:
+ logger.warning("No bearer token available for DRM config")
+ return None
+
+ # Remove 'Bearer ' prefix if present
+ if access_token.startswith('Bearer '):
+ access_token = access_token[7:]
+
+ # Decode JWT token to get account details
+ try:
+ # Get current token from authenticator
+ current_token = self.authenticator.current_token
+
+ # Use the helper method if token is MagentaAuthToken
+ if isinstance(current_token, MagentaAuthToken) and hasattr(current_token, 'get_jwt_claims'):
+ decoded_payload = current_token.get_jwt_claims()
+ if not decoded_payload:
+ logger.warning("Failed to get JWT claims from token")
+ return None
+ else:
+ # Fallback: use decode_jwt helper
+ decoded_payload = decode_jwt(access_token, verify=False)
+
+ except Exception as e:
+ logger.warning(f"Error decoding JWT token for DRM: {e}")
+ return None
+
+ # Extract account information from JWT payload
+ account_id = decoded_payload.get('dc_cts_accountId', '')
+ persona_token = decoded_payload.get('dc_cts_personaToken', '')
+
+ if not account_id or not persona_token:
+ logger.warning("Missing account ID or persona token in JWT payload")
+ return None
+
+ # Create reencoded session for Basic auth
+ import base64
+ reencoded_session = f"{get_base_url(self.country)}/{account_id}:{persona_token}"
+ basic_auth = base64.b64encode(reencoded_session.encode()).decode()
+
+ # Build license headers
+ headers = {
+ 'Authorization': f'Basic {basic_auth}',
+ 'Content-Type': DRM_REQUEST_HEADERS.get('Content-Type', 'application/octet-stream'),
'Origin': get_base_url(self.country),
'Referer': f"{get_base_url(self.country)}/",
- })
+ 'User-Agent': USER_AGENT
+ }
- return DRMConfig(
+ # Remove any None values from headers
+ headers = {k: v for k, v in headers.items() if v is not None}
+
+ # Create DRM configuration using LicenseConfig
+ import json
+ drm_config = DRMConfig(
system=DRMSystem.WIDEVINE,
- license_url=license_url,
- headers=headers,
- challenge_data=b'',
- session_id=str(int(time.time()))
+ priority=1,
+ license=LicenseConfig(
+ server_url=license_url,
+ req_headers=json.dumps(headers),
+ req_data="{CHA-RAW}",
+ use_http_get_request=False
+ )
)
+ logger.debug(f"DRM config created successfully for channel {channel.name}")
+ return drm_config
+
except Exception as e:
logger.warning(f"Error creating DRM config for {channel.name}: {e}")
return None
- def validate_credentials(self, credentials: MagentaCredentials) -> bool:
+ def validate_credentials(self, credentials: UserPasswordCredentials) -> bool:
"""Validate Magenta TV credentials"""
try:
# Test authentication with provided credentials
temp_authenticator = MagentaAuthenticator(
- country=credentials.country,
+# country=credentials.country,
config_dir=self.authenticator.settings_manager.config_dir if hasattr(
self.authenticator.settings_manager, 'config_dir') else None,
http_manager=self.http_manager,
@@ -307,6 +391,7 @@ class MagentaProvider(StreamingProvider):
logger.debug(f"Credential validation failed: {e}")
return False
- def get_supported_countries(self) -> List[str]:
+ @staticmethod
+ def get_supported_countries() -> List[str]:
"""Get list of supported countries"""
return SUPPORTED_COUNTRIES.copy()
\ No newline at end of file
diff --git a/lib/streaming_providers/providers/rtlplus/provider.py b/lib/streaming_providers/providers/rtlplus/provider.py
index cc1189c..e220d03 100644
--- a/lib/streaming_providers/providers/rtlplus/provider.py
+++ b/lib/streaming_providers/providers/rtlplus/provider.py
@@ -396,11 +396,11 @@ class RTLPlusProvider(StreamingProvider):
print(f"Error parsing DRM configs for RTL+ channel {channel_id}: {e}")
return []
- def get_drm_configs(self, channel: StreamingChannel, **kwargs) -> List[DRMConfig]:
- """
- Get DRM configurations for a channel
- """
- return self.get_drm_configs_by_id(channel.channel_id, **kwargs)
+# def get_drm_configs(self, channel: StreamingChannel, **kwargs) -> List[DRMConfig]:
+# """
+# Get DRM configurations for a channel
+# """
+# return self.get_drm_configs_by_id(channel.channel_id, **kwargs)
@staticmethod
def get_epg_data(channel_id: str, **kwargs) -> Optional[Dict]:
@@ -415,7 +415,7 @@ class RTLPlusProvider(StreamingProvider):
"""
Get license URL for a DRM-protected channel
"""
- drm_configs = self.get_drm_configs(channel, **kwargs)
+ drm_configs = self.get_drm_configs_by_id(channel.channel_id, **kwargs)
if drm_configs:
# Return the first license URL found
return drm_configs[0].license.server_url
diff --git a/resources/settings.xml b/resources/settings.xml
index 5db16c4..62b0eac 100644
--- a/resources/settings.xml
+++ b/resources/settings.xml
@@ -9,7 +9,7 @@
-
+
@@ -67,4 +67,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+