Restructure for enabling/disabling

This commit is contained in:
Nirvana
2025-12-23 16:51:49 +01:00
parent 03d46fc194
commit 3c9b39acf9
13 changed files with 1259 additions and 1265 deletions
@@ -0,0 +1,116 @@
# ============================================================================
# streaming_providers/base/catchup_operations.py
"""
Catchup/timeshift operations.
"""
from typing import Optional, List, Dict
from .utils.logger import logger
class CatchupOperations:
"""Handles all catchup-related operations."""
def __init__(self, registry, drm_operations):
self.registry = registry
self.drm_operations = drm_operations
logger.debug("CatchupOperations: Initialized")
def get_catchup_manifest(self, provider_name: str, channel_id: str,
start_time: int, end_time: int,
epg_id: Optional[str] = None,
country: Optional[str] = None) -> Optional[str]:
"""Get catchup manifest URL."""
provider = self.registry.get_provider(provider_name)
if not provider:
raise ValueError(f"Provider '{provider_name}' not found or disabled")
if not provider.supports_catchup:
logger.warning(f"Provider '{provider_name}' doesn't support catchup")
return None
is_valid, error = provider.validate_catchup_request(start_time, end_time)
if not is_valid:
logger.error(f"Invalid catchup request: {error}")
return None
try:
return provider.get_catchup_manifest(
channel_id=channel_id,
start_time=start_time,
end_time=end_time,
epg_id=epg_id,
country=country
)
except NotImplementedError:
logger.error(f"Provider '{provider_name}' hasn't implemented catchup")
return None
def get_catchup_drm_configs(self, provider_name: str, channel_id: str,
start_time: int, end_time: int,
epg_id: Optional[str] = None,
country: Optional[str] = None) -> List:
"""Get DRM configs for catchup content."""
provider = self.registry.get_provider(provider_name)
if not provider:
raise ValueError(f"Provider '{provider_name}' not found or disabled")
if not provider.supports_catchup:
return self.drm_operations.get_channel_drm_configs(
provider_name, channel_id, country=country
)
try:
return provider.get_catchup_drm(
channel_id=channel_id,
start_time=start_time,
end_time=end_time,
epg_id=epg_id,
country=country
)
except NotImplementedError:
return self.drm_operations.get_channel_drm_configs(
provider_name, channel_id, country=country
)
def get_catchup_window(self, provider_name: str,
channel_id: Optional[str] = None) -> int:
"""Get catchup window in hours."""
provider = self.registry.get_provider(provider_name)
if not provider:
raise ValueError(f"Provider '{provider_name}' not found or disabled")
if channel_id:
try:
return provider.get_catchup_window_for_channel(channel_id)
except Exception as e:
logger.warning(f"Error getting channel catchup window: {e}")
return provider.catchup_window
def supports_catchup(self, provider_name: str) -> bool:
"""Check if provider supports catchup."""
provider = self.registry.get_provider(provider_name)
if not provider:
raise ValueError(f"Provider '{provider_name}' not found or disabled")
return provider.supports_catchup
def get_all_catchup_capabilities(self) -> Dict[str, Dict]:
"""Get catchup capabilities for all providers."""
capabilities = {}
for name in self.registry.list_providers():
try:
provider = self.registry.get_provider(name)
capabilities[name] = {
'supports_catchup': provider.supports_catchup,
'catchup_window': provider.catchup_window,
'catchup_enabled': provider.supports_catchup and
provider.catchup_window > 0
}
except Exception as e:
logger.warning(f"Error getting catchup for '{name}': {e}")
capabilities[name] = {
'supports_catchup': False,
'catchup_window': 0,
'catchup_enabled': False
}
return capabilities
@@ -0,0 +1,71 @@
# ============================================================================
# streaming_providers/base/channel_operations.py
"""
Channel-related operations separated from core registry.
"""
from typing import List, Optional, Dict
from .models import StreamingChannel
from .utils.logger import logger
class ChannelOperations:
"""Handles all channel-related operations."""
def __init__(self, registry):
self.registry = registry
logger.debug("ChannelOperations: Initialized")
def get_channels(self, provider_name: str, fetch_manifests: bool = False,
**kwargs) -> List[StreamingChannel]:
"""Get channels from a specific provider."""
provider = self.registry.get_provider(provider_name)
if not provider:
raise ValueError(f"Provider '{provider_name}' not found or disabled")
channels = provider.get_channels(**kwargs)
logger.info(f"Retrieved {len(channels)} channels from '{provider_name}'")
if fetch_manifests and not provider.uses_dynamic_manifests:
enriched = []
for channel in channels:
enriched_channel = provider.enrich_channel_data(channel, **kwargs)
if enriched_channel:
enriched.append(enriched_channel)
logger.info(f"Enriched {len(enriched)}/{len(channels)} channels")
return enriched
return channels
def get_channel_manifest(self, provider_name: str, channel_id: str,
**kwargs) -> Optional[str]:
"""Get manifest URL for a specific channel."""
provider = self.registry.get_provider(provider_name)
if not provider:
raise ValueError(f"Provider '{provider_name}' not found or disabled")
manifest_url = provider.get_manifest(channel_id, **kwargs)
if manifest_url:
logger.debug(f"Retrieved manifest for '{channel_id}' from '{provider_name}'")
return manifest_url
def get_all_channels(self, fetch_manifests: bool = True,
**kwargs) -> Dict[str, List[StreamingChannel]]:
"""Get channels from all enabled providers."""
enabled = self.registry.get_enabled_providers()
logger.info(f"Fetching channels from {len(enabled)} providers")
result = {}
total = 0
for name in enabled:
try:
channels = self.get_channels(name, fetch_manifests, **kwargs)
result[name] = channels
total += len(channels)
except Exception as e:
logger.error(f"Failed to get channels from '{name}': {e}")
result[name] = []
logger.info(f"Retrieved {total} total channels")
return result
@@ -0,0 +1,77 @@
# ============================================================================
# streaming_providers/base/drm_operations.py
"""
DRM-related operations.
"""
from typing import List, Dict
from .drm import DRMPluginManager
from .models import DRMSystem
from .utils.logger import logger
class DRMOperations:
"""Handles all DRM-related operations."""
def __init__(self, registry):
self.registry = registry
self.drm_plugin_manager = DRMPluginManager()
logger.debug("DRMOperations: Initialized")
def get_channel_drm_configs(self, provider_name: str, channel_id: str,
**kwargs) -> List:
"""Get DRM configurations for a channel."""
provider = self.registry.get_provider(provider_name)
if not provider:
raise ValueError(f"Provider '{provider_name}' not found or disabled")
drm_configs = provider.get_drm(channel_id, **kwargs)
# Extract PSSH if needed
pssh_data_list = []
if drm_configs and self.drm_plugin_manager.plugins:
if self._needs_pssh_extraction(drm_configs):
manifest_url = provider.get_manifest(channel_id, **kwargs)
if manifest_url:
pssh_data_list = self._extract_pssh_from_manifest(manifest_url)
# Process through plugins
processed = self.drm_plugin_manager.process_drm_configs(
drm_configs, pssh_data_list, **kwargs
)
logger.info(f"Processed DRM for '{channel_id}': {len(processed)} configs")
return processed
def _needs_pssh_extraction(self, drm_configs) -> bool:
"""Check if PSSH extraction is needed."""
config_systems = {config.system for config in drm_configs}
plugin_systems = set(self.drm_plugin_manager.plugins.keys())
return bool(
config_systems & plugin_systems or
DRMSystem.GENERIC in plugin_systems
)
def _extract_pssh_from_manifest(self, manifest_url: str) -> List:
"""Extract PSSH data from manifest."""
import requests
from .utils.manifest_parser import ManifestParser
try:
response = requests.get(manifest_url, timeout=10)
response.raise_for_status()
return ManifestParser.extract_pssh_from_manifest(
response.text, manifest_url
)
except Exception as e:
logger.warning(f"Failed to extract PSSH: {e}")
return []
def list_drm_plugins(self) -> Dict:
"""List registered DRM plugins."""
return self.drm_plugin_manager.list_plugins()
def clear_drm_plugins(self):
"""Clear all DRM plugins."""
self.drm_plugin_manager.clear_plugins()
@@ -0,0 +1,72 @@
# ============================================================================
# streaming_providers/base/epg_operations.py
"""
EPG-related operations.
"""
from typing import List, Dict, Optional
from .epg import EPGManager
from .utils.logger import logger
class EPGOperations:
"""Handles all EPG-related operations."""
def __init__(self, registry):
self.registry = registry
self.epg_manager = EPGManager()
logger.debug("EPGOperations: Initialized")
def get_channel_epg(self, provider_name: str, channel_id: str,
**kwargs) -> List[Dict]:
"""Get EPG data for a specific channel."""
provider = self.registry.get_provider(provider_name)
if not provider:
raise ValueError(f"Provider '{provider_name}' not found or disabled")
if provider.implements_epg:
logger.debug(f"Using native EPG for '{provider_name}'")
epg_data = provider.get_epg(channel_id, **kwargs)
else:
logger.debug(f"Using generic EPG for '{provider_name}'")
epg_data = self.epg_manager.get_epg(
provider_name=provider_name,
channel_id=channel_id,
start_time=kwargs.get('start_time'),
end_time=kwargs.get('end_time')
)
logger.debug(f"Retrieved {len(epg_data)} EPG entries for '{channel_id}'")
return epg_data
def get_provider_epg_xmltv(self, provider_name: str, **kwargs) -> Optional[str]:
"""Get complete EPG data in XMLTV format."""
provider = self.registry.get_provider(provider_name)
if not provider:
raise ValueError(f"Provider '{provider_name}' not found or disabled")
if provider.implements_epg:
return provider.get_epg_xmltv(**kwargs)
logger.warning(f"Provider '{provider_name}' has no XMLTV EPG")
return None
def clear_epg_cache(self) -> bool:
"""Clear the generic EPG cache."""
return self.epg_manager.clear_cache()
def reload_epg_mapping(self) -> bool:
"""Reload EPG channel mapping."""
return self.epg_manager.reload_mapping()
def get_epg_cache_info(self) -> Optional[Dict]:
"""Get EPG cache information."""
return self.epg_manager.get_cache_info()
def get_epg_mapping_stats(self) -> Dict:
"""Get EPG mapping statistics."""
return self.epg_manager.get_mapping_stats()
def has_epg_mapping(self, provider_name: str, channel_id: str) -> bool:
"""Check if EPG mapping exists."""
return self.epg_manager.has_mapping_for_channel(provider_name, channel_id)
File diff suppressed because it is too large Load Diff
+236 -186
View File
@@ -1,75 +1,15 @@
# streaming_providers/base/provider.py - Enhanced with Header Abstractions
# streaming_providers/base/provider.py - Enhanced with Static Metadata
"""
Streaming Provider Base Class
Streaming Provider Base Class with Static Metadata Support
Authentication System:
---------------------
Providers implement authentication through three core components:
1. CAPABILITIES (Declarative):
- supported_auth_types: List[str] - What auth methods the provider CAN use
- preferred_auth_type: str - Which method SHOULD be used by default
2. STATE (Dynamic):
- get_current_auth_type(context) -> str - Which method IS currently active
- get_auth_status(context) -> AuthStatus - Complete authentication status
3. LOGIC (Optional Overrides):
- _calculate_auth_state(context) -> Optional[AuthState] - Custom auth state logic
- _calculate_readiness(context) -> Optional[Tuple[bool, str]] - Custom readiness
- get_auth_details(context) -> Dict[str, Any] - Provider-specific details
Auth Type Definitions:
- 'user_credentials': Username/password (Joyn, RTL+)
- 'client_credentials': Client ID/secret (Joyn fallback, some APIs)
- 'network_based': Fixed-line/network auth (Magenta2, cable providers)
- 'anonymous': No auth needed (ZDF, ARD)
- 'device_registration': Device-based auth (Smart TV apps)
- 'embedded_client': Built-in credentials
Implementation Examples:
----------------------
# Simple provider (ZDF)
class ZDFProvider(StreamingProvider):
@property
def supported_auth_types(self) -> List[str]:
return ['anonymous'] # Just one type
# Multi-auth provider (Joyn)
class JoynProvider(StreamingProvider):
@property
def supported_auth_types(self) -> List[str]:
return ['client_credentials', 'user_credentials']
@property
def preferred_auth_type(self) -> str:
return 'user_credentials' # Prefer full access
def get_current_auth_type(self, context: AuthContext) -> str:
# Custom logic to detect current auth mode
token = context.get_token(self.provider_name, None, self.country)
return 'user_credentials' if token and token.get('auth_level') == 'user_authenticated' else 'client_credentials'
# Network-based provider (Magenta2)
class Magenta2Provider(StreamingProvider):
@property
def supported_auth_types(self) -> List[str]:
return ['network_based']
@property
def primary_token_scope(self) -> Optional[str]:
return 'yo_digital' # Uses scoped tokens
def _calculate_readiness(self, context: AuthContext):
# Check multiple token scopes
yo_token = context.get_token(self.provider_name, 'yo_digital', self.country)
if yo_token and not context.session._is_token_expired(yo_token):
return True, "Has valid streaming token"
return False, "No valid tokens found"
New Features:
- Class attributes for static metadata (PROVIDER_LABEL, etc.)
- Static methods to get metadata without instantiation
- Backward compatible with existing @property methods
"""
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Callable, Any
from typing import Dict, List, Optional, Callable, Any, ClassVar
from enum import Enum
import json
from datetime import datetime
@@ -97,7 +37,22 @@ class StreamingProvider(ABC):
Abstract base class for streaming providers with centralized HTTP and auth management
"""
SUPPORTED_COUNTRIES: List[str] = []
# ============================================================================
# STATIC METADATA (NEW)
# ============================================================================
# Class attributes for static metadata (accessible without instantiation)
PROVIDER_LABEL: ClassVar[str] = ""
"""Base provider label without country suffix (e.g., 'Joyn', 'RTL+')"""
SUPPORTED_AUTH_TYPES: ClassVar[List[str]] = []
"""Authentication types supported by this provider"""
PROVIDER_LOGO: ClassVar[str] = ""
"""URL to provider logo"""
SUPPORTED_COUNTRIES: ClassVar[List[str]] = []
"""List of ISO country codes this provider supports (empty = single country)"""
def __init__(self, country: str = 'DE'):
self.country = country
@@ -106,6 +61,215 @@ class StreamingProvider(ABC):
self._default_user_agent = 'StreamingProvider/1.0'
self.authenticator = None # Optional: set by concrete providers
# ============================================================================
# STATIC METHODS FOR METADATA EXTRACTION (NEW)
# ============================================================================
@classmethod
def get_static_label(cls, country: str = None) -> str:
"""
Get provider label without instantiation.
Args:
country: Optional country code for country-specific labels
Returns:
Provider label string
"""
base_label = cls.PROVIDER_LABEL or cls.__name__.replace('Provider', '')
if country:
# Format country code
country_upper = country.upper()
# Special handling for common cases
if country_upper == 'DE':
return f"{base_label} Germany"
elif country_upper == 'AT':
return f"{base_label} Austria"
elif country_upper == 'CH':
return f"{base_label} Switzerland"
else:
return f"{base_label} ({country_upper})"
return base_label
@classmethod
def get_static_auth_types(cls) -> List[str]:
"""
Get supported authentication types without instantiation.
Returns:
List of supported auth type strings
"""
return cls.SUPPORTED_AUTH_TYPES.copy()
@classmethod
def get_static_logo(cls) -> str:
"""
Get provider logo URL without instantiation.
Returns:
Logo URL string
"""
return cls.PROVIDER_LOGO
@classmethod
def get_static_supported_countries(cls) -> List[str]:
"""
Get supported countries without instantiation.
Returns:
List of ISO country codes
"""
return cls.SUPPORTED_COUNTRIES.copy()
@classmethod
def get_all_possible_instances(cls) -> List[Dict[str, Any]]:
"""
Get metadata for all possible instances of this provider.
Returns:
List of instance metadata dictionaries
"""
instances = []
if cls.supports_multiple_countries():
for country in cls.SUPPORTED_COUNTRIES:
instances.append({
'plugin': cls.__name__.lower().replace('provider', ''),
'country': country.upper(),
'label': cls.get_static_label(country),
'requires_country_suffix': True
})
else:
# Single-country provider
instances.append({
'plugin': cls.__name__.lower().replace('provider', ''),
'country': 'DE', # Default country for single-country providers
'label': cls.get_static_label(),
'requires_country_suffix': False
})
return instances
# ============================================================================
# INSTANCE PROPERTIES (Backward Compatible)
# ============================================================================
@property
@abstractmethod
def provider_name(self) -> str:
"""Return the provider name (e.g., 'joyn', 'zdf', 'ard')"""
pass
@property
def provider_label(self) -> str:
"""Return the provider label (e.g., 'JOYN', 'ZDF', 'RTL+')"""
# Use static method with instance's country
return self.get_static_label(self.country)
@property
def provider_logo(self) -> str:
"""Return the provider logo URL"""
return self.get_static_logo()
@property
def supported_auth_types(self) -> List[str]:
"""List of authentication types this provider supports."""
return self.get_static_auth_types()
@property
@abstractmethod
def uses_dynamic_manifests(self) -> bool:
"""Return True if provider uses truly dynamic manifests"""
pass
@property
@abstractmethod
def implements_epg(self) -> bool:
"""
Indicates whether this provider has its own EPG implementation.
If False, the generic EPG manager will be used.
Override in subclass and return True if provider has native EPG.
Returns:
True if provider implements its own EPG, False to use generic EPG
"""
pass
@abstractmethod
def get_channels(self, **kwargs) -> List[StreamingChannel]:
"""Fetch channels from the provider"""
pass
@abstractmethod
def get_drm(self, channel_id: str, **kwargs) -> List[DRMConfig]:
"""Get all DRM configurations for a channel by ID"""
return []
@property
def catchup_window(self) -> int:
"""
Return the catchup window in HOURS for this provider.
Returns:
int: Number of hours of catchup available (0 = no catchup support)
"""
return 0
@property
def supports_catchup(self) -> bool:
"""
Check if provider supports catchup/timeshift functionality.
Returns:
bool: True if catchup is supported
"""
return self.catchup_window > 0
def get_epg(self, channel_id: str,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
**kwargs) -> List[Dict]:
"""Get EPG data for a channel"""
return []
@staticmethod
def get_epg_xmltv(**kwargs) -> Optional[str]:
"""Get complete EPG data for this provider in XMLTV format"""
return None
@abstractmethod
def enrich_channel_data(self, channel: StreamingChannel, **kwargs) -> Optional[StreamingChannel]:
"""Enrich channel with additional data including manifest URL"""
return None
@abstractmethod
def get_manifest(self, channel_id: str, **kwargs) -> Optional[str]:
"""Get manifest URL for a specific channel by ID"""
return None
def get_dynamic_manifest_params(self, channel: StreamingChannel, **kwargs) -> Optional[str]:
"""Optional: Get dynamic manifest parameters for a channel"""
return None
def to_output_format(self, channels: List[StreamingChannel] = None) -> Dict:
"""Convert channels to output format"""
if channels is None:
channels = self.channels
return {
'Provider': self.provider_name,
'Country': self.country,
'Channels': [channel.to_dict() for channel in channels]
}
def to_json(self, channels: List[StreamingChannel] = None, indent: int = 2) -> str:
"""Convert to JSON string"""
return json.dumps(self.to_output_format(channels), indent=indent, ensure_ascii=False)
# ============================================================================
# HTTP MANAGER SETUP (Already Implemented)
# ============================================================================
@@ -212,7 +376,7 @@ class StreamingProvider(ABC):
if 'user_agent' in manager_kwargs:
ua_preview = manager_kwargs['user_agent'][:50] + '...' if len(manager_kwargs['user_agent']) > 50 else \
manager_kwargs['user_agent']
manager_kwargs['user_agent']
info_parts.append(f"user-agent: {ua_preview}")
if 'timeout' in manager_kwargs:
@@ -241,7 +405,7 @@ class StreamingProvider(ABC):
return http_manager
# ============================================================================
# AUTHENTICATION HEADER ABSTRACTIONS (NEW)
# AUTHENTICATION HEADER ABSTRACTIONS
# ============================================================================
def _get_base_headers(self,
@@ -446,120 +610,6 @@ class StreamingProvider(ABC):
logger.error(f"{self.provider_name}: Error getting {token_type} token: {e}")
return None
# ============================================================================
# ABSTRACT METHODS (Required by all providers)
# ============================================================================
@property
@abstractmethod
def provider_name(self) -> str:
"""Return the provider name (e.g., 'joyn', 'zdf', 'ard')"""
pass
@property
@abstractmethod
def provider_label(self) -> str:
"""Return the provider label (e.g., 'JOYN', 'ZDF', 'RTL+')"""
pass
@property
@abstractmethod
def provider_logo(self) -> str:
"""Return the provider logo URL"""
pass
@property
@abstractmethod
def uses_dynamic_manifests(self) -> bool:
"""Return True if provider uses truly dynamic manifests"""
pass
@property
@abstractmethod
def implements_epg(self) -> bool:
"""
Indicates whether this provider has its own EPG implementation.
If False, the generic EPG manager will be used.
Override in subclass and return True if provider has native EPG.
Returns:
True if provider implements its own EPG, False to use generic EPG
"""
pass
@abstractmethod
def get_channels(self, **kwargs) -> List[StreamingChannel]:
"""Fetch channels from the provider"""
pass
@abstractmethod
def get_drm(self, channel_id: str, **kwargs) -> List[DRMConfig]:
"""Get all DRM configurations for a channel by ID"""
return []
@property
def catchup_window(self) -> int:
"""
Return the catchup window in HOURS for this provider.
Returns:
int: Number of hours of catchup available (0 = no catchup support)
"""
return 0
@property
def supports_catchup(self) -> bool:
"""
Check if provider supports catchup/timeshift functionality.
Returns:
bool: True if catchup is supported
"""
return self.catchup_window > 0
def get_epg(self, channel_id: str,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
**kwargs) -> List[Dict]:
"""Get EPG data for a channel"""
return []
@staticmethod
def get_epg_xmltv(**kwargs) -> Optional[str]:
"""Get complete EPG data for this provider in XMLTV format"""
return None
@abstractmethod
def enrich_channel_data(self, channel: StreamingChannel, **kwargs) -> Optional[StreamingChannel]:
"""Enrich channel with additional data including manifest URL"""
return None
@abstractmethod
def get_manifest(self, channel_id: str, **kwargs) -> Optional[str]:
"""Get manifest URL for a specific channel by ID"""
return None
def get_dynamic_manifest_params(self, channel: StreamingChannel, **kwargs) -> Optional[str]:
"""Optional: Get dynamic manifest parameters for a channel"""
return None
def to_output_format(self, channels: List[StreamingChannel] = None) -> Dict:
"""Convert channels to output format"""
if channels is None:
channels = self.channels
return {
'Provider': self.provider_name,
'Country': self.country,
'Channels': [channel.to_dict() for channel in channels]
}
def to_json(self, channels: List[StreamingChannel] = None, indent: int = 2) -> str:
"""Convert to JSON string"""
return json.dumps(self.to_output_format(channels), indent=indent, ensure_ascii=False)
# ============================================================================
# CATCHUP ABSTRACT METHODS
# ============================================================================
@@ -762,7 +812,7 @@ class StreamingProvider(ABC):
return f"{base_url}{separator}start={start_time}&end={end_time}"
# ============================================================================
# SUBSCRIPTION METHODS (NEW)
# SUBSCRIPTION METHODS
# ============================================================================
def get_subscription_status(self, **kwargs) -> Optional[UserSubscription]:
@@ -1130,4 +1180,4 @@ class StreamingProvider(ABC):
Returns:
Dictionary with provider-specific information
"""
return {}
return {}
@@ -0,0 +1,222 @@
# streaming_providers/base/provider_registry.py
"""
Core provider registry handling discovery, metadata, and lifecycle management.
"""
from typing import Dict, List, Optional, Any
from .provider import StreamingProvider
from .utils.logger import logger
class ProviderMetadata:
"""Metadata for a provider instance with lazy initialization."""
def __init__(self, plugin_class, country: str, enabled: bool = False):
self.plugin_class = plugin_class
self.country = country.lower()
self.enabled = enabled
self.instance: Optional[StreamingProvider] = None
self._extract_metadata()
def _extract_metadata(self):
"""Extract static metadata from provider class without instantiation"""
self.plugin_name = self.plugin_class.__name__.lower().replace('provider', '')
if self.plugin_class.supports_multiple_countries():
self.name = f"{self.plugin_name}_{self.country}"
else:
self.name = self.plugin_name
self.label = self.plugin_class.get_static_label(self.country)
self.supported_auth_types = self.plugin_class.get_static_auth_types()
self.logo = self.plugin_class.get_static_logo()
self.supported_countries = self.plugin_class.get_static_supported_countries()
self.requires_credentials = any(
auth_type in ['user_credentials', 'client_credentials']
for auth_type in self.supported_auth_types
)
self.is_multi_country = len(self.supported_countries) > 0
def create_instance(self) -> Optional[StreamingProvider]:
"""Lazily create provider instance if enabled"""
if not self.enabled:
return None
if self.instance is None:
try:
logger.info(f"Creating instance for provider: {self.name}")
self.instance = self.plugin_class(country=self.country)
logger.debug(f"Successfully created instance for {self.name}")
except Exception as e:
logger.error(f"Failed to create instance for {self.name}: {e}")
self.instance = None
return self.instance
def destroy_instance(self):
"""Clean up provider instance"""
if self.instance:
logger.debug(f"Destroying instance for provider: {self.name}")
self.instance = None
def set_enabled(self, enabled: bool):
"""Update enabled status and manage instance accordingly"""
self.enabled = enabled
if enabled and self.instance is None:
self.create_instance()
elif not enabled and self.instance is not None:
self.destroy_instance()
def to_dict(self) -> Dict[str, Any]:
"""Convert metadata to dictionary for API response"""
return {
'name': self.name,
'label': self.label,
'plugin': self.plugin_name,
'country': self.country.upper(),
'enabled': self.enabled,
'instance_ready': self.instance is not None,
'requires_credentials': self.requires_credentials,
'supported_auth_types': self.supported_auth_types,
'logo': self.logo,
'is_multi_country': self.is_multi_country,
'supported_countries': self.supported_countries
}
class ProviderRegistry:
"""
Core registry for provider discovery, metadata, and lifecycle management.
Separated concern: Provider registration and access.
"""
def __init__(self):
self.providers: Dict[str, StreamingProvider] = {} # Active instances
self.provider_metadata: Dict[str, ProviderMetadata] = {} # All providers
logger.info("ProviderRegistry: Initialized")
@staticmethod
def _is_provider_enabled(provider_name: str, country: Optional[str] = None) -> bool:
"""Check if a provider is enabled via settings manager."""
try:
from .settings.provider_enable_manager import ProviderEnableManager
enable_manager = ProviderEnableManager()
instance_name = f"{provider_name}_{country}" if country else provider_name
return enable_manager.is_provider_enabled(instance_name)
except Exception as e:
logger.warning(f"Could not check enable status for '{provider_name}': {e}")
return True
def discover_all_providers(self, default_country: str = 'DE') -> List[str]:
"""Discover ALL provider instances and extract metadata."""
from streaming_providers import AVAILABLE_PROVIDERS
logger.info("ProviderRegistry: Discovering provider instances")
discovered = []
for plugin_name, plugin_class in AVAILABLE_PROVIDERS.items():
if plugin_class.supports_multiple_countries():
for country in plugin_class.get_static_supported_countries():
instance_name = f"{plugin_name}_{country}"
enabled = self._is_provider_enabled(plugin_name, country)
metadata = ProviderMetadata(plugin_class, country, enabled)
self.provider_metadata[instance_name] = metadata
discovered.append(instance_name)
if enabled:
instance = metadata.create_instance()
if instance:
self.providers[instance_name] = instance
else:
instance_name = plugin_name
enabled = self._is_provider_enabled(plugin_name)
metadata = ProviderMetadata(plugin_class, default_country, enabled)
self.provider_metadata[instance_name] = metadata
discovered.append(instance_name)
if enabled:
instance = metadata.create_instance()
if instance:
self.providers[instance_name] = instance
logger.info(f"ProviderRegistry: Discovered {len(discovered)} provider instances")
return discovered
def get_provider(self, provider_name: str) -> Optional[StreamingProvider]:
"""Get provider instance, creating it lazily if needed."""
provider = self.providers.get(provider_name)
if provider:
return provider
metadata = self.provider_metadata.get(provider_name)
if not metadata or not metadata.enabled:
return None
provider = metadata.create_instance()
if provider:
self.providers[provider_name] = provider
return provider
def set_provider_enabled(self, provider_name: str, enabled: bool) -> bool:
"""Enable or disable a provider dynamically."""
metadata = self.provider_metadata.get(provider_name)
if not metadata:
logger.error(f"Cannot enable/disable unknown provider '{provider_name}'")
return False
metadata.set_enabled(enabled)
if enabled and metadata.instance:
self.providers[provider_name] = metadata.instance
elif not enabled and provider_name in self.providers:
del self.providers[provider_name]
try:
from .settings.provider_enable_manager import ProviderEnableManager
enable_manager = ProviderEnableManager()
success, message = enable_manager.set_provider_enabled(provider_name, enabled)
return success
except Exception as e:
logger.error(f"Error updating enable status: {e}")
return False
def reinitialize_provider(self, provider_name: str) -> bool:
"""Reinitialize a provider instance."""
metadata = self.provider_metadata.get(provider_name)
if not metadata or not metadata.enabled:
return False
try:
metadata.destroy_instance()
new_instance = metadata.create_instance()
if new_instance:
self.providers[provider_name] = new_instance
return True
return False
except Exception as e:
logger.error(f"Failed to reinitialize '{provider_name}': {e}")
return False
def get_all_providers_metadata(self) -> List[Dict[str, Any]]:
"""Get metadata for ALL provider instances."""
return [m.to_dict() for m in self.provider_metadata.values()]
def list_providers(self) -> List[str]:
"""List enabled provider names."""
return list(self.providers.keys())
def list_all_providers(self) -> List[str]:
"""List ALL provider names (enabled + disabled)."""
return list(self.provider_metadata.keys())
def get_enabled_providers(self) -> List[str]:
"""Get list of enabled provider names."""
return [name for name, m in self.provider_metadata.items() if m.enabled]
def clear_providers(self):
"""Clear all providers."""
self.providers.clear()
logger.info("ProviderRegistry: Cleared all providers")
@@ -0,0 +1,79 @@
# ============================================================================
# streaming_providers/base/subscription_operations.py
"""
Subscription and package management operations.
"""
from typing import Optional, List
from .models import UserSubscription, SubscriptionPackage, StreamingChannel
from .utils.logger import logger
class SubscriptionOperations:
"""Handles all subscription-related operations."""
def __init__(self, registry):
self.registry = registry
logger.debug("SubscriptionOperations: Initialized")
def get_subscription_status(self, provider_name: str,
**kwargs) -> Optional[UserSubscription]:
"""Get subscription status for a provider."""
provider = self.registry.get_provider(provider_name)
if not provider:
raise ValueError(f"Provider '{provider_name}' not found or disabled")
try:
subscription = provider.get_subscription_status(**kwargs)
if subscription:
logger.debug(f"Got subscription for '{provider_name}': "
f"{subscription.package_count} packages")
return subscription
except Exception as e:
logger.warning(f"Error getting subscription for '{provider_name}': {e}")
return None
def get_subscribed_channels(self, provider_name: str,
**kwargs) -> List[StreamingChannel]:
"""Get subscribed channels."""
provider = self.registry.get_provider(provider_name)
if not provider:
raise ValueError(f"Provider '{provider_name}' not found or disabled")
try:
channels = provider.get_subscribed_channels(**kwargs)
logger.info(f"Got {len(channels)} subscribed channels from '{provider_name}'")
return channels
except Exception as e:
logger.error(f"Error getting subscribed channels: {e}")
return provider.get_channels(**kwargs)
def get_available_packages(self, provider_name: str,
**kwargs) -> List[SubscriptionPackage]:
"""Get available subscription packages."""
provider = self.registry.get_provider(provider_name)
if not provider:
raise ValueError(f"Provider '{provider_name}' not found or disabled")
try:
packages = provider.get_available_packages(**kwargs)
logger.debug(f"Got {len(packages)} packages from '{provider_name}'")
return packages
except Exception as e:
logger.warning(f"Error getting packages for '{provider_name}': {e}")
return []
def is_channel_accessible(self, provider_name: str, channel_id: str,
**kwargs) -> bool:
"""Check if channel is accessible with current subscription."""
provider = self.registry.get_provider(provider_name)
if not provider:
raise ValueError(f"Provider '{provider_name}' not found or disabled")
try:
accessible = provider.is_channel_accessible(channel_id, **kwargs)
logger.debug(f"Channel '{channel_id}' accessible: {accessible}")
return accessible
except Exception as e:
logger.warning(f"Error checking accessibility: {e}")
return True # Assume accessible on error
@@ -1,18 +1,31 @@
# lib/streaming_providers/providers/hrti/provider.py
import json
import requests
from typing import Dict, List, Optional
from typing import Dict, List, Optional, ClassVar
from ...base.provider import StreamingProvider, AuthType # ← ADD AuthType import
from ...base.provider import StreamingProvider, AuthType
from ...base.models.streaming_channel import StreamingChannel
from ...base.models import DRMConfig, LicenseConfig, DRMSystem, LicenseUnwrapperParams
from .auth import HRTiAuthenticator
from .constants import HRTiConfig
from ...base.utils import logger
from ...base.models.proxy_models import ProxyConfig
from .constants import HRTiDefaults
class HRTiProvider(StreamingProvider):
"""
HRTi (Croatian Radio Television Internet) provider implementation.
"""
# ============================================================================
# STATIC METADATA (NEW)
# ============================================================================
PROVIDER_LABEL: ClassVar[str] = "HRTi"
SUPPORTED_AUTH_TYPES: ClassVar[List[str]] = ['user_credentials']
PROVIDER_LOGO: ClassVar[str] = HRTiDefaults.PROVIDER_LOGO
SUPPORTED_COUNTRIES: ClassVar[List[str]] = ['HR'] # Croatia only
def __init__(self, country: str = 'HR', config: Optional[Dict] = None, proxy_config: Optional[ProxyConfig] = None):
super().__init__(country)
@@ -20,7 +33,7 @@ class HRTiProvider(StreamingProvider):
self.hrti_config = HRTiConfig(config)
self.channels_cache = None
# ✅ Use abstraction for HTTP manager setup
# Setup HTTP manager using abstraction
self.http_manager = self._setup_http_manager(
provider_name='hrti',
proxy_config=proxy_config,
@@ -50,11 +63,13 @@ class HRTiProvider(StreamingProvider):
@property
def provider_label(self) -> str:
return 'HRTi'
# Override to use static metadata with country context
return self.get_static_label(self.country)
@property
def provider_logo(self) -> str:
return self.hrti_config.logo
# Use instance config for backward compatibility
return self.hrti_config.logo or self.PROVIDER_LOGO
@property
def uses_dynamic_manifests(self) -> bool:
@@ -67,7 +82,7 @@ class HRTiProvider(StreamingProvider):
@property
def supported_auth_types(self) -> List[str]:
return ['user_credentials']
return self.SUPPORTED_AUTH_TYPES # Use class attribute
def _get_hrti_authenticated_headers(self) -> Dict[str, str]:
"""
@@ -76,7 +91,7 @@ class HRTiProvider(StreamingProvider):
This is a provider-specific wrapper that uses the base class abstraction.
"""
return self._build_provider_headers(
auth_type=AuthType.CLIENT, # ← Now properly imported
auth_type=AuthType.CLIENT,
token_key='authorization', # HRTi uses lowercase
provider_headers={
'deviceid': self.authenticator.get_device_id(),
@@ -93,7 +108,7 @@ class HRTiProvider(StreamingProvider):
Fetch channels from HRTi API
"""
try:
# Use provider-specific method
# Use provider-specific method
headers = self._get_hrti_authenticated_headers()
# Log the request for debugging
@@ -452,7 +467,7 @@ class HRTiProvider(StreamingProvider):
logger.error(f"Traceback: {traceback.format_exc()}")
return []
def get_epg_data(self, channel_id: str, **kwargs) -> Optional[Dict]:
def get_epg(self, channel_id: str, **kwargs) -> List[Dict]:
"""
Get EPG data for a channel
"""
@@ -479,12 +494,22 @@ class HRTiProvider(StreamingProvider):
epg_data = response.json()
if 'Result' in epg_data:
return epg_data['Result']
return None
# Convert to standard EPG format
epg_entries = []
for entry in epg_data['Result']:
epg_entries.append({
'title': entry.get('Title', ''),
'description': entry.get('Description', ''),
'start': entry.get('StartTime', ''),
'end': entry.get('EndTime', ''),
'genre': entry.get('Genre', '')
})
return epg_entries
return []
except Exception as e:
logger.error(f"Error getting EPG data for channel {channel_id}: {e}")
return None
return []
def get_license_url(self, channel: StreamingChannel, **kwargs) -> Optional[str]:
"""
@@ -493,4 +518,41 @@ class HRTiProvider(StreamingProvider):
drm_configs = self.get_drm(channel.channel_id, **kwargs)
if drm_configs:
return drm_configs[0].license.server_url
return None
# ============================================================================
# CATCHUP METHODS (Implementing abstract methods)
# ============================================================================
@property
def catchup_window(self) -> int:
"""
Return the catchup window in HOURS for HRTi.
Note: HRTi doesn't officially support catchup for live channels,
but may have some VOD content available.
"""
return 0 # No catchup support for live streams
def get_epg_xmltv(self, **kwargs) -> Optional[str]:
"""
Get complete EPG data for HRTi in XMLTV format.
Returns:
XMLTV formatted string, or None if not available
"""
# HRTi doesn't provide XMLTV format natively
return None
def get_dynamic_manifest_params(self, channel: StreamingChannel, **kwargs) -> Optional[str]:
"""
Get dynamic manifest parameters for HRTi channels.
Args:
channel: StreamingChannel to get parameters for
Returns:
Parameters string or None
"""
# HRTi requires session authorization which is handled in enrich_channel_data
return None
@@ -1,6 +1,6 @@
# streaming_providers/providers/joyn/provider.py
# -*- coding: utf-8 -*-
from typing import Dict, Optional, List
from typing import Dict, Optional, List, ClassVar
import json
import time
import hashlib
@@ -73,7 +73,14 @@ class JoynProvider(StreamingProvider):
"""
Joyn streaming provider implementation with centralized HTTP management
"""
SUPPORTED_COUNTRIES = SUPPORTED_COUNTRIES
# ============================================================================
# STATIC METADATA (NEW)
# ============================================================================
PROVIDER_LABEL: ClassVar[str] = "Joyn"
SUPPORTED_AUTH_TYPES: ClassVar[List[str]] = ['client_credentials', 'user_credentials']
PROVIDER_LOGO: ClassVar[str] = JOYN_LOGO
SUPPORTED_COUNTRIES: ClassVar[List[str]] = SUPPORTED_COUNTRIES
def __init__(self, country: str = 'de',
platform: str = DEFAULT_PLATFORM,
@@ -105,27 +112,7 @@ class JoynProvider(StreamingProvider):
self.distribution_tenant = COUNTRY_TENANT_MAPPING[country]
self.platform = platform
# ✅ BEFORE: Manual proxy resolution and HTTP manager setup (15 lines)
# self.proxy_config = (
# proxy_config or
# (ProxyConfig.from_url(proxy_url) if proxy_url else None) or
# self._load_proxy_from_manager(config_dir)
# )
#
# if self.proxy_config:
# logger.info("Using proxy configuration for Joyn")
# else:
# logger.debug("No proxy configuration found for Joyn")
#
# self.http_manager = HTTPManagerFactory.create_for_provider(
# provider_name='joyn',
# proxy_config=self.proxy_config,
# user_agent=JOYN_USER_AGENT,
# timeout=DEFAULT_REQUEST_TIMEOUT,
# max_retries=DEFAULT_MAX_RETRIES
# )
# ✅ AFTER: Using abstraction (6 lines)
# Setup HTTP manager using abstraction
self.http_manager = self._setup_http_manager(
provider_name='joyn',
proxy_config=proxy_config,
@@ -145,9 +132,6 @@ class JoynProvider(StreamingProvider):
proxy_config=self.http_manager.config.proxy_config # Use resolved proxy
)
# ✅ Optional: Share HTTP manager (if authenticator might have its own)
# self.http_manager = self._share_http_manager_with_authenticator(self.authenticator)
# Authenticate
try:
self.bearer_token = self.authenticator.get_bearer_token()
@@ -155,29 +139,28 @@ class JoynProvider(StreamingProvider):
logger.warning(f"Could not authenticate during initialization: {e}")
self.bearer_token = None
# ✅ BEFORE: Had to implement this helper method (12 lines)
# 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('joyn', self.country)
# except Exception as e:
# logger.warning(f"Could not load proxy from ProxyConfigManager: {e}")
# return None
# ✅ AFTER: No longer needed - handled by abstraction!
@property
def provider_name(self) -> str:
return 'joyn'
# Override provider_label to provide custom country formatting
@property
def provider_label(self) -> str:
return f'Joyn ({self.country})'
"""Return country-specific label"""
country_map = {
'de': 'Joyn Germany',
'at': 'Joyn Austria',
'ch': 'Joyn Switzerland'
}
return country_map.get(self.country, f"Joyn ({self.country.upper()})")
@property
def provider_logo(self) -> str:
return JOYN_LOGO
return self.PROVIDER_LOGO # Use class attribute
@property
def supported_auth_types(self) -> List[str]:
return self.SUPPORTED_AUTH_TYPES # Use class attribute
@property
def uses_dynamic_manifests(self) -> bool:
@@ -187,10 +170,6 @@ class JoynProvider(StreamingProvider):
def implements_epg(self) -> bool:
return False
@property
def supported_auth_types(self) -> List[str]:
return ['client_credentials','user_credentials']
def authenticate(self, **kwargs) -> str:
"""Authenticate and return bearer token"""
self.bearer_token = self.authenticator.get_bearer_token(
@@ -221,10 +200,10 @@ class JoynProvider(StreamingProvider):
return self.bearer_token
def get_channels(self,
time_window_hours: int = DEFAULT_EPG_WINDOW_HOURS,
fetch_manifests: bool = False,
populate_streaming_data: bool = True,
**kwargs) -> List[StreamingChannel]:
time_window_hours: int = DEFAULT_EPG_WINDOW_HOURS,
fetch_manifests: bool = False,
populate_streaming_data: bool = True,
**kwargs) -> List[StreamingChannel]:
"""
Fetch available channels from Joyn GraphQL API
@@ -263,7 +242,6 @@ class JoynProvider(StreamingProvider):
url = f"{JOYN_GRAPHQL_ENDPOINTS['LIVE_CHANNELS']}&variables={variables_encoded}&extensions={extensions_encoded}"
# ✅ Already using http_manager correctly
response = self.http_manager.get(
url,
operation='api',
@@ -369,7 +347,6 @@ class JoynProvider(StreamingProvider):
}
try:
# ✅ Already using http_manager correctly
response = self.http_manager.post(
JOYN_STREAMING_ENDPOINTS['ENTITLEMENT'],
operation='auth',
@@ -426,7 +403,6 @@ class JoynProvider(StreamingProvider):
headers['Authorization'] = f'Bearer {entitlement_token}'
try:
# ✅ Already using http_manager correctly
response = self.http_manager.post(
url,
operation='manifest',
@@ -1,7 +1,7 @@
# streaming_providers/providers/magentaeu/provider.py
# -*- coding: utf-8 -*-
import time
from typing import Dict, Optional, List
from typing import Dict, Optional, List, ClassVar
from ...base.auth import UserPasswordCredentials
from ...base.provider import StreamingProvider
@@ -21,6 +21,7 @@ from .constants import (
DRM_SYSTEM_WIDEVINE,
MAGENTA_TV_AT_LOGO,
MAGENTA_TV_PL_LOGO,
MAX_TV_LOGO,
WV_URL,
CONTENT_TYPE_LIVE,
STREAMING_FORMAT_DASH,
@@ -28,11 +29,15 @@ from .constants import (
get_natco_key,
get_guest_headers,
get_base_url,
get_language, MAX_TV_LOGO
get_language
)
class MagentaProvider(StreamingProvider):
# Provider constants with country-specific logos
PROVIDER_LOGO_AT: ClassVar[str] = MAGENTA_TV_AT_LOGO
PROVIDER_LOGO_PL: ClassVar[str] = MAGENTA_TV_PL_LOGO
PROVIDER_LOGO_HR: ClassVar[str] = MAX_TV_LOGO
SUPPORTED_COUNTRIES = SUPPORTED_COUNTRIES
@@ -53,21 +58,6 @@ class MagentaProvider(StreamingProvider):
if country not in SUPPORTED_COUNTRIES:
raise ValueError(f"Unsupported country: {country}")
# ✅ BEFORE: Complex proxy resolution logic (10+ lines)
# self.proxy_config = (
# proxy_config or
# (ProxyConfig.from_url(proxy_url) if proxy_url else None) or
# self._load_proxy_from_manager(config_dir)
# )
#
# self.http_manager = HTTPManagerFactory.create_for_provider(
# provider_name='magentaeu',
# proxy_config=self.proxy_config,
# user_agent=USER_AGENT,
# timeout=DEFAULT_REQUEST_TIMEOUT,
# max_retries=DEFAULT_MAX_RETRIES
# )
# ✅ AFTER: Using abstraction with automatic proxy resolution (6 lines)
self.http_manager = self._setup_http_manager(
provider_name='magentaeu',
@@ -119,12 +109,13 @@ class MagentaProvider(StreamingProvider):
@property
def provider_logo(self) -> str:
if self.country.lower() == 'at':
return MAGENTA_TV_AT_LOGO
if self.country.lower() == 'hr':
return MAX_TV_LOGO
elif self.country.lower() == 'pl':
return MAGENTA_TV_PL_LOGO
country_lower = self.country.lower()
if country_lower == 'at':
return self.PROVIDER_LOGO_AT
elif country_lower == 'hr':
return self.PROVIDER_LOGO_HR
elif country_lower == 'pl':
return self.PROVIDER_LOGO_PL
else:
return ''
@@ -1,8 +1,8 @@
# lib/streaming_providers/providers/rtlplus/provider.py
import json
import requests
from typing import Dict, List, Optional
from typing import Dict, List, Optional, ClassVar
import requests
from ...base.provider import StreamingProvider
from ...base.models.streaming_channel import StreamingChannel
from ...base.models import DRMConfig, LicenseConfig, DRMSystem
@@ -13,6 +13,9 @@ from ...base.models.proxy_models import ProxyConfig
class RTLPlusProvider(StreamingProvider):
# Provider constants
PROVIDER_LOGO: ClassVar[str] = RTLPlusDefaults.RTLPLUS_LOGO
def __init__(self, country: str = 'DE', config: Optional[Dict] = None,
proxy_config: Optional[ProxyConfig] = None):
super().__init__(country)
@@ -58,7 +61,7 @@ class RTLPlusProvider(StreamingProvider):
@property
def provider_logo(self) -> str:
return self.rtl_config.logo
return self.PROVIDER_LOGO
@property
def uses_dynamic_manifests(self) -> bool:
@@ -73,7 +76,7 @@ class RTLPlusProvider(StreamingProvider):
def supported_auth_types(self) -> List[str]:
return ['user_credentials']
# ============================================================================
# ============================================================================
# OPTION 1: Provider-specific method (RECOMMENDED - No signature conflict)
# ============================================================================
def _get_rtlplus_authenticated_headers(self) -> Dict[str, str]:
+69 -72
View File
@@ -712,66 +712,71 @@ class UltimateService:
@self.app.route('/api/providers')
def list_providers():
try:
provider_names = self.manager.list_providers()
default_country = self._get_setting('default_country', 'DE')
# Get metadata for ALL providers (enabled + disabled)
all_metadata = self.manager.get_all_providers_metadata()
providers_details = []
for provider_name in provider_names:
provider_instance = self.manager.get_provider(provider_name)
if provider_instance:
provider_label = getattr(provider_instance, 'provider_label', provider_name)
country = getattr(provider_instance, 'country', default_country)
provider_logo = getattr(provider_instance, 'provider_logo', '')
# For backward compatibility, also get details for enabled providers
enabled_providers = []
for metadata in all_metadata:
if metadata['enabled'] and metadata['instance_ready']:
provider_instance = self.manager.get_provider(metadata['name'])
if provider_instance:
# Get detailed auth info from instance
supported_auth_types = getattr(provider_instance, 'supported_auth_types', [])
preferred_auth_type = getattr(provider_instance, 'preferred_auth_type', 'unknown')
requires_stored_credentials = getattr(
provider_instance, 'requires_stored_credentials', False
)
# Get authentication properties
supported_auth_types = getattr(provider_instance, 'supported_auth_types', [])
preferred_auth_type = getattr(provider_instance, 'preferred_auth_type', 'unknown')
requires_stored_credentials = getattr(
provider_instance, 'requires_stored_credentials', False
)
# Check specific auth type needs
needs_user_creds = 'user_credentials' in supported_auth_types
needs_client_creds = 'client_credentials' in supported_auth_types
is_network_based = 'network_based' in supported_auth_types
is_anonymous = 'anonymous' in supported_auth_types
uses_device_reg = 'device_registration' in supported_auth_types
uses_embedded = 'embedded_client' in supported_auth_types
# Check specific auth type needs
needs_user_creds = 'user_credentials' in supported_auth_types
needs_client_creds = 'client_credentials' in supported_auth_types
is_network_based = 'network_based' in supported_auth_types
is_anonymous = 'anonymous' in supported_auth_types
uses_device_reg = 'device_registration' in supported_auth_types
uses_embedded = 'embedded_client' in supported_auth_types
provider_details = {
'name': metadata['name'],
'label': metadata['label'],
'logo': metadata['logo'],
'country': metadata['country'],
providers_details.append({
'name': provider_name,
'label': provider_label,
'logo': provider_logo,
'country': country,
# Core authentication properties
'auth': {
'supported_auth_types': supported_auth_types,
'preferred_auth_type': preferred_auth_type,
'requires_stored_credentials': requires_stored_credentials,
# Core authentication properties
'auth': {
'supported_auth_types': supported_auth_types,
'preferred_auth_type': preferred_auth_type,
'requires_stored_credentials': requires_stored_credentials,
# Specific auth type flags for easy UI decisions
'needs_user_credentials': needs_user_creds,
'needs_client_credentials': needs_client_creds,
'is_network_based': is_network_based,
'is_anonymous': is_anonymous,
'uses_device_registration': uses_device_reg,
'uses_embedded_client': uses_embedded,
# Specific auth type flags for easy UI decisions
'needs_user_credentials': needs_user_creds,
'needs_client_credentials': needs_client_creds,
'is_network_based': is_network_based,
'is_anonymous': is_anonymous,
'uses_device_registration': uses_device_reg,
'uses_embedded_client': uses_embedded,
# Derived summary for UI
'needs_user_input': needs_user_creds or uses_device_reg,
'needs_configuration': needs_user_creds or needs_client_creds,
'is_automatic': is_network_based or is_anonymous or uses_embedded,
},
# Derived summary for UI
'needs_user_input': needs_user_creds or uses_device_reg,
'needs_configuration': needs_user_creds or needs_client_creds,
'is_automatic': is_network_based or is_anonymous or uses_embedded,
},
# Token properties
'primary_token_scope': getattr(provider_instance, 'primary_token_scope', None),
'token_scopes': getattr(provider_instance, 'token_scopes', []),
# Token properties
'primary_token_scope': getattr(provider_instance, 'primary_token_scope', None),
'token_scopes': getattr(provider_instance, 'token_scopes', []),
})
# Metadata fields
'enabled': metadata['enabled'],
'instance_ready': metadata['instance_ready'],
'requires_credentials': metadata['requires_credentials']
}
enabled_providers.append(provider_details)
return {
'providers': providers_details,
'default_country': default_country
'providers': enabled_providers,
'all_providers': all_metadata, # NEW: Include all providers metadata
'default_country': self.default_country
}
except Exception as api_err:
logger.error(f"API Error in /api/providers: {str(api_err)}")
@@ -2099,13 +2104,8 @@ class UltimateService:
@self.app.route('/api/providers/<provider>/enabled', method='POST')
def set_provider_enabled(provider):
"""Set enabled status for provider (writes to file only)"""
"""Set enabled status for provider"""
try:
# Validate provider exists
if not self.manager.get_provider(provider):
response.status = 404
return {'error': f'Provider {provider} not found'}
# Parse request
try:
data = request.json
@@ -2118,31 +2118,28 @@ class UltimateService:
response.status = 400
return {'error': 'Invalid JSON'}
# Check if controlled by Kodi
enable_manager = ProviderEnableManager()
source = enable_manager.get_enabled_source(provider)
if source == 'kodi':
response.status = 403
return {
'error': f'Provider {provider} is controlled by Kodi settings',
'hint': 'Change the setting in Kodi addon settings'
}
# Write to file
success = enable_manager.set_provider_enabled(provider, enabled)
# Use the new manager method
success = self.manager.set_provider_enabled(provider, enabled)
if success:
# Get updated metadata
metadata = None
all_metadata = self.manager.get_all_providers_metadata()
for md in all_metadata:
if md['name'] == provider:
metadata = md
break
return {
'success': True,
'provider': provider,
'enabled': enabled,
'source': 'file',
'message': f'Provider {provider} {"enabled" if enabled else "disabled"} in file'
'metadata': metadata,
'message': f'Provider {provider} {"enabled" if enabled else "disabled"}'
}
else:
response.status = 500
return {'error': 'Failed to save setting'}
return {'error': f'Failed to {"enable" if enabled else "disable"} provider {provider}'}
except Exception as e:
logger.error(f"Error setting enabled status for {provider}: {e}")