mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-23 17:42:19 +02:00
586 lines
23 KiB
Python
586 lines
23 KiB
Python
# streaming_providers/base/settings/kodi_settings_bridge.py
|
|
import json
|
|
import xml.etree.ElementTree as ElementTree
|
|
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
|
|
from ..auth.credentials import BaseCredentials, ClientCredentials, UserPasswordCredentials
|
|
from ..models.proxy_models import ProxyConfig
|
|
from ..utils.environment import get_environment_manager, get_vfs_instance, is_kodi_environment
|
|
from ..utils.logger import logger
|
|
|
|
|
|
class KodiSettingsBridge:
|
|
"""Bridge between Kodi addon settings and internal configuration system"""
|
|
|
|
# Markers that identify credential settings
|
|
CREDENTIAL_MARKERS = {"_username", "_password", "_client_id", "_client_secret"}
|
|
|
|
def __init__(self, addon_id: Optional[str] = None, config_dir: Optional[str] = None):
|
|
"""Initialize Kodi settings bridge"""
|
|
self.addon = None
|
|
self.addon_id = addon_id
|
|
self._env_manager = get_environment_manager()
|
|
|
|
# Initialize VFS with config directory support
|
|
self.vfs = get_vfs_instance(config_dir=config_dir)
|
|
|
|
# Settings storage for standalone mode
|
|
self._standalone_settings: Dict[str, str] = {}
|
|
self._settings_file = "standalone_settings.json"
|
|
self._load_standalone_settings()
|
|
|
|
if is_kodi_environment():
|
|
try:
|
|
import xbmcaddon
|
|
|
|
if addon_id:
|
|
self.addon = xbmcaddon.Addon(addon_id)
|
|
else:
|
|
self.addon = xbmcaddon.Addon()
|
|
self.addon_id = self.addon.getAddonInfo("id")
|
|
logger.info(f"Kodi settings bridge initialized for addon: {self.addon_id}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize Kodi addon: {e}")
|
|
self.addon = None
|
|
|
|
def is_kodi_environment(self) -> bool:
|
|
"""Check if currently running in Kodi environment"""
|
|
return is_kodi_environment() and self.addon is not None
|
|
|
|
def _load_standalone_settings(self) -> None:
|
|
"""Load settings from file in standalone mode"""
|
|
if not is_kodi_environment() and self.vfs.exists(self._settings_file):
|
|
try:
|
|
content = self.vfs.read_text(self._settings_file)
|
|
if content:
|
|
self._standalone_settings = json.loads(content)
|
|
logger.debug(f"Loaded {len(self._standalone_settings)} standalone settings")
|
|
except Exception as e:
|
|
logger.error(f"Error loading standalone settings: {e}")
|
|
|
|
def _save_standalone_settings(self) -> None:
|
|
"""Save settings to file in standalone mode"""
|
|
if not is_kodi_environment():
|
|
try:
|
|
self.vfs.write_json(self._settings_file, self._standalone_settings)
|
|
logger.debug(f"Saved {len(self._standalone_settings)} standalone settings")
|
|
except Exception as e:
|
|
logger.error(f"Error saving standalone settings: {e}")
|
|
|
|
def get_setting(self, setting_id: str, default: str = "") -> str:
|
|
"""Get setting value, works in both Kodi and standalone mode"""
|
|
if self.is_kodi_environment():
|
|
try:
|
|
return self.addon.getSetting(setting_id) or default
|
|
except Exception as e:
|
|
logger.error(f"Error getting setting {setting_id}: {e}")
|
|
return default
|
|
else:
|
|
# Standalone mode: use local storage
|
|
return self._standalone_settings.get(setting_id, default)
|
|
|
|
def set_setting(self, setting_id: str, value: str) -> bool:
|
|
"""Set setting value, works in both Kodi and standalone mode"""
|
|
if self.is_kodi_environment():
|
|
try:
|
|
self.addon.setSetting(setting_id, value)
|
|
logger.debug(f"Set Kodi setting {setting_id}")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error setting {setting_id}: {e}")
|
|
return False
|
|
else:
|
|
# Standalone mode: update local storage
|
|
self._standalone_settings[setting_id] = value
|
|
self._save_standalone_settings()
|
|
logger.debug(f"Set standalone setting {setting_id}")
|
|
return True
|
|
|
|
def get_addon_info(self) -> Dict[str, str]:
|
|
"""Get information about current Kodi addon or environment"""
|
|
if not self.is_kodi_environment():
|
|
# Return environment info in standalone mode
|
|
return {
|
|
"environment": "standalone",
|
|
"id": self._env_manager.get_config("addon_id", "standalone"),
|
|
"name": self._env_manager.get_config("addon_name", "Ultimate Backend"),
|
|
"version": self._env_manager.get_config("addon_version", "1.0.0"),
|
|
"config_dir": self.vfs.base_path,
|
|
"profile_path": self._env_manager.get_config("profile_path", ""),
|
|
}
|
|
|
|
return {
|
|
"environment": "kodi",
|
|
"id": self.addon.getAddonInfo("id"),
|
|
"name": self.addon.getAddonInfo("name"),
|
|
"version": self.addon.getAddonInfo("version"),
|
|
"author": self.addon.getAddonInfo("author"),
|
|
"path": self.addon.getAddonInfo("path"),
|
|
}
|
|
|
|
# ============= Dynamic Discovery =============
|
|
|
|
def _get_all_setting_ids(self) -> List[str]:
|
|
"""
|
|
Get all setting IDs from settings.xml by parsing the file.
|
|
|
|
Returns:
|
|
List of all setting IDs found in settings.xml
|
|
"""
|
|
if not self.is_kodi_environment():
|
|
# In standalone mode, return keys from standalone settings
|
|
return list(self._standalone_settings.keys())
|
|
|
|
try:
|
|
# Read settings.xml from VFS base path (addon profile directory)
|
|
xml_content = self.vfs.read_text("settings.xml")
|
|
if not xml_content:
|
|
logger.warning("settings.xml not found or empty")
|
|
return []
|
|
|
|
root = ElementTree.fromstring(xml_content)
|
|
|
|
# Extract all setting IDs
|
|
setting_ids = []
|
|
for setting in root.findall(".//setting"):
|
|
setting_id = setting.get("id")
|
|
if setting_id:
|
|
setting_ids.append(setting_id)
|
|
|
|
logger.info(f"Found {len(setting_ids)} settings in settings.xml")
|
|
logger.debug(f"Setting IDs: {setting_ids}")
|
|
|
|
return setting_ids
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error reading settings.xml: {e}")
|
|
return []
|
|
|
|
def _parse_provider_country(self, setting_id: str) -> Optional[Tuple[str, Optional[str]]]:
|
|
"""
|
|
Parse a setting ID to extract provider and optional country.
|
|
|
|
Pattern:
|
|
- provider_marker → (provider, None)
|
|
- provider_country_marker → (provider, country)
|
|
- anything else → None
|
|
|
|
Args:
|
|
setting_id: Setting ID like "joyn_username" or "joyn_de_username"
|
|
|
|
Returns:
|
|
Tuple of (provider, country) or None if pattern doesn't match
|
|
"""
|
|
# Check if ends with a credential marker
|
|
marker = None
|
|
for m in self.CREDENTIAL_MARKERS:
|
|
if setting_id.endswith(m):
|
|
marker = m
|
|
break
|
|
|
|
if not marker:
|
|
return None
|
|
|
|
# Remove the marker
|
|
prefix = setting_id[: -len(marker)]
|
|
|
|
# Split by underscore
|
|
parts = prefix.split("_")
|
|
|
|
if len(parts) == 1:
|
|
# provider only
|
|
return parts[0], None
|
|
elif len(parts) == 2:
|
|
# provider_country
|
|
return parts[0], parts[1]
|
|
else:
|
|
# Too many parts, doesn't match our pattern
|
|
return None
|
|
|
|
def discover_all_providers(self) -> Dict[str, List[str]]:
|
|
"""
|
|
Scan all settings and discover providers with their countries dynamically.
|
|
|
|
Works in both Kodi and standalone mode.
|
|
|
|
Returns:
|
|
Dict mapping provider names to list of countries.
|
|
Empty list means provider without country.
|
|
Example: {'joyn': ['de', 'at'], 'rtlplus': ['de'], 'zattoo': []}
|
|
"""
|
|
discovered: Dict[str, Set[Optional[str]]] = {}
|
|
|
|
# Get all setting IDs
|
|
setting_ids = self._get_all_setting_ids()
|
|
|
|
for setting_id in setting_ids:
|
|
result = self._parse_provider_country(setting_id)
|
|
if result:
|
|
provider, country = result
|
|
if provider not in discovered:
|
|
discovered[provider] = set()
|
|
discovered[provider].add(country)
|
|
|
|
# Convert sets to lists, with None values converted to empty list
|
|
result = {}
|
|
for provider, countries in discovered.items():
|
|
country_list = []
|
|
for country in countries:
|
|
if country is None:
|
|
# Provider without country - represent as empty list for that provider
|
|
if not country_list: # Only add if we haven't already
|
|
result[provider] = []
|
|
else:
|
|
country_list.append(country)
|
|
|
|
if country_list:
|
|
result[provider] = sorted(country_list)
|
|
elif provider not in result:
|
|
result[provider] = []
|
|
|
|
logger.info(f"Discovered providers: {result}")
|
|
return result
|
|
|
|
def detect_all_providers_from_kodi(self) -> Dict[str, List[str]]:
|
|
"""
|
|
Alias for discover_all_providers() for backward compatibility.
|
|
"""
|
|
return self.discover_all_providers()
|
|
|
|
def detect_countries_for_provider(self, provider: str) -> List[str]:
|
|
"""
|
|
Discover which countries are configured for a specific provider.
|
|
|
|
Args:
|
|
provider: Provider name (e.g., 'joyn')
|
|
|
|
Returns:
|
|
List of detected country codes (e.g., ['de', 'at'])
|
|
"""
|
|
all_providers = self.discover_all_providers()
|
|
countries = all_providers.get(provider, [])
|
|
|
|
# Filter out empty list (which means provider without country)
|
|
if not countries:
|
|
return []
|
|
|
|
return countries
|
|
|
|
def get_all_countries_for_provider(
|
|
self, provider: str, available_countries: List[str]
|
|
) -> List[str]:
|
|
"""
|
|
Check which countries from a given list have credentials configured for a provider.
|
|
|
|
Args:
|
|
provider: Provider name
|
|
available_countries: List of country codes to check
|
|
|
|
Returns:
|
|
List of country codes that have credentials
|
|
"""
|
|
discovered_countries = self.detect_countries_for_provider(provider)
|
|
return [c for c in available_countries if c in discovered_countries]
|
|
|
|
# ============= Credential Operations =============
|
|
|
|
def read_credentials_from_kodi(
|
|
self, provider: str, country: Optional[str] = None
|
|
) -> Optional[BaseCredentials]:
|
|
"""
|
|
Read authentication credentials from settings for a provider.
|
|
Uses convention: {provider}_{country}_username, {provider}_{country}_password, etc.
|
|
If country is None, tries without country suffix for backward compatibility.
|
|
Works in both Kodi and standalone mode.
|
|
"""
|
|
country_suffix = f"_{country}" if country else ""
|
|
|
|
try:
|
|
# Use unified get_setting method
|
|
username = self.get_setting(f"{provider}{country_suffix}_username")
|
|
password = self.get_setting(f"{provider}{country_suffix}_password")
|
|
client_id = self.get_setting(f"{provider}{country_suffix}_client_id")
|
|
client_secret = self.get_setting(f"{provider}{country_suffix}_client_secret")
|
|
|
|
logger.debug(f"Settings for {provider}{country_suffix}:")
|
|
logger.debug(f" username: '{username}' (empty={not username})")
|
|
logger.debug(f" password: {'***' if password else '(empty)'}")
|
|
logger.debug(f" client_id: '{client_id}' (empty={not client_id})")
|
|
logger.debug(f" client_secret: {'***' if client_secret else '(empty)'}")
|
|
|
|
# Determine credential type based on available values
|
|
if username and password:
|
|
logger.info(f"Found username/password credentials for {provider}{country_suffix}")
|
|
return UserPasswordCredentials(
|
|
username=username.strip(),
|
|
password=password.strip(),
|
|
client_id=client_id.strip() if client_id else None,
|
|
)
|
|
elif client_id and client_secret:
|
|
logger.info(f"Found client credentials for {provider}{country_suffix}")
|
|
return ClientCredentials(
|
|
client_id=client_id.strip(), client_secret=client_secret.strip()
|
|
)
|
|
|
|
logger.debug(f"No valid credentials found for {provider}{country_suffix}")
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error reading credentials for {provider}: {e}")
|
|
return None
|
|
|
|
def write_credentials_to_kodi(
|
|
self, provider: str, credentials: BaseCredentials, country: Optional[str] = None
|
|
) -> bool:
|
|
"""Write authentication credentials to settings"""
|
|
country_suffix = f"_{country}" if country else ""
|
|
|
|
try:
|
|
if isinstance(credentials, UserPasswordCredentials):
|
|
self.set_setting(f"{provider}{country_suffix}_username", credentials.username)
|
|
self.set_setting(f"{provider}{country_suffix}_password", credentials.password)
|
|
if credentials.client_id:
|
|
self.set_setting(f"{provider}{country_suffix}_client_id", credentials.client_id)
|
|
logger.info(f"Wrote username/password credentials for {provider}{country_suffix}")
|
|
return True
|
|
|
|
elif isinstance(credentials, ClientCredentials):
|
|
self.set_setting(f"{provider}{country_suffix}_client_id", credentials.client_id)
|
|
self.set_setting(
|
|
f"{provider}{country_suffix}_client_secret",
|
|
credentials.client_secret,
|
|
)
|
|
logger.info(f"Wrote client credentials for {provider}{country_suffix}")
|
|
return True
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error writing credentials for {provider}: {e}")
|
|
return False
|
|
|
|
def sync_credentials_to_file(
|
|
self, provider: str, credential_manager, country: Optional[str] = None
|
|
) -> bool:
|
|
"""Sync provider credentials from settings to credential file"""
|
|
credentials = self.read_credentials_from_kodi(provider, country)
|
|
if not credentials:
|
|
logger.debug(f"No credentials to sync for {provider}")
|
|
return False
|
|
|
|
# Check if credentials are different from file
|
|
file_credentials = credential_manager.load_credentials(provider, country)
|
|
if self._credentials_equal(credentials, file_credentials):
|
|
logger.debug(f"Credentials already in sync for {provider}")
|
|
return True # Already in sync
|
|
|
|
success = credential_manager.save_credentials(provider, credentials, country)
|
|
if success:
|
|
logger.info(f"Synced credentials from settings to file for {provider}")
|
|
return success
|
|
|
|
# ============= Proxy Operations =============
|
|
|
|
def read_proxy_config_from_kodi(
|
|
self, provider: str, country: Optional[str] = None
|
|
) -> Optional[ProxyConfig]:
|
|
"""
|
|
Read proxy configuration from settings for a provider.
|
|
Uses convention: {provider}_{country}_proxy_enabled, {provider}_{country}_proxy_host, etc.
|
|
"""
|
|
country_suffix = f"_{country}" if country else ""
|
|
|
|
try:
|
|
# Check if proxy is enabled
|
|
proxy_enabled = self.get_setting(f"{provider}{country_suffix}_proxy_enabled")
|
|
logger.debug(f"Proxy enabled setting for {provider}{country_suffix}: '{proxy_enabled}'")
|
|
|
|
if not proxy_enabled or proxy_enabled.lower() not in ["true", "1", "yes"]:
|
|
logger.debug(f"Proxy not enabled for {provider}{country_suffix}")
|
|
return None
|
|
|
|
proxy_host = self.get_setting(f"{provider}{country_suffix}_proxy_host")
|
|
proxy_port_str = self.get_setting(f"{provider}{country_suffix}_proxy_port")
|
|
|
|
logger.debug(f"Proxy settings for {provider}{country_suffix}:")
|
|
logger.debug(f" host: '{proxy_host}'")
|
|
logger.debug(f" port: '{proxy_port_str}'")
|
|
|
|
if not proxy_host or not proxy_port_str:
|
|
logger.debug(f"Proxy host or port missing for {provider}{country_suffix}")
|
|
return None
|
|
|
|
try:
|
|
proxy_port = int(proxy_port_str)
|
|
except ValueError:
|
|
logger.error(
|
|
f"Invalid proxy port '{proxy_port_str}' for {provider}{country_suffix}"
|
|
)
|
|
return None
|
|
|
|
# Create proxy config
|
|
proxy_config = ProxyConfig(host=proxy_host.strip(), port=proxy_port)
|
|
|
|
logger.info(
|
|
f"Found proxy config for {provider}{country_suffix}: {proxy_host}:{proxy_port}"
|
|
)
|
|
return proxy_config
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error reading proxy config for {provider}: {e}")
|
|
return None
|
|
|
|
def write_proxy_config_to_kodi(
|
|
self, provider: str, proxy_config: ProxyConfig, country: Optional[str] = None
|
|
) -> bool:
|
|
"""Write proxy configuration to settings"""
|
|
country_suffix = f"_{country}" if country else ""
|
|
|
|
try:
|
|
self.set_setting(f"{provider}{country_suffix}_proxy_enabled", "true")
|
|
self.set_setting(f"{provider}{country_suffix}_proxy_host", proxy_config.host)
|
|
self.set_setting(f"{provider}{country_suffix}_proxy_port", str(proxy_config.port))
|
|
|
|
logger.info(f"Wrote proxy config for {provider}{country_suffix}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error writing proxy config for {provider}: {e}")
|
|
return False
|
|
|
|
def sync_proxy_config_to_file(
|
|
self, provider: str, proxy_manager, country: Optional[str] = None
|
|
) -> bool:
|
|
"""Sync provider proxy config from settings to proxy config file"""
|
|
proxy_config = self.read_proxy_config_from_kodi(provider, country)
|
|
if not proxy_config:
|
|
logger.debug(f"No proxy config to sync for {provider}")
|
|
return False
|
|
|
|
# Check if proxy config is different from file
|
|
file_proxy = proxy_manager.get_proxy_config(provider, country)
|
|
if self._proxy_configs_equal(proxy_config, file_proxy):
|
|
logger.debug(f"Proxy config already in sync for {provider}")
|
|
return True # Already in sync
|
|
|
|
success = proxy_manager.set_proxy_config(provider, proxy_config, country)
|
|
if success:
|
|
logger.info(f"Synced proxy config from settings to file for {provider}")
|
|
return success
|
|
|
|
def read_ip_address_from_kodi(
|
|
self, provider: str, country: Optional[str] = None
|
|
) -> Optional[str]:
|
|
"""
|
|
Read IP address from settings for a provider.
|
|
Uses convention: {provider}_{country}_ipaddress or {provider}_ipaddress
|
|
|
|
Args:
|
|
provider: Provider name (e.g., 'hrti')
|
|
country: Optional country code
|
|
|
|
Returns:
|
|
Configured IP address or None if not set
|
|
"""
|
|
country_suffix = f"_{country}" if country else ""
|
|
|
|
try:
|
|
ip_address = self.get_setting(f"{provider}{country_suffix}_ipaddress")
|
|
|
|
logger.debug(f"IP address setting for {provider}{country_suffix}: '{ip_address}'")
|
|
|
|
if ip_address and ip_address.strip():
|
|
logger.info(
|
|
f"Found configured IP address for {provider}{country_suffix}: {ip_address}"
|
|
)
|
|
return ip_address.strip()
|
|
|
|
logger.debug(f"No IP address configured for {provider}{country_suffix}")
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error reading IP address for {provider}: {e}")
|
|
return None
|
|
|
|
# ============= Comparison Helpers =============
|
|
|
|
@staticmethod
|
|
def _credentials_equal(
|
|
cred1: Optional[BaseCredentials], cred2: Optional[BaseCredentials]
|
|
) -> bool:
|
|
"""Compare two credentials for equality"""
|
|
if cred1 is None and cred2 is None:
|
|
return True
|
|
if cred1 is None or cred2 is None:
|
|
return False
|
|
if type(cred1) != type(cred2):
|
|
return False
|
|
|
|
if isinstance(cred1, UserPasswordCredentials) and isinstance(
|
|
cred2, UserPasswordCredentials
|
|
):
|
|
return (
|
|
cred1.username == cred2.username
|
|
and cred1.password == cred2.password
|
|
and cred1.client_id == cred2.client_id
|
|
)
|
|
elif isinstance(cred1, ClientCredentials) and isinstance(cred2, ClientCredentials):
|
|
return cred1.client_id == cred2.client_id and cred1.client_secret == cred2.client_secret
|
|
|
|
return False
|
|
|
|
@staticmethod
|
|
def _proxy_configs_equal(proxy1: Optional[ProxyConfig], proxy2: Optional[ProxyConfig]) -> bool:
|
|
"""Compare two proxy configurations for equality"""
|
|
if proxy1 is None and proxy2 is None:
|
|
return True
|
|
if proxy1 is None or proxy2 is None:
|
|
return False
|
|
|
|
# Compare basic properties
|
|
if (
|
|
proxy1.host != proxy2.host
|
|
or proxy1.port != proxy2.port
|
|
or proxy1.proxy_type != proxy2.proxy_type
|
|
):
|
|
return False
|
|
|
|
# Compare authentication
|
|
if (proxy1.auth is None) != (proxy2.auth is None):
|
|
return False
|
|
if proxy1.auth and proxy2.auth:
|
|
if (
|
|
proxy1.auth.username != proxy2.auth.username
|
|
or proxy1.auth.password != proxy2.auth.password
|
|
):
|
|
return False
|
|
|
|
# Compare scope
|
|
if (
|
|
proxy1.scope.api_calls != proxy2.scope.api_calls
|
|
or proxy1.scope.authentication != proxy2.scope.authentication
|
|
or proxy1.scope.manifests != proxy2.scope.manifests
|
|
or proxy1.scope.license != proxy2.scope.license
|
|
):
|
|
return False
|
|
|
|
return True
|
|
|
|
def debug_info(self) -> Dict[str, Any]:
|
|
"""Get debug information about the settings bridge"""
|
|
info = {
|
|
"environment": "kodi" if self.is_kodi_environment() else "standalone",
|
|
"addon_info": self.get_addon_info(),
|
|
"has_addon": self.addon is not None,
|
|
"standalone_settings_count": len(self._standalone_settings),
|
|
"vfs_base_path": self.vfs.base_path,
|
|
}
|
|
|
|
if self.is_kodi_environment():
|
|
info["addon_id"] = self.addon_id
|
|
info["kodi_available"] = True
|
|
else:
|
|
info["kodi_available"] = False
|
|
|
|
return info
|