inital push

This commit is contained in:
Admin9705
2025-05-03 23:34:17 -04:00
parent da426d8358
commit dbfe5365d8
19 changed files with 1499 additions and 6 deletions

View File

@@ -0,0 +1,196 @@
/**
* Eros.js - Handles Eros settings and interactions in the Huntarr UI
*/
document.addEventListener('DOMContentLoaded', function() {
// Don't call setupErosForm here, new-main.js will call it when the tab is active
// setupErosForm();
// setupErosLogs(); // Assuming logs are handled by the main logs section
// setupClearProcessedButtons('eros'); // Assuming this is handled elsewhere or not needed immediately
});
/**
* Setup Eros settings form and connection test
* This function is now called by new-main.js when the Eros settings tab is shown.
*/
function setupErosForm() {
console.log("[eros.js] Setting up Eros form...");
const panel = document.getElementById('erosSettings');
if (!panel) {
console.warn("[eros.js] Eros settings panel not found.");
return;
}
const testErosButton = panel.querySelector('#test-eros-button');
const erosStatusIndicator = panel.querySelector('#eros-connection-status');
const erosVersionDisplay = panel.querySelector('#eros-version');
const apiUrlInput = panel.querySelector('#eros_api_url');
const apiKeyInput = panel.querySelector('#eros_api_key');
// Check if event listener is already attached (prevents duplicate handlers)
if (!testErosButton || testErosButton.dataset.listenerAttached === 'true') {
console.log("[eros.js] Test button not found or listener already attached.");
return;
}
console.log("[eros.js] Setting up Eros form listeners.");
testErosButton.dataset.listenerAttached = 'true'; // Mark as attached
// Add event listener for connection test
testErosButton.addEventListener('click', function() {
console.log("[eros.js] Testing Eros connection...");
// Basic validation
if (!apiUrlInput.value || !apiKeyInput.value) {
if (typeof huntarrUI !== 'undefined') {
huntarrUI.showNotification('Please enter both API URL and API Key for Eros', 'error');
} else {
alert('Please enter both API URL and API Key for Eros');
}
return;
}
// Disable button during test and show pending status
testErosButton.disabled = true;
if (erosStatusIndicator) {
erosStatusIndicator.className = 'connection-status pending';
erosStatusIndicator.textContent = 'Testing...';
}
// Call API to test connection
HuntarrUtils.fetchWithTimeout('/api/eros/test-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
api_url: apiUrlInput.value,
api_key: apiKeyInput.value,
api_timeout: 30
})
}, 30000) // 30 second timeout
.then(response => response.json())
.then(data => {
// Enable the button again
testErosButton.disabled = false;
if (erosStatusIndicator) {
if (data.success) {
erosStatusIndicator.className = 'connection-status success';
erosStatusIndicator.textContent = 'Connected';
if (typeof huntarrUI !== 'undefined') {
huntarrUI.showNotification('Successfully connected to Eros', 'success');
}
getErosVersion(); // Fetch version after successful connection
} else {
erosStatusIndicator.className = 'connection-status failure';
erosStatusIndicator.textContent = 'Failed';
if (typeof huntarrUI !== 'undefined') {
huntarrUI.showNotification(data.message || 'Failed to connect to Eros', 'error');
} else {
alert(data.message || 'Failed to connect to Eros');
}
}
}
})
.catch(error => {
console.error('[eros.js] Error testing connection:', error);
testErosButton.disabled = false;
if (erosStatusIndicator) {
erosStatusIndicator.className = 'connection-status failure';
erosStatusIndicator.textContent = 'Error';
}
if (typeof huntarrUI !== 'undefined') {
huntarrUI.showNotification('Error testing connection: ' + error.message, 'error');
} else {
alert('Error testing connection: ' + error.message);
}
});
});
// Initialize form state and fetch data
refreshErosStatusAndVersion();
}
/**
* Get the Eros software version from the instance.
* This is separate from the API test.
*/
function getErosVersion() {
const panel = document.getElementById('erosSettings');
if (!panel) return;
const versionDisplay = panel.querySelector('#eros-version');
if (!versionDisplay) return;
// Try to get the API settings from the form
const apiUrlInput = panel.querySelector('#eros_api_url');
const apiKeyInput = panel.querySelector('#eros_api_key');
if (!apiUrlInput || !apiUrlInput.value || !apiKeyInput || !apiKeyInput.value) {
versionDisplay.textContent = 'N/A';
return;
}
// Endpoint to get version info - using the test endpoint since it returns version
HuntarrUtils.fetchWithTimeout('/api/eros/test-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
api_url: apiUrlInput.value,
api_key: apiKeyInput.value,
api_timeout: 10
})
}, 10000)
.then(response => response.json())
.then(data => {
if (data.success && data.version) {
versionDisplay.textContent = 'v' + data.version;
} else {
versionDisplay.textContent = 'Unknown';
}
})
.catch(error => {
console.error('[eros.js] Error fetching version:', error);
versionDisplay.textContent = 'Error';
});
}
/**
* Refresh the connection status and version display for Eros.
*/
function refreshErosStatusAndVersion() {
// Try to get current connection status from the server
fetch('/api/eros/status')
.then(response => response.json())
.then(data => {
const panel = document.getElementById('erosSettings');
if (!panel) return;
const statusIndicator = panel.querySelector('#eros-connection-status');
if (statusIndicator) {
if (data.connected) {
statusIndicator.className = 'connection-status success';
statusIndicator.textContent = 'Connected';
getErosVersion(); // Try to get version if connected
} else if (data.configured) {
statusIndicator.className = 'connection-status failure';
statusIndicator.textContent = 'Not Connected';
} else {
statusIndicator.className = 'connection-status pending';
statusIndicator.textContent = 'Not Configured';
}
}
})
.catch(error => {
console.error('[eros.js] Error checking status:', error);
});
}
// Mark functions as global if needed by other parts of the application
window.setupErosForm = setupErosForm;
window.getErosVersion = getErosVersion;
window.refreshErosStatusAndVersion = refreshErosStatusAndVersion;

View File

@@ -11,6 +11,7 @@
<a href="#" class="log-option" data-app="lidarr">Lidarr</a>
<a href="#" class="log-option" data-app="readarr">Readarr</a>
<a href="#" class="log-option" data-app="whisparr">Whisparr V2</a>
<a href="#" class="log-option" data-app="eros">Eros</a>
<a href="#" class="log-option" data-app="swaparr">Swaparr</a>
</div>
</div>
@@ -25,6 +26,7 @@
<div id="lidarrApps" class="app-apps-panel app-content-panel"></div>
<div id="readarrApps" class="app-apps-panel app-content-panel"></div>
<div id="whisparrApps" class="app-apps-panel app-content-panel"></div>
<div id="erosApps" class="app-apps-panel app-content-panel"></div>
<div id="swaparrApps" class="app-apps-panel app-content-panel"></div>
<!-- Fixed banner at the bottom of the viewport -->

View File

@@ -15,6 +15,7 @@
<a href="#" class="history-option" data-app="lidarr">Lidarr</a>
<a href="#" class="history-option" data-app="readarr">Readarr</a>
<a href="#" class="history-option" data-app="whisparr">Whisparr</a>
<a href="#" class="history-option" data-app="eros">Eros</a>
</div>
</div>
</div>

View File

@@ -173,6 +173,29 @@
</div>
</div>
</div>
<!-- Eros Card -->
<div class="app-stats-card eros">
<div class="status-container">
<span id="erosHomeStatus" class="status-badge loading"><i class="fas fa-spinner fa-spin"></i> Loading...</span>
</div>
<div class="app-content">
<div class="app-icon-wrapper">
<img src="/static/arrs/48-whisparr.png" alt="Eros Logo" class="app-logo">
</div>
<h4>Eros</h4>
</div>
<div class="stats-numbers">
<div class="stat-box">
<span class="stat-number" id="eros-hunted">0</span>
<span class="stat-label">Searches Triggered</span>
</div>
<div class="stat-box">
<span class="stat-number" id="eros-upgraded">0</span>
<span class="stat-label">Upgrades Triggered</span>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -416,6 +439,12 @@
box-shadow: 0 0 15px rgba(195, 0, 230, 0.4);
}
/* Eros gold/amber icon ring */
.app-stats-card.eros .app-icon-wrapper {
border: 2px solid rgba(255, 193, 7, 0.8);
box-shadow: 0 0 15px rgba(255, 193, 7, 0.4);
}
.app-logo {
width: 40px;
height: 40px;
@@ -528,6 +557,19 @@
-webkit-text-fill-color: transparent;
}
/* App-specific accent colors for eros */
.app-stats-card.eros .app-icon-wrapper {
border-color: rgba(255, 193, 7, 0.5);
box-shadow: 0 0 15px rgba(255, 193, 7, 0.2);
}
.app-stats-card.eros .stat-number {
background: linear-gradient(to bottom right, #ffd54f, #ffb300);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Status badge colors */
.status-badge.connected {
background-color: rgba(39, 174, 96, 0.15);

View File

@@ -15,6 +15,7 @@
<a href="#" class="log-option" data-app="lidarr">Lidarr</a>
<a href="#" class="log-option" data-app="readarr">Readarr</a>
<a href="#" class="log-option" data-app="whisparr">Whisparr V2</a>
<a href="#" class="log-option" data-app="eros">Eros</a>
<a href="#" class="log-option" data-app="swaparr">Swaparr</a>
<a href="#" class="log-option" data-app="system">System</a>
</div>

View File

@@ -7,7 +7,7 @@ from src.primary.settings_manager import load_settings
logger = get_logger("app_manager")
# List of supported app types
SUPPORTED_APP_TYPES = ["sonarr", "radarr", "lidarr", "readarr", "whisparr"]
SUPPORTED_APP_TYPES = ["sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros"]
def initialize_apps():
"""Initialize all supported applications"""

View File

@@ -11,6 +11,7 @@ from src.primary.apps.lidarr_routes import lidarr_bp
from src.primary.apps.readarr_routes import readarr_bp
from src.primary.apps.whisparr_routes import whisparr_bp
from src.primary.apps.swaparr_routes import swaparr_bp
from src.primary.apps.eros_routes import eros_bp
__all__ = [
"sonarr_bp",
@@ -18,5 +19,6 @@ __all__ = [
"lidarr_bp",
"readarr_bp",
"whisparr_bp",
"swaparr_bp"
"swaparr_bp",
"eros_bp"
]

157
src/primary/apps/eros.py Normal file
View File

@@ -0,0 +1,157 @@
from flask import Blueprint, request, jsonify
import datetime, os, requests
from primary import keys_manager
from src.primary.utils.logger import get_logger
from src.primary.state import get_state_file_path
from src.primary.settings_manager import load_settings
eros_bp = Blueprint('eros', __name__)
eros_logger = get_logger("eros")
# Make sure we're using the correct state files
PROCESSED_MISSING_FILE = get_state_file_path("eros", "processed_missing")
PROCESSED_UPGRADES_FILE = get_state_file_path("eros", "processed_upgrades")
@eros_bp.route('/test-connection', methods=['POST'])
def test_connection():
"""Test connection to an Eros API instance with comprehensive diagnostics"""
data = request.json
api_url = data.get('api_url')
api_key = data.get('api_key')
api_timeout = data.get('api_timeout', 30) # Use longer timeout for connection test
if not api_url or not api_key:
return jsonify({"success": False, "message": "API URL and API Key are required"}), 400
# Log the test attempt
eros_logger.info(f"Testing connection to Eros API at {api_url}")
# First check if URL is properly formatted
if not (api_url.startswith('http://') or api_url.startswith('https://')):
error_msg = "API URL must start with http:// or https://"
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 400
# For Eros, we always use /api/v3 path
test_url = f"{api_url.rstrip('/')}/api/v3/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
eros_logger.debug(f"Eros API status code: {response.status_code}")
# Check HTTP status code
response.raise_for_status()
# Ensure the response is valid JSON
try:
response_data = response.json()
eros_logger.debug(f"Eros API response: {response_data}")
# Verify this is actually an Eros 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 Eros"
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 400
# The version number should start with 3 for Eros
if version.startswith('3'):
eros_logger.info(f"Successfully connected to Eros API version {version}")
return jsonify({
"success": True,
"message": f"Successfully connected to Eros (version {version})",
"version": version
})
elif version.startswith('2'):
error_msg = f"Connected to Whisparr V2 (version {version}). Use the Whisparr integration for V2."
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 400
else:
# Connected to some other version
error_msg = f"Connected to unknown version {version}, but Huntarr requires Eros V3"
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 400
except ValueError:
error_msg = "Invalid JSON response from API. Are you sure this is an Eros API?"
eros_logger.error(f"{error_msg}. Response: {response.text[:200]}")
return jsonify({"success": False, "message": error_msg}), 400
except requests.exceptions.Timeout:
error_msg = f"Connection timed out after {api_timeout} seconds. Check that Eros is running and accessible."
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 408
except requests.exceptions.ConnectionError:
error_msg = "Failed to connect. Check that the URL is correct and that Eros is running."
eros_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)}"
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), response.status_code
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 500
# Function to check if Eros is configured
def is_configured():
"""Check if Eros API credentials are configured"""
try:
api_keys = keys_manager.load_api_keys("eros")
instances = api_keys.get("instances", [])
for instance in instances:
if instance.get("enabled", True):
return True
return False
except Exception as e:
eros_logger.error(f"Error checking if Eros is configured: {str(e)}")
return False
# Get all valid instances from settings
def get_configured_instances():
"""Get all configured and enabled Eros instances"""
try:
api_keys = keys_manager.load_api_keys("eros")
instances = api_keys.get("instances", [])
enabled_instances = []
for instance in instances:
if not instance.get("enabled", True):
continue
api_url = instance.get("api_url")
api_key = instance.get("api_key")
if not api_url or not api_key:
continue
# 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:
eros_logger.error(f"Error getting configured Eros instances: {str(e)}")
return []

View File

@@ -0,0 +1,84 @@
"""
Eros app module for Huntarr
Contains functionality for missing items and quality upgrades in Eros
Exclusively supports the v3 API.
"""
# Module exports
from src.primary.apps.eros.missing import process_missing_items
from src.primary.apps.eros.upgrade import process_cutoff_upgrades
from src.primary.settings_manager import load_settings
from src.primary.utils.logger import get_logger
# Define logger for this module
eros_logger = get_logger("eros")
# For backward compatibility
process_missing_scenes = process_missing_items
def get_configured_instances():
"""Get all configured and enabled Eros instances"""
settings = load_settings("eros")
instances = []
eros_logger.info(f"Loaded Eros settings for instance check: {settings}")
if not settings:
eros_logger.debug("No settings found for Eros")
return instances
# Always use Eros V3 API
eros_logger.info("Using Eros API v3 exclusively")
# Check if instances are configured
if "instances" in settings and isinstance(settings["instances"], list) and settings["instances"]:
eros_logger.info(f"Found 'instances' list with {len(settings['instances'])} items. Processing...")
for idx, instance in enumerate(settings["instances"]):
eros_logger.debug(f"Checking instance #{idx}: {instance}")
# Enhanced validation
api_url = instance.get("api_url", "").strip()
api_key = instance.get("api_key", "").strip()
# Enhanced URL validation - ensure URL has proper scheme
if api_url and not (api_url.startswith('http://') or api_url.startswith('https://')):
eros_logger.warning(f"Instance '{instance.get('name', 'Unnamed')}' has URL without http(s) scheme: {api_url}")
api_url = f"http://{api_url}"
eros_logger.warning(f"Auto-correcting URL to: {api_url}")
is_enabled = instance.get("enabled", True)
# Only include properly configured instances
if is_enabled and api_url and api_key:
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
eros_logger.info(f"Adding configured Eros instance: {instance_name}")
instances.append(instance_settings)
else:
name = instance.get("name", "Unnamed")
if not is_enabled:
eros_logger.debug(f"Skipping disabled instance: {name}")
else:
eros_logger.warning(f"Skipping instance {name} due to missing API URL or API Key")
else:
eros_logger.debug("No instances array found in settings or it's empty")
eros_logger.info(f"Found {len(instances)} configured and enabled Eros instances")
return instances
__all__ = ["process_missing_items", "process_missing_scenes", "process_cutoff_upgrades", "get_configured_instances"]

View File

@@ -0,0 +1,376 @@
#!/usr/bin/env python3
"""
Eros-specific API functions
Handles all communication with the Eros API
Exclusively uses the Eros API v3
"""
import requests
import json
import time
import datetime
import traceback
import sys
from typing import List, Dict, Any, Optional, Union
from src.primary.utils.logger import get_logger
# Get logger for the Eros app
eros_logger = get_logger("eros")
# Use a session for better performance
session = requests.Session()
def arr_request(api_url: str, api_key: str, api_timeout: int, endpoint: str, method: str = "GET", data: Dict = None) -> Any:
"""
Make a request to the Eros API.
Args:
api_url: The base URL of the Eros API
api_key: The API key for authentication
api_timeout: Timeout for the API request
endpoint: The API endpoint to call
method: HTTP method (GET, POST, PUT, DELETE)
data: Optional data to send with the request
Returns:
The JSON response from the API, or None if the request failed
"""
if not api_url or not api_key:
eros_logger.error("API URL or API key is missing. Check your settings.")
return None
# Always use v3 API path
api_base = "api/v3"
eros_logger.debug(f"Using Eros 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
eros_logger.debug(f"Making {method} request to: {url}")
# Headers
headers = {
"X-Api-Key": api_key,
"Content-Type": "application/json"
}
try:
if method == "GET":
response = session.get(url, headers=headers, timeout=api_timeout)
elif method == "POST":
response = session.post(url, headers=headers, json=data, timeout=api_timeout)
elif method == "PUT":
response = session.put(url, headers=headers, json=data, timeout=api_timeout)
elif method == "DELETE":
response = session.delete(url, headers=headers, timeout=api_timeout)
else:
eros_logger.error(f"Unsupported HTTP method: {method}")
return None
# Check if the request was successful
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
eros_logger.error(f"Error during {method} request to {endpoint}: {e}, Status Code: {response.status_code}")
eros_logger.debug(f"Response content: {response.text[:200]}")
return None
# Try to parse JSON response
try:
if response.text:
result = response.json()
eros_logger.debug(f"Response from {response.url}: Status {response.status_code}, JSON parsed successfully")
return result
else:
eros_logger.debug(f"Response from {response.url}: Status {response.status_code}, Empty response")
return {}
except json.JSONDecodeError:
eros_logger.error(f"Invalid JSON response from API: {response.text[:200]}")
return None
except requests.exceptions.RequestException as e:
eros_logger.error(f"Request failed: {e}")
return None
except Exception as e:
eros_logger.error(f"Unexpected error during API request: {e}")
return None
def get_download_queue_size(api_url: str, api_key: str, api_timeout: int) -> int:
"""
Get the current size of the download queue.
Args:
api_url: The base URL of the Eros API
api_key: The API key for authentication
api_timeout: Timeout for the API request
Returns:
The number of items in the download queue, or -1 if the request failed
"""
response = arr_request(api_url, api_key, api_timeout, "queue")
if response is None:
return -1
# V3 API returns a list directly
if isinstance(response, list):
return len(response)
# Fallback to records format if needed
elif isinstance(response, dict) and "records" in response:
return len(response["records"])
else:
return -1
def get_items_with_missing(api_url: str, api_key: str, api_timeout: int, monitored_only: bool) -> List[Dict[str, Any]]:
"""
Get a list of items with missing files (not downloaded/available).
Args:
api_url: The base URL of the Eros API
api_key: The API key for authentication
api_timeout: Timeout for the API request
monitored_only: If True, only return monitored items.
Returns:
A list of item objects with missing files, or None if the request failed.
"""
try:
eros_logger.debug(f"Retrieving missing items...")
# Endpoint parameters
endpoint = "wanted/missing?pageSize=1000&sortKey=airDateUtc&sortDirection=descending"
response = arr_request(api_url, api_key, api_timeout, endpoint)
if response is None:
return None
# Extract the episodes/items
items = []
if isinstance(response, dict) and "records" in response:
items = response["records"]
elif isinstance(response, list):
items = response
# Filter monitored if needed
if monitored_only:
items = [item for item in items if item.get("monitored", False)]
eros_logger.debug(f"Found {len(items)} missing items")
return items
except Exception as e:
eros_logger.error(f"Error retrieving missing items: {str(e)}")
return None
def get_cutoff_unmet_items(api_url: str, api_key: str, api_timeout: int, monitored_only: bool) -> List[Dict[str, Any]]:
"""
Get a list of items that don't meet their quality profile cutoff.
Args:
api_url: The base URL of the Eros API
api_key: The API key for authentication
api_timeout: Timeout for the API request
monitored_only: If True, only return monitored items.
Returns:
A list of item objects that need quality upgrades, or None if the request failed.
"""
try:
eros_logger.debug(f"Retrieving cutoff unmet items...")
# Endpoint
endpoint = "wanted/cutoff?pageSize=1000&sortKey=airDateUtc&sortDirection=descending"
response = arr_request(api_url, api_key, api_timeout, endpoint)
if response is None:
return None
# Extract the episodes/items
items = []
if isinstance(response, dict) and "records" in response:
items = response["records"]
elif isinstance(response, list):
items = response
eros_logger.debug(f"Found {len(items)} cutoff unmet items")
# Just filter monitored if needed
if monitored_only:
items = [item for item in items if item.get("monitored", False)]
eros_logger.debug(f"Found {len(items)} cutoff unmet items after filtering monitored")
return items
except Exception as e:
eros_logger.error(f"Error retrieving cutoff unmet items: {str(e)}")
return None
def refresh_item(api_url: str, api_key: str, api_timeout: int, item_id: int) -> int:
"""
Refresh an item in Eros.
Args:
api_url: The base URL of the Eros API
api_key: The API key for authentication
api_timeout: Timeout for the API request
item_id: The ID of the item to refresh
Returns:
The command ID if the refresh was triggered successfully, None otherwise
"""
try:
eros_logger.debug(f"Refreshing item with ID {item_id}")
# Get episode details to find the series ID
episode_endpoint = f"episode/{item_id}"
episode_data = arr_request(api_url, api_key, api_timeout, episode_endpoint)
if episode_data and "seriesId" in episode_data:
# We have the series ID, use series refresh which is more reliable
series_id = episode_data["seriesId"]
eros_logger.debug(f"Retrieved series ID {series_id} for episode {item_id}, using series refresh")
# RefreshSeries is generally more reliable
payload = {
"name": "RefreshSeries",
"seriesId": series_id
}
else:
# Fall back to episode refresh if we can't get the series ID
eros_logger.debug(f"Could not retrieve series ID for episode {item_id}, using episode refresh")
payload = {
"name": "RefreshEpisode",
"episodeIds": [item_id]
}
# Command endpoint
command_endpoint = "command"
# Make the API request
response = arr_request(api_url, api_key, api_timeout, command_endpoint, "POST", payload)
if response and "id" in response:
command_id = response["id"]
eros_logger.debug(f"Refresh command triggered with ID {command_id}")
return command_id
else:
eros_logger.error("Failed to trigger refresh command - no command ID returned")
return None
except Exception as e:
eros_logger.error(f"Error refreshing item: {str(e)}")
return None
def item_search(api_url: str, api_key: str, api_timeout: int, item_ids: List[int]) -> int:
"""
Trigger a search for one or more items.
Args:
api_url: The base URL of the Eros API
api_key: The API key for authentication
api_timeout: Timeout for the API request
item_ids: A list of item IDs to search for
Returns:
The command ID if the search command was triggered successfully, None otherwise
"""
try:
eros_logger.debug(f"Searching for items with IDs: {item_ids}")
payload = {
"name": "EpisodeSearch",
"episodeIds": item_ids
}
# Command endpoint
command_endpoint = "command"
# Make the API request
response = arr_request(api_url, api_key, api_timeout, command_endpoint, "POST", payload)
if response and "id" in response:
command_id = response["id"]
eros_logger.debug(f"Search command triggered with ID {command_id}")
return command_id
else:
eros_logger.error("Failed to trigger search command - no command ID returned")
return None
except Exception as e:
eros_logger.error(f"Error searching for items: {str(e)}")
return None
def get_command_status(api_url: str, api_key: str, api_timeout: int, command_id: int) -> Optional[Dict]:
"""
Get the status of a specific command.
Args:
api_url: The base URL of the Eros API
api_key: The API key for authentication
api_timeout: Timeout for the API request
command_id: The ID of the command to check
Returns:
A dictionary containing the command status, or None if the request failed.
"""
if not command_id:
eros_logger.error("No command ID provided for status check.")
return None
try:
command_endpoint = f"command/{command_id}"
# Make the API request
result = arr_request(api_url, api_key, api_timeout, command_endpoint)
if result:
eros_logger.debug(f"Command {command_id} status: {result.get('status', 'unknown')}")
return result
else:
eros_logger.error(f"Failed to get command status for ID {command_id}")
return None
except Exception as e:
eros_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 Eros API.
Args:
api_url: The base URL of the Eros API
api_key: The API key for authentication
api_timeout: Timeout for the API request
Returns:
True if the connection is successful, False otherwise
"""
try:
eros_logger.debug(f"Checking connection to Eros instance at {api_url}")
endpoint = "system/status"
response = arr_request(api_url, api_key, api_timeout, endpoint)
if response is not None:
# Get the version information if available
version = response.get("version", "unknown")
# Check if this is a v3.x version
if version and version.startswith('3'):
eros_logger.info(f"Successfully connected to Eros API version: {version}")
return True
else:
eros_logger.warning(f"Connected to server but found unexpected version: {version}, expected 3.x")
return False
else:
eros_logger.error("Failed to connect to Eros API")
return False
except Exception as e:
eros_logger.error(f"Error checking connection to Eros API: {str(e)}")
return False

View File

@@ -0,0 +1,225 @@
#!/usr/bin/env python3
"""
Missing Items Processing for Eros
Handles searching for missing items in Eros
Exclusively supports the v3 API.
"""
import time
import random
import datetime
from typing import List, Dict, Any, Set, Callable
from src.primary.utils.logger import get_logger
from src.primary.apps.eros import api as eros_api
from src.primary.stats_manager import increment_stat
from src.primary.stateful_manager import is_processed, add_processed_id
from src.primary.utils.history_utils import log_processed_media
from src.primary.settings_manager import get_advanced_setting
from src.primary.state import check_state_reset
# Get logger for the app
eros_logger = get_logger("eros")
def process_missing_items(
app_settings: Dict[str, Any],
stop_check: Callable[[], bool] # Function to check if stop is requested
) -> bool:
"""
Process missing items in Eros based on provided settings.
Args:
app_settings: Dictionary containing all settings for Eros
stop_check: A function that returns True if the process should stop
Returns:
True if any items were processed, False otherwise.
"""
eros_logger.info("Starting missing items processing cycle for Eros.")
processed_any = False
# Reset state files if enough time has passed
check_state_reset("eros")
# Extract necessary settings
api_url = app_settings.get("api_url")
api_key = app_settings.get("api_key")
instance_name = app_settings.get("instance_name", "Eros Default")
api_timeout = app_settings.get("api_timeout", 90) # Default timeout
monitored_only = app_settings.get("monitored_only", True)
skip_future_releases = app_settings.get("skip_future_releases", True)
skip_item_refresh = app_settings.get("skip_item_refresh", False)
# Use the new hunt_missing_items parameter name, falling back to hunt_missing_scenes for backwards compatibility
hunt_missing_items = app_settings.get("hunt_missing_items", app_settings.get("hunt_missing_scenes", 0))
command_wait_delay = app_settings.get("command_wait_delay", 5)
command_wait_attempts = app_settings.get("command_wait_attempts", 12)
# 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 v3 API
eros_logger.info(f"Using Eros API v3 for instance: {instance_name}")
# Skip if hunt_missing_items is set to a negative value or 0
if hunt_missing_items <= 0:
eros_logger.info("'hunt_missing_items' setting is 0 or less. Skipping missing item processing.")
return False
# Check for stop signal
if stop_check():
eros_logger.info("Stop requested before starting missing items. Aborting...")
return False
# Get missing items
eros_logger.info(f"Retrieving items with missing files...")
missing_items = eros_api.get_items_with_missing(api_url, api_key, api_timeout, monitored_only)
if missing_items is None: # API call failed
eros_logger.error("Failed to retrieve missing items from Eros API.")
return False
if not missing_items:
eros_logger.info("No missing items found.")
return False
# Check for stop signal after retrieving items
if stop_check():
eros_logger.info("Stop requested after retrieving missing items. Aborting...")
return False
eros_logger.info(f"Found {len(missing_items)} items with missing files.")
# Filter out future releases if configured
if skip_future_releases:
now = datetime.datetime.now(datetime.timezone.utc)
original_count = len(missing_items)
# Eros item object has 'airDateUtc' for release dates
missing_items = [
item for item in missing_items
if not item.get('airDateUtc') or (
item.get('airDateUtc') and
datetime.datetime.fromisoformat(item['airDateUtc'].replace('Z', '+00:00')) < now
)
]
skipped_count = original_count - len(missing_items)
if skipped_count > 0:
eros_logger.info(f"Skipped {skipped_count} future item releases based on air date.")
if not missing_items:
eros_logger.info("No missing items left to process after filtering future releases.")
return False
# Filter out already processed items using stateful management
unprocessed_items = []
for item in missing_items:
item_id = str(item.get("id"))
if not is_processed("eros", instance_name, item_id):
unprocessed_items.append(item)
else:
eros_logger.debug(f"Skipping already processed item ID: {item_id}")
eros_logger.info(f"Found {len(unprocessed_items)} unprocessed items out of {len(missing_items)} total items with missing files.")
if not unprocessed_items:
eros_logger.info(f"No unprocessed items found for {instance_name}. All available items have been processed.")
return False
items_processed = 0
processing_done = False
# Select items to search based on configuration
eros_logger.info(f"Randomly selecting up to {hunt_missing_items} missing items.")
items_to_search = random.sample(unprocessed_items, min(len(unprocessed_items), hunt_missing_items))
eros_logger.info(f"Selected {len(items_to_search)} missing items to search.")
# Process selected items
for item in items_to_search:
# Check for stop signal before each item
if stop_check():
eros_logger.info("Stop requested during item processing. Aborting...")
break
# Re-check limit in case it changed
current_limit = app_settings.get("hunt_missing_items", app_settings.get("hunt_missing_scenes", 1))
if items_processed >= current_limit:
eros_logger.info(f"Reached HUNT_MISSING_ITEMS limit ({current_limit}) for this cycle.")
break
item_id = item.get("id")
title = item.get("title", "Unknown Title")
season_episode = f"S{item.get('seasonNumber', 0):02d}E{item.get('episodeNumber', 0):02d}"
eros_logger.info(f"Processing missing item: \"{title}\" - {season_episode} (Item ID: {item_id})")
# Mark the item as processed BEFORE triggering any searches
add_processed_id("eros", instance_name, str(item_id))
eros_logger.debug(f"Added item ID {item_id} to processed list for {instance_name}")
# Refresh the item information if not skipped
refresh_command_id = None
if not skip_item_refresh:
eros_logger.info(" - Refreshing item information...")
refresh_command_id = eros_api.refresh_item(api_url, api_key, api_timeout, item_id)
if refresh_command_id:
eros_logger.info(f"Triggered refresh command {refresh_command_id}. Waiting a few seconds...")
time.sleep(5) # Basic wait
else:
eros_logger.warning(f"Failed to trigger refresh command for item ID: {item_id}. Proceeding without refresh.")
else:
eros_logger.info(" - Skipping item refresh (skip_item_refresh=true)")
# Check for stop signal before searching
if stop_check():
eros_logger.info(f"Stop requested before searching for {title}. Aborting...")
break
# Search for the item
eros_logger.info(" - Searching for missing item...")
search_command_id = eros_api.item_search(api_url, api_key, api_timeout, [item_id])
if search_command_id:
eros_logger.info(f"Triggered search command {search_command_id}. Assuming success for now.")
# Log to history system
media_name = f"{title} - {season_episode}"
log_processed_media("eros", media_name, item_id, instance_name, "missing")
eros_logger.debug(f"Logged history entry for item: {media_name}")
items_processed += 1
processing_done = True
# Increment the hunted statistics for Eros
increment_stat("eros", "hunted", 1)
eros_logger.debug(f"Incremented eros hunted statistics by 1")
# Log progress
current_limit = app_settings.get("hunt_missing_items", app_settings.get("hunt_missing_scenes", 1))
eros_logger.info(f"Processed {items_processed}/{current_limit} missing items this cycle.")
else:
eros_logger.warning(f"Failed to trigger search command for item ID {item_id}.")
# Do not mark as processed if search couldn't be triggered
continue
# Log final status
if items_processed > 0:
eros_logger.info(f"Completed processing {items_processed} missing items for this cycle.")
else:
eros_logger.info("No new missing items were processed in this run.")
return processing_done
# For backward compatibility with the background processing system
def process_missing_scenes(app_settings, stop_check):
"""
Backwards compatibility function that calls process_missing_items.
Args:
app_settings: Dictionary containing all settings for Eros
stop_check: A function that returns True if the process should stop
Returns:
Result from process_missing_items
"""
return process_missing_items(app_settings, stop_check)

View File

@@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""
Quality Upgrade Processing for Eros
Handles searching for items that need quality upgrades in Eros
Exclusively supports the v3 API.
"""
import time
import random
import datetime
from typing import List, Dict, Any, Set, Callable
from src.primary.utils.logger import get_logger
from src.primary.apps.eros import api as eros_api
from src.primary.stats_manager import increment_stat
from src.primary.stateful_manager import is_processed, add_processed_id
from src.primary.utils.history_utils import log_processed_media
from src.primary.settings_manager import get_advanced_setting
from src.primary.state import check_state_reset
# Get logger for the app
eros_logger = get_logger("eros")
def process_cutoff_upgrades(
app_settings: Dict[str, Any],
stop_check: Callable[[], bool] # Function to check if stop is requested
) -> bool:
"""
Process quality cutoff upgrades for Eros based on settings.
Args:
app_settings: Dictionary containing all settings for Eros
stop_check: A function that returns True if the process should stop
Returns:
True if any items were processed for upgrades, False otherwise.
"""
eros_logger.info("Starting quality cutoff upgrades processing cycle for Eros.")
processed_any = False
# Reset state files if enough time has passed
check_state_reset("eros")
# Extract necessary settings
api_url = app_settings.get("api_url")
api_key = app_settings.get("api_key")
instance_name = app_settings.get("instance_name", "Eros Default")
api_timeout = app_settings.get("api_timeout", 90) # Default timeout
monitored_only = app_settings.get("monitored_only", True)
skip_item_refresh = app_settings.get("skip_item_refresh", False)
# Use the new hunt_upgrade_items parameter name, falling back to hunt_upgrade_scenes for backwards compatibility
hunt_upgrade_items = app_settings.get("hunt_upgrade_items", app_settings.get("hunt_upgrade_scenes", 0))
command_wait_delay = app_settings.get("command_wait_delay", 5)
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
eros_logger.info(f"Using Eros API v3 for instance: {instance_name}")
# Skip if hunt_upgrade_items is set to 0
if hunt_upgrade_items <= 0:
eros_logger.info("'hunt_upgrade_items' setting is 0 or less. Skipping quality upgrade processing.")
return False
# Check for stop signal
if stop_check():
eros_logger.info("Stop requested before starting quality upgrades. Aborting...")
return False
# Get items eligible for upgrade
eros_logger.info(f"Retrieving items eligible for cutoff upgrade...")
upgrade_eligible_data = eros_api.get_cutoff_unmet_items(api_url, api_key, api_timeout, monitored_only)
if not upgrade_eligible_data:
eros_logger.info("No items found eligible for upgrade or error retrieving them.")
return False
# Check for stop signal after retrieving eligible items
if stop_check():
eros_logger.info("Stop requested after retrieving upgrade eligible items. Aborting...")
return False
eros_logger.info(f"Found {len(upgrade_eligible_data)} items eligible for quality upgrade.")
# Filter out already processed items using stateful management
unprocessed_items = []
for item in upgrade_eligible_data:
item_id = str(item.get("id"))
if not is_processed("eros", instance_name, item_id):
unprocessed_items.append(item)
else:
eros_logger.debug(f"Skipping already processed item ID: {item_id}")
eros_logger.info(f"Found {len(unprocessed_items)} unprocessed items out of {len(upgrade_eligible_data)} total items eligible for quality upgrade.")
if not unprocessed_items:
eros_logger.info(f"No unprocessed items found for {instance_name}. All available items have been processed.")
return False
items_processed = 0
processing_done = False
# Always use random selection for upgrades
eros_logger.info(f"Randomly selecting up to {hunt_upgrade_items} items for quality upgrade.")
items_to_upgrade = random.sample(unprocessed_items, min(len(unprocessed_items), hunt_upgrade_items))
eros_logger.info(f"Selected {len(items_to_upgrade)} items for quality upgrade.")
# Process selected items
for item in items_to_upgrade:
# Check for stop signal before each item
if stop_check():
eros_logger.info("Stop requested during item processing. Aborting...")
break
# Re-check limit in case it changed
current_limit = app_settings.get("hunt_upgrade_items", app_settings.get("hunt_upgrade_scenes", 1))
if items_processed >= current_limit:
eros_logger.info(f"Reached HUNT_UPGRADE_ITEMS limit ({current_limit}) for this cycle.")
break
item_id = item.get("id")
title = item.get("title", "Unknown Title")
season_episode = f"S{item.get('seasonNumber', 0):02d}E{item.get('episodeNumber', 0):02d}"
current_quality = item.get("episodeFile", {}).get("quality", {}).get("quality", {}).get("name", "Unknown")
eros_logger.info(f"Processing item for quality upgrade: \"{title}\" - {season_episode} (Item ID: {item_id})")
eros_logger.info(f" - Current quality: {current_quality}")
# Mark the item as processed BEFORE triggering any searches
add_processed_id("eros", instance_name, str(item_id))
eros_logger.debug(f"Added item ID {item_id} to processed list for {instance_name}")
# Refresh the item information if not skipped
refresh_command_id = None
if not skip_item_refresh:
eros_logger.info(" - Refreshing item information...")
refresh_command_id = eros_api.refresh_item(api_url, api_key, api_timeout, item_id)
if refresh_command_id:
eros_logger.info(f"Triggered refresh command {refresh_command_id}. Waiting a few seconds...")
time.sleep(5) # Basic wait
else:
eros_logger.warning(f"Failed to trigger refresh command for item ID: {item_id}. Proceeding without refresh.")
else:
eros_logger.info(" - Skipping item refresh (skip_item_refresh=true)")
# Check for stop signal before searching
if stop_check():
eros_logger.info(f"Stop requested before searching for {title}. Aborting...")
break
# Search for the item
eros_logger.info(" - Searching for quality upgrade...")
search_command_id = eros_api.item_search(api_url, api_key, api_timeout, [item_id])
if search_command_id:
eros_logger.info(f"Triggered search command {search_command_id}. Assuming success for now.")
# Log to history so the upgrade appears in the history UI
series_title = item.get("series", {}).get("title", "Unknown Series")
media_name = f"{series_title} - {season_episode} - {title}"
log_processed_media("eros", media_name, item_id, instance_name, "upgrade")
eros_logger.debug(f"Logged quality upgrade to history for item ID {item_id}")
items_processed += 1
processing_done = True
# Increment the upgraded statistics for Eros
increment_stat("eros", "upgraded", 1)
eros_logger.debug(f"Incremented eros upgraded statistics by 1")
# Log progress
current_limit = app_settings.get("hunt_upgrade_items", app_settings.get("hunt_upgrade_scenes", 1))
eros_logger.info(f"Processed {items_processed}/{current_limit} items for quality upgrade this cycle.")
else:
eros_logger.warning(f"Failed to trigger search command for item ID {item_id}.")
# Do not mark as processed if search couldn't be triggered
continue
# Log final status
if items_processed > 0:
eros_logger.info(f"Completed processing {items_processed} items for quality upgrade for this cycle.")
else:
eros_logger.info("No new items were processed for quality upgrade in this run.")
return processing_done

View File

@@ -0,0 +1,194 @@
#!/usr/bin/env python3
from flask import Blueprint, request, jsonify
import datetime, os, requests
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
import socket
from urllib.parse import urlparse
from src.primary.apps.eros import api as eros_api
eros_bp = Blueprint('eros', __name__)
eros_logger = get_logger("eros")
# Make sure we're using the correct state files
PROCESSED_MISSING_FILE = get_state_file_path("eros", "processed_missing")
PROCESSED_UPGRADES_FILE = get_state_file_path("eros", "processed_upgrades")
@eros_bp.route('/status', methods=['GET'])
def get_status():
"""Get the status of all configured Eros instances"""
try:
# Get all configured instances
api_keys = keys_manager.load_api_keys("eros")
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 eros_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:
eros_logger.error(f"Error getting Eros status: {str(e)}")
return jsonify({
"configured": False,
"connected": False,
"error": str(e)
}), 500
@eros_bp.route('/test-connection', methods=['POST'])
def test_connection():
"""Test connection to an Eros API instance"""
data = request.json
api_url = data.get('api_url')
api_key = data.get('api_key')
api_timeout = data.get('api_timeout', 30) # Use longer timeout for connection test
if not api_url or not api_key:
return jsonify({"success": False, "message": "API URL and API Key are required"}), 400
eros_logger.info(f"Testing connection to Eros API at {api_url}")
# Validate URL format
if not (api_url.startswith('http://') or api_url.startswith('https://')):
error_msg = "API URL must start with http:// or https://"
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 400
# Try to establish a socket connection first to check basic connectivity
parsed_url = urlparse(api_url)
hostname = parsed_url.hostname
port = parsed_url.port or (443 if parsed_url.scheme == 'https' else 80)
try:
# Try socket connection for quick feedback on connectivity issues
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(3) # Short timeout for quick feedback
result = sock.connect_ex((hostname, port))
sock.close()
if result != 0:
error_msg = f"Connection refused - Unable to connect to {hostname}:{port}. Please check if the server is running and the port is correct."
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 404
except socket.gaierror:
error_msg = f"DNS resolution failed - Cannot resolve hostname: {hostname}. Please check your URL."
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 404
except Exception as e:
# Log the socket testing error but continue with the full request
eros_logger.debug(f"Socket test error, continuing with full request: {str(e)}")
# For Eros, we only use v3 API path
api_url = f"{api_url.rstrip('/')}/api/v3/system/status"
headers = {'X-Api-Key': api_key}
try:
# Make the request with appropriate timeouts
eros_logger.debug(f"Trying API path: {api_url}")
response = requests.get(api_url, headers=headers, timeout=(5, api_timeout))
try:
response.raise_for_status()
# Check if we got a valid JSON response
try:
response_data = response.json()
# Verify this is actually an Eros server by checking for version
version = response_data.get('version')
if not version:
error_msg = "API response doesn't contain version information. This doesn't appear to be a valid Eros server."
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 400
# Version check - should be v3.x for Eros
if version.startswith('3'):
detected_version = "v3"
eros_logger.info(f"Successfully connected to Eros API version: {version} (API {detected_version})")
# Success!
return jsonify({
"success": True,
"message": "Successfully connected to Eros API",
"version": version,
"api_version": detected_version
})
elif version.startswith('2'):
error_msg = f"Incompatible version detected: {version}. This appears to be Whisparr V2, not Eros."
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 400
else:
error_msg = f"Unexpected version {version} detected. Eros requires API v3."
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 400
except ValueError:
error_msg = "Invalid JSON response from Eros API - This doesn't appear to be a valid Eros server"
eros_logger.error(f"{error_msg}. Response content: {response.text[:200]}")
return jsonify({"success": False, "message": error_msg}), 400
except requests.exceptions.HTTPError:
# Handle specific HTTP errors
if response.status_code == 401:
error_msg = "Invalid API key - Authentication failed"
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 401
elif response.status_code == 404:
error_msg = "API endpoint not found: This doesn't appear to be a valid Eros server. Check your URL."
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 404
else:
error_msg = f"Eros server error (HTTP {response.status_code}): The Eros server is experiencing issues"
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), response.status_code
except requests.exceptions.ConnectionError as e:
# Connection error - server might be down or unreachable
error_details = str(e)
if "Connection refused" in error_details:
error_msg = f"Connection refused - Eros is not running on {api_url} or the port is incorrect"
else:
error_msg = f"Connection error - Check if Eros is running: {error_details}"
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 502
except requests.exceptions.Timeout:
error_msg = f"Connection timed out - Eros took too long to respond"
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 504
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
eros_logger.error(f"{error_msg}\n{traceback.format_exc()}")
return jsonify({"success": False, "message": error_msg}), 500
@eros_bp.route('/reset-processed', methods=['POST'])
def reset_processed_state():
"""Reset the processed state files for Eros"""
try:
# Reset the state files for missing and upgrades
reset_state_file("eros", "processed_missing")
reset_state_file("eros", "processed_upgrades")
eros_logger.info("Successfully reset Eros processed state files")
return jsonify({"success": True, "message": "Successfully reset processed state"})
except Exception as e:
error_msg = f"Error resetting Eros state: {str(e)}"
eros_logger.error(error_msg)
return jsonify({"success": False, "message": error_msg}), 500

View File

@@ -109,6 +109,13 @@ def app_specific_loop(app_type: str) -> None:
process_upgrades = getattr(upgrade_module, 'process_cutoff_upgrades')
hunt_missing_setting = "hunt_missing_items" # Updated to new name
hunt_upgrade_setting = "hunt_upgrade_items" # Updated to new name
elif app_type == "eros":
missing_module = importlib.import_module('src.primary.apps.eros.missing')
upgrade_module = importlib.import_module('src.primary.apps.eros.upgrade')
process_missing = getattr(missing_module, 'process_missing_items')
process_upgrades = getattr(upgrade_module, 'process_cutoff_upgrades')
hunt_missing_setting = "hunt_missing_items"
hunt_upgrade_setting = "hunt_upgrade_items"
else:
app_logger.error(f"Unsupported app_type: {app_type}")
return # Exit thread if app type is invalid

View File

@@ -0,0 +1,17 @@
{
"instances": [
{
"name": "Default",
"api_url": "",
"api_key": "",
"enabled": true
}
],
"hunt_missing_items": 1,
"hunt_upgrade_items": 0,
"sleep_duration": 900,
"monitored_only": true,
"skip_series_refresh": true,
"skip_future_releases": true,
"skip_scene_refresh": true
}

View File

@@ -26,7 +26,7 @@ SETTINGS_DIR.mkdir(parents=True, exist_ok=True)
DEFAULT_CONFIGS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), 'default_configs'))
# Update or add this as a class attribute or constant
KNOWN_APP_TYPES = ["sonarr", "radarr", "lidarr", "readarr", "whisparr", "general", "swaparr"]
KNOWN_APP_TYPES = ["sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros", "general", "swaparr"]
# Add a settings cache with timestamps to avoid excessive disk reads
settings_cache = {} # Format: {app_name: {'timestamp': timestamp, 'data': settings_dict}}

View File

@@ -30,7 +30,7 @@ def get_state_file_path(app_type, state_name):
The path to the state file
"""
# Define known app types
known_app_types = ["sonarr", "radarr", "lidarr", "readarr", "whisparr"]
known_app_types = ["sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros"]
# If app_type is not in known types, log a warning but don't fail
if app_type not in known_app_types and app_type != "general":

View File

@@ -28,7 +28,7 @@ except Exception as e:
stateful_logger.error(f"Error creating stateful directory: {e}")
# Create app directories
APP_TYPES = ["sonarr", "radarr", "lidarr", "readarr", "whisparr"]
APP_TYPES = ["sonarr", "radarr", "lidarr", "readarr", "whisparr", "eros"]
for app_type in APP_TYPES:
(STATEFUL_DIR / app_type).mkdir(exist_ok=True)

View File

@@ -37,7 +37,7 @@ from src.primary.auth import (
from src.primary.routes.common import common_bp
# Import blueprints for each app from the centralized blueprints module
from src.primary.apps.blueprints import sonarr_bp, radarr_bp, lidarr_bp, readarr_bp, whisparr_bp, swaparr_bp
from src.primary.apps.blueprints import sonarr_bp, radarr_bp, lidarr_bp, readarr_bp, whisparr_bp, swaparr_bp, eros_bp
# Import stateful blueprint
from src.primary.stateful_routes import stateful_api
@@ -64,6 +64,7 @@ app.register_blueprint(radarr_bp, url_prefix='/api/radarr')
app.register_blueprint(lidarr_bp, url_prefix='/api/lidarr')
app.register_blueprint(readarr_bp, url_prefix='/api/readarr')
app.register_blueprint(whisparr_bp, url_prefix='/api/whisparr')
app.register_blueprint(eros_bp, url_prefix='/api/eros')
app.register_blueprint(swaparr_bp, url_prefix='/api/swaparr')
app.register_blueprint(stateful_api, url_prefix='/api/stateful')
app.register_blueprint(history_blueprint, url_prefix='/api/history')