# streaming_providers/base/settings/models/provider_settings.py from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Set from ...utils.logger import logger from .settings_models import ( SettingValue, boolean_setting, integer_setting, password_setting, port_setting, select_setting, string_setting, ) @dataclass class ProviderSettingsSchema: """Schema definition for a provider's settings""" provider_name: str _settings: Dict[str, SettingValue] = field(default_factory=dict) _categories: Dict[str, Set[str]] = field(default_factory=dict) _kodi_mapping: Dict[str, str] = field(default_factory=dict) def __post_init__(self): """Initialize schema with default settings""" self._build_default_schema() def _build_default_schema(self) -> None: """Build the complete settings schema for this provider""" # Build all setting categories credential_settings = self.define_credential_settings() proxy_settings = self.define_proxy_settings() video_settings = self.define_video_settings() drm_settings = self.define_drm_settings() network_settings = self.define_network_settings() # Merge all settings self._settings.update(credential_settings) self._settings.update(proxy_settings) self._settings.update(video_settings) self._settings.update(drm_settings) self._settings.update(network_settings) # Build category mappings self._categories = { "credentials": set(credential_settings.keys()), "proxy": set(proxy_settings.keys()), "video": set(video_settings.keys()), "drm": set(drm_settings.keys()), "network": set(network_settings.keys()), } # Build Kodi mapping self._build_kodi_mapping() def _build_kodi_mapping(self) -> None: """Build mapping from internal setting names to Kodi setting IDs""" for setting_name, setting in self._settings.items(): if setting.kodi_setting_id: self._kodi_mapping[setting_name] = setting.kodi_setting_id def define_credential_settings(self) -> Dict[str, SettingValue]: """Define credential-related settings for this provider""" settings = {} # Common credential settings - can be overridden in subclasses settings["username"] = ( string_setting() .required() .min_length(1) .display_name("Username") .description("Account username or email address") .kodi_setting(f"{self.provider_name}_username") .build() ) settings["password"] = ( password_setting() .display_name("Password") .description("Account password") .kodi_setting(f"{self.provider_name}_password") .build() ) # Optional client ID for OAuth providers settings["client_id"] = ( string_setting() .display_name("Client ID") .description("OAuth client identifier (if required)") .kodi_setting(f"{self.provider_name}_client_id") .build() ) # Optional client secret for OAuth providers settings["client_secret"] = ( password_setting(required=False) .display_name("Client Secret") .description("OAuth client secret (if required)") .kodi_setting(f"{self.provider_name}_client_secret") .build() ) return settings def define_proxy_settings(self) -> Dict[str, SettingValue]: """Define proxy-related settings for this provider""" settings = {} # Enable proxy for this provider settings["proxy_enabled"] = ( boolean_setting(False) .display_name("Enable Proxy") .description("Use proxy for this provider") .kodi_setting(f"{self.provider_name}_proxy_enabled") .build() ) # Proxy host settings["proxy_host"] = ( string_setting() .display_name("Proxy Host") .description("Proxy server hostname or IP address") .kodi_setting(f"{self.provider_name}_proxy_host") .build() ) # Proxy port settings["proxy_port"] = ( port_setting(8080) .display_name("Proxy Port") .description("Proxy server port") .kodi_setting(f"{self.provider_name}_proxy_port") .build() ) # Proxy type settings["proxy_type"] = ( select_setting(["http", "https", "socks4", "socks5"], "http") .display_name("Proxy Type") .description("Type of proxy server") .kodi_setting(f"{self.provider_name}_proxy_type") .build() ) # Proxy authentication settings["proxy_auth_enabled"] = ( boolean_setting(False) .display_name("Proxy Authentication") .description("Proxy server requires authentication") .kodi_setting(f"{self.provider_name}_proxy_auth_enabled") .build() ) # Proxy username settings["proxy_username"] = ( string_setting() .display_name("Proxy Username") .description("Username for proxy authentication") .kodi_setting(f"{self.provider_name}_proxy_username") .build() ) # Proxy password settings["proxy_password"] = ( password_setting(required=False) .display_name("Proxy Password") .description("Password for proxy authentication") .kodi_setting(f"{self.provider_name}_proxy_password") .build() ) # Proxy scope settings settings["proxy_scope_api"] = ( boolean_setting(True) .display_name("Proxy API Calls") .description("Use proxy for API requests") .kodi_setting(f"{self.provider_name}_proxy_scope_api") .build() ) settings["proxy_scope_auth"] = ( boolean_setting(True) .display_name("Proxy Authentication") .description("Use proxy for authentication requests") .kodi_setting(f"{self.provider_name}_proxy_scope_auth") .build() ) settings["proxy_scope_manifest"] = ( boolean_setting(True) .display_name("Proxy Manifests") .description("Use proxy for manifest downloads") .kodi_setting(f"{self.provider_name}_proxy_scope_manifest") .build() ) settings["proxy_scope_license"] = ( boolean_setting(True) .display_name("Proxy DRM Licenses") .description("Use proxy for DRM license requests") .kodi_setting(f"{self.provider_name}_proxy_scope_license") .build() ) return settings def define_video_settings(self) -> Dict[str, SettingValue]: """Define video quality/format settings for this provider""" settings = {} # Video quality preference settings["video_quality"] = ( select_setting(["best", "worst", "720p", "1080p", "4k"], "best") .display_name("Video Quality") .description("Preferred video quality") .kodi_setting(f"{self.provider_name}_video_quality") .build() ) # Audio language preference settings["audio_language"] = ( select_setting(["de", "en", "original"], "de") .display_name("Audio Language") .description("Preferred audio language") .kodi_setting(f"{self.provider_name}_audio_language") .build() ) # Subtitle language preference settings["subtitle_language"] = ( select_setting(["none", "de", "en", "auto"], "none") .display_name("Subtitle Language") .description("Preferred subtitle language") .kodi_setting(f"{self.provider_name}_subtitle_language") .build() ) # Speed up streams settings["speed_up"] = ( boolean_setting(True) .display_name("Speed Up Playback") .description("Enable faster stream startup") .kodi_setting(f"{self.provider_name}_speed_up") .build() ) return settings def define_drm_settings(self) -> Dict[str, SettingValue]: """Define DRM/CDM related settings for this provider""" settings = {} # CDM usage settings["use_cdm"] = ( boolean_setting(True) .display_name("Use CDM") .description("Enable Content Decryption Module for DRM") .kodi_setting(f"{self.provider_name}_use_cdm") .build() ) # CDM type settings["cdm_type"] = ( select_setting(["widevine", "playready", "clearkey"], "widevine") .display_name("CDM Type") .description("Type of Content Decryption Module") .kodi_setting(f"{self.provider_name}_cdm_type") .build() ) # CDM mode settings["cdm_mode"] = ( select_setting(["external", "internal", "auto"], "external") .display_name("CDM Mode") .description("How to handle CDM operations") .kodi_setting(f"{self.provider_name}_cdm_mode") .build() ) return settings def define_network_settings(self) -> Dict[str, SettingValue]: """Define network timeout/retry settings for this provider""" settings = {} # Request timeout settings["request_timeout"] = ( integer_setting(30, 5, 120) .display_name("Request Timeout") .description("Timeout for network requests in seconds") .kodi_setting(f"{self.provider_name}_request_timeout") .build() ) # Max retries settings["max_retries"] = ( integer_setting(3, 0, 10) .display_name("Max Retries") .description("Maximum number of retry attempts") .kodi_setting(f"{self.provider_name}_max_retries") .build() ) # Retry delay settings["retry_delay"] = ( integer_setting(1, 0, 10) .display_name("Retry Delay") .description("Delay between retry attempts in seconds") .kodi_setting(f"{self.provider_name}_retry_delay") .build() ) # Verify SSL settings["verify_ssl"] = ( boolean_setting(True) .display_name("Verify SSL") .description("Verify SSL certificates for HTTPS requests") .kodi_setting(f"{self.provider_name}_verify_ssl") .build() ) return settings def get_all_settings(self) -> Dict[str, SettingValue]: """Get complete settings schema for this provider""" return self._settings.copy() def get_setting(self, setting_name: str) -> Optional[SettingValue]: """Get a specific setting by name""" return self._settings.get(setting_name) def get_settings_by_category(self, category: str) -> Dict[str, SettingValue]: """Get all settings in a specific category""" if category not in self._categories: return {} category_settings = {} for setting_name in self._categories[category]: if setting_name in self._settings: category_settings[setting_name] = self._settings[setting_name] return category_settings def get_categories(self) -> List[str]: """Get list of all setting categories""" return list(self._categories.keys()) def get_kodi_setting_mapping(self) -> Dict[str, str]: """Get mapping from internal setting names to Kodi setting IDs""" return self._kodi_mapping.copy() def get_reverse_kodi_mapping(self) -> Dict[str, str]: """Get mapping from Kodi setting IDs to internal setting names""" return {v: k for k, v in self._kodi_mapping.items()} def validate_all_settings(self) -> Dict[str, tuple[bool, List[str]]]: """Validate all settings in the schema""" results = {} for setting_name, setting in self._settings.items(): results[setting_name] = setting.validate() return results def get_required_settings(self) -> List[str]: """Get list of setting names that are required""" required = [] for setting_name, setting in self._settings.items(): # Check if setting has a "not_empty" validation rule has_required_rule = any(rule.name == "not_empty" for rule in setting.validation_rules) if has_required_rule: required.append(setting_name) return required def get_incomplete_settings(self) -> List[str]: """Get list of required settings that don't have values""" incomplete = [] required_settings = self.get_required_settings() for setting_name in required_settings: setting = self._settings[setting_name] if not setting.has_value(): incomplete.append(setting_name) return incomplete def is_configuration_complete(self) -> bool: """Check if all required settings have values""" return len(self.get_incomplete_settings()) == 0 def get_configuration_completeness(self) -> Dict[str, Any]: """Get detailed information about configuration completeness""" required_settings = self.get_required_settings() incomplete_settings = self.get_incomplete_settings() completeness_by_category = {} for category in self.get_categories(): category_settings = self.get_settings_by_category(category) category_required = [ name for name in category_settings.keys() if name in required_settings ] category_incomplete = [ name for name in category_settings.keys() if name in incomplete_settings ] completeness_by_category[category] = { "total_settings": len(category_settings), "required_settings": len(category_required), "incomplete_settings": len(category_incomplete), "is_complete": len(category_incomplete) == 0, "completion_percentage": ( 100.0 if len(category_required) == 0 else ( (len(category_required) - len(category_incomplete)) / len(category_required) * 100 ) ), } return { "is_complete": self.is_configuration_complete(), "total_settings": len(self._settings), "required_settings": len(required_settings), "incomplete_settings": len(incomplete_settings), "completion_percentage": ( 100.0 if len(required_settings) == 0 else ( (len(required_settings) - len(incomplete_settings)) / len(required_settings) * 100 ) ), "categories": completeness_by_category, "incomplete_setting_names": incomplete_settings, } def add_custom_setting( self, setting_name: str, setting: SettingValue, category: str = "custom" ) -> None: """Add a custom setting to the schema""" self._settings[setting_name] = setting if category not in self._categories: self._categories[category] = set() self._categories[category].add(setting_name) if setting.kodi_setting_id: self._kodi_mapping[setting_name] = setting.kodi_setting_id def remove_setting(self, setting_name: str) -> bool: """Remove a setting from the schema""" if setting_name not in self._settings: return False # Remove from settings del self._settings[setting_name] # Remove from categories for category_settings in self._categories.values(): category_settings.discard(setting_name) # Remove from Kodi mapping if setting_name in self._kodi_mapping: del self._kodi_mapping[setting_name] return True def to_dict(self) -> Dict[str, Any]: """Convert schema to dictionary representation""" return { "provider_name": self.provider_name, "settings": {name: setting.to_dict() for name, setting in self._settings.items()}, "categories": { category: list(settings) for category, settings in self._categories.items() }, "kodi_mapping": self._kodi_mapping.copy(), "configuration_status": self.get_configuration_completeness(), } class StandardProviderSettings: """Standard settings schemas for common providers""" _registered_schemas: Dict[str, ProviderSettingsSchema] = {} @classmethod def get_rtlplus_schema(cls) -> ProviderSettingsSchema: """Get RTL Plus settings schema""" if "rtlplus" not in cls._registered_schemas: schema = ProviderSettingsSchema("rtlplus") # RTL Plus specific customizations # Override default credential settings for RTL Plus specifics schema._settings["username"].description = "RTL Plus account email address" # Add RTL Plus specific settings schema.add_custom_setting( "device_id", string_setting() .display_name("Device ID") .description("Unique device identifier for RTL Plus") .kodi_setting("rtlplus_device_id") .build(), "credentials", ) cls._registered_schemas["rtlplus"] = schema return cls._registered_schemas["rtlplus"] @classmethod def get_joyn_schema(cls) -> ProviderSettingsSchema: """Get Joyn settings schema""" if "joyn" not in cls._registered_schemas: schema = ProviderSettingsSchema("joyn") # Joyn doesn't require authentication for basic content schema._settings["username"].validation_rules = [ rule for rule in schema._settings["username"].validation_rules if rule.name != "not_empty" ] schema._settings["password"].validation_rules = [ rule for rule in schema._settings["password"].validation_rules if rule.name != "not_empty" ] cls._registered_schemas["joyn"] = schema return cls._registered_schemas["joyn"] @classmethod def get_zdf_schema(cls) -> ProviderSettingsSchema: """Get ZDF settings schema""" if "zdf" not in cls._registered_schemas: schema = ProviderSettingsSchema("zdf") # ZDF is free, no authentication required schema.remove_setting("username") schema.remove_setting("password") schema.remove_setting("client_id") schema.remove_setting("client_secret") # Add ZDF specific settings schema.add_custom_setting( "geo_location", select_setting(["DE", "AT", "CH"], "DE") .display_name("Geographic Location") .description("Your geographic location for content filtering") .kodi_setting("zdf_geo_location") .build(), "network", ) cls._registered_schemas["zdf"] = schema return cls._registered_schemas["zdf"] @classmethod def get_ard_schema(cls) -> ProviderSettingsSchema: """Get ARD settings schema""" if "ard" not in cls._registered_schemas: schema = ProviderSettingsSchema("ard") # ARD is free, no authentication required schema.remove_setting("username") schema.remove_setting("password") schema.remove_setting("client_id") schema.remove_setting("client_secret") cls._registered_schemas["ard"] = schema return cls._registered_schemas["ard"] @classmethod def register_provider_schema(cls, provider_name: str, schema: ProviderSettingsSchema) -> None: """Register a custom provider schema""" cls._registered_schemas[provider_name] = schema logger.info(f"Registered custom settings schema for provider: {provider_name}") @classmethod def get_provider_schema(cls, provider_name: str) -> Optional[ProviderSettingsSchema]: """Get schema for a provider, creating default if not found""" # Check if we have a specific schema method method_name = f"get_{provider_name}_schema" if hasattr(cls, method_name): return getattr(cls, method_name)() # Check registered schemas if provider_name in cls._registered_schemas: return cls._registered_schemas[provider_name] # Create default schema logger.info(f"Creating default settings schema for provider: {provider_name}") schema = ProviderSettingsSchema(provider_name) cls._registered_schemas[provider_name] = schema return schema @classmethod def list_registered_providers(cls) -> List[str]: """Get list of all registered provider schemas""" # Include both registered schemas and built-in methods builtin_providers = [] for attr_name in dir(cls): if ( attr_name.startswith("get_") and attr_name.endswith("_schema") and attr_name != "get_provider_schema" ): provider_name = attr_name[4:-7] # Remove 'get_' and '_schema' builtin_providers.append(provider_name) all_providers = list(set(builtin_providers + list(cls._registered_schemas.keys()))) return sorted(all_providers) @classmethod def unregister_provider_schema(cls, provider_name: str) -> bool: """Unregister a provider schema""" if provider_name in cls._registered_schemas: del cls._registered_schemas[provider_name] logger.info(f"Unregistered settings schema for provider: {provider_name}") return True return False