269 lines
9.9 KiB
Python
269 lines
9.9 KiB
Python
import logging
|
|
from jackett_api import JackettAPI
|
|
from prowlarr_api import ProwlarrAPI
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class IndexerManager:
|
|
"""
|
|
Gestionnaire unifié pour Jackett et Prowlarr.
|
|
Permet d'utiliser les deux sources simultanément ou séparément.
|
|
"""
|
|
|
|
def __init__(self, config):
|
|
self.config = config
|
|
self.jackett = None
|
|
self.prowlarr = None
|
|
|
|
# Initialiser Jackett si configuré
|
|
if config.has_jackett:
|
|
self.jackett = JackettAPI(config.jackett_url, config.jackett_api_key)
|
|
logger.info("✅ Jackett initialisé")
|
|
|
|
# Initialiser Prowlarr si configuré
|
|
if config.has_prowlarr:
|
|
self.prowlarr = ProwlarrAPI(config.prowlarr_url, config.prowlarr_api_key)
|
|
logger.info("✅ Prowlarr initialisé")
|
|
|
|
def get_indexers(self):
|
|
"""
|
|
Récupère la liste de tous les indexers (Jackett + Prowlarr).
|
|
Fusionne les doublons (même nom) et indique les sources disponibles.
|
|
Priorité à Jackett si disponible sur les deux.
|
|
"""
|
|
jackett_indexers = []
|
|
prowlarr_indexers = []
|
|
|
|
# Récupérer les indexers Jackett
|
|
if self.jackett:
|
|
try:
|
|
jackett_indexers = self.jackett.get_indexers()
|
|
for idx in jackett_indexers:
|
|
idx['source'] = 'jackett'
|
|
idx['original_id'] = idx['id']
|
|
idx['id'] = f"jackett:{idx['id']}"
|
|
except Exception as e:
|
|
logger.error(f"❌ Erreur récupération indexers Jackett: {e}")
|
|
|
|
# Récupérer les indexers Prowlarr
|
|
if self.prowlarr:
|
|
try:
|
|
prowlarr_indexers = self.prowlarr.get_indexers()
|
|
for idx in prowlarr_indexers:
|
|
idx['source'] = 'prowlarr'
|
|
idx['original_id'] = idx['id']
|
|
idx['id'] = f"prowlarr:{idx['id']}"
|
|
except Exception as e:
|
|
logger.error(f"❌ Erreur récupération indexers Prowlarr: {e}")
|
|
|
|
# Fusionner les doublons par nom (normalisé)
|
|
merged = {}
|
|
|
|
# D'abord Jackett (prioritaire)
|
|
for idx in jackett_indexers:
|
|
name_key = idx['name'].lower().strip()
|
|
merged[name_key] = {
|
|
'id': idx['id'],
|
|
'name': idx['name'],
|
|
'type': idx.get('type', 'public'),
|
|
'source': 'jackett',
|
|
'sources': ['jackett'],
|
|
'jackett_id': idx['id'],
|
|
'prowlarr_id': None
|
|
}
|
|
|
|
# Ensuite Prowlarr
|
|
for idx in prowlarr_indexers:
|
|
name_key = idx['name'].lower().strip()
|
|
if name_key in merged:
|
|
# Doublon trouvé - ajouter Prowlarr comme source alternative
|
|
merged[name_key]['sources'].append('prowlarr')
|
|
merged[name_key]['prowlarr_id'] = idx['id']
|
|
# Garder Jackett comme source principale (déjà fait)
|
|
else:
|
|
# Nouveau tracker (Prowlarr uniquement)
|
|
merged[name_key] = {
|
|
'id': idx['id'],
|
|
'name': idx['name'],
|
|
'type': idx.get('type', 'public'),
|
|
'source': 'prowlarr',
|
|
'sources': ['prowlarr'],
|
|
'jackett_id': None,
|
|
'prowlarr_id': idx['id']
|
|
}
|
|
|
|
# Convertir en liste et trier par nom
|
|
all_indexers = list(merged.values())
|
|
all_indexers.sort(key=lambda x: x['name'].lower())
|
|
|
|
logger.info(f"📊 Total: {len(all_indexers)} indexers uniques (Jackett: {len(jackett_indexers)}, Prowlarr: {len(prowlarr_indexers)})")
|
|
return all_indexers
|
|
|
|
def search(self, query, indexers=None, category=None, max_results=2000):
|
|
"""
|
|
Effectue une recherche sur les indexers spécifiés.
|
|
Utilise des requêtes parallèles pour Jackett et Prowlarr.
|
|
|
|
Args:
|
|
query: Terme de recherche
|
|
indexers: Liste des IDs d'indexers (format: "jackett:id" ou "prowlarr:id")
|
|
category: Catégorie de recherche
|
|
max_results: Nombre max de résultats
|
|
|
|
Returns:
|
|
Liste combinée de résultats
|
|
"""
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
import time
|
|
|
|
all_results = []
|
|
start_time = time.time()
|
|
|
|
# Séparer les indexers par source
|
|
jackett_indexers = []
|
|
prowlarr_indexers = []
|
|
|
|
if indexers:
|
|
for idx in indexers:
|
|
if idx.startswith('jackett:'):
|
|
jackett_indexers.append(idx.replace('jackett:', ''))
|
|
elif idx.startswith('prowlarr:'):
|
|
prowlarr_indexers.append(idx.replace('prowlarr:', ''))
|
|
else:
|
|
# Format ancien (compatibilité) - supposer Jackett
|
|
jackett_indexers.append(idx)
|
|
else:
|
|
# Si aucun indexer spécifié, chercher sur tous
|
|
jackett_indexers = None
|
|
prowlarr_indexers = None
|
|
|
|
def search_jackett():
|
|
"""Recherche sur Jackett"""
|
|
if not self.jackett:
|
|
return []
|
|
if jackett_indexers is not None and len(jackett_indexers) == 0:
|
|
return []
|
|
try:
|
|
results = self.jackett.search(
|
|
query,
|
|
indexers=jackett_indexers if jackett_indexers else None,
|
|
category=category,
|
|
max_results=max_results
|
|
)
|
|
for r in results:
|
|
r['Source'] = 'jackett'
|
|
logger.info(f"📦 Jackett: {len(results)} résultats")
|
|
return results
|
|
except Exception as e:
|
|
logger.error(f"❌ Erreur recherche Jackett: {e}")
|
|
return []
|
|
|
|
def search_prowlarr():
|
|
"""Recherche sur Prowlarr"""
|
|
if not self.prowlarr:
|
|
return []
|
|
if prowlarr_indexers is not None and len(prowlarr_indexers) == 0:
|
|
return []
|
|
try:
|
|
results = self.prowlarr.search(
|
|
query,
|
|
indexers=prowlarr_indexers if prowlarr_indexers else None,
|
|
category=category,
|
|
max_results=max_results
|
|
)
|
|
for r in results:
|
|
r['Source'] = 'prowlarr'
|
|
logger.info(f"📦 Prowlarr: {len(results)} résultats")
|
|
return results
|
|
except Exception as e:
|
|
logger.error(f"❌ Erreur recherche Prowlarr: {e}")
|
|
return []
|
|
|
|
# Exécuter les recherches en parallèle
|
|
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
futures = {
|
|
executor.submit(search_jackett): 'jackett',
|
|
executor.submit(search_prowlarr): 'prowlarr'
|
|
}
|
|
|
|
for future in as_completed(futures, timeout=120):
|
|
source = futures[future]
|
|
try:
|
|
results = future.result()
|
|
all_results.extend(results)
|
|
except Exception as e:
|
|
logger.error(f"❌ Erreur {source}: {e}")
|
|
|
|
# Trier par seeders (descending) et limiter
|
|
all_results.sort(key=lambda x: x.get('Seeders', 0), reverse=True)
|
|
|
|
elapsed = time.time() - start_time
|
|
logger.info(f"✅ Total combiné: {len(all_results)} résultats (en {elapsed:.2f}s)")
|
|
return all_results[:max_results]
|
|
|
|
def get_indexer_categories(self, indexer_id):
|
|
"""Récupère les catégories d'un indexer spécifique"""
|
|
if indexer_id.startswith('jackett:'):
|
|
tracker_id = indexer_id.replace('jackett:', '')
|
|
if self.jackett:
|
|
# Utiliser l'endpoint caps de Jackett
|
|
return self._get_jackett_categories(tracker_id)
|
|
elif indexer_id.startswith('prowlarr:'):
|
|
idx_id = indexer_id.replace('prowlarr:', '')
|
|
if self.prowlarr:
|
|
return self.prowlarr.get_indexer_categories(idx_id)
|
|
|
|
return []
|
|
|
|
def _get_jackett_categories(self, tracker_id):
|
|
"""Récupère les catégories via l'API Jackett"""
|
|
try:
|
|
import xml.etree.ElementTree as ET
|
|
|
|
url = f"{self.config.jackett_url}/api/v2.0/indexers/{tracker_id}/results/torznab/api"
|
|
params = {
|
|
'apikey': self.config.jackett_api_key,
|
|
't': 'caps'
|
|
}
|
|
|
|
response = self.jackett.session.get(url, params=params, timeout=15)
|
|
response.raise_for_status()
|
|
|
|
root = ET.fromstring(response.content)
|
|
|
|
categories = []
|
|
for cat in root.findall('.//category'):
|
|
cat_id = cat.get('id')
|
|
cat_name = cat.get('name') or cat.text
|
|
if cat_id:
|
|
categories.append({
|
|
'id': cat_id,
|
|
'name': cat_name or f'Catégorie {cat_id}'
|
|
})
|
|
|
|
categories.sort(key=lambda x: int(x['id']) if x['id'].isdigit() else 999999)
|
|
return categories
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Erreur récupération catégories Jackett: {e}")
|
|
return []
|
|
|
|
@property
|
|
def has_any_source(self):
|
|
"""Vérifie si au moins une source est configurée"""
|
|
return self.jackett is not None or self.prowlarr is not None
|
|
|
|
@property
|
|
def sources_status(self):
|
|
"""Retourne le statut des sources"""
|
|
return {
|
|
'jackett': {
|
|
'enabled': self.jackett is not None,
|
|
'url': self.config.jackett_url if self.jackett else None
|
|
},
|
|
'prowlarr': {
|
|
'enabled': self.prowlarr is not None,
|
|
'url': self.config.prowlarr_url if self.prowlarr else None
|
|
}
|
|
} |