Knaben API

This commit is contained in:
2025-11-30 02:17:19 +01:00
parent 2eed654f89
commit ec9a2d4d36
5 changed files with 357 additions and 15 deletions
+230
View File
@@ -0,0 +1,230 @@
"""Knaben torrent search API client."""
from typing import Dict, Any, Optional, List
from dataclasses import dataclass
import logging
import requests
from requests.exceptions import RequestException, Timeout, HTTPError
from ..config import Settings, settings
logger = logging.getLogger(__name__)
class KnabenError(Exception):
"""Base exception for Knaben-related errors."""
pass
class KnabenConfigurationError(KnabenError):
"""Raised when Knaben API is not properly configured."""
pass
class KnabenAPIError(KnabenError):
"""Raised when Knaben API returns an error."""
pass
class KnabenNotFoundError(KnabenError):
"""Raised when no torrents are found."""
pass
@dataclass
class TorrentResult:
"""Represents a torrent search result from Knaben."""
title: str
size: str
seeders: int
leechers: int
magnet: str
info_hash: Optional[str] = None
tracker: Optional[str] = None
upload_date: Optional[str] = None
category: Optional[str] = None
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:
endpoint: API endpoint (e.g., '/search')
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: 50)
Returns:
List of TorrentResult objects
Raises:
KnabenAPIError: If request fails
KnabenNotFoundError: If no results found
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 as e:
# No results found
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
)
# Global Knaben client instance (singleton)
knaben_client = KnabenClient()