Files
alfred/infrastructure/api/knaben/client.py
Francwa 2c8cdd3ab1 New archi: domain driven development
Working but need to check out code
2025-12-01 07:10:03 +01:00

192 lines
5.8 KiB
Python

"""Knaben torrent search API client."""
from typing import Dict, Any, Optional, List
import logging
import requests
from requests.exceptions import RequestException, Timeout, HTTPError
from agent.config import Settings, settings
from .dto import TorrentResult
from .exceptions import KnabenError, KnabenAPIError, KnabenNotFoundError
logger = logging.getLogger(__name__)
class KnabenClient:
"""
Client for interacting with Knaben torrent search API.
Knaben is a torrent search engine that aggregates results from multiple trackers.
Example:
>>> client = KnabenClient()
>>> results = client.search("Inception 1080p")
>>> for torrent in results[:5]:
... print(f"{torrent.name} - Seeders: {torrent.seeders}")
"""
def __init__(
self,
base_url: Optional[str] = None,
timeout: Optional[int] = None,
config: Optional[Settings] = None
):
"""
Initialize Knaben client.
Args:
base_url: Knaben API base URL (defaults to https://api.knaben.org/v1)
timeout: Request timeout in seconds (defaults to settings)
config: Optional Settings instance (for testing)
Note:
Knaben API doesn't require an API key
"""
cfg = config or settings
self.base_url = base_url or "https://api.knaben.org/v1"
self.timeout = timeout or cfg.request_timeout
logger.info("Knaben client initialized")
def _make_request(
self,
params: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Make a request to Knaben API.
Args:
params: Query parameters
Returns:
JSON response as dict
Raises:
KnabenAPIError: If request fails
"""
try:
logger.debug(f"Knaben request with params: {params}")
response = requests.post(self.base_url, json=params, timeout=self.timeout)
response.raise_for_status()
return response.json()
except Timeout as e:
logger.error(f"Knaben API timeout: {e}")
raise KnabenAPIError(f"Request timeout after {self.timeout} seconds") from e
except HTTPError as e:
logger.error(f"Knaben API HTTP error: {e}")
if e.response is not None:
status_code = e.response.status_code
if status_code == 404:
raise KnabenNotFoundError("Resource not found") from e
elif status_code == 429:
raise KnabenAPIError("Rate limit exceeded") from e
else:
raise KnabenAPIError(f"HTTP {status_code}: {e}") from e
raise KnabenAPIError(f"HTTP error: {e}") from e
except RequestException as e:
logger.error(f"Knaben API request failed: {e}")
raise KnabenAPIError(f"Failed to connect to Knaben API: {e}") from e
def search(
self,
query: str,
limit: int = 10
) -> List[TorrentResult]:
"""
Search for torrents.
Args:
query: Search query (e.g., "Inception 1080p")
limit: Maximum number of results (default: 10)
Returns:
List of TorrentResult objects
Raises:
KnabenAPIError: If request fails
ValueError: If query is invalid
"""
if not query or not isinstance(query, str):
raise ValueError("Query must be a non-empty string")
if len(query) > 500:
raise ValueError("Query is too long (max 500 characters)")
# Build params
params = {
"query": query,
"search_field": "title",
"order_by": "peers",
"order_direction": "desc",
"from": 0,
"size": limit,
"hide_unsafe": True,
"hide_xxx": True,
}
try:
data = self._make_request(params)
except KnabenNotFoundError:
logger.info(f"No torrents found for '{query}'")
return []
except Exception as e:
logger.error(f"Unexpected error in search: {e}", exc_info=True)
raise
# Parse results
results = []
torrents = data.get('hits', [])
if not torrents:
logger.info(f"No torrents found for '{query}'")
return []
for torrent in torrents:
try:
result = self._parse_torrent(torrent)
results.append(result)
except Exception as e:
logger.warning(f"Failed to parse torrent: {e}")
continue
logger.info(f"Found {len(results)} torrents for '{query}'")
return results
def _parse_torrent(self, torrent: Dict[str, Any]) -> TorrentResult:
"""
Parse a torrent result into a TorrentResult object.
Args:
torrent: Raw torrent dict from API
Returns:
TorrentResult object
"""
# Extract required fields (API uses camelCase)
title = torrent.get('title', 'Unknown')
size = torrent.get('size', 'Unknown')
seeders = int(torrent.get('seeders', 0) or 0)
leechers = int(torrent.get('leechers', 0) or 0)
magnet = torrent.get('magnetUrl', '')
# Extract optional fields
info_hash = torrent.get('hash')
tracker = torrent.get('tracker')
upload_date = torrent.get('date')
category = torrent.get('category')
return TorrentResult(
title=title,
size=size,
seeders=seeders,
leechers=leechers,
magnet=magnet,
info_hash=info_hash,
tracker=tracker,
upload_date=upload_date,
category=category
)