mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-16 14:12:20 +02:00
Restructure for enabling/disabling
This commit is contained in:
@@ -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
@@ -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
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user