mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-16 06:02:35 +02:00
Add Magenta TV (EU)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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, "
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
-2
@@ -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',
|
||||
+278
-129
@@ -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
|
||||
|
||||
+34
-2
@@ -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']
|
||||
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
|
||||
|
||||
+146
-61
@@ -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()
|
||||
@@ -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
|
||||
|
||||
+66
-1
@@ -9,7 +9,7 @@
|
||||
</category>
|
||||
|
||||
<category label="Joyn (DE)">
|
||||
<setting id="enable_joyn_de" type="bool" label="Enable Joyn (DE)" default="true" />
|
||||
<setting id="enable_joyn_de" type="bool" label="Enable Joyn (DE)" default="false" />
|
||||
<setting id="joyn_de_country" type="text" label="Country Code" default="de" visible="false" />
|
||||
<setting id="joyn_de_username" type="text" label="Username" default="" enable="eq(-2,true)" />
|
||||
<setting id="joyn_de_password" type="text" label="Password" option="hidden" default="" enable="eq(-3,true)" />
|
||||
@@ -67,4 +67,69 @@
|
||||
<setting id="rtlplus_proxy_port" type="number" label="30112" visible="eq(-2,true)" />
|
||||
</category>
|
||||
|
||||
<!-- Magenta TV Austria -->
|
||||
<category label="Magenta TV (AT)">
|
||||
<setting id="enable_magentaeu_at" type="bool" label="Enable Magenta TV (AT)" default="false" />
|
||||
<setting id="magentaeu_at_country" type="text" label="Country Code" default="at" visible="false" />
|
||||
<setting id="magentaeu_at_username" type="text" label="Username" default="" enable="eq(-2,true)" />
|
||||
<setting id="magentaeu_at_password" type="text" label="Password" option="hidden" default="" enable="eq(-3,true)" />
|
||||
|
||||
<!-- Optional: Proxy settings for AT -->
|
||||
<setting id="magentaeu_at_proxy_enabled" type="bool" label="Enable Proxy" default="false" enable="eq(-4,true)" />
|
||||
<setting id="magentaeu_at_proxy_host" type="text" label="Proxy Host" default="" visible="eq(-1,true)" />
|
||||
<setting id="magentaeu_at_proxy_port" type="number" label="Proxy Port" default="8080" visible="eq(-2,true)" />
|
||||
</category>
|
||||
|
||||
<!-- Magenta TV Croatia -->
|
||||
<category label="Max TV (HR)">
|
||||
<setting id="enable_magentaeu_hr" type="bool" label="Enable Max TV (HR)" default="false" />
|
||||
<setting id="magentaeu_hr_country" type="text" label="Country Code" default="hr" visible="false" />
|
||||
<setting id="magentaeu_hr_username" type="text" label="Username" default="" enable="eq(-2,true)" />
|
||||
<setting id="magentaeu_hr_password" type="text" label="Password" option="hidden" default="" enable="eq(-3,true)" />
|
||||
|
||||
<!-- Optional: Proxy settings for HR -->
|
||||
<setting id="magentaeu_hr_proxy_enabled" type="bool" label="Enable Proxy" default="false" enable="eq(-4,true)" />
|
||||
<setting id="magentaeu_hr_proxy_host" type="text" label="Proxy Host" default="" visible="eq(-1,true)" />
|
||||
<setting id="magentaeu_hr_proxy_port" type="number" label="Proxy Port" default="8080" visible="eq(-2,true)" />
|
||||
</category>
|
||||
|
||||
<!-- Magenta TV Poland -->
|
||||
<category label="Magenta TV (PL)">
|
||||
<setting id="enable_magentaeu_pl" type="bool" label="Enable Magenta TV (PL)" default="false" />
|
||||
<setting id="magentaeu_pl_country" type="text" label="Country Code" default="pl" visible="false" />
|
||||
<setting id="magentaeu_pl_username" type="text" label="Username" default="" enable="eq(-2,true)" />
|
||||
<setting id="magentaeu_pl_password" type="text" label="Password" option="hidden" default="" enable="eq(-3,true)" />
|
||||
|
||||
<!-- Optional: Proxy settings for PL -->
|
||||
<setting id="magentaeu_pl_proxy_enabled" type="bool" label="Enable Proxy" default="false" enable="eq(-4,true)" />
|
||||
<setting id="magentaeu_pl_proxy_host" type="text" label="Proxy Host" default="" visible="eq(-1,true)" />
|
||||
<setting id="magentaeu_pl_proxy_port" type="number" label="Proxy Port" default="8080" visible="eq(-2,true)" />
|
||||
</category>
|
||||
|
||||
<!-- Magenta TV Montenegro -->
|
||||
<category label="Magenta TV (ME)">
|
||||
<setting id="enable_magentaeu_me" type="bool" label="Enable Magenta TV (ME)" default="false" />
|
||||
<setting id="magentaeu_me_country" type="text" label="Country Code" default="me" visible="false" />
|
||||
<setting id="magentaeu_me_username" type="text" label="Username" default="" enable="eq(-2,true)" />
|
||||
<setting id="magentaeu_me_password" type="text" label="Password" option="hidden" default="" enable="eq(-3,true)" />
|
||||
|
||||
<!-- Optional: Proxy settings for ME -->
|
||||
<setting id="magentaeu_me_proxy_enabled" type="bool" label="Enable Proxy" default="false" enable="eq(-4,true)" />
|
||||
<setting id="magentaeu_me_proxy_host" type="text" label="Proxy Host" default="" visible="eq(-1,true)" />
|
||||
<setting id="magentaeu_me_proxy_port" type="number" label="Proxy Port" default="8080" visible="eq(-2,true)" />
|
||||
</category>
|
||||
|
||||
<!-- Magenta TV Hungary -->
|
||||
<category label="Magenta TV (HU)">
|
||||
<setting id="enable_magentaeu_hu" type="bool" label="Enable Magenta TV (HU)" default="false" />
|
||||
<setting id="magentaeu_hu_country" type="text" label="Country Code" default="hu" visible="false" />
|
||||
<setting id="magentaeu_hu_username" type="text" label="Username" default="" enable="eq(-2,true)" />
|
||||
<setting id="magentaeu_hu_password" type="text" label="Password" option="hidden" default="" enable="eq(-3,true)" />
|
||||
|
||||
<!-- Optional: Proxy settings for HU -->
|
||||
<setting id="magentaeu_hu_proxy_enabled" type="bool" label="Enable Proxy" default="false" enable="eq(-4,true)" />
|
||||
<setting id="magentaeu_hu_proxy_host" type="text" label="Proxy Host" default="" visible="eq(-1,true)" />
|
||||
<setting id="magentaeu_hu_proxy_port" type="number" label="Proxy Port" default="8080" visible="eq(-2,true)" />
|
||||
</category>
|
||||
|
||||
</settings>
|
||||
|
||||
Reference in New Issue
Block a user