mirror of
https://github.com/plexguide/Huntarr.git
synced 2026-05-07 16:49:25 -05:00
updates
This commit is contained in:
+96
-113
@@ -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 []
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user