Start Magent beta overhaul
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
from typing import Any, Dict, Optional
|
||||
import logging
|
||||
import time
|
||||
import httpx
|
||||
|
||||
from ..logging_config import sanitize_headers, sanitize_value
|
||||
|
||||
|
||||
class ApiClient:
|
||||
def __init__(self, base_url: Optional[str], api_key: Optional[str] = None):
|
||||
self.base_url = base_url.rstrip("/") if base_url else None
|
||||
self.api_key = api_key
|
||||
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
||||
|
||||
def configured(self) -> bool:
|
||||
return bool(self.base_url)
|
||||
|
||||
def headers(self) -> Dict[str, str]:
|
||||
return {"X-Api-Key": self.api_key} if self.api_key else {}
|
||||
|
||||
def _response_summary(self, response: Optional[httpx.Response]) -> Optional[Any]:
|
||||
if response is None:
|
||||
return None
|
||||
try:
|
||||
payload = sanitize_value(response.json())
|
||||
except ValueError:
|
||||
payload = sanitize_value(response.text)
|
||||
if isinstance(payload, str) and len(payload) > 500:
|
||||
return f"{payload[:500]}..."
|
||||
return payload
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
payload: Optional[Dict[str, Any]] = None,
|
||||
) -> Optional[Any]:
|
||||
if not self.base_url:
|
||||
self.logger.warning("client request skipped method=%s path=%s reason=not-configured", method, path)
|
||||
return None
|
||||
url = f"{self.base_url}{path}"
|
||||
started_at = time.perf_counter()
|
||||
self.logger.debug(
|
||||
"outbound request started method=%s url=%s params=%s payload=%s headers=%s",
|
||||
method,
|
||||
url,
|
||||
sanitize_value(params),
|
||||
sanitize_value(payload),
|
||||
sanitize_headers(self.headers()),
|
||||
)
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.request(
|
||||
method,
|
||||
url,
|
||||
headers=self.headers(),
|
||||
params=params,
|
||||
json=payload,
|
||||
)
|
||||
response.raise_for_status()
|
||||
duration_ms = round((time.perf_counter() - started_at) * 1000, 2)
|
||||
self.logger.debug(
|
||||
"outbound request completed method=%s url=%s status=%s duration_ms=%s",
|
||||
method,
|
||||
url,
|
||||
response.status_code,
|
||||
duration_ms,
|
||||
)
|
||||
if not response.content:
|
||||
return None
|
||||
return response.json()
|
||||
except httpx.HTTPStatusError as exc:
|
||||
duration_ms = round((time.perf_counter() - started_at) * 1000, 2)
|
||||
response = exc.response
|
||||
status = response.status_code if response is not None else "unknown"
|
||||
log_fn = self.logger.error if isinstance(status, int) and status >= 500 else self.logger.warning
|
||||
log_fn(
|
||||
"outbound request returned error method=%s url=%s status=%s duration_ms=%s response=%s",
|
||||
method,
|
||||
url,
|
||||
status,
|
||||
duration_ms,
|
||||
self._response_summary(response),
|
||||
)
|
||||
raise
|
||||
except Exception:
|
||||
duration_ms = round((time.perf_counter() - started_at) * 1000, 2)
|
||||
self.logger.exception(
|
||||
"outbound request failed method=%s url=%s duration_ms=%s",
|
||||
method,
|
||||
url,
|
||||
duration_ms,
|
||||
)
|
||||
raise
|
||||
|
||||
async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Optional[Any]:
|
||||
return await self._request("GET", path, params=params)
|
||||
|
||||
async def post(self, path: str, payload: Optional[Dict[str, Any]] = None) -> Optional[Any]:
|
||||
return await self._request("POST", path, payload=payload)
|
||||
|
||||
async def put(self, path: str, payload: Optional[Dict[str, Any]] = None) -> Optional[Any]:
|
||||
return await self._request("PUT", path, payload=payload)
|
||||
|
||||
async def delete(self, path: str) -> Optional[Any]:
|
||||
return await self._request("DELETE", path)
|
||||
@@ -0,0 +1,201 @@
|
||||
from typing import Any, Dict, Optional
|
||||
import httpx
|
||||
from .base import ApiClient
|
||||
|
||||
|
||||
class JellyfinClient(ApiClient):
|
||||
def __init__(self, base_url: Optional[str], api_key: Optional[str]):
|
||||
super().__init__(base_url, api_key)
|
||||
|
||||
def configured(self) -> bool:
|
||||
return bool(self.base_url and self.api_key)
|
||||
|
||||
def _emby_headers(self) -> Dict[str, str]:
|
||||
return {"X-Emby-Token": self.api_key} if self.api_key else {}
|
||||
|
||||
@staticmethod
|
||||
def _extract_user_id(payload: Any) -> Optional[str]:
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
candidate = payload.get("User") if isinstance(payload.get("User"), dict) else payload
|
||||
if not isinstance(candidate, dict):
|
||||
return None
|
||||
for key in ("Id", "id", "UserId", "userId"):
|
||||
value = candidate.get(key)
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, (str, int)):
|
||||
text = str(value).strip()
|
||||
if text:
|
||||
return text
|
||||
return None
|
||||
|
||||
async def get_users(self) -> Optional[Dict[str, Any]]:
|
||||
if not self.base_url:
|
||||
return None
|
||||
url = f"{self.base_url}/Users"
|
||||
headers = self._emby_headers()
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_user(self, user_id: str) -> Optional[Dict[str, Any]]:
|
||||
if not self.base_url or not self.api_key:
|
||||
return None
|
||||
url = f"{self.base_url}/Users/{user_id}"
|
||||
headers = self._emby_headers()
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def find_user_by_name(self, username: str) -> Optional[Dict[str, Any]]:
|
||||
users = await self.get_users()
|
||||
if not isinstance(users, list):
|
||||
return None
|
||||
target = username.strip().lower()
|
||||
for user in users:
|
||||
if not isinstance(user, dict):
|
||||
continue
|
||||
name = str(user.get("Name") or "").strip().lower()
|
||||
if name and name == target:
|
||||
return user
|
||||
return None
|
||||
|
||||
async def authenticate_by_name(self, username: str, password: str) -> Optional[Dict[str, Any]]:
|
||||
if not self.base_url:
|
||||
return None
|
||||
url = f"{self.base_url}/Users/AuthenticateByName"
|
||||
headers = self._emby_headers()
|
||||
payload = {"Username": username, "Pw": password}
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def create_user(self, username: str) -> Optional[Dict[str, Any]]:
|
||||
if not self.base_url or not self.api_key:
|
||||
return None
|
||||
url = f"{self.base_url}/Users/New"
|
||||
headers = self._emby_headers()
|
||||
payload = {"Name": username}
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
if not response.content:
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
async def set_user_password(self, user_id: str, password: str) -> None:
|
||||
if not self.base_url or not self.api_key:
|
||||
return None
|
||||
headers = self._emby_headers()
|
||||
payloads = [
|
||||
{"CurrentPw": "", "NewPw": password},
|
||||
{"CurrentPwd": "", "NewPw": password},
|
||||
{"CurrentPw": "", "NewPw": password, "ResetPassword": False},
|
||||
{"CurrentPwd": "", "NewPw": password, "ResetPassword": False},
|
||||
{"NewPw": password, "ResetPassword": False},
|
||||
]
|
||||
paths = [
|
||||
f"/Users/{user_id}/Password",
|
||||
f"/Users/{user_id}/EasyPassword",
|
||||
]
|
||||
last_error: Exception | None = None
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
for path in paths:
|
||||
url = f"{self.base_url}{path}"
|
||||
for payload in payloads:
|
||||
try:
|
||||
response = await client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
return
|
||||
except httpx.HTTPStatusError as exc:
|
||||
last_error = exc
|
||||
continue
|
||||
except Exception as exc:
|
||||
last_error = exc
|
||||
continue
|
||||
if last_error:
|
||||
raise last_error
|
||||
|
||||
async def set_user_disabled(self, user_id: str, disabled: bool = True) -> None:
|
||||
if not self.base_url or not self.api_key:
|
||||
return None
|
||||
user = await self.get_user(user_id)
|
||||
if not isinstance(user, dict):
|
||||
raise RuntimeError("Jellyfin user details not available")
|
||||
policy = user.get("Policy") if isinstance(user.get("Policy"), dict) else {}
|
||||
payload = {**policy, "IsDisabled": bool(disabled)}
|
||||
url = f"{self.base_url}/Users/{user_id}/Policy"
|
||||
headers = self._emby_headers()
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
|
||||
async def delete_user(self, user_id: str) -> None:
|
||||
if not self.base_url or not self.api_key:
|
||||
return None
|
||||
url = f"{self.base_url}/Users/{user_id}"
|
||||
headers = self._emby_headers()
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.delete(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
async def create_user_with_password(self, username: str, password: str) -> Optional[Dict[str, Any]]:
|
||||
created = await self.create_user(username)
|
||||
user_id = self._extract_user_id(created)
|
||||
if not user_id:
|
||||
users = await self.get_users()
|
||||
if isinstance(users, list):
|
||||
for user in users:
|
||||
if not isinstance(user, dict):
|
||||
continue
|
||||
name = str(user.get("Name") or "").strip()
|
||||
if name.lower() == username.strip().lower():
|
||||
created = user
|
||||
user_id = self._extract_user_id(user)
|
||||
break
|
||||
if not user_id:
|
||||
raise RuntimeError("Jellyfin user created but user ID was not returned")
|
||||
await self.set_user_password(user_id, password)
|
||||
return created
|
||||
|
||||
async def search_items(
|
||||
self, term: str, item_types: Optional[list[str]] = None, limit: int = 20
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if not self.base_url or not self.api_key:
|
||||
return None
|
||||
url = f"{self.base_url}/Items"
|
||||
params = {
|
||||
"SearchTerm": term,
|
||||
"IncludeItemTypes": ",".join(item_types or []),
|
||||
"Recursive": "true",
|
||||
"Limit": limit,
|
||||
}
|
||||
headers = self._emby_headers()
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(url, headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_system_info(self) -> Optional[Dict[str, Any]]:
|
||||
if not self.base_url or not self.api_key:
|
||||
return None
|
||||
url = f"{self.base_url}/System/Info"
|
||||
headers = self._emby_headers()
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def refresh_library(self, recursive: bool = True) -> None:
|
||||
if not self.base_url or not self.api_key:
|
||||
return None
|
||||
url = f"{self.base_url}/Library/Refresh"
|
||||
headers = self._emby_headers()
|
||||
params = {"Recursive": "true" if recursive else "false"}
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.post(url, headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
@@ -0,0 +1,79 @@
|
||||
from typing import Any, Dict, Optional
|
||||
import httpx
|
||||
from .base import ApiClient
|
||||
|
||||
|
||||
class JellyseerrClient(ApiClient):
|
||||
async def get_status(self) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v1/status")
|
||||
|
||||
async def get_request(self, request_id: str) -> Optional[Dict[str, Any]]:
|
||||
return await self.get(f"/api/v1/request/{request_id}")
|
||||
|
||||
async def get_recent_requests(self, take: int = 10, skip: int = 0) -> Optional[Dict[str, Any]]:
|
||||
return await self.get(
|
||||
"/api/v1/request",
|
||||
params={
|
||||
"take": take,
|
||||
"skip": skip,
|
||||
},
|
||||
)
|
||||
|
||||
async def get_movie(self, tmdb_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.get(f"/api/v1/movie/{tmdb_id}")
|
||||
|
||||
async def get_tv(self, tmdb_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.get(f"/api/v1/tv/{tmdb_id}")
|
||||
|
||||
async def search(self, query: str, page: int = 1) -> Optional[Dict[str, Any]]:
|
||||
return await self.get(
|
||||
"/api/v1/search",
|
||||
params={
|
||||
"query": query,
|
||||
"page": page,
|
||||
},
|
||||
)
|
||||
|
||||
async def create_request(
|
||||
self,
|
||||
*,
|
||||
media_type: str,
|
||||
media_id: int,
|
||||
seasons: Optional[list[int]] = None,
|
||||
is_4k: Optional[bool] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
payload: Dict[str, Any] = {
|
||||
"mediaType": media_type,
|
||||
"mediaId": media_id,
|
||||
}
|
||||
if isinstance(seasons, list) and seasons:
|
||||
payload["seasons"] = seasons
|
||||
if isinstance(is_4k, bool):
|
||||
payload["is4k"] = is_4k
|
||||
return await self.post("/api/v1/request", payload=payload)
|
||||
|
||||
async def get_users(self, take: int = 50, skip: int = 0) -> Optional[Dict[str, Any]]:
|
||||
return await self.get(
|
||||
"/api/v1/user",
|
||||
params={
|
||||
"take": take,
|
||||
"skip": skip,
|
||||
},
|
||||
)
|
||||
|
||||
async def get_user(self, user_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.get(f"/api/v1/user/{user_id}")
|
||||
|
||||
async def delete_user(self, user_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.delete(f"/api/v1/user/{user_id}")
|
||||
|
||||
async def login_local(self, email: str, password: str) -> Optional[Dict[str, Any]]:
|
||||
payload = {"email": email, "password": password}
|
||||
try:
|
||||
return await self.post("/api/v1/auth/local", payload=payload)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
# Backward compatibility for older Seerr/Overseerr deployments
|
||||
# that still expose /auth/login instead of /auth/local.
|
||||
if exc.response is not None and exc.response.status_code in {404, 405}:
|
||||
return await self.post("/api/v1/auth/login", payload=payload)
|
||||
raise
|
||||
@@ -0,0 +1,10 @@
|
||||
from typing import Any, Dict, Optional
|
||||
from .base import ApiClient
|
||||
|
||||
|
||||
class ProwlarrClient(ApiClient):
|
||||
async def get_health(self) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v1/health")
|
||||
|
||||
async def search(self, query: str) -> Optional[Any]:
|
||||
return await self.get("/api/v1/search", params={"query": query})
|
||||
@@ -0,0 +1,108 @@
|
||||
from typing import Any, Dict, Optional
|
||||
import httpx
|
||||
import logging
|
||||
from .base import ApiClient
|
||||
|
||||
|
||||
class QBittorrentClient(ApiClient):
|
||||
def __init__(self, base_url: Optional[str], username: Optional[str], password: Optional[str]):
|
||||
super().__init__(base_url, None)
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def configured(self) -> bool:
|
||||
return bool(self.base_url and self.username and self.password)
|
||||
|
||||
async def _login(self, client: httpx.AsyncClient) -> None:
|
||||
if not self.base_url or not self.username or not self.password:
|
||||
raise RuntimeError("qBittorrent not configured")
|
||||
response = await client.post(
|
||||
f"{self.base_url}/api/v2/auth/login",
|
||||
data={"username": self.username, "password": self.password},
|
||||
headers={"Referer": self.base_url},
|
||||
)
|
||||
response.raise_for_status()
|
||||
text = response.text.strip().lower()
|
||||
has_session_cookie = any(name.upper().startswith("QBT_SID") for name in client.cookies.keys())
|
||||
if text not in {"ok.", ""} or (text == "" and not has_session_cookie):
|
||||
raise RuntimeError("qBittorrent login failed")
|
||||
|
||||
async def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Optional[Any]:
|
||||
if not self.base_url:
|
||||
return None
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
await self._login(client)
|
||||
response = await client.get(f"{self.base_url}{path}", params=params)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def _get_text(self, path: str, params: Optional[Dict[str, Any]] = None) -> Optional[str]:
|
||||
if not self.base_url:
|
||||
return None
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
await self._login(client)
|
||||
response = await client.get(f"{self.base_url}{path}", params=params)
|
||||
response.raise_for_status()
|
||||
return response.text.strip()
|
||||
|
||||
async def _post_form(self, path: str, data: Dict[str, Any]) -> None:
|
||||
if not self.base_url:
|
||||
return None
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
await self._login(client)
|
||||
response = await client.post(f"{self.base_url}{path}", data=data)
|
||||
response.raise_for_status()
|
||||
|
||||
async def is_webui_reachable(self) -> bool:
|
||||
if not self.base_url:
|
||||
return False
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client:
|
||||
response = await client.get(self.base_url)
|
||||
response.raise_for_status()
|
||||
return True
|
||||
except httpx.HTTPError:
|
||||
return False
|
||||
|
||||
async def get_torrents(self) -> Optional[Any]:
|
||||
return await self._get("/api/v2/torrents/info")
|
||||
|
||||
async def get_torrents_by_hashes(self, hashes: str) -> Optional[Any]:
|
||||
return await self._get("/api/v2/torrents/info", params={"hashes": hashes})
|
||||
|
||||
async def get_torrents_by_category(self, category: str) -> Optional[Any]:
|
||||
return await self._get("/api/v2/torrents/info", params={"category": category})
|
||||
|
||||
async def get_torrents_by_tag(self, tag: str) -> Optional[Any]:
|
||||
return await self._get("/api/v2/torrents/info", params={"tag": tag})
|
||||
|
||||
async def get_app_version(self) -> Optional[Any]:
|
||||
return await self._get_text("/api/v2/app/version")
|
||||
|
||||
async def resume_torrents(self, hashes: str) -> None:
|
||||
try:
|
||||
await self._post_form("/api/v2/torrents/resume", data={"hashes": hashes})
|
||||
except httpx.HTTPStatusError as exc:
|
||||
if exc.response is not None and exc.response.status_code == 404:
|
||||
await self._post_form("/api/v2/torrents/start", data={"hashes": hashes})
|
||||
return
|
||||
raise
|
||||
|
||||
async def add_torrent_url(
|
||||
self, url: str, category: Optional[str] = None, tags: Optional[str] = None
|
||||
) -> None:
|
||||
url_host = None
|
||||
if isinstance(url, str) and "://" in url:
|
||||
url_host = url.split("://", 1)[-1].split("/", 1)[0]
|
||||
self.logger.warning(
|
||||
"qBittorrent add_torrent_url invoked: category=%s host=%s",
|
||||
category,
|
||||
url_host or "unknown",
|
||||
)
|
||||
data: Dict[str, Any] = {"urls": url}
|
||||
if category:
|
||||
data["category"] = category
|
||||
if tags:
|
||||
data["tags"] = tags
|
||||
await self._post_form("/api/v2/torrents/add", data=data)
|
||||
@@ -0,0 +1,63 @@
|
||||
from typing import Any, Dict, Optional
|
||||
from .base import ApiClient
|
||||
|
||||
|
||||
class RadarrClient(ApiClient):
|
||||
async def get_system_status(self) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v3/system/status")
|
||||
|
||||
async def get_movie_by_tmdb_id(self, tmdb_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v3/movie", params={"tmdbId": tmdb_id})
|
||||
|
||||
async def get_movie(self, movie_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.get(f"/api/v3/movie/{movie_id}")
|
||||
|
||||
async def get_movies(self) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v3/movie")
|
||||
|
||||
async def get_root_folders(self) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v3/rootfolder")
|
||||
|
||||
async def get_quality_profiles(self) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v3/qualityprofile")
|
||||
|
||||
async def get_queue(self, movie_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v3/queue", params={"movieId": movie_id})
|
||||
|
||||
async def get_indexers(self) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v3/indexer")
|
||||
|
||||
async def search(self, movie_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.post("/api/v3/command", payload={"name": "MoviesSearch", "movieIds": [movie_id]})
|
||||
|
||||
async def add_movie(
|
||||
self,
|
||||
tmdb_id: int,
|
||||
quality_profile_id: int,
|
||||
root_folder: str,
|
||||
monitored: bool = True,
|
||||
search_for_movie: bool = True,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
payload = {
|
||||
"tmdbId": tmdb_id,
|
||||
"qualityProfileId": quality_profile_id,
|
||||
"rootFolderPath": root_folder,
|
||||
"monitored": monitored,
|
||||
"addOptions": {"searchForMovie": search_for_movie},
|
||||
}
|
||||
return await self.post("/api/v3/movie", payload=payload)
|
||||
|
||||
async def update_movie(self, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
return await self.put("/api/v3/movie", payload=payload)
|
||||
|
||||
async def grab_release(self, guid: str, indexer_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.post("/api/v3/release", payload={"guid": guid, "indexerId": indexer_id})
|
||||
|
||||
async def push_release(self, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
return await self.post("/api/v3/release/push", payload=payload)
|
||||
|
||||
async def download_release(self, guid: str, indexer_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.post(
|
||||
"/api/v3/command",
|
||||
payload={"name": "DownloadRelease", "guid": guid, "indexerId": indexer_id},
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
from typing import Any, Dict, Optional
|
||||
from .base import ApiClient
|
||||
|
||||
|
||||
class SonarrClient(ApiClient):
|
||||
async def get_system_status(self) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v3/system/status")
|
||||
|
||||
async def get_series_by_tvdb_id(self, tvdb_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v3/series", params={"tvdbId": tvdb_id})
|
||||
|
||||
async def get_series(self, series_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.get(f"/api/v3/series/{series_id}")
|
||||
|
||||
async def get_root_folders(self) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v3/rootfolder")
|
||||
|
||||
async def get_quality_profiles(self) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v3/qualityprofile")
|
||||
|
||||
async def get_queue(self, series_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v3/queue", params={"seriesId": series_id})
|
||||
|
||||
async def get_indexers(self) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v3/indexer")
|
||||
|
||||
async def get_episodes(self, series_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.get("/api/v3/episode", params={"seriesId": series_id})
|
||||
|
||||
async def search(self, series_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.post("/api/v3/command", payload={"name": "SeriesSearch", "seriesId": series_id})
|
||||
|
||||
async def search_episodes(self, episode_ids: list[int]) -> Optional[Dict[str, Any]]:
|
||||
return await self.post("/api/v3/command", payload={"name": "EpisodeSearch", "episodeIds": episode_ids})
|
||||
|
||||
async def add_series(
|
||||
self,
|
||||
tvdb_id: int,
|
||||
quality_profile_id: int,
|
||||
root_folder: str,
|
||||
monitored: bool = True,
|
||||
title: Optional[str] = None,
|
||||
search_missing: bool = True,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
payload = {
|
||||
"tvdbId": tvdb_id,
|
||||
"qualityProfileId": quality_profile_id,
|
||||
"rootFolderPath": root_folder,
|
||||
"monitored": monitored,
|
||||
"seasonFolder": True,
|
||||
"addOptions": {"searchForMissingEpisodes": search_missing},
|
||||
}
|
||||
if title:
|
||||
payload["title"] = title
|
||||
return await self.post("/api/v3/series", payload=payload)
|
||||
|
||||
async def update_series(self, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
return await self.put("/api/v3/series", payload=payload)
|
||||
|
||||
async def grab_release(self, guid: str, indexer_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.post("/api/v3/release", payload={"guid": guid, "indexerId": indexer_id})
|
||||
|
||||
async def push_release(self, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
return await self.post("/api/v3/release/push", payload=payload)
|
||||
|
||||
async def download_release(self, guid: str, indexer_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.post(
|
||||
"/api/v3/command",
|
||||
payload={"name": "DownloadRelease", "guid": guid, "indexerId": indexer_id},
|
||||
)
|
||||
Reference in New Issue
Block a user