#!/usr/bin/env python3 import json import math import os import sys import threading import time from urllib.parse import parse_qsl, urlencode from typing import Optional import requests from bottle import Bottle, redirect, request, response, run # Add lib path for imports script_dir = os.path.dirname(os.path.abspath(__file__)) LIB_PATH = os.path.join(script_dir, "lib") if os.path.exists(LIB_PATH): sys.path.insert(0, LIB_PATH) try: from streaming_providers import get_configured_manager # Add EPG Manager import from streaming_providers.base.epg.epg_manager import EPGManager from streaming_providers.base.models import StreamingChannel from streaming_providers.base.settings.provider_enable_manager import ( ProviderEnableManager, ) from streaming_providers.base.utils import MPDCacheManager, MPDRewriter, logger from streaming_providers.base.utils.environment import ( get_environment_manager, get_vfs_instance, is_kodi_environment, ) except ImportError as import_err: print( f"Ultimate Backend: Critical import failed - {str(import_err)}", file=sys.stderr ) raise class UltimateService: def __init__(self, config_dir: str = None): self.app = Bottle() # Get environment manager self.env_manager = get_environment_manager() # Override config directory if provided if config_dir: self.env_manager.set_config("profile_path", config_dir) # Get settings self.server_port = self.env_manager.get_config("server_port", 7777) self.default_country = self.env_manager.get_config("default_country", "DE") # Initialize manager try: self.manager = get_configured_manager() logger.info("Manager initialized successfully") except Exception as init_err: logger.error(f"Failed to initialize manager - {str(init_err)}") raise # Get media proxy URL from environment variable self.media_proxy_url = os.environ.get("MEDIA_PROXY_URL", "").strip() if not self.media_proxy_url: logger.warning( "MEDIA_PROXY_URL environment variable not set - media proxy features disabled" ) else: logger.info(f"Media proxy URL: {self.media_proxy_url}") # Initialize VFS for M3U caching self.vfs = get_vfs_instance(subdir="m3u_cache") logger.info(f"VFS initialized for M3U caching: {self.vfs.base_path}") self.mpd_cache = MPDCacheManager() logger.info(f"MPD cache initialized: {self.mpd_cache.vfs.base_path}") self._decrypted_cache = {} # key -> (content, expiry_ts) — memory-only, never persisted # 1. Determine EPG URL FIRST (with proper precedence) self.epg_url = self._determine_epg_url() logger.info(f"UltimateService: Final EPG URL determined: {self.epg_url}") # 2. Initialize EPG Manager WITH the URL try: self.epg_manager = EPGManager(self.epg_url) # Pass the URL here logger.info(f"EPG Manager initialized with URL: {self.epg_url}") except ImportError as e: logger.warning(f"Could not import EPG Manager: {e}") self.epg_manager = None except Exception as e: logger.warning(f"Could not initialize EPG Manager: {e}") self.epg_manager = None # 3. Fetch EPG aliases for tvg-epgid mapping (initialize first) self.epg_alias_map = {} # Initialize to empty dict as fallback self._fetch_epg_aliases() # This will update self.epg_alias_map self.setup_routes() self.config_html = self._load_config_html() def _determine_epg_url(self) -> str: """ Determine EPG URL with proper precedence. Must match the precedence logic in EPGManager. """ import os # 1. Environment variable (highest priority for Docker) env_url = os.environ.get("ULTIMATE_EPG_URL") if env_url and env_url.strip() and env_url != "https://example.com/epg.xml.gz": logger.info( f"UltimateService: Using EPG URL from environment variable: {env_url}" ) return env_url.strip() # 2. Try config.json via environment manager try: config_url = self.env_manager.get_config("epg_url") if ( config_url and config_url.strip() and config_url != "https://example.com/epg.xml.gz" ): logger.info( f"UltimateService: Using EPG URL from config.json: {config_url}" ) return config_url.strip() except Exception as e: logger.debug( f"UltimateService: Could not get EPG URL from environment manager: {e}" ) # 3. Try Kodi addon setting try: if is_kodi_environment(): import xbmcaddon addon = xbmcaddon.Addon() kodi_url = addon.getSetting("epg_xml_url") if ( kodi_url and kodi_url.strip() and kodi_url != "https://example.com/epg.xml.gz" ): logger.info( f"UltimateService: Using EPG URL from Kodi settings: {kodi_url}" ) return kodi_url.strip() except Exception as e: logger.debug( f"UltimateService: Could not get EPG URL from Kodi settings: {e}" ) # 4. Default fallback default_url = "https://example.com/epg.xml.gz" logger.warning( f"UltimateService: No valid EPG URL found, using default: {default_url}" ) logger.warning("Please set ULTIMATE_EPG_URL environment variable!") return default_url def _load_config_html(self): """Load the web interface HTML template with embedded CSS and JS""" base_dir = os.path.dirname(os.path.abspath(__file__)) web_dir = os.path.join(base_dir, "resources", "web") # Define file paths html_path = os.path.join(web_dir, "config.html") css_path = os.path.join(web_dir, "config.css") js_path = os.path.join(web_dir, "config.js") # Proxy files proxy_css_path = os.path.join(web_dir, "proxy.css") proxy_js_path = os.path.join(web_dir, "proxy.js") # EPG mapping files epg_css_path = os.path.join(web_dir, "epg_mapping.css") epg_js_path = os.path.join(web_dir, "epg_mapping.js") fuzzyset_path = os.path.join(web_dir, "lib", "fuzzyset.js") debounce_path = os.path.join(web_dir, "lib", "debounce.js") # Enable/disable files enable_css_path = os.path.join(web_dir, "provider_enable.css") enable_js_path = os.path.join(web_dir, "provider_enable.js") try: # Load HTML with open(html_path, "r", encoding="utf-8") as f: html = f.read() # Load CSS files with open(css_path, "r", encoding="utf-8") as f: css = f.read() with open(proxy_css_path, "r", encoding="utf-8") as f: proxy_css = f.read() with open(epg_css_path, "r", encoding="utf-8") as f: epg_css = f.read() with open(enable_css_path, "r", encoding="utf-8") as f: enable_css = f.read() # Load JS files with open(js_path, "r", encoding="utf-8") as f: js = f.read() with open(proxy_js_path, "r", encoding="utf-8") as f: proxy_js = f.read() with open(epg_js_path, "r", encoding="utf-8") as f: epg_js = f.read() with open(fuzzyset_path, "r", encoding="utf-8") as f: fuzzyset_js = f.read() with open(debounce_path, "r", encoding="utf-8") as f: debounce_js = f.read() with open(enable_js_path, "r", encoding="utf-8") as f: enable_js = f.read() # Combine all CSS (correct order: base -> proxy -> epg -> enable) combined_css = f"{css}\n\n/* Proxy CSS */\n{proxy_css}\n\n/* EPG Mapping CSS */\n{epg_css}\n\n/* Provider Enable/Disable CSS */\n{enable_css}" # Combine all JS (with proper order) combined_js = f""" /* Debounce Utility */ {debounce_js} /* FuzzySet Library */ {fuzzyset_js} /* Main Config JS */ {js} /* Proxy Management JS */ {proxy_js} /* EPG Mapping JS */ {epg_js} /* Provider Enable/Disable JS */ {enable_js} """ # Replace CSS in HTML html = html.replace( '', f"", ) # Inject JavaScript before
Warning: Full interface not loaded. Using basic mode.
tag script_tag = f"" if '' in html: html = html.replace('', script_tag) else: html = html.replace("", f"{script_tag}\n") return html except Exception as e: logger.error(f"Failed to load config files: {e}") return self._get_fallback_html() @staticmethod def _get_fallback_html(): """Generate a minimal fallback HTML if file is not found""" return """