Skip to content
Merged

Dev #65

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<h2 align="center">Huntarr [Sonarr Edition] - Hunt Missing and Upgrade Episodes</h2>
<h2 align="center">Huntarr - Find Missing & Upgrade Media Items</h2>

<p align="center">
<img src="https://github.com/plexguide/Huntarr-Sonarr/blob/main/logo/128.png?raw=true" alt="Huntarr Logo" width="100" height="100">
Expand All @@ -13,6 +13,14 @@
<td colspan="2"><img src="https://github.com/user-attachments/assets/34264f2e-928d-44e5-adb7-0dbd08fadfd0" width="100%"/></td>
</tr>
</table>

**NOTE**: Working to Intergrate Apps into UI and Drop Extra Variables.

* Sonarr [Good]
* Radarr [Not Incorporated Yet]
* Lidarr [Not Incorporated Yet]
* Readarr [ Not Incorporated Yet]


**NOTE**: This utilizes Sonarr API Version - `5`. Legacy name of this program: Sonarr Hunter.

Expand Down
85 changes: 77 additions & 8 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,27 @@
import sys
import os
import socket
import signal
import importlib
from utils.logger import logger
from config import HUNT_MODE, SLEEP_DURATION, MINIMUM_DOWNLOAD_QUEUE_SIZE, ENABLE_WEB_UI, log_configuration, refresh_settings
from missing import process_missing_episodes
from upgrade import process_cutoff_upgrades
from state import check_state_reset, calculate_reset_time
from api import get_download_queue_size

# Flag to indicate if cycle should restart
restart_cycle = False

def signal_handler(signum, frame):
"""Handle signals from the web UI for cycle restart"""
global restart_cycle
if signum == signal.SIGUSR1:
logger.warning("⚠️ Received restart signal from web UI. Immediately aborting current operations... ⚠️")
restart_cycle = True

# Register signal handler for SIGUSR1
signal.signal(signal.SIGUSR1, signal_handler)

def get_ip_address():
"""Get the host's IP address from API_URL for display"""
try:
Expand All @@ -39,8 +53,35 @@ def get_ip_address():
except:
return "YOUR_SERVER_IP"

def force_reload_all_modules():
"""Force reload of all relevant modules to ensure fresh settings"""
try:
# Force reload the config module
import config
importlib.reload(config)

# Reload any modules that might cache config values
import missing
importlib.reload(missing)

import upgrade
importlib.reload(upgrade)

# Call the refresh function to ensure settings are updated
config.refresh_settings()

# Log the reloaded settings for verification
logger.warning("⚠️ Settings reloaded from JSON file after restart signal ⚠️")
config.log_configuration(logger)

return True
except Exception as e:
logger.error(f"Error reloading modules: {e}")
return False

def main_loop() -> None:
"""Main processing loop for Huntarr-Sonarr"""
global restart_cycle

# Log welcome message for web interface
logger.info("=== Huntarr [Sonarr Edition] Starting ===")
Expand All @@ -53,8 +94,15 @@ def main_loop() -> None:
logger.info("GitHub: https://github.com/plexguide/huntarr-sonarr")

while True:
# Refresh settings from the settings manager before each cycle
refresh_settings()
# Set restart_cycle flag to False at the beginning of each cycle
restart_cycle = False

# Always force reload all modules at the start of each cycle
force_reload_all_modules()

# Import after reload to ensure we get fresh values
from config import HUNT_MODE, HUNT_MISSING_SHOWS, HUNT_UPGRADE_EPISODES
from upgrade import process_cutoff_upgrades

# Check if state files need to be reset
check_state_reset()
Expand All @@ -69,13 +117,29 @@ def main_loop() -> None:
if MINIMUM_DOWNLOAD_QUEUE_SIZE < 0 or (MINIMUM_DOWNLOAD_QUEUE_SIZE >= 0 and download_queue_size <= MINIMUM_DOWNLOAD_QUEUE_SIZE):

# Process shows/episodes based on HUNT_MODE
if HUNT_MODE in ["missing", "both"]:
if restart_cycle:
logger.warning("⚠️ Restarting cycle due to settings change... ⚠️")
continue

if HUNT_MODE in ["missing", "both"] and HUNT_MISSING_SHOWS > 0:
if process_missing_episodes():
processing_done = True

# Check if restart signal received
if restart_cycle:
logger.warning("⚠️ Restarting cycle due to settings change... ⚠️")
continue

if HUNT_MODE in ["upgrade", "both"]:
if HUNT_MODE in ["upgrade", "both"] and HUNT_UPGRADE_EPISODES > 0:
logger.info(f"Starting upgrade process with HUNT_UPGRADE_EPISODES={HUNT_UPGRADE_EPISODES}")

if process_cutoff_upgrades():
processing_done = True

# Check if restart signal received
if restart_cycle:
logger.warning("⚠️ Restarting cycle due to settings change... ⚠️")
continue

else:
logger.info(f"Download queue size ({download_queue_size}) is above the minimum threshold ({MINIMUM_DOWNLOAD_QUEUE_SIZE}). Skipped processing.")
Expand All @@ -101,14 +165,19 @@ def main_loop() -> None:
sleep_start = time.time()
sleep_end = sleep_start + CURRENT_SLEEP_DURATION

while time.time() < sleep_end:
# Sleep in smaller chunks for more responsive shutdown
time.sleep(min(10, sleep_end - time.time()))
while time.time() < sleep_end and not restart_cycle:
# Sleep in smaller chunks for more responsive shutdown and restart
time.sleep(min(1, sleep_end - time.time()))

# Every minute, log the remaining sleep time for web interface visibility
if int((time.time() - sleep_start) % 60) == 0 and time.time() < sleep_end - 10:
remaining = int(sleep_end - time.time())
logger.debug(f"Sleeping... {remaining}s remaining until next cycle")

# Check if restart signal received
if restart_cycle:
logger.warning("⚠️ Sleep interrupted due to settings change. Restarting cycle immediately... ⚠️")
break

if __name__ == "__main__":
# Log configuration settings
Expand Down
17 changes: 17 additions & 0 deletions static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,23 @@ input:checked + .toggle-slider:before {
transform: translateX(26px);
}

/* Disabled button styles */
.disabled-button {
background-color: #cccccc !important;
color: #666666 !important;
cursor: not-allowed !important;
opacity: 0.7;
}

/* Adjust the existing save button styles to ensure they work with the disabled state */
.save-button:hover:not(.disabled-button) {
background-color: var(--save-button-hover);
}

.reset-button:hover:not(.disabled-button) {
background-color: var(--reset-button-hover);
}

@media (max-width: 768px) {
.header {
flex-direction: column;
Expand Down
92 changes: 89 additions & 3 deletions static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ document.addEventListener('DOMContentLoaded', function() {
const saveSettingsBottomButton = document.getElementById('saveSettingsBottom');
const resetSettingsBottomButton = document.getElementById('resetSettingsBottom');

// Store original settings values
let originalSettings = {};

// Update sleep duration display
function updateSleepDurationDisplay() {
const seconds = parseInt(sleepDurationInput.value) || 900;
Expand All @@ -59,7 +62,10 @@ document.addEventListener('DOMContentLoaded', function() {
sleepDurationHoursSpan.textContent = displayText;
}

sleepDurationInput.addEventListener('input', updateSleepDurationDisplay);
sleepDurationInput.addEventListener('input', function() {
updateSleepDurationDisplay();
checkForChanges();
});

// Theme management
function loadTheme() {
Expand Down Expand Up @@ -126,6 +132,58 @@ document.addEventListener('DOMContentLoaded', function() {
}
}

// Function to check if settings have changed from original values
function checkForChanges() {
if (!originalSettings.huntarr) return; // Don't check if original settings not loaded

let hasChanges = false;

// Check Basic Settings
if (parseInt(huntMissingShowsInput.value) !== originalSettings.huntarr.hunt_missing_shows) hasChanges = true;
if (parseInt(huntUpgradeEpisodesInput.value) !== originalSettings.huntarr.hunt_upgrade_episodes) hasChanges = true;
if (parseInt(sleepDurationInput.value) !== originalSettings.huntarr.sleep_duration) hasChanges = true;
if (parseInt(stateResetIntervalInput.value) !== originalSettings.huntarr.state_reset_interval_hours) hasChanges = true;
if (monitoredOnlyInput.checked !== originalSettings.huntarr.monitored_only) hasChanges = true;
if (skipFutureEpisodesInput.checked !== originalSettings.huntarr.skip_future_episodes) hasChanges = true;
if (skipSeriesRefreshInput.checked !== originalSettings.huntarr.skip_series_refresh) hasChanges = true;

// Check Advanced Settings
if (parseInt(apiTimeoutInput.value) !== originalSettings.advanced.api_timeout) hasChanges = true;
if (debugModeInput.checked !== originalSettings.advanced.debug_mode) hasChanges = true;
if (parseInt(commandWaitDelayInput.value) !== originalSettings.advanced.command_wait_delay) hasChanges = true;
if (parseInt(commandWaitAttemptsInput.value) !== originalSettings.advanced.command_wait_attempts) hasChanges = true;
if (parseInt(minimumDownloadQueueSizeInput.value) !== originalSettings.advanced.minimum_download_queue_size) hasChanges = true;
if (randomMissingInput.checked !== originalSettings.advanced.random_missing) hasChanges = true;
if (randomUpgradesInput.checked !== originalSettings.advanced.random_upgrades) hasChanges = true;

// Enable/disable save buttons based on whether there are changes
saveSettingsButton.disabled = !hasChanges;
saveSettingsBottomButton.disabled = !hasChanges;

// Apply visual indicator based on disabled state
if (hasChanges) {
saveSettingsButton.classList.remove('disabled-button');
saveSettingsBottomButton.classList.remove('disabled-button');
} else {
saveSettingsButton.classList.add('disabled-button');
saveSettingsBottomButton.classList.add('disabled-button');
}

return hasChanges;
}

// Add change event listeners to all form elements
[huntMissingShowsInput, huntUpgradeEpisodesInput, stateResetIntervalInput,
apiTimeoutInput, commandWaitDelayInput, commandWaitAttemptsInput,
minimumDownloadQueueSizeInput].forEach(input => {
input.addEventListener('input', checkForChanges);
});

[monitoredOnlyInput, randomMissingInput, randomUpgradesInput,
skipFutureEpisodesInput, skipSeriesRefreshInput, debugModeInput].forEach(checkbox => {
checkbox.addEventListener('change', checkForChanges);
});

// Load settings from API
function loadSettings() {
fetch('/api/settings')
Expand All @@ -134,6 +192,9 @@ document.addEventListener('DOMContentLoaded', function() {
const huntarr = data.huntarr || {};
const advanced = data.advanced || {};

// Store original settings for comparison
originalSettings = JSON.parse(JSON.stringify(data));

// Fill form with current settings - Basic settings
huntMissingShowsInput.value = huntarr.hunt_missing_shows !== undefined ? huntarr.hunt_missing_shows : 1;
huntUpgradeEpisodesInput.value = huntarr.hunt_upgrade_episodes !== undefined ? huntarr.hunt_upgrade_episodes : 5;
Expand All @@ -154,12 +215,23 @@ document.addEventListener('DOMContentLoaded', function() {
// Handle random settings
randomMissingInput.checked = advanced.random_missing !== false;
randomUpgradesInput.checked = advanced.random_upgrades !== false;

// Initialize save buttons state
saveSettingsButton.disabled = true;
saveSettingsBottomButton.disabled = true;
saveSettingsButton.classList.add('disabled-button');
saveSettingsBottomButton.classList.add('disabled-button');
})
.catch(error => console.error('Error loading settings:', error));
}

// Function to save settings
function saveSettings() {
if (!checkForChanges()) {
// If no changes, don't do anything
return;
}

const settings = {
huntarr: {
hunt_missing_shows: parseInt(huntMissingShowsInput.value) || 0,
Expand Down Expand Up @@ -191,7 +263,21 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Settings saved successfully!');
// Update original settings after successful save
originalSettings = JSON.parse(JSON.stringify(settings));

// Disable save buttons
saveSettingsButton.disabled = true;
saveSettingsBottomButton.disabled = true;
saveSettingsButton.classList.add('disabled-button');
saveSettingsBottomButton.classList.add('disabled-button');

// Show success message
if (data.changes_made) {
alert('Settings saved successfully and cycle restarted to apply changes!');
} else {
alert('No changes detected.');
}
} else {
alert('Error saving settings: ' + (data.message || 'Unknown error'));
}
Expand All @@ -211,7 +297,7 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Settings reset to defaults.');
alert('Settings reset to defaults and cycle restarted.');
loadSettings();
} else {
alert('Error resetting settings: ' + (data.message || 'Unknown error'));
Expand Down
Loading