import requests import logging import xml.etree.ElementTree as ET from datetime import datetime from dateutil import parser as date_parser logger = logging.getLogger(__name__) class JackettAPI: """Classe pour interagir avec l'API Jackett""" def __init__(self, base_url, api_key): self.base_url = base_url.rstrip('/') self.api_key = api_key self.session = requests.Session() self.session.headers.update({ 'User-Agent': 'Lycostorrent/2.0' }) def get_indexers(self): """Récupère la liste des indexers (trackers) configurés dans Jackett""" try: url = f"{self.base_url}/api/v2.0/indexers/all/results/torznab/api" params = { 'apikey': self.api_key, 't': 'indexers', 'configured': 'true' } response = self.session.get(url, params=params, timeout=10) response.raise_for_status() root = ET.fromstring(response.content) indexers = [] for indexer in root.findall('.//indexer'): indexer_data = { 'id': indexer.get('id'), 'name': indexer.find('title').text if indexer.find('title') is not None else 'Unknown', 'type': indexer.get('type', 'public'), } indexers.append(indexer_data) logger.info(f"✅ {len(indexers)} indexers récupérés depuis Jackett") return indexers except requests.exceptions.RequestException as e: logger.error(f"❌ Erreur connexion Jackett: {e}") return self._get_indexers_fallback() except Exception as e: logger.error(f"❌ Erreur récupération indexers: {e}") return [] def _get_indexers_fallback(self): """Méthode alternative pour récupérer les indexers""" try: url = f"{self.base_url}/api/v2.0/indexers" params = {'apikey': self.api_key} response = self.session.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() indexers = [] for indexer in data: if indexer.get('configured', False): indexers.append({ 'id': indexer.get('id'), 'name': indexer.get('name', 'Unknown'), 'type': indexer.get('type', 'public'), }) return indexers except Exception as e: logger.error(f"❌ Erreur fallback indexers: {e}") return [] def search(self, query, indexers=None, category=None, max_results=2000): """ Effectue une recherche sur Jackett avec pagination. Args: query: Terme de recherche indexers: Liste des trackers à utiliser category: ID de catégorie Jackett (2000=films, 5000=séries, etc.) max_results: Nombre max de résultats Returns: Liste de résultats formatés """ try: url = f"{self.base_url}/api/v2.0/indexers/all/results" all_results = [] offset = 0 limit_per_request = 500 while len(all_results) < max_results: params = { 'apikey': self.api_key, 'Query': query, 'limit': limit_per_request, 'offset': offset, } # Ajouter les trackers si spécifiés if indexers and len(indexers) > 0: params['Tracker[]'] = indexers # Ajouter la catégorie si spécifiée if category: params['Category'] = category logger.info(f"🔍 Requête Jackett: query='{query}', offset={offset}, limit={limit_per_request}") response = self.session.get(url, params=params, timeout=60) response.raise_for_status() data = response.json() results = data.get('Results', []) if not results: break all_results.extend(results) # Si on a moins de résultats que la limite, c'est qu'on a tout récupéré if len(results) < limit_per_request: break offset += limit_per_request # Formater les résultats formatted_results = [self._format_result(r) for r in all_results[:max_results]] logger.info(f"✅ {len(formatted_results)} résultats récupérés au total") return formatted_results except requests.exceptions.Timeout: logger.error("⏱️ Timeout lors de la recherche Jackett") return [] except requests.exceptions.RequestException as e: logger.error(f"❌ Erreur connexion Jackett: {e}") return [] except Exception as e: logger.error(f"❌ Erreur recherche: {e}", exc_info=True) return [] def _format_result(self, result): """Formate un résultat Jackett""" try: # Parser la date publish_date = result.get('PublishDate', '') try: if publish_date: dt = date_parser.parse(publish_date) formatted_date = dt.strftime('%Y-%m-%d %H:%M') else: formatted_date = 'N/A' except: formatted_date = 'N/A' return { 'Title': result.get('Title', 'Sans titre'), 'Tracker': result.get('Tracker', 'Unknown'), 'Category': result.get('CategoryDesc', 'N/A'), 'PublishDate': formatted_date, 'PublishDateRaw': publish_date, 'Size': result.get('Size', 0), 'SizeFormatted': self._format_size(result.get('Size', 0)), 'Seeders': result.get('Seeders', 0), 'Peers': result.get('Peers', 0), 'Link': result.get('Link', ''), 'MagnetUri': result.get('MagnetUri', ''), 'Guid': result.get('Guid', ''), 'Details': result.get('Details', ''), } except Exception as e: logger.warning(f"⚠️ Erreur formatage résultat: {e}") return result def _format_size(self, size_bytes): """Convertit une taille en bytes vers un format lisible""" try: size_bytes = int(size_bytes) if size_bytes == 0: return "0 B" units = ['B', 'KB', 'MB', 'GB', 'TB'] unit_index = 0 size = float(size_bytes) while size >= 1024 and unit_index < len(units) - 1: size /= 1024 unit_index += 1 if unit_index >= 2: # MB et plus return f"{size:.2f} {units[unit_index]}" else: return f"{size:.0f} {units[unit_index]}" except: return "N/A"