262 lines
7.7 KiB
Python
262 lines
7.7 KiB
Python
"""
|
||
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() |