mirror of
https://github.com/plexguide/Huntarr.git
synced 2026-01-28 14:18:40 -06:00
update
This commit is contained in:
@@ -1342,3 +1342,73 @@ input:checked + .slider:before {
|
||||
font-weight: 600;
|
||||
background-color: rgba(var(--accent-color-rgb), 0.1);
|
||||
}
|
||||
|
||||
/* Settings Dropdown Styles - Matching log dropdown styles */
|
||||
.settings-dropdown-container {
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.settings-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.settings-dropdown-btn {
|
||||
padding: 10px 20px;
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 140px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.settings-dropdown-btn:hover {
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
.settings-dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: var(--bg-secondary);
|
||||
min-width: 180px;
|
||||
z-index: 10;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.settings-dropdown-content.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-option {
|
||||
display: block;
|
||||
padding: 10px 15px;
|
||||
text-decoration: none;
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.settings-option:hover {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.settings-option.active {
|
||||
color: var(--accent-color);
|
||||
font-weight: 600;
|
||||
background-color: rgba(var(--accent-color-rgb), 0.1);
|
||||
}
|
||||
|
||||
@@ -56,47 +56,72 @@ function setupWhisparrForm() {
|
||||
whisparrStatusIndicator.textContent = 'Testing...';
|
||||
}
|
||||
|
||||
fetch('/api/whisparr/test-connection', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
api_url: apiUrl,
|
||||
api_key: apiKey
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (whisparrStatusIndicator) {
|
||||
if (data.success) {
|
||||
whisparrStatusIndicator.className = 'connection-status success';
|
||||
whisparrStatusIndicator.textContent = 'Connected';
|
||||
if (typeof huntarrUI !== 'undefined' && huntarrUI.showNotification) {
|
||||
huntarrUI.showNotification('Successfully connected to Whisparr', 'success');
|
||||
// First check API version to ensure it's v3 (Eros)
|
||||
checkWhisparrApiVersion(apiUrl, apiKey)
|
||||
.then(isErosApi => {
|
||||
if (!isErosApi) {
|
||||
// Show error if not using Eros API
|
||||
if (whisparrStatusIndicator) {
|
||||
whisparrStatusIndicator.className = 'connection-status failure';
|
||||
whisparrStatusIndicator.textContent = 'Legacy API Detected';
|
||||
}
|
||||
getWhisparrVersion(); // Fetch version after successful connection
|
||||
} else {
|
||||
whisparrStatusIndicator.className = 'connection-status failure';
|
||||
whisparrStatusIndicator.textContent = 'Failed';
|
||||
if (typeof huntarrUI !== 'undefined' && huntarrUI.showNotification) {
|
||||
huntarrUI.showNotification('Connection to Whisparr failed: ' + data.message, 'error');
|
||||
|
||||
if (typeof huntarrUI !== 'undefined' && huntarrUI.showNotification) {
|
||||
huntarrUI.showNotification('Incompatible Whisparr version detected. Please upgrade to Whisparr Eros (v3) to use this integration.', 'error');
|
||||
}
|
||||
|
||||
testWhisparrButton.disabled = false;
|
||||
return Promise.reject('Legacy API detected');
|
||||
}
|
||||
|
||||
// If using Eros API, proceed with connection test
|
||||
return fetch('/api/whisparr/test-connection', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
api_url: apiUrl,
|
||||
api_key: apiKey
|
||||
})
|
||||
});
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (whisparrStatusIndicator) {
|
||||
if (data.success) {
|
||||
whisparrStatusIndicator.className = 'connection-status success';
|
||||
whisparrStatusIndicator.textContent = 'Connected';
|
||||
if (typeof huntarrUI !== 'undefined' && huntarrUI.showNotification) {
|
||||
huntarrUI.showNotification('Successfully connected to Whisparr Eros', 'success');
|
||||
}
|
||||
getWhisparrVersion(); // Fetch version after successful connection
|
||||
} else {
|
||||
whisparrStatusIndicator.className = 'connection-status failure';
|
||||
whisparrStatusIndicator.textContent = 'Failed';
|
||||
if (typeof huntarrUI !== 'undefined' && huntarrUI.showNotification) {
|
||||
huntarrUI.showNotification('Connection to Whisparr failed: ' + data.message, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (whisparrStatusIndicator) {
|
||||
whisparrStatusIndicator.className = 'connection-status failure';
|
||||
whisparrStatusIndicator.textContent = 'Error';
|
||||
}
|
||||
if (typeof huntarrUI !== 'undefined' && huntarrUI.showNotification) {
|
||||
huntarrUI.showNotification('Error testing Whisparr connection: ' + error, 'error');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
testWhisparrButton.disabled = false;
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
// Skip additional error notification if it's the legacy API error we already handled
|
||||
if (error !== 'Legacy API detected') {
|
||||
if (whisparrStatusIndicator) {
|
||||
whisparrStatusIndicator.className = 'connection-status failure';
|
||||
whisparrStatusIndicator.textContent = 'Error';
|
||||
}
|
||||
if (typeof huntarrUI !== 'undefined' && huntarrUI.showNotification) {
|
||||
huntarrUI.showNotification('Error testing Whisparr connection: ' + error, 'error');
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (testWhisparrButton.disabled) {
|
||||
testWhisparrButton.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Get Whisparr version if connection details are present and version display exists
|
||||
@@ -132,4 +157,39 @@ function escapeHtml(unsafe) {
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// Function to check Whisparr API version
|
||||
function checkWhisparrApiVersion(apiUrl, apiKey) {
|
||||
// Use the Eros API endpoint to check version
|
||||
return fetch(`${apiUrl}/api/v3/system/status`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Api-Key': apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
// Check if response is OK
|
||||
if (!response.ok) {
|
||||
// If we get a 404, it might be a non-Eros API
|
||||
if (response.status === 404) {
|
||||
return false;
|
||||
}
|
||||
// For other status codes, throw to trigger the catch
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Check if the response contains version info that starts with 3 (Eros)
|
||||
if (data && data.version && data.version.startsWith('3')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.catch(() => {
|
||||
// Any error means the API is not compatible
|
||||
return false;
|
||||
});
|
||||
}
|
||||
@@ -81,7 +81,13 @@ let huntarrUI = {
|
||||
this.elements.currentLogApp = document.getElementById('current-log-app'); // New: dropdown current selection text
|
||||
this.elements.logDropdownBtn = document.querySelector('.log-dropdown-btn'); // New: dropdown toggle button
|
||||
this.elements.logDropdownContent = document.querySelector('.log-dropdown-content'); // New: dropdown content
|
||||
this.elements.settingsTabs = document.querySelectorAll('.settings-tab');
|
||||
|
||||
// Settings dropdown elements
|
||||
this.elements.settingsOptions = document.querySelectorAll('.settings-option'); // New: settings dropdown options
|
||||
this.elements.currentSettingsApp = document.getElementById('current-settings-app'); // New: current settings app text
|
||||
this.elements.settingsDropdownBtn = document.querySelector('.settings-dropdown-btn'); // New: settings dropdown button
|
||||
this.elements.settingsDropdownContent = document.querySelector('.settings-dropdown-content'); // New: dropdown content
|
||||
|
||||
this.elements.appSettingsPanels = document.querySelectorAll('.app-settings-panel');
|
||||
|
||||
// Logs
|
||||
@@ -155,9 +161,24 @@ let huntarrUI = {
|
||||
});
|
||||
}
|
||||
|
||||
// Settings tabs
|
||||
this.elements.settingsTabs.forEach(tab => {
|
||||
tab.addEventListener('click', (e) => this.handleSettingsTabChange(e));
|
||||
// Settings dropdown toggle
|
||||
if (this.elements.settingsDropdownBtn) {
|
||||
this.elements.settingsDropdownBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.elements.settingsDropdownContent.classList.toggle('show');
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.settings-dropdown') && this.elements.settingsDropdownContent.classList.contains('show')) {
|
||||
this.elements.settingsDropdownContent.classList.remove('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Settings options
|
||||
this.elements.settingsOptions.forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleSettingsOptionChange(e));
|
||||
});
|
||||
|
||||
// Save settings button
|
||||
@@ -474,50 +495,37 @@ let huntarrUI = {
|
||||
this.connectToLogs(); // Reconnect to the new log source
|
||||
},
|
||||
|
||||
// Settings tab switching
|
||||
handleSettingsTabChange: function(e) {
|
||||
// Use currentTarget to ensure we get the button, not the inner element
|
||||
const targetTab = e.currentTarget;
|
||||
const app = targetTab.dataset.app; // Use dataset.app as added in SettingsForms
|
||||
|
||||
if (!app) {
|
||||
console.error("Settings tab clicked, but no data-app attribute found.");
|
||||
return; // Should not happen if HTML is correct
|
||||
}
|
||||
e.preventDefault(); // Prevent default if it was an anchor
|
||||
|
||||
// Check for unsaved changes before switching tabs
|
||||
if (this.settingsChanged) {
|
||||
if (!confirm('You have unsaved changes on the current tab. Switch tabs anyway? Changes will be lost.')) {
|
||||
return; // Stop tab switch if user cancels
|
||||
}
|
||||
// User confirmed, reset flag before switching
|
||||
this.settingsChanged = false;
|
||||
this.updateSaveResetButtonState(false);
|
||||
}
|
||||
|
||||
// Remove active class from all tabs and panels
|
||||
this.elements.settingsTabs.forEach(tab => tab.classList.remove('active'));
|
||||
// Settings option handling
|
||||
handleSettingsOptionChange: function(e) {
|
||||
e.preventDefault(); // Prevent default anchor behavior
|
||||
|
||||
const app = e.target.getAttribute('data-app');
|
||||
if (!app || app === this.currentSettingsApp) return; // Do nothing if same tab clicked
|
||||
|
||||
// Update active option
|
||||
this.elements.settingsOptions.forEach(option => {
|
||||
option.classList.remove('active');
|
||||
});
|
||||
e.target.classList.add('active');
|
||||
|
||||
// Update the current settings app text with proper capitalization
|
||||
let displayName = app.charAt(0).toUpperCase() + app.slice(1);
|
||||
this.elements.currentSettingsApp.textContent = displayName;
|
||||
|
||||
// Close the dropdown
|
||||
this.elements.settingsDropdownContent.classList.remove('show');
|
||||
|
||||
// Hide all settings panels
|
||||
this.elements.appSettingsPanels.forEach(panel => {
|
||||
panel.classList.remove('active');
|
||||
panel.style.display = 'none'; // Explicitly hide
|
||||
panel.style.display = 'none';
|
||||
});
|
||||
|
||||
// Set the target tab as active
|
||||
targetTab.classList.add('active');
|
||||
|
||||
// Show the corresponding settings panel
|
||||
const panelElement = document.getElementById(`${app}Settings`);
|
||||
if (panelElement) {
|
||||
panelElement.classList.add('active');
|
||||
panelElement.style.display = 'block'; // Explicitly show
|
||||
this.currentSettingsTab = app; // Update current tab state
|
||||
// Ensure settings are populated for this tab using the stored originalSettings
|
||||
this.populateSettingsForm(app, this.originalSettings[app] || {});
|
||||
// Reset save button state when switching tabs (already done above if confirmed)
|
||||
this.updateSaveResetButtonState(false); // Ensure it's disabled on new tab
|
||||
} else {
|
||||
console.error(`Settings panel not found for app: ${app}`);
|
||||
|
||||
// Show the selected app's settings panel
|
||||
const selectedPanel = document.getElementById(app + 'Settings');
|
||||
if (selectedPanel) {
|
||||
selectedPanel.classList.add('active');
|
||||
selectedPanel.style.display = 'block';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -748,7 +756,7 @@ let huntarrUI = {
|
||||
SettingsForms.updateDurationDisplay();
|
||||
}
|
||||
|
||||
// Load stateful management info
|
||||
// Load stateful info immediately, don't wait for loadAllSettings to complete
|
||||
this.loadStatefulInfo();
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
@@ -713,7 +713,7 @@ const SettingsForms = {
|
||||
// Create a container for instances with a scrollable area for many instances
|
||||
let instancesHtml = `
|
||||
<div class="settings-group">
|
||||
<h3>Whisparr Instances</h3>
|
||||
<h3>Whisparr Instances (Eros API v3 Only)</h3>
|
||||
<div class="instances-container">
|
||||
`;
|
||||
|
||||
@@ -774,18 +774,6 @@ const SettingsForms = {
|
||||
container.innerHTML = `
|
||||
${instancesHtml}
|
||||
|
||||
<div class="settings-group">
|
||||
<h3>API Version</h3>
|
||||
<div class="setting-item">
|
||||
<label for="whisparr_version">Whisparr Version:</label>
|
||||
<select id="whisparr_version">
|
||||
<option value="v3" ${settings.whisparr_version === 'v3' || !settings.whisparr_version ? 'selected' : ''}>v3 (Eros)</option>
|
||||
<option value="v2" ${settings.whisparr_version === 'v2' ? 'selected' : ''}>v2 (Legacy)</option>
|
||||
</select>
|
||||
<p class="setting-help">Select the API version of your Whisparr installation. Default is v3 (Eros). NOTE: Only v2 works for now!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<h3>Search Settings</h3>
|
||||
<div class="setting-item">
|
||||
@@ -1034,9 +1022,33 @@ const SettingsForms = {
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (!resetStrikesBtn) {
|
||||
console.warn('Could not find #reset_swaparr_strikes to attach listener.');
|
||||
} else {
|
||||
console.warn('huntarrUI or huntarrUI.resetStatefulManagement is not available.');
|
||||
}
|
||||
|
||||
// Add confirmation dialog for local access bypass toggle
|
||||
const localAccessBypassCheckbox = container.querySelector('#local_access_bypass');
|
||||
if (localAccessBypassCheckbox) {
|
||||
// Store original state
|
||||
const originalState = localAccessBypassCheckbox.checked;
|
||||
|
||||
localAccessBypassCheckbox.addEventListener('change', function(event) {
|
||||
const newState = this.checked;
|
||||
|
||||
// Preview the UI changes immediately, but they'll be reverted if user doesn't save
|
||||
if (typeof huntarrUI !== 'undefined' && typeof huntarrUI.updateUIForLocalAccessBypass === 'function') {
|
||||
huntarrUI.updateUIForLocalAccessBypass(newState);
|
||||
}
|
||||
// Also ensure the main app knows settings have changed if the preview runs
|
||||
if (typeof huntarrUI !== 'undefined' && typeof huntarrUI.markSettingsAsChanged === 'function') {
|
||||
huntarrUI.markSettingsAsChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Generate General settings form
|
||||
generateGeneralForm: function(container, settings = {}) {
|
||||
container.innerHTML = `
|
||||
@@ -1169,28 +1181,8 @@ const SettingsForms = {
|
||||
} else {
|
||||
console.warn('huntarrUI or huntarrUI.resetStatefulManagement is not available.');
|
||||
}
|
||||
|
||||
// Add confirmation dialog for local access bypass toggle
|
||||
const localAccessBypassCheckbox = container.querySelector('#local_access_bypass');
|
||||
if (localAccessBypassCheckbox) {
|
||||
// Store original state
|
||||
const originalState = localAccessBypassCheckbox.checked;
|
||||
|
||||
localAccessBypassCheckbox.addEventListener('change', function(event) {
|
||||
const newState = this.checked;
|
||||
|
||||
// Preview the UI changes immediately, but they'll be reverted if user doesn't save
|
||||
if (typeof huntarrUI !== 'undefined' && typeof huntarrUI.updateUIForLocalAccessBypass === 'function') {
|
||||
huntarrUI.updateUIForLocalAccessBypass(newState);
|
||||
}
|
||||
// Also ensure the main app knows settings have changed if the preview runs
|
||||
if (typeof huntarrUI !== 'undefined' && typeof huntarrUI.markSettingsAsChanged === 'function') {
|
||||
huntarrUI.markSettingsAsChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Update duration display - e.g., convert seconds to hours
|
||||
updateDurationDisplay: function() {
|
||||
// Function to update a specific sleep duration display
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
<section id="settingsSection" class="content-section">
|
||||
<div class="section-header">
|
||||
<div class="app-tabs">
|
||||
<button class="settings-tab active" data-app="general">General</button>
|
||||
<button class="settings-tab" data-app="sonarr">Sonarr</button>
|
||||
<button class="settings-tab" data-app="radarr">Radarr</button>
|
||||
<button class="settings-tab" data-app="lidarr">Lidarr</button>
|
||||
<button class="settings-tab" data-app="readarr">Readarr</button>
|
||||
<button class="settings-tab" data-app="whisparr">Whisparr</button>
|
||||
<button class="settings-tab" data-app="swaparr">Swaparr</button>
|
||||
<!-- Replace settings tabs with dropdown -->
|
||||
<div class="settings-dropdown-container">
|
||||
<div class="settings-dropdown">
|
||||
<button class="settings-dropdown-btn">
|
||||
<span id="current-settings-app">General</span>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<div class="settings-dropdown-content">
|
||||
<a href="#" class="settings-option active" data-app="general">General</a>
|
||||
<a href="#" class="settings-option" data-app="sonarr">Sonarr</a>
|
||||
<a href="#" class="settings-option" data-app="radarr">Radarr</a>
|
||||
<a href="#" class="settings-option" data-app="lidarr">Lidarr</a>
|
||||
<a href="#" class="settings-option" data-app="readarr">Readarr</a>
|
||||
<a href="#" class="settings-option" data-app="whisparr">Whisparr</a>
|
||||
<a href="#" class="settings-option" data-app="swaparr">Swaparr</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
|
||||
@@ -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 API at {api_url}")
|
||||
whisparr_logger.info(f"Testing connection to Whisparr Eros API at {api_url}")
|
||||
|
||||
# First check if URL is properly formatted
|
||||
if not (api_url.startswith('http://') or api_url.startswith('https://')):
|
||||
@@ -32,7 +32,7 @@ def test_connection():
|
||||
whisparr_logger.error(error_msg)
|
||||
return jsonify({"success": False, "message": error_msg}), 400
|
||||
|
||||
# For Whisparr, use api/v3
|
||||
# For Whisparr Eros, use api/v3
|
||||
api_base = "api/v3"
|
||||
test_url = f"{api_url.rstrip('/')}/{api_base}/system/status"
|
||||
headers = {'X-Api-Key': api_key}
|
||||
@@ -42,7 +42,7 @@ def test_connection():
|
||||
response = requests.get(test_url, headers=headers, timeout=(10, api_timeout))
|
||||
|
||||
# Log HTTP status code for diagnostic purposes
|
||||
whisparr_logger.debug(f"Whisparr API status code: {response.status_code}")
|
||||
whisparr_logger.debug(f"Whisparr Eros API status code: {response.status_code}")
|
||||
|
||||
# Check HTTP status code
|
||||
response.raise_for_status()
|
||||
@@ -51,19 +51,23 @@ def test_connection():
|
||||
try:
|
||||
response_data = response.json()
|
||||
|
||||
# We no longer save keys here since we use instances
|
||||
# keys_manager.save_api_keys("whisparr", api_url, api_key)
|
||||
# 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."
|
||||
whisparr_logger.error(error_msg)
|
||||
return jsonify({"success": False, "message": error_msg, "is_eros": False}), 400
|
||||
|
||||
whisparr_logger.info(f"Successfully connected to Whisparr API version: {response_data.get('version', 'unknown')}")
|
||||
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 API",
|
||||
"version": response_data.get('version', 'unknown')
|
||||
"message": "Successfully connected to Whisparr Eros API",
|
||||
"version": response_data.get('version', 'unknown'),
|
||||
"is_eros": True
|
||||
})
|
||||
except ValueError:
|
||||
error_msg = "Invalid JSON response from Whisparr API"
|
||||
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
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
Whisparr app module for Huntarr
|
||||
Contains functionality for missing items and quality upgrades in Whisparr
|
||||
|
||||
Supports both v2 (legacy) and v3 (Eros) API versions.
|
||||
v2 - Original Whisparr API
|
||||
v3 - Eros version of the Whisparr API
|
||||
Exclusively supports the v3 Eros API.
|
||||
"""
|
||||
|
||||
# Module exports
|
||||
@@ -29,9 +27,8 @@ def get_configured_instances():
|
||||
whisparr_logger.debug("No settings found for Whisparr")
|
||||
return instances
|
||||
|
||||
# Get the API version to use (v2 or v3/Eros)
|
||||
api_version = settings.get("whisparr_version", "v3")
|
||||
whisparr_logger.info(f"Using Whisparr API version: {api_version}")
|
||||
# Always use Eros API v3
|
||||
whisparr_logger.info("Using Whisparr Eros API v3 exclusively")
|
||||
|
||||
# Check if instances are configured
|
||||
if "instances" in settings and isinstance(settings["instances"], list) and settings["instances"]:
|
||||
@@ -56,8 +53,7 @@ def get_configured_instances():
|
||||
instance_data = {
|
||||
"instance_name": instance.get("name", "Default"),
|
||||
"api_url": api_url,
|
||||
"api_key": api_key,
|
||||
"api_version": api_version # Add the API version to the instance data
|
||||
"api_key": api_key
|
||||
}
|
||||
instances.append(instance_data)
|
||||
whisparr_logger.info(f"Added valid instance: {instance_data}")
|
||||
@@ -83,8 +79,7 @@ def get_configured_instances():
|
||||
instance_data = {
|
||||
"instance_name": "Default",
|
||||
"api_url": api_url,
|
||||
"api_key": api_key,
|
||||
"api_version": api_version # Add the API version to the instance data
|
||||
"api_key": api_key
|
||||
}
|
||||
instances.append(instance_data)
|
||||
whisparr_logger.info(f"Added valid legacy instance: {instance_data}")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Whisparr-specific API functions
|
||||
Handles all communication with the Whisparr API
|
||||
|
||||
Supports both v2 (legacy) and v3 (Eros) API versions
|
||||
Exclusively uses the Eros API v3
|
||||
"""
|
||||
|
||||
import requests
|
||||
@@ -21,9 +21,9 @@ whisparr_logger = get_logger("whisparr")
|
||||
# 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, api_version: str = "v3") -> Any:
|
||||
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 Whisparr API.
|
||||
Make a request to the Whisparr Eros API.
|
||||
|
||||
Args:
|
||||
api_url: The base URL of the Whisparr API
|
||||
@@ -32,7 +32,6 @@ def arr_request(api_url: str, api_key: str, api_timeout: int, endpoint: str, met
|
||||
endpoint: The API endpoint to call
|
||||
method: HTTP method (GET, POST, PUT, DELETE)
|
||||
data: Optional data to send with the request
|
||||
api_version: API version to use ("v2" or "v3")
|
||||
|
||||
Returns:
|
||||
The JSON response from the API, or None if the request failed
|
||||
@@ -41,10 +40,9 @@ 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
|
||||
|
||||
# IMPORTANT: Whisparr 2.x uses v3 API endpoints even though it's labeled as v2 in our settings
|
||||
# Always use v3 for API path
|
||||
api_base = f"api/v3"
|
||||
whisparr_logger.debug(f"Using Whisparr API base path: {api_base}")
|
||||
# Always use v3 for Eros API
|
||||
api_base = "api/v3"
|
||||
whisparr_logger.debug(f"Using Whisparr Eros API: {api_base}")
|
||||
|
||||
# Full URL - ensure no double slashes
|
||||
url = f"{api_url.rstrip('/')}/{api_base}/{endpoint.lstrip('/')}"
|
||||
@@ -92,7 +90,7 @@ def arr_request(api_url: str, api_key: str, api_timeout: int, endpoint: str, met
|
||||
whisparr_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, api_version: str = "v3") -> int:
|
||||
def get_download_queue_size(api_url: str, api_key: str, api_timeout: int) -> int:
|
||||
"""
|
||||
Get the current size of the download queue.
|
||||
|
||||
@@ -100,12 +98,11 @@ def get_download_queue_size(api_url: str, api_key: str, api_timeout: int, api_ve
|
||||
api_url: The base URL of the Whisparr API
|
||||
api_key: The API key for authentication
|
||||
api_timeout: Timeout for the API request
|
||||
api_version: API version to use ("v2" or "v3")
|
||||
|
||||
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", api_version=api_version)
|
||||
response = arr_request(api_url, api_key, api_timeout, "queue")
|
||||
|
||||
if response is None:
|
||||
return -1
|
||||
@@ -118,7 +115,7 @@ def get_download_queue_size(api_url: str, api_key: str, api_timeout: int, api_ve
|
||||
else:
|
||||
return -1
|
||||
|
||||
def get_items_with_missing(api_url: str, api_key: str, api_timeout: int, monitored_only: bool, api_version: str = "v3") -> List[Dict[str, Any]]:
|
||||
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).
|
||||
|
||||
@@ -127,7 +124,6 @@ def get_items_with_missing(api_url: str, api_key: str, api_timeout: int, monitor
|
||||
api_key: The API key for authentication
|
||||
api_timeout: Timeout for the API request
|
||||
monitored_only: If True, only return monitored items.
|
||||
api_version: API version to use ("v2" or "v3")
|
||||
|
||||
Returns:
|
||||
A list of item objects with missing files, or None if the request failed.
|
||||
@@ -138,7 +134,7 @@ def get_items_with_missing(api_url: str, api_key: str, api_timeout: int, monitor
|
||||
# Endpoint parameters - always use v3 format since we're using v3 API
|
||||
endpoint = "wanted/missing?pageSize=1000&sortKey=airDateUtc&sortDirection=descending"
|
||||
|
||||
response = arr_request(api_url, api_key, api_timeout, endpoint, api_version=api_version)
|
||||
response = arr_request(api_url, api_key, api_timeout, endpoint)
|
||||
|
||||
if response is None:
|
||||
return None
|
||||
@@ -159,7 +155,7 @@ def get_items_with_missing(api_url: str, api_key: str, api_timeout: int, monitor
|
||||
whisparr_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, api_version: str = "v3") -> List[Dict[str, Any]]:
|
||||
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.
|
||||
|
||||
@@ -168,7 +164,6 @@ def get_cutoff_unmet_items(api_url: str, api_key: str, api_timeout: int, monitor
|
||||
api_key: The API key for authentication
|
||||
api_timeout: Timeout for the API request
|
||||
monitored_only: If True, only return monitored items.
|
||||
api_version: API version to use ("v2" or "v3")
|
||||
|
||||
Returns:
|
||||
A list of item objects that need quality upgrades, or None if the request failed.
|
||||
@@ -179,7 +174,7 @@ def get_cutoff_unmet_items(api_url: str, api_key: str, api_timeout: int, monitor
|
||||
# Endpoint - always use v3 format
|
||||
endpoint = "wanted/cutoff?pageSize=1000&sortKey=airDateUtc&sortDirection=descending"
|
||||
|
||||
response = arr_request(api_url, api_key, api_timeout, endpoint, api_version=api_version)
|
||||
response = arr_request(api_url, api_key, api_timeout, endpoint)
|
||||
|
||||
if response is None:
|
||||
return None
|
||||
@@ -202,7 +197,7 @@ def get_cutoff_unmet_items(api_url: str, api_key: str, api_timeout: int, monitor
|
||||
whisparr_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, api_version: str = "v3") -> int:
|
||||
def refresh_item(api_url: str, api_key: str, api_timeout: int, item_id: int) -> int:
|
||||
"""
|
||||
Refresh an item in Whisparr.
|
||||
|
||||
@@ -211,7 +206,6 @@ def refresh_item(api_url: str, api_key: str, api_timeout: int, item_id: int, api
|
||||
api_key: The API key for authentication
|
||||
api_timeout: Timeout for the API request
|
||||
item_id: The ID of the item to refresh
|
||||
api_version: API version to use ("v2" or "v3")
|
||||
|
||||
Returns:
|
||||
The command ID if the refresh was triggered successfully, None otherwise
|
||||
@@ -223,7 +217,7 @@ def refresh_item(api_url: str, api_key: str, api_timeout: int, item_id: int, api
|
||||
# Use series refresh instead if we can get the series ID from the episode
|
||||
# First, attempt to get the episode details
|
||||
episode_endpoint = f"episode/{item_id}"
|
||||
episode_data = arr_request(api_url, api_key, api_timeout, episode_endpoint, api_version=api_version)
|
||||
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
|
||||
@@ -243,7 +237,7 @@ def refresh_item(api_url: str, api_key: str, api_timeout: int, item_id: int, api
|
||||
"episodeIds": [item_id]
|
||||
}
|
||||
|
||||
response = arr_request(api_url, api_key, api_timeout, "command", method="POST", data=payload, api_version=api_version)
|
||||
response = arr_request(api_url, api_key, api_timeout, "command", method="POST", data=payload)
|
||||
|
||||
if response and "id" in response:
|
||||
command_id = response["id"]
|
||||
@@ -257,7 +251,7 @@ def refresh_item(api_url: str, api_key: str, api_timeout: int, item_id: int, api
|
||||
whisparr_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], api_version: str = "v3") -> int:
|
||||
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.
|
||||
|
||||
@@ -266,7 +260,6 @@ def item_search(api_url: str, api_key: str, api_timeout: int, item_ids: List[int
|
||||
api_key: The API key for authentication
|
||||
api_timeout: Timeout for the API request
|
||||
item_ids: A list of item IDs to search for
|
||||
api_version: API version to use ("v2" or "v3")
|
||||
|
||||
Returns:
|
||||
The command ID if the search command was triggered successfully, None otherwise
|
||||
@@ -280,7 +273,7 @@ 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, api_version=api_version)
|
||||
response = arr_request(api_url, api_key, api_timeout, "command", method="POST", data=payload)
|
||||
|
||||
if response and "id" in response:
|
||||
command_id = response["id"]
|
||||
@@ -294,7 +287,7 @@ def item_search(api_url: str, api_key: str, api_timeout: int, item_ids: List[int
|
||||
whisparr_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, api_version: str = "v3") -> Optional[Dict]:
|
||||
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.
|
||||
|
||||
@@ -303,7 +296,6 @@ def get_command_status(api_url: str, api_key: str, api_timeout: int, command_id:
|
||||
api_key: The API key for authentication
|
||||
api_timeout: Timeout for the API request
|
||||
command_id: The ID of the command to check
|
||||
api_version: API version to use ("v2" or "v3")
|
||||
|
||||
Returns:
|
||||
A dictionary containing the command status, or None if the request failed.
|
||||
@@ -314,7 +306,7 @@ def get_command_status(api_url: str, api_key: str, api_timeout: int, command_id:
|
||||
|
||||
try:
|
||||
endpoint = f"command/{command_id}"
|
||||
result = arr_request(api_url, api_key, api_timeout, endpoint, api_version=api_version)
|
||||
result = arr_request(api_url, api_key, api_timeout, endpoint)
|
||||
|
||||
if result:
|
||||
whisparr_logger.debug(f"Command {command_id} status: {result.get('status', 'unknown')}")
|
||||
@@ -326,7 +318,7 @@ def get_command_status(api_url: str, api_key: str, api_timeout: int, command_id:
|
||||
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, api_version: str = "v3") -> bool:
|
||||
def check_connection(api_url: str, api_key: str, api_timeout: int) -> bool:
|
||||
"""
|
||||
Check the connection to Whisparr API.
|
||||
|
||||
@@ -334,14 +326,13 @@ def check_connection(api_url: str, api_key: str, api_timeout: int, api_version:
|
||||
api_url: The base URL of the Whisparr API
|
||||
api_key: The API key for authentication
|
||||
api_timeout: Timeout for the API request
|
||||
api_version: API version to use ("v2" or "v3")
|
||||
|
||||
Returns:
|
||||
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", api_version=api_version)
|
||||
response = arr_request(api_url, api_key, api_timeout, "system/status")
|
||||
|
||||
if response is not None:
|
||||
# Get the version information if available
|
||||
|
||||
@@ -51,9 +51,8 @@ def process_missing_items(
|
||||
command_wait_attempts = app_settings.get("command_wait_attempts", 12)
|
||||
state_reset_interval_hours = app_settings.get("state_reset_interval_hours", 168)
|
||||
|
||||
# Get the API version to use (v2 or v3)
|
||||
api_version = app_settings.get("whisparr_version", "v3")
|
||||
whisparr_logger.info(f"Using Whisparr API version: {api_version}")
|
||||
# Log that we're using Eros API v3
|
||||
whisparr_logger.info(f"Using Whisparr Eros API v3 for instance: {instance_name}")
|
||||
|
||||
# Skip if hunt_missing_items is set to 0
|
||||
if hunt_missing_items <= 0:
|
||||
@@ -67,7 +66,7 @@ def process_missing_items(
|
||||
|
||||
# Get missing items
|
||||
whisparr_logger.info(f"Retrieving items with missing files...")
|
||||
missing_items = whisparr_api.get_items_with_missing(api_url, api_key, api_timeout, monitored_only, api_version)
|
||||
missing_items = whisparr_api.get_items_with_missing(api_url, api_key, api_timeout, monitored_only)
|
||||
|
||||
if missing_items is None: # API call failed
|
||||
whisparr_logger.error("Failed to retrieve missing items from Whisparr API.")
|
||||
@@ -151,7 +150,7 @@ def process_missing_items(
|
||||
refresh_command_id = None
|
||||
if not skip_item_refresh:
|
||||
whisparr_logger.info(" - Refreshing item information...")
|
||||
refresh_command_id = whisparr_api.refresh_item(api_url, api_key, api_timeout, item_id, api_version)
|
||||
refresh_command_id = whisparr_api.refresh_item(api_url, api_key, api_timeout, item_id)
|
||||
if refresh_command_id:
|
||||
whisparr_logger.info(f"Triggered refresh command {refresh_command_id}. Waiting a few seconds...")
|
||||
time.sleep(5) # Basic wait
|
||||
@@ -167,7 +166,7 @@ def process_missing_items(
|
||||
|
||||
# Search for the item
|
||||
whisparr_logger.info(" - Searching for missing item...")
|
||||
search_command_id = whisparr_api.item_search(api_url, api_key, api_timeout, [item_id], api_version)
|
||||
search_command_id = whisparr_api.item_search(api_url, api_key, api_timeout, [item_id])
|
||||
if search_command_id:
|
||||
whisparr_logger.info(f"Triggered search command {search_command_id}. Assuming success for now.")
|
||||
|
||||
|
||||
@@ -50,9 +50,8 @@ def process_cutoff_upgrades(
|
||||
command_wait_attempts = app_settings.get("command_wait_attempts", 12)
|
||||
state_reset_interval_hours = app_settings.get("state_reset_interval_hours", 168)
|
||||
|
||||
# Get the API version to use (v2 or v3)
|
||||
api_version = app_settings.get("whisparr_version", "v3")
|
||||
whisparr_logger.info(f"Using Whisparr API version: {api_version}")
|
||||
# Log that we're using Eros API v3
|
||||
whisparr_logger.info(f"Using Whisparr Eros API v3 for instance: {instance_name}")
|
||||
|
||||
# Skip if hunt_upgrade_items is set to 0
|
||||
if hunt_upgrade_items <= 0:
|
||||
@@ -66,7 +65,7 @@ def process_cutoff_upgrades(
|
||||
|
||||
# Get items eligible for upgrade
|
||||
whisparr_logger.info(f"Retrieving items eligible for cutoff upgrade...")
|
||||
upgrade_eligible_data = whisparr_api.get_cutoff_unmet_items(api_url, api_key, api_timeout, monitored_only, api_version)
|
||||
upgrade_eligible_data = whisparr_api.get_cutoff_unmet_items(api_url, api_key, api_timeout, monitored_only)
|
||||
|
||||
if not upgrade_eligible_data:
|
||||
whisparr_logger.info("No items found eligible for upgrade or error retrieving them.")
|
||||
@@ -129,7 +128,7 @@ def process_cutoff_upgrades(
|
||||
refresh_command_id = None
|
||||
if not skip_item_refresh:
|
||||
whisparr_logger.info(" - Refreshing item information...")
|
||||
refresh_command_id = whisparr_api.refresh_item(api_url, api_key, api_timeout, item_id, api_version)
|
||||
refresh_command_id = whisparr_api.refresh_item(api_url, api_key, api_timeout, item_id)
|
||||
if refresh_command_id:
|
||||
whisparr_logger.info(f"Triggered refresh command {refresh_command_id}. Waiting a few seconds...")
|
||||
time.sleep(5) # Basic wait
|
||||
@@ -145,7 +144,7 @@ def process_cutoff_upgrades(
|
||||
|
||||
# Search for the item
|
||||
whisparr_logger.info(" - Searching for quality upgrade...")
|
||||
search_command_id = whisparr_api.item_search(api_url, api_key, api_timeout, [item_id], api_version)
|
||||
search_command_id = whisparr_api.item_search(api_url, api_key, api_timeout, [item_id])
|
||||
if search_command_id:
|
||||
whisparr_logger.info(f"Triggered search command {search_command_id}. Assuming success for now.")
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ 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 Eros API at {api_url}")
|
||||
|
||||
# Always use v3 API endpoint for Whisparr
|
||||
# Use v3 API endpoint for Whisparr Eros
|
||||
url = f"{api_url}/api/v3/system/status"
|
||||
headers = {
|
||||
"X-Api-Key": api_key,
|
||||
@@ -40,12 +40,20 @@ def test_connection():
|
||||
try:
|
||||
response_data = response.json()
|
||||
version = response_data.get('version', 'unknown')
|
||||
whisparr_logger.info(f"Successfully connected to Whisparr API version: {version}")
|
||||
|
||||
# Validate that this is actually Eros API v3
|
||||
if not version.startswith('3'):
|
||||
error_msg = f"Whisparr version {version} detected. Huntarr requires Eros API v3."
|
||||
whisparr_logger.error(error_msg)
|
||||
return jsonify({"success": False, "message": error_msg, "is_eros": False}), 400
|
||||
|
||||
whisparr_logger.info(f"Successfully connected to Whisparr Eros API version: {version}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Successfully connected to Whisparr API",
|
||||
"version": version
|
||||
"message": "Successfully connected to Whisparr Eros API",
|
||||
"version": version,
|
||||
"is_eros": True
|
||||
})
|
||||
except ValueError:
|
||||
error_msg = "Invalid JSON response from Whisparr API"
|
||||
@@ -65,13 +73,13 @@ def is_configured():
|
||||
|
||||
@whisparr_bp.route('/get-versions', methods=['GET'])
|
||||
def get_versions():
|
||||
"""Get the version information from the Whisparr API"""
|
||||
"""Get the version information from the Whisparr Eros 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
|
||||
return jsonify({"success": False, "message": "Whisparr Eros API is not configured"}), 400
|
||||
|
||||
headers = {'X-Api-Key': api_key}
|
||||
version_url = f"{api_url.rstrip('/')}/api/v3/system/status"
|
||||
@@ -83,9 +91,21 @@ def get_versions():
|
||||
version_data = response.json()
|
||||
version = version_data.get("version", "Unknown")
|
||||
|
||||
return jsonify({"success": True, "version": version})
|
||||
# Validate that it's Eros API v3
|
||||
if not version.startswith('3'):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": f"Incompatible Whisparr version detected: {version}. Huntarr requires Eros API v3.",
|
||||
"is_eros": False
|
||||
}), 400
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"version": version,
|
||||
"is_eros": True
|
||||
})
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_message = f"Error fetching Whisparr version: {str(e)}"
|
||||
error_message = f"Error fetching Whisparr Eros version: {str(e)}"
|
||||
return jsonify({"success": False, "message": error_message}), 500
|
||||
|
||||
@whisparr_bp.route('/logs', methods=['GET'])
|
||||
|
||||
Reference in New Issue
Block a user