import json
import logging
from types import SimpleNamespace
from typing import Any, Callable, Dict, List, Optional
from requests import ConnectTimeout, ReadTimeout
from requests.exceptions import RequestException
from lib.clients.base import BaseClient, TorrentStream
from lib.utils.general.utils import USER_AGENT_HEADER, info_hash_to_magnet
from lib.utils.kodi.utils import notification
class Zilean(BaseClient):
def __init__(self, host: str, timeout: int, notification: Callable) -> None:
super().__init__(host, notification)
self.timeout = timeout
self.initialized = self.validate()
if not self.initialized:
return
def validate(self) -> bool:
try:
response = self.ping()
return response.ok
except Exception as e:
notification(f"Zilean failed to initialize: {e}")
return False
def search(
self,
query: str,
mode: str,
media_type: str,
season: Optional[int],
episode: Optional[int],
) -> Optional[List[TorrentStream]]:
try:
data = self.api_scrape(query, mode, media_type, season, episode)
if not data:
return None
return self.parse_response(data)
except RateLimitExceeded:
logging.warning(f"Zilean ratelimit exceeded for query: {query}")
except ConnectTimeout:
logging.warning(f"Zilean connection timeout for query: {query}")
except ReadTimeout:
logging.warning(f"Zilean read timeout for query: {query}")
except RequestException as e:
logging.error(f"Zilean request exception: {e}")
except Exception as e:
logging.error(f"Zilean exception thrown: {e}")
def api_scrape(
self,
query: str,
mode: str,
media_type: str,
season: Optional[int],
episode: Optional[int],
) -> Optional[List[Dict[str, Any]]]:
filtered_url = f"{self.host}/dmm/filtered"
search_url = f"{self.host}/dmm/search"
if mode in {"tv", "movies"} or media_type in {"tv", "movies"}:
params: Dict[str, Any] = {"Query": query}
if mode == "tv" or media_type == "tv":
params.update({"Season": season, "Episode": episode})
res = self.session.get(
filtered_url, params=params, headers=USER_AGENT_HEADER, timeout=10
)
else:
payload = {"queryText": query}
res = self.session.post(
search_url,
json=payload,
headers=USER_AGENT_HEADER,
timeout=self.timeout,
)
if res.status_code != 200:
return
response = json.loads(res.content, object_hook=lambda item: SimpleNamespace(**item))
torrents = []
for result in response:
torrents.append(
{
"infoHash": result.info_hash,
"filename": result.raw_title,
"filesize": result.size,
"languages": result.languages,
}
)
return torrents
def parse_response(self, data: List[Dict[str, Any]]) -> List[TorrentStream]:
results = []
for item in data:
results.append(
TorrentStream(
title=item["filename"],
type="Torrent",
indexer="Zilean",
guid=info_hash_to_magnet(item["infoHash"]),
infoHash=item["infoHash"],
size=item["filesize"],
seeders=0,
languages=item["languages"],
fullLanguages="",
publishDate="",
peers=0,
provider="",
)
)
return results
def ping(self, additional_headers: Optional[Dict[str, str]] = None) -> Any:
return self.session.get(
f"{self.host}/healthchecks/ping",
headers=additional_headers,
timeout=self.timeout,
)
class RateLimitExceeded(Exception):
pass