Files
Lycostorrent/app/jackett_api.py
2026-03-23 20:59:26 +01:00

202 lines
7.1 KiB
Python

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"