202 lines
7.1 KiB
Python
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" |