""" Gestionnaire de plugins pour les clients torrent. Découvre automatiquement les plugins disponibles. """ import os import importlib import logging from typing import Dict, List, Optional, Type, Any from .base import TorrentClientPlugin, TorrentClientConfig logger = logging.getLogger(__name__) # Registre des plugins disponibles _plugins: Dict[str, Type[TorrentClientPlugin]] = {} # Instance active du client _active_client: Optional[TorrentClientPlugin] = None _active_config: Optional[Dict[str, Any]] = None def discover_plugins() -> Dict[str, Type[TorrentClientPlugin]]: """ Découvre automatiquement tous les plugins dans le dossier. Returns: Dictionnaire {nom_plugin: classe_plugin} """ global _plugins _plugins = {} plugins_dir = os.path.dirname(__file__) for filename in os.listdir(plugins_dir): if filename.endswith('.py') and filename not in ('__init__.py', 'base.py'): module_name = filename[:-3] try: module = importlib.import_module(f'.{module_name}', package=__package__) # Chercher PLUGIN_CLASS ou une classe héritant de TorrentClientPlugin if hasattr(module, 'PLUGIN_CLASS'): plugin_class = module.PLUGIN_CLASS _plugins[plugin_class.PLUGIN_NAME.lower()] = plugin_class logger.info(f"✅ Plugin chargé: {plugin_class.PLUGIN_NAME} v{plugin_class.PLUGIN_VERSION}") except Exception as e: logger.warning(f"⚠️ Impossible de charger le plugin {module_name}: {e}") return _plugins def get_available_plugins() -> List[Dict[str, str]]: """ Retourne la liste des plugins disponibles. Returns: Liste de dictionnaires avec les infos de chaque plugin """ if not _plugins: discover_plugins() return [ { "id": name, "name": plugin.PLUGIN_NAME, "description": plugin.PLUGIN_DESCRIPTION, "version": plugin.PLUGIN_VERSION, "author": plugin.PLUGIN_AUTHOR } for name, plugin in _plugins.items() ] def get_plugin(name: str) -> Optional[Type[TorrentClientPlugin]]: """ Récupère une classe de plugin par son nom. Args: name: Nom du plugin (insensible à la casse) Returns: Classe du plugin ou None """ if not _plugins: discover_plugins() return _plugins.get(name.lower()) def create_client(plugin_name: str, config: Dict[str, Any]) -> Optional[TorrentClientPlugin]: """ Crée une instance de client torrent. Args: plugin_name: Nom du plugin à utiliser config: Configuration (host, port, username, password, use_ssl, path) Returns: Instance du client ou None """ plugin_class = get_plugin(plugin_name) if not plugin_class: logger.error(f"❌ Plugin non trouvé: {plugin_name}") return None try: from urllib.parse import urlparse # Nettoyer le host (enlever http:// ou https:// si présent) host = config.get('host', 'localhost') path = config.get('path', '') use_ssl = config.get('use_ssl', False) port = config.get('port', None) # None = pas spécifié # Si l'utilisateur a entré une URL complète, l'analyser if host.startswith('http://') or host.startswith('https://'): parsed = urlparse(host) host = parsed.netloc or parsed.path.split('/')[0] use_ssl = parsed.scheme == 'https' # Extraire le port de l'URL si présent (ex: host:8080) if ':' in host: host_part, port_part = host.rsplit(':', 1) if port_part.isdigit(): host = host_part if port is None: # Seulement si pas déjà spécifié port = int(port_part) # Extraire le chemin de l'URL si pas déjà fourni if parsed.path and not path: path = parsed.path.rstrip('/') # Enlever le trailing slash du host host = host.rstrip('/') # Gérer le port # - Si port est None ou vide: pas de port explicite (0) # - Si port est un nombre valide: l'utiliser # - Rétrocompatibilité: les anciennes configs avec port=8080 fonctionnent if port is None or port == '': port = 0 elif isinstance(port, str): port = int(port) if port.strip().isdigit() else 0 else: port = int(port) if port else 0 client_config = TorrentClientConfig( host=host, port=port, username=config.get('username', ''), password=config.get('password', ''), use_ssl=use_ssl, path=path ) logger.info(f"🔧 Configuration client: {client_config.base_url}") client = plugin_class(client_config) return client except Exception as e: logger.error(f"❌ Erreur création client {plugin_name}: {e}") return None def get_active_client() -> Optional[TorrentClientPlugin]: """Retourne le client actif ou None.""" global _active_client return _active_client def set_active_client(client: Optional[TorrentClientPlugin], config: Optional[Dict[str, Any]] = None) -> None: """Définit le client actif.""" global _active_client, _active_config # Déconnecter l'ancien client if _active_client: try: _active_client.disconnect() except: pass _active_client = client _active_config = config def get_active_config() -> Optional[Dict[str, Any]]: """Retourne la configuration du client actif.""" return _active_config def load_client_from_config(config_path: str = '/app/config/torrent_client.json') -> Optional[TorrentClientPlugin]: """ Charge le client depuis un fichier de configuration. Args: config_path: Chemin du fichier de configuration Returns: Instance du client ou None """ import json try: if not os.path.exists(config_path): return None with open(config_path, 'r') as f: config = json.load(f) if not config.get('enabled', False): logger.info("ℹ️ Client torrent désactivé") return None plugin_name = config.get('plugin', '') if not plugin_name: return None client = create_client(plugin_name, config) if client and client.connect(): set_active_client(client, config) logger.info(f"✅ Client torrent actif: {plugin_name}") return client return None except Exception as e: logger.error(f"❌ Erreur chargement config client torrent: {e}") return None def save_client_config(config: Dict[str, Any], config_path: str = '/app/config/torrent_client.json') -> bool: """ Sauvegarde la configuration du client torrent. Args: config: Configuration à sauvegarder config_path: Chemin du fichier Returns: True si succès """ import json try: os.makedirs(os.path.dirname(config_path), exist_ok=True) with open(config_path, 'w') as f: json.dump(config, f, indent=2) return True except Exception as e: logger.error(f"❌ Erreur sauvegarde config client torrent: {e}") return False # Découvrir les plugins au chargement du module discover_plugins()