This commit is contained in:
Admin9705
2025-05-02 22:45:31 -04:00
parent c781c28a3a
commit b9bfaec71c
9 changed files with 553 additions and 407 deletions
+96 -113
View File
@@ -24,7 +24,7 @@ def test_connection():
return jsonify({"success": False, "message": "API URL and API Key are required"}), 400
# Log the test attempt
whisparr_logger.info(f"Testing connection to Whisparr Eros API at {api_url}")
whisparr_logger.info(f"Testing connection to Whisparr V2 API at {api_url}")
# First check if URL is properly formatted
if not (api_url.startswith('http://') or api_url.startswith('https://')):
@@ -32,17 +32,24 @@ def test_connection():
whisparr_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 400
# For Whisparr Eros, use api/v3
api_base = "api/v3"
test_url = f"{api_url.rstrip('/')}/{api_base}/system/status"
# For Whisparr V2, we can try both with and without /api/v3 path
# First try with direct API access
test_url = f"{api_url.rstrip('/')}/api/system/status"
headers = {'X-Api-Key': api_key}
try:
# Use a connection timeout separate from read timeout
response = requests.get(test_url, headers=headers, timeout=(10, api_timeout))
# Log HTTP status code for diagnostic purposes
whisparr_logger.debug(f"Whisparr Eros API status code: {response.status_code}")
whisparr_logger.debug(f"Whisparr API status code: {response.status_code}")
# If we get a 404, try with /api/v3 path since Whisparr V2 might be using API V3 format
if response.status_code == 404:
test_url = f"{api_url.rstrip('/')}/api/v3/system/status"
whisparr_logger.debug(f"First attempt failed, trying alternate API path: {test_url}")
response = requests.get(test_url, headers=headers, timeout=(10, api_timeout))
whisparr_logger.debug(f"Whisparr API V3 status code: {response.status_code}")
# Check HTTP status code
response.raise_for_status()
@@ -50,134 +57,110 @@ def test_connection():
# Ensure the response is valid JSON
try:
response_data = response.json()
whisparr_logger.debug(f"Whisparr API response: {response_data}")
# Validate that this is actually Eros API v3
if 'version' in response_data and not response_data['version'].startswith('3'):
error_msg = f"Incompatible Whisparr version detected: {response_data.get('version')}. Huntarr requires Eros API v3."
# Verify this is actually a Whisparr API by checking for version
version = response_data.get('version', None)
if not version:
error_msg = "API response doesn't contain version information, may not be Whisparr"
whisparr_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg, "is_eros": False}), 400
return jsonify({"success": False, "message": error_msg}), 400
whisparr_logger.info(f"Successfully connected to Whisparr Eros API version: {response_data.get('version', 'unknown')}")
# Return success with some useful information
return jsonify({
"success": True,
"message": "Successfully connected to Whisparr Eros API",
"version": response_data.get('version', 'unknown'),
"is_eros": True
})
# Accept both V2 and V3 API formats for Whisparr V2
# The version number should still start with 2 for Whisparr V2, even if using API V3
if version.startswith('2'):
whisparr_logger.info(f"Successfully connected to Whisparr V2 API version {version}")
return jsonify({
"success": True,
"message": f"Successfully connected to Whisparr V2 (version {version})",
"version": version
})
elif version.startswith('3'):
error_msg = f"Connected to Whisparr Eros (version {version}). Huntarr requires Whisparr V2."
whisparr_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 400
else:
# Connected to some other version
error_msg = f"Connected to Whisparr version {version}, but Huntarr requires Whisparr V2"
whisparr_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 400
except ValueError:
error_msg = "Invalid JSON response from Whisparr Eros API"
whisparr_logger.error(f"{error_msg}. Response content: {response.text[:200]}")
return jsonify({"success": False, "message": error_msg}), 500
except requests.exceptions.Timeout as e:
error_msg = f"Connection timed out after {api_timeout} seconds"
whisparr_logger.error(f"{error_msg}: {str(e)}")
return jsonify({"success": False, "message": error_msg}), 504
except requests.exceptions.ConnectionError as e:
error_msg = "Connection error - check hostname and port"
details = str(e)
# Check for common DNS resolution errors
if "Name or service not known" in details or "getaddrinfo failed" in details:
error_msg = "DNS resolution failed - check hostname"
# Check for common connection refused errors
elif "Connection refused" in details:
error_msg = "Connection refused - check if Whisparr is running and the port is correct"
whisparr_logger.error(f"{error_msg}: {details}")
return jsonify({"success": False, "message": f"{error_msg}: {details}"}), 502
except requests.exceptions.RequestException as e:
error_message = f"Connection failed: {str(e)}"
if hasattr(e, 'response') and e.response is not None:
status_code = e.response.status_code
error_msg = "Invalid JSON response from API. Are you sure this is a Whisparr API?"
whisparr_logger.error(f"{error_msg}. Response: {response.text[:200]}")
return jsonify({"success": False, "message": error_msg}), 400
# Add specific messages based on common status codes
if status_code == 401:
error_message = "Authentication failed: Invalid API key"
elif status_code == 403:
error_message = "Access forbidden: Check API key permissions"
elif status_code == 404:
error_message = "API endpoint not found: Check API URL"
elif status_code >= 500:
error_message = f"Whisparr server error (HTTP {status_code}): The Whisparr server is experiencing issues"
# Try to extract more error details if available
try:
error_details = e.response.json()
error_message += f" - {error_details.get('message', 'No details')}"
except ValueError:
if e.response.text:
error_message += f" - Response: {e.response.text[:200]}"
except requests.exceptions.Timeout:
error_msg = f"Connection timed out after {api_timeout} seconds. Check that Whisparr is running and accessible."
whisparr_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 408
whisparr_logger.error(error_message)
return jsonify({"success": False, "message": error_message}), 500
except requests.exceptions.ConnectionError:
error_msg = "Failed to connect. Check that the URL is correct and that Whisparr is running."
whisparr_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 502
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
error_msg = "API key invalid or unauthorized"
elif response.status_code == 404:
error_msg = "API endpoint not found. Check that the URL is correct."
else:
error_msg = f"HTTP error: {str(e)}"
whisparr_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), response.status_code
except Exception as e:
error_msg = f"An unexpected error occurred: {str(e)}"
error_msg = f"Unexpected error: {str(e)}"
whisparr_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 500
# Function to check if Whisparr is configured
def is_configured():
"""Check if Whisparr API credentials are configured by checking if at least one instance is enabled"""
settings = load_settings("whisparr")
if not settings:
whisparr_logger.debug("No settings found for Whisparr")
return False
"""Check if Whisparr API credentials are configured"""
try:
api_keys = keys_manager.load_api_keys("whisparr")
instances = api_keys.get("instances", [])
# Check if instances are configured
if "instances" in settings and isinstance(settings["instances"], list) and settings["instances"]:
for instance in settings["instances"]:
if instance.get("enabled", True) and instance.get("api_url") and instance.get("api_key"):
whisparr_logger.debug(f"Found configured Whisparr instance: {instance.get('name', 'Unnamed')}")
for instance in instances:
if instance.get("enabled", True):
return True
whisparr_logger.debug("No enabled Whisparr instances found with valid API URL and key")
return False
# Fallback to legacy single-instance config
api_url = settings.get("api_url")
api_key = settings.get("api_key")
return bool(api_url and api_key)
except Exception as e:
whisparr_logger.error(f"Error checking if Whisparr is configured: {str(e)}")
return False
# Get all valid instances from settings
def get_configured_instances():
"""Get all configured and enabled Whisparr instances"""
settings = load_settings("whisparr")
instances = []
if not settings:
whisparr_logger.debug("No settings found for Whisparr")
return instances
try:
api_keys = keys_manager.load_api_keys("whisparr")
instances = api_keys.get("instances", [])
# Check if instances are configured
if "instances" in settings and isinstance(settings["instances"], list) and settings["instances"]:
for instance in settings["instances"]:
if instance.get("enabled", True) and instance.get("api_url") and instance.get("api_key"):
# Create a settings object for this instance by combining global settings with instance-specific ones
instance_settings = settings.copy()
# Remove instances list to avoid confusion
if "instances" in instance_settings:
del instance_settings["instances"]
enabled_instances = []
for instance in instances:
if not instance.get("enabled", True):
continue
# Override with instance-specific connection settings
instance_settings["api_url"] = instance.get("api_url")
instance_settings["api_key"] = instance.get("api_key")
instance_settings["instance_name"] = instance.get("name", "Default")
api_url = instance.get("api_url")
api_key = instance.get("api_key")
if not api_url or not api_key:
continue
instances.append(instance_settings)
else:
# Fallback to legacy single-instance config
api_url = settings.get("api_url")
api_key = settings.get("api_key")
if api_url and api_key:
settings["instance_name"] = "Default"
instances.append(settings)
whisparr_logger.info(f"Found {len(instances)} configured and enabled Whisparr instances")
return instances
# Add name and timeout
instance_name = instance.get("name", "Default")
api_timeout = instance.get("api_timeout", 90)
enabled_instances.append({
"api_url": api_url,
"api_key": api_key,
"instance_name": instance_name,
"api_timeout": api_timeout
})
return enabled_instances
except Exception as e:
whisparr_logger.error(f"Error getting configured Whisparr instances: {str(e)}")
return []
+31 -39
View File
@@ -2,7 +2,7 @@
Whisparr app module for Huntarr
Contains functionality for missing items and quality upgrades in Whisparr
Exclusively supports the v3 Eros API.
Exclusively supports the v2 API (legacy).
"""
# Module exports
@@ -27,8 +27,8 @@ def get_configured_instances():
whisparr_logger.debug("No settings found for Whisparr")
return instances
# Always use Eros API v3
whisparr_logger.info("Using Whisparr Eros API v3 exclusively")
# Always use Whisparr V2 API
whisparr_logger.info("Using Whisparr V2 API exclusively")
# Check if instances are configured
if "instances" in settings and isinstance(settings["instances"], list) and settings["instances"]:
@@ -49,44 +49,36 @@ def get_configured_instances():
# Only include properly configured instances
if is_enabled and api_url and api_key:
# Return only essential instance details
instance_data = {
"instance_name": instance.get("name", "Default"),
"api_url": api_url,
"api_key": api_key
}
instances.append(instance_data)
whisparr_logger.info(f"Added valid instance: {instance_data}")
elif not is_enabled:
whisparr_logger.debug(f"Skipping disabled instance: {instance.get('name', 'Unnamed')}")
instance_name = instance.get("name", "Default")
# Create a settings object for this instance by combining global settings with instance-specific ones
instance_settings = settings.copy()
# Remove instances list to avoid confusion
if "instances" in instance_settings:
del instance_settings["instances"]
# Override with instance-specific settings
instance_settings["api_url"] = api_url
instance_settings["api_key"] = api_key
instance_settings["instance_name"] = instance_name
# Add timeout setting with default if not present
if "api_timeout" not in instance_settings:
instance_settings["api_timeout"] = 30
whisparr_logger.info(f"Adding configured Whisparr instance: {instance_name}")
instances.append(instance_settings)
else:
# Log specifically why it's skipped (missing URL/Key but enabled)
whisparr_logger.warning(f"Skipping instance '{instance.get('name', 'Unnamed')}' due to missing API URL or key (URL: '{api_url}', Key Set: {bool(api_key)}) ")
name = instance.get("name", "Unnamed")
if not is_enabled:
whisparr_logger.debug(f"Skipping disabled instance: {name}")
else:
whisparr_logger.warning(f"Skipping instance {name} due to missing API URL or API Key")
else:
whisparr_logger.info("No 'instances' list found or list is empty. Checking legacy config.")
# Fallback to legacy single-instance config
api_url = settings.get("api_url", "").strip()
api_key = settings.get("api_key", "").strip()
# Ensure URL has proper scheme
if api_url and not (api_url.startswith('http://') or api_url.startswith('https://')):
whisparr_logger.warning(f"API URL missing http(s) scheme: {api_url}")
api_url = f"http://{api_url}"
whisparr_logger.warning(f"Auto-correcting URL to: {api_url}")
if api_url and api_key:
# Create a clean instance_data dict for the legacy instance
instance_data = {
"instance_name": "Default",
"api_url": api_url,
"api_key": api_key
}
instances.append(instance_data)
whisparr_logger.info(f"Added valid legacy instance: {instance_data}")
else:
whisparr_logger.warning("No API URL or key found in legacy configuration")
whisparr_logger.info(f"Returning {len(instances)} configured instances: {instances}")
whisparr_logger.debug("No instances array found in settings or it's empty")
whisparr_logger.info(f"Found {len(instances)} configured and enabled Whisparr instances")
return instances
__all__ = ["process_missing_items", "process_missing_scenes", "process_cutoff_upgrades", "get_configured_instances"]
+157 -30
View File
@@ -40,13 +40,16 @@ def arr_request(api_url: str, api_key: str, api_timeout: int, endpoint: str, met
whisparr_logger.error("API URL or API key is missing. Check your settings.")
return None
# Always use v2 for Whisparr API
# Always try standard path first
api_base = "api"
whisparr_logger.debug(f"Using Whisparr V2 API: {api_base}")
whisparr_logger.debug(f"Using Whisparr API path: {api_base}")
# Full URL - ensure no double slashes
url = f"{api_url.rstrip('/')}/{api_base}/{endpoint.lstrip('/')}"
# Add debug logging for the exact URL being called
whisparr_logger.debug(f"Making {method} request to: {url}")
# Headers
headers = {
"X-Api-Key": api_key,
@@ -66,18 +69,39 @@ def arr_request(api_url: str, api_key: str, api_timeout: int, endpoint: str, met
whisparr_logger.error(f"Unsupported HTTP method: {method}")
return None
# If we get a 404, try with v3 path instead
if response.status_code == 404:
api_base = "api/v3"
v3_url = f"{api_url.rstrip('/')}/{api_base}/{endpoint.lstrip('/')}"
whisparr_logger.debug(f"Standard path returned 404, trying with V3 path: {v3_url}")
if method == "GET":
response = session.get(v3_url, headers=headers, timeout=api_timeout)
elif method == "POST":
response = session.post(v3_url, headers=headers, json=data, timeout=api_timeout)
elif method == "PUT":
response = session.put(v3_url, headers=headers, json=data, timeout=api_timeout)
elif method == "DELETE":
response = session.delete(v3_url, headers=headers, timeout=api_timeout)
whisparr_logger.debug(f"V3 path request returned status code: {response.status_code}")
# Check if the request was successful
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
whisparr_logger.error(f"Error during {method} request to {endpoint}: {e}, Status Code: {response.status_code}")
whisparr_logger.debug(f"Response content: {response.text[:200]}")
return None
# Try to parse JSON response
try:
if response.text:
return response.json()
result = response.json()
whisparr_logger.debug(f"Response from {response.url}: Status {response.status_code}, JSON parsed successfully")
return result
else:
whisparr_logger.debug(f"Response from {response.url}: Status {response.status_code}, Empty response")
return {}
except json.JSONDecodeError:
whisparr_logger.error(f"Invalid JSON response from API: {response.text[:200]}")
@@ -237,14 +261,42 @@ def refresh_item(api_url: str, api_key: str, api_timeout: int, item_id: int) ->
"episodeIds": [item_id]
}
response = arr_request(api_url, api_key, api_timeout, "command", method="POST", data=payload)
# For commands, we need to directly try both path formats since command endpoints
# may have different structures in different Whisparr versions
command_endpoint = "command"
url = f"{api_url.rstrip('/')}/api/{command_endpoint}"
backup_url = f"{api_url.rstrip('/')}/api/v3/{command_endpoint}"
if response and "id" in response:
command_id = response["id"]
whisparr_logger.debug(f"Refresh command triggered with ID {command_id}")
return command_id
else:
whisparr_logger.error("Failed to trigger refresh command")
headers = {
"X-Api-Key": api_key,
"Content-Type": "application/json"
}
# Try standard API path first
whisparr_logger.debug(f"Attempting command with standard API path: {url}")
try:
response = session.post(url, headers=headers, json=payload, timeout=api_timeout)
# If we get a 404 or 405, try the v3 path
if response.status_code in [404, 405]:
whisparr_logger.debug(f"Standard path returned {response.status_code}, trying with V3 path: {backup_url}")
response = session.post(backup_url, headers=headers, json=payload, timeout=api_timeout)
response.raise_for_status()
result = response.json()
if result and "id" in result:
command_id = result["id"]
whisparr_logger.debug(f"Refresh command triggered with ID {command_id}")
return command_id
else:
whisparr_logger.error("Failed to trigger refresh command - no command ID returned")
return None
except requests.exceptions.HTTPError as e:
whisparr_logger.error(f"HTTP error during refresh command: {e}, Status Code: {response.status_code}")
whisparr_logger.debug(f"Response content: {response.text[:200]}")
return None
except Exception as e:
whisparr_logger.error(f"Error sending refresh command: {e}")
return None
except Exception as e:
@@ -273,16 +325,43 @@ def item_search(api_url: str, api_key: str, api_timeout: int, item_ids: List[int
"episodeIds": item_ids
}
response = arr_request(api_url, api_key, api_timeout, "command", method="POST", data=payload)
# For commands, we need to directly try both path formats
command_endpoint = "command"
url = f"{api_url.rstrip('/')}/api/{command_endpoint}"
backup_url = f"{api_url.rstrip('/')}/api/v3/{command_endpoint}"
if response and "id" in response:
command_id = response["id"]
whisparr_logger.debug(f"Search command triggered with ID {command_id}")
return command_id
else:
whisparr_logger.error("Failed to trigger search command")
return None
headers = {
"X-Api-Key": api_key,
"Content-Type": "application/json"
}
# Try standard API path first
whisparr_logger.debug(f"Attempting command with standard API path: {url}")
try:
response = session.post(url, headers=headers, json=payload, timeout=api_timeout)
# If we get a 404 or 405, try the v3 path
if response.status_code in [404, 405]:
whisparr_logger.debug(f"Standard path returned {response.status_code}, trying with V3 path: {backup_url}")
response = session.post(backup_url, headers=headers, json=payload, timeout=api_timeout)
response.raise_for_status()
result = response.json()
if result and "id" in result:
command_id = result["id"]
whisparr_logger.debug(f"Search command triggered with ID {command_id}")
return command_id
else:
whisparr_logger.error("Failed to trigger search command - no command ID returned")
return None
except requests.exceptions.HTTPError as e:
whisparr_logger.error(f"HTTP error during search command: {e}, Status Code: {response.status_code}")
whisparr_logger.debug(f"Response content: {response.text[:200]}")
return None
except Exception as e:
whisparr_logger.error(f"Error sending search command: {e}")
return None
except Exception as e:
whisparr_logger.error(f"Error searching for items: {str(e)}")
return None
@@ -305,22 +384,45 @@ def get_command_status(api_url: str, api_key: str, api_timeout: int, command_id:
return None
try:
endpoint = f"command/{command_id}"
result = arr_request(api_url, api_key, api_timeout, endpoint)
# For commands, we need to directly try both path formats
command_endpoint = f"command/{command_id}"
url = f"{api_url.rstrip('/')}/api/{command_endpoint}"
backup_url = f"{api_url.rstrip('/')}/api/v3/{command_endpoint}"
if result:
headers = {
"X-Api-Key": api_key,
"Content-Type": "application/json"
}
# Try standard API path first
whisparr_logger.debug(f"Checking command status with standard API path: {url}")
try:
response = session.get(url, headers=headers, timeout=api_timeout)
# If we get a 404, try the v3 path
if response.status_code == 404:
whisparr_logger.debug(f"Standard path returned 404, trying with V3 path: {backup_url}")
response = session.get(backup_url, headers=headers, timeout=api_timeout)
response.raise_for_status()
result = response.json()
whisparr_logger.debug(f"Command {command_id} status: {result.get('status', 'unknown')}")
return result
else:
whisparr_logger.error(f"Failed to get status for command ID {command_id}")
except requests.exceptions.HTTPError as e:
whisparr_logger.error(f"HTTP error getting command status: {e}, Status Code: {response.status_code}")
whisparr_logger.debug(f"Response content: {response.text[:200]}")
return None
except Exception as e:
whisparr_logger.error(f"Error getting command status: {e}")
return None
except Exception as e:
whisparr_logger.error(f"Error getting command status for ID {command_id}: {e}")
return None
def check_connection(api_url: str, api_key: str, api_timeout: int) -> bool:
"""
Check the connection to Whisparr API.
Check the connection to Whisparr V2 API.
Args:
api_url: The base URL of the Whisparr API
@@ -331,18 +433,43 @@ def check_connection(api_url: str, api_key: str, api_timeout: int) -> bool:
True if the connection is successful, False otherwise
"""
try:
# System status is a good endpoint for verifying API connectivity
response = arr_request(api_url, api_key, api_timeout, "system/status")
# For Whisparr V2, we need to handle both regular and v3 API formats
whisparr_logger.debug(f"Checking connection to Whisparr V2 instance at {api_url}")
# First try with standard path
endpoint = "system/status"
response = arr_request(api_url, api_key, api_timeout, endpoint)
# If that failed, try with v3 path format
if response is None:
whisparr_logger.debug("Standard API path failed, trying v3 format...")
# Try direct HTTP request to v3 endpoint without using arr_request
url = f"{api_url.rstrip('/')}/api/v3/system/status"
headers = {'X-Api-Key': api_key}
try:
resp = session.get(url, headers=headers, timeout=api_timeout)
resp.raise_for_status()
response = resp.json()
except Exception as e:
whisparr_logger.debug(f"V3 API path also failed: {str(e)}")
return False
if response is not None:
# Get the version information if available
version = response.get("version", "unknown")
whisparr_logger.info(f"Successfully connected to Whisparr {version} using API v2")
return True
# Check if this is a v2.x version
if version and version.startswith('2'):
whisparr_logger.info(f"Successfully connected to Whisparr V2 API version: {version}")
return True
else:
whisparr_logger.warning(f"Connected to Whisparr but found unexpected version: {version}, expected 2.x")
return False
else:
whisparr_logger.error("Failed to connect to Whisparr API")
whisparr_logger.error("Failed to connect to Whisparr V2 API")
return False
except Exception as e:
whisparr_logger.error(f"Error checking connection to Whisparr API: {str(e)}")
whisparr_logger.error(f"Error checking connection to Whisparr V2 API: {str(e)}")
return False
+2 -2
View File
@@ -59,8 +59,8 @@ def process_missing_items(
# Use the centralized advanced setting for stateful management hours
stateful_management_hours = get_advanced_setting("stateful_management_hours", 168)
# Log that we're using Eros API v3
whisparr_logger.info(f"Using Whisparr Eros API v3 for instance: {instance_name}")
# Log that we're using Whisparr V2 API
whisparr_logger.info(f"Using Whisparr V2 API for instance: {instance_name}")
# Skip if hunt_missing_items is set to 0
if hunt_missing_items <= 0:
+2 -2
View File
@@ -56,8 +56,8 @@ def process_cutoff_upgrades(
command_wait_attempts = app_settings.get("command_wait_attempts", 12)
state_reset_interval_hours = get_advanced_setting("stateful_management_hours", 168)
# Log that we're using Eros API v3
whisparr_logger.info(f"Using Whisparr Eros API v3 for instance: {instance_name}")
# Log that we're using Whisparr V2 API
whisparr_logger.info(f"Using Whisparr V2 API for instance: {instance_name}")
# Skip if hunt_upgrade_items is set to 0
if hunt_upgrade_items <= 0:
+137 -118
View File
@@ -6,6 +6,7 @@ from src.primary import keys_manager
from src.primary.state import get_state_file_path, reset_state_file
from src.primary.utils.logger import get_logger, APP_LOG_FILES
import traceback
from src.primary.apps.whisparr import api as whisparr_api
whisparr_bp = Blueprint('whisparr', __name__)
whisparr_logger = get_logger("whisparr")
@@ -14,6 +15,39 @@ whisparr_logger = get_logger("whisparr")
PROCESSED_MISSING_FILE = get_state_file_path("whisparr", "processed_missing")
PROCESSED_UPGRADES_FILE = get_state_file_path("whisparr", "processed_upgrades")
@whisparr_bp.route('/status', methods=['GET'])
def get_status():
"""Get the status of all configured Whisparr instances"""
try:
# Get all configured instances
api_keys = keys_manager.load_api_keys("whisparr")
instances = api_keys.get("instances", [])
connected_count = 0
total_configured = len(instances)
for instance in instances:
api_url = instance.get("api_url")
api_key = instance.get("api_key")
if api_url and api_key and instance.get("enabled", True):
# Use a short timeout for status checks
if whisparr_api.check_connection(api_url, api_key, 5):
connected_count += 1
return jsonify({
"configured": total_configured > 0,
"connected": connected_count > 0,
"connected_count": connected_count,
"total_configured": total_configured
})
except Exception as e:
whisparr_logger.error(f"Error getting Whisparr status: {str(e)}")
return jsonify({
"configured": False,
"connected": False,
"error": str(e)
}), 500
@whisparr_bp.route('/test-connection', methods=['POST'])
def test_connection():
"""Test connection to a Whisparr API instance"""
@@ -24,10 +58,10 @@ def test_connection():
if not api_url or not api_key:
return jsonify({"success": False, "message": "API URL and API Key are required"}), 400
whisparr_logger.info(f"Testing connection to Whisparr API at {api_url}")
whisparr_logger.info(f"Testing connection to Whisparr V2 API at {api_url}")
# Use v3 API endpoint for Whisparr Eros
url = f"{api_url}/api/v3/system/status"
# First try the standard API endpoint
url = f"{api_url.rstrip('/')}/api/system/status"
headers = {
"X-Api-Key": api_key,
"Content-Type": "application/json"
@@ -36,43 +70,20 @@ def test_connection():
try:
response = requests.get(url, headers=headers, timeout=10)
# Check if we received a 404, which might indicate this is a v2 API
# If we get a 404, try with the v3 path format
if response.status_code == 404:
# Try v2 API endpoint instead
url_v2 = f"{api_url}/api/system/status"
response_v2 = requests.get(url_v2, headers=headers, timeout=10)
if response_v2.status_code == 200:
try:
response_data = response_v2.json()
version = response_data.get('version', 'unknown')
# Make sure it's a version 2.x
if version and version.startswith('2'):
whisparr_logger.info(f"Successfully connected to Whisparr V2 API version: {version}")
return jsonify({
"success": True,
"message": f"Successfully connected to Whisparr V2 API (version {version})",
"version": version,
"is_v2": True
})
else:
error_msg = f"Unexpected Whisparr version {version} detected via v2 API path."
whisparr_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 400
except ValueError:
error_msg = "Invalid JSON response from Whisparr V2 API"
whisparr_logger.error(f"{error_msg}. Response content: {response_v2.text[:200]}")
return jsonify({"success": False, "message": error_msg}), 500
whisparr_logger.debug("Standard API path returned 404, trying V3 path format")
v3_url = f"{api_url.rstrip('/')}/api/v3/system/status"
response = requests.get(v3_url, headers=headers, timeout=10)
whisparr_logger.debug(f"V3 path request returned status code: {response.status_code}")
# If we got a successful response from the v3 endpoint
# Direct request to V2 API
if response.status_code == 200:
try:
response_data = response.json()
version = response_data.get('version', 'unknown')
# Special case: check if it's actually a v2 API instance
# Some Whisparr v2 instances might respond to v3 endpoint too
# Make sure it's a version 2.x
if version and version.startswith('2'):
whisparr_logger.info(f"Successfully connected to Whisparr V2 API version: {version}")
return jsonify({
@@ -81,26 +92,24 @@ def test_connection():
"version": version,
"is_v2": True
})
# Reject version 3.x (Eros API)
if version and version.startswith('3'):
error_msg = f"Whisparr version {version} (Eros API) detected. Huntarr requires Whisparr V2."
elif version and version.startswith('3'):
# Detected Eros API (V3)
error_msg = f"Incompatible Whisparr version {version} detected. Huntarr requires Whisparr V2."
whisparr_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg, "is_eros": True}), 400
# If we're here, it's some other version
error_msg = f"Unexpected Whisparr version {version} detected. Huntarr requires Whisparr V2."
whisparr_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 400
return jsonify({"success": False, "message": error_msg}), 400
else:
error_msg = f"Unexpected Whisparr version {version} detected. Huntarr requires Whisparr V2."
whisparr_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 400
except ValueError:
error_msg = "Invalid JSON response from Whisparr API"
error_msg = "Invalid JSON response from Whisparr V2 API"
whisparr_logger.error(f"{error_msg}. Response content: {response.text[:200]}")
return jsonify({"success": False, "message": error_msg}), 500
# If we reached here, the status code wasn't 200 or 404
error_msg = f"Received HTTP {response.status_code} from Whisparr API"
whisparr_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 500
else:
error_msg = f"Received HTTP {response.status_code} from Whisparr API"
whisparr_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 500
except requests.exceptions.RequestException as e:
error_msg = f"Connection test failed: {str(e)}"
whisparr_logger.error(error_msg)
@@ -112,83 +121,93 @@ def is_configured():
api_keys = keys_manager.load_api_keys("whisparr")
return api_keys.get("api_url") and api_keys.get("api_key")
@whisparr_bp.route('/get-versions', methods=['GET'])
@whisparr_bp.route('/versions', methods=['GET'])
def get_versions():
"""Get the version information from the Whisparr API"""
api_keys = keys_manager.load_api_keys("whisparr")
api_url = api_keys.get("api_url")
api_key = api_keys.get("api_key")
if not api_url or not api_key:
return jsonify({"success": False, "message": "Whisparr API is not configured"}), 400
headers = {'X-Api-Key': api_key}
# Try v2 API endpoint first since that's what we want
version_url_v2 = f"{api_url.rstrip('/')}/api/system/status"
try:
# Check v2 endpoint first
response = requests.get(version_url_v2, headers=headers, timeout=10)
# Get all configured instances
api_keys = keys_manager.load_api_keys("whisparr")
instances = api_keys.get("instances", [])
if response.status_code == 200:
version_data = response.json()
version = version_data.get("version", "Unknown")
if not instances:
return jsonify({"success": False, "message": "No Whisparr instances configured"}), 404
# Validate that it's v2.x
if version.startswith('2'):
return jsonify({
"success": True,
"version": version,
"is_v2": True
results = []
for instance in instances:
if not instance.get("enabled", True):
continue
api_url = instance.get("api_url")
api_key = instance.get("api_key")
instance_name = instance.get("name", "Default")
if not api_url or not api_key:
results.append({
"name": instance_name,
"success": False,
"message": "API URL or API Key missing"
})
else:
return jsonify({
"success": False,
"message": f"Unexpected Whisparr version detected: {version}. Huntarr requires Whisparr V2.",
"is_v2": False
}), 400
# If v2 failed, check if it's v3 (Eros API)
version_url_v3 = f"{api_url.rstrip('/')}/api/v3/system/status"
response_v3 = requests.get(version_url_v3, headers=headers, timeout=10)
if response_v3.status_code == 200:
version_data = response_v3.json()
version = version_data.get("version", "Unknown")
continue
# Special case: check if it's actually a v2 API instance
# Some Whisparr v2 instances might also respond to v3 endpoint
if version and version.startswith('2'):
return jsonify({
"success": True,
"version": version,
"is_v2": True
# First try standard API endpoint
version_url = f"{api_url.rstrip('/')}/api/system/status"
headers = {"X-Api-Key": api_key}
try:
response = requests.get(version_url, headers=headers, timeout=10)
# If we get a 404, try with the v3 path
if response.status_code == 404:
whisparr_logger.debug(f"Standard API path failed for {instance_name}, trying v3 path")
v3_url = f"{api_url.rstrip('/')}/api/v3/system/status"
response = requests.get(v3_url, headers=headers, timeout=10)
if response.status_code == 200:
version_data = response.json()
version = version_data.get("version", "Unknown")
# Validate that it's a V2 version
if version and version.startswith('2'):
results.append({
"name": instance_name,
"success": True,
"version": version,
"is_v2": True
})
elif version and version.startswith('3'):
# Reject Eros API version
results.append({
"name": instance_name,
"success": False,
"message": f"Incompatible Whisparr version {version} detected. Huntarr requires Whisparr V2.",
"version": version
})
else:
# Unexpected version
results.append({
"name": instance_name,
"success": False,
"message": f"Unexpected Whisparr version {version} detected. Huntarr requires Whisparr V2.",
"version": version
})
else:
# API call failed
results.append({
"name": instance_name,
"success": False,
"message": f"Failed to get version information: HTTP {response.status_code}"
})
except requests.exceptions.RequestException as e:
results.append({
"name": instance_name,
"success": False,
"message": f"Connection error: {str(e)}"
})
# If it's v3, reject it
if version and version.startswith('3'):
return jsonify({
"success": False,
"message": f"Incompatible Whisparr version detected: {version} (Eros API). Huntarr requires Whisparr V2.",
"is_eros": True
}), 400
# If we get here, it's some other version
return jsonify({
"success": False,
"message": f"Unexpected Whisparr version: {version}. Huntarr requires Whisparr V2.",
"is_eros": False
}), 400
# If we get here, both v2 and v3 failed with non-200 status
return jsonify({
"success": False,
"message": f"Could not determine Whisparr version. V2 API: HTTP {response.status_code}, V3 API: HTTP {response_v3.status_code}."
}), 500
except requests.exceptions.RequestException as e:
error_message = f"Error fetching Whisparr version: {str(e)}"
return jsonify({"success": False, "message": error_message}), 500
return jsonify({"success": True, "results": results})
except Exception as e:
whisparr_logger.error(f"Error getting Whisparr versions: {str(e)}")
return jsonify({"success": False, "message": str(e)}), 500
@whisparr_bp.route('/logs', methods=['GET'])
def get_logs():
@@ -7,7 +7,6 @@
"enabled": true
}
],
"whisparr_version": "v3",
"hunt_missing_items": 1,
"hunt_upgrade_items": 0,
"sleep_duration": 900,