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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +