Files
profilarr/backend/app/importer/arr_handler.py
Samuel Chau d7d6b13e46 feat(profiles): radarr/sonarr split functionality (#215)
- added option to set radarr/sonarr specific scores that profilarr's compiler will handle on import
- revise design for arr settings container - now styled as a table
- completely rewrote import module. Now uses connection pooling to reuse connections.
- fixed import progress bug where 1 failed format causes all other formats to be labelled as failed (even if they succeeded)
- fixed bug where on pull sync wasn't working
- improve styling for link / unlink database modals
- fixed issue where 0 score formats were removed in selective mode
2025-08-11 01:51:51 +09:30

150 lines
4.6 KiB
Python

"""ArrHandler class - manages all Arr API communication."""
import logging
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from typing import Dict, List, Any, Optional
logger = logging.getLogger(__name__)
class ArrApiError(Exception):
"""Custom exception for Arr API errors."""
def __init__(self, message: str, status_code: Optional[int] = None):
super().__init__(message)
self.status_code = status_code
class ArrHandler:
"""Manages all communication with Radarr/Sonarr API."""
def __init__(self, base_url: str, api_key: str):
"""
Initialize the Arr API handler.
Args:
base_url: Base URL of the Arr instance
api_key: API key for authentication
"""
self.base_url = base_url.rstrip('/')
self.headers = {
'X-Api-Key': api_key,
'Content-Type': 'application/json'
}
self.session = self._create_session()
def _create_session(self) -> requests.Session:
"""Create a session with connection pooling and retry logic."""
session = requests.Session()
# Configure retry strategy
retry = Retry(
total=3,
backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504]
)
# Configure connection pooling
adapter = HTTPAdapter(
pool_connections=5,
pool_maxsize=5,
max_retries=retry
)
session.mount('http://', adapter)
session.mount('https://', adapter)
session.headers.update(self.headers)
return session
def get(self, endpoint: str) -> Any:
"""
Make a GET request to the Arr API.
Args:
endpoint: API endpoint path
Returns:
JSON response data
Raises:
ArrApiError: If request fails
"""
url = f"{self.base_url}{endpoint}"
try:
response = self.session.get(url, timeout=30)
if response.status_code != 200:
raise ArrApiError(
f"GET {endpoint} failed: {response.text}",
response.status_code
)
return response.json()
except requests.RequestException as e:
raise ArrApiError(f"GET {endpoint} failed: {str(e)}")
def post(self, endpoint: str, data: Dict[str, Any]) -> Any:
"""
Make a POST request to the Arr API.
Args:
endpoint: API endpoint path
data: JSON data to send
Returns:
JSON response data
Raises:
ArrApiError: If request fails
"""
url = f"{self.base_url}{endpoint}"
try:
response = self.session.post(url, json=data, timeout=30)
if response.status_code not in [200, 201]:
raise ArrApiError(
f"POST {endpoint} failed: {response.text}",
response.status_code
)
return response.json()
except requests.RequestException as e:
raise ArrApiError(f"POST {endpoint} failed: {str(e)}")
def put(self, endpoint: str, data: Dict[str, Any]) -> Any:
"""
Make a PUT request to the Arr API.
Args:
endpoint: API endpoint path
data: JSON data to send
Returns:
JSON response data (if any)
Raises:
ArrApiError: If request fails
"""
url = f"{self.base_url}{endpoint}"
try:
response = self.session.put(url, json=data, timeout=30)
if response.status_code not in [200, 202, 204]:
raise ArrApiError(
f"PUT {endpoint} failed: {response.text}",
response.status_code
)
# 204 No Content won't have JSON
if response.status_code == 204:
return {}
return response.json()
except requests.RequestException as e:
raise ArrApiError(f"PUT {endpoint} failed: {str(e)}")
def get_all_formats(self) -> List[Dict[str, Any]]:
"""Get all custom formats from the Arr instance."""
return self.get("/api/v3/customformat")
def get_all_profiles(self) -> List[Dict[str, Any]]:
"""Get all quality profiles from the Arr instance."""
return self.get("/api/v3/qualityprofile")
def close(self):
"""Close the session."""
self.session.close()