diff --git a/README.md b/README.md index 0c8f7ced..a55dbc67 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ My 12-year-old daughter is passionate about singing, dancing, and exploring STEM - ๐ **Continuous Operation**: Runs indefinitely until manually stopped - ๐ฏ **Dual Targeting System**: Targets both missing episodes and quality upgrades -- ๐ฒ **Random Selection**: By default, selects shows and episodes randomly to distribute searches across your library +- ๐ฒ **Separate Random Controls**: Separate toggles for random missing shows and random upgrades - โฑ๏ธ **Throttled Searches**: Includes configurable delays to prevent overloading indexers - ๐ **Status Reporting**: Provides clear feedback about what it's doing and which shows it's searching for - ๐ก๏ธ **Error Handling**: Gracefully handles connection issues and API failures @@ -66,6 +66,7 @@ My 12-year-old daughter is passionate about singing, dancing, and exploring STEM - ๐พ **Reduced Disk Activity**: Option to skip series refresh before processing - ๐ฟ **Persistent Configuration**: All settings are saved to disk and persist across container restarts - ๐ **Stateful Operation**: Processed state is now permanently saved between restarts +- โ๏ธ **Advanced Settings**: Control API timeout, command wait parameters, and more ## Indexers Approving of Huntarr: * https://ninjacentral.co.za @@ -75,13 +76,14 @@ My 12-year-old daughter is passionate about singing, dancing, and exploring STEM 1. **Initialization**: Connects to your Sonarr instance and analyzes your library 2. **Missing Episodes**: - Identifies shows with missing episodes - - Randomly selects shows to process (up to configurable limit) + - Randomly or sequentially selects shows to process (configurable) - Refreshes metadata (optional) and triggers searches - Skips episodes with future air dates (configurable) 3. **Quality Upgrades**: - Finds episodes that don't meet your quality cutoff settings - Processes them in configurable batches - Uses smart pagination to handle large libraries + - Can operate in random or sequential mode (configurable) - Skips episodes with future air dates (configurable) 4. **State Management**: - Tracks which shows and episodes have been processed @@ -121,7 +123,8 @@ The following environment variables can be configured: | `HUNT_MISSING_SHOWS` | Maximum missing shows to process per cycle | 1 | | `HUNT_UPGRADE_EPISODES` | Maximum upgrade episodes to process per cycle | 5 | | `SLEEP_DURATION` | Seconds to wait after completing a cycle (900 = 15 minutes) | 900 | -| `RANDOM_SELECTION` | Use random selection (`true`) or sequential (`false`) | true | +| `RANDOM_MISSING` | Select missing shows randomly instead of sequentially | true | +| `RANDOM_UPGRADES` | Select upgrade episodes randomly instead of sequentially | true | | `STATE_RESET_INTERVAL_HOURS` | Hours which the processed state files reset (168=1 week, 0=never reset) | 168 | | `DEBUG_MODE` | Enable detailed debug logging (`true` or `false`) | false | | `ENABLE_WEB_UI` | Enable or disable the web interface (`true` or `false`) | true | @@ -155,9 +158,13 @@ The following environment variables can be configured: - When this limit is reached, the upgrade portion of the cycle stops. - Set to `0` to disable quality upgrade processing completely. -- **RANDOM_SELECTION** - - When `true`, selects shows and episodes randomly, which helps distribute searches across your library. - - When `false`, processes items sequentially, which can be more predictable and methodical. +- **RANDOM_MISSING** + - When `true`, selects missing shows randomly, which helps distribute searches across your library. + - When `false`, processes missing shows sequentially, which can be more predictable and methodical. + +- **RANDOM_UPGRADES** + - When `true`, selects episodes for quality upgrades randomly from different pages. + - When `false`, processes episodes sequentially beginning from page 1. - **STATE_RESET_INTERVAL_HOURS** - Controls how often the script "forgets" which items it has already processed. @@ -258,10 +265,18 @@ The web interface allows you to configure all of Huntarr's settings without havi - **Processing Options** - **Monitored Only**: Only process monitored shows and episodes - - **Random Selection**: Select shows and episodes randomly instead of sequentially + - **Random Missing Shows**: Select missing shows randomly instead of sequentially + - **Random Upgrades**: Select upgrade episodes randomly instead of sequentially - **Skip Future Episodes**: Skip processing episodes with future air dates - **Skip Series Refresh**: Skip refreshing series metadata before processing +- **Advanced Settings** + - **API Timeout**: Timeout in seconds for API requests to Sonarr + - **Debug Mode**: Enable detailed debug logging + - **Command Wait Delay**: Delay between checking command status + - **Command Wait Attempts**: Number of attempts before giving up + - **Minimum Queue Size**: Minimum download queue size threshold + ### Port Configuration Explained When running with Docker, you need to map the container's internal port to a port on your host system. The format is `HOST_PORT:CONTAINER_PORT`. @@ -332,18 +347,17 @@ docker run -d --name huntarr-sonarr \ -e HUNT_MISSING_SHOWS="1" \ -e HUNT_UPGRADE_EPISODES="0" \ -e SLEEP_DURATION="900" \ - -e RANDOM_SELECTION="true" \ -e STATE_RESET_INTERVAL_HOURS="168" \ -e DEBUG_MODE="false" \ -e ENABLE_WEB_UI="true" \ -e SKIP_FUTURE_EPISODES="true" \ -e SKIP_SERIES_REFRESH="false" \ + -e COMMAND_WAIT_DELAY="1" \ + -e COMMAND_WAIT_ATTEMPTS="600" \ + -e MINIMUM_DOWNLOAD_QUEUE_SIZE="-1" \ + -e RANDOM_MISSING="true" \ + -e RANDOM_UPGRADES="true" \ huntarr/4sonarr:latest - - # Optional advanced settings - # -e COMMAND_WAIT_DELAY="1" \ - # -e COMMAND_WAIT_ATTEMPTS="600" \ - # -e MINIMUM_DOWNLOAD_QUEUE_SIZE="-1" \ ``` To check on the status of the program, you can use the web interface at http://YOUR_SERVER_IP:8988 or check the logs with: @@ -374,17 +388,16 @@ services: HUNT_MISSING_SHOWS: "1" HUNT_UPGRADE_EPISODES: "0" SLEEP_DURATION: "900" - RANDOM_SELECTION: "true" STATE_RESET_INTERVAL_HOURS: "168" DEBUG_MODE: "false" ENABLE_WEB_UI: "true" SKIP_FUTURE_EPISODES: "true" SKIP_SERIES_REFRESH: "false" - - # Optional advanced settings - # COMMAND_WAIT_DELAY: "1" - # COMMAND_WAIT_ATTEMPTS: "600" - # MINIMUM_DOWNLOAD_QUEUE_SIZE: "-1" + COMMAND_WAIT_DELAY: "1" + COMMAND_WAIT_ATTEMPTS: "600" + MINIMUM_DOWNLOAD_QUEUE_SIZE: "-1" + RANDOM_MISSING: "true" + RANDOM_UPGRADES: "true" ``` Then run: @@ -409,18 +422,17 @@ docker run -d --name huntarr-sonarr \ -e HUNT_MISSING_SHOWS="1" \ -e HUNT_UPGRADE_EPISODES="0" \ -e SLEEP_DURATION="900" \ - -e RANDOM_SELECTION="true" \ -e STATE_RESET_INTERVAL_HOURS="168" \ -e DEBUG_MODE="false" \ -e ENABLE_WEB_UI="true" \ -e SKIP_FUTURE_EPISODES="true" \ -e SKIP_SERIES_REFRESH="false" \ + -e COMMAND_WAIT_DELAY="1" \ + -e COMMAND_WAIT_ATTEMPTS="600" \ + -e MINIMUM_DOWNLOAD_QUEUE_SIZE="-1" \ + -e RANDOM_MISSING="true" \ + -e RANDOM_UPGRADES="true" \ huntarr/4sonarr:latest - - # Optional advanced settings - # -e COMMAND_WAIT_DELAY="1" \ - # -e COMMAND_WAIT_ATTEMPTS="600" \ - # -e MINIMUM_DOWNLOAD_QUEUE_SIZE="-1" \ ``` ### SystemD Service @@ -446,12 +458,16 @@ Environment="MONITORED_ONLY=true" Environment="HUNT_MISSING_SHOWS=1" Environment="HUNT_UPGRADE_EPISODES=0" Environment="SLEEP_DURATION=900" -Environment="RANDOM_SELECTION=true" Environment="STATE_RESET_INTERVAL_HOURS=168" Environment="DEBUG_MODE=false" Environment="ENABLE_WEB_UI=true" Environment="SKIP_FUTURE_EPISODES=true" Environment="SKIP_SERIES_REFRESH=false" +Environment="COMMAND_WAIT_DELAY=1" +Environment="COMMAND_WAIT_ATTEMPTS=600" +Environment="MINIMUM_DOWNLOAD_QUEUE_SIZE=-1" +Environment="RANDOM_MISSING=true" +Environment="RANDOM_UPGRADES=true" ExecStartPre=/bin/sleep 30 ExecStart=/usr/local/bin/huntarr.sh Restart=on-failure @@ -497,6 +513,7 @@ sudo systemctl start huntarr - **Persistent Storage**: Make sure to map the `/config` volume to preserve settings and state - **Dark Mode**: Toggle between light and dark themes in the web interface for comfortable viewing - **Settings Persistence**: Any settings changed in the web UI are saved immediately and permanently +- **Random vs Sequential**: Configure `RANDOM_MISSING` and `RANDOM_UPGRADES` based on your preference for processing style ## Troubleshooting @@ -510,6 +527,7 @@ sudo systemctl start huntarr - **State Files**: The script stores state in `/config/stateful/` - if something seems stuck, you can try deleting these files - **Excessive Disk Activity**: If you notice high disk usage, try enabling `SKIP_SERIES_REFRESH=true` - **Web UI URL Issues**: The web interface URL shown in logs is now derived from your API_URL setting +- **API Timeout Errors**: For large libraries, try increasing the `API_TIMEOUT` value to 120 or higher --- @@ -520,4 +538,4 @@ This script helps automate the tedious process of finding missing episodes and q Thanks to: * [IntensiveCareCub](https://www.reddit.com/user/IntensiveCareCub/) for the Hunter to Huntarr idea! -* [ZPatten](https://github.com/zpatten) for adding the Queue Size and Delay Commands! +* [ZPatten](https://github.com/zpatten) for adding the Queue Size and Delay Commands! \ No newline at end of file diff --git a/api.py b/api.py index 1cf21b94..a7b06181 100644 --- a/api.py +++ b/api.py @@ -71,7 +71,7 @@ def get_series() -> List[Dict]: debug_log("Raw series API response sample:", series_list[:2] if len(series_list) > 2 else series_list) return series_list or [] -def refresh_series(series_id: int) -> Optional[Dict]: +def refresh_series(series_id: int) -> bool: """ POST /api/v3/command { @@ -84,9 +84,11 @@ def refresh_series(series_id: int) -> Optional[Dict]: "seriesId": series_id } response = sonarr_request("command", method="POST", data=data) + if not response or 'id' not in response: + return False return wait_for_command(response['id']) -def episode_search_episodes(episode_ids: List[int]) -> Optional[Dict]: +def episode_search_episodes(episode_ids: List[int]) -> bool: """ POST /api/v3/command { @@ -99,14 +101,19 @@ def episode_search_episodes(episode_ids: List[int]) -> Optional[Dict]: "episodeIds": episode_ids } response = sonarr_request("command", method="POST", data=data) + if not response or 'id' not in response: + return False return wait_for_command(response['id']) -def get_download_queue_size() -> Optional[int]: +def get_download_queue_size() -> int: """ GET /api/v3/queue Returns total number of items in the queue with the status 'downloading'. """ response = sonarr_request("queue?status=downloading") + if not response: + return 0 + total_records = response.get("totalRecords", 0) if not isinstance(total_records, int): total_records = 0 diff --git a/config.py b/config.py index f2748576..c1acec08 100644 --- a/config.py +++ b/config.py @@ -15,7 +15,7 @@ API_KEY = os.environ.get("API_KEY", "your-api-key") API_URL = os.environ.get("API_URL", "http://your-sonarr-address:8989") -# API timeout in seconds +# API timeout in seconds - load from environment first, will be overridden by settings if they exist try: API_TIMEOUT = int(os.environ.get("API_TIMEOUT", "60")) except ValueError: @@ -61,7 +61,7 @@ SKIP_FUTURE_EPISODES = os.environ.get("SKIP_FUTURE_EPISODES", "true").lower() == "true" SKIP_SERIES_REFRESH = os.environ.get("SKIP_SERIES_REFRESH", "false").lower() == "true" -# Delay in seconds between checking the status of a command (default 1 second) +# Advanced settings - load from environment first, will be overridden by settings if they exist try: COMMAND_WAIT_DELAY = int(os.environ.get("COMMAND_WAIT_DELAY", "1")) except ValueError: @@ -82,21 +82,29 @@ MINIMUM_DOWNLOAD_QUEUE_SIZE = -1 print(f"Warning: Invalid MINIMUM_DOWNLOAD_QUEUE_SIZE value, using default: {MINIMUM_DOWNLOAD_QUEUE_SIZE}") -# Hunt mode: "missing", "upgrade", or "both" -HUNT_MODE = os.environ.get("HUNT_MODE", "both") - # Debug Settings DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true" +# Random selection for missing and upgrades - default to RANDOM_SELECTION for backward compatibility +# These can be overridden by environment variables or settings +RANDOM_MISSING = os.environ.get("RANDOM_MISSING", str(RANDOM_SELECTION)).lower() == "true" +RANDOM_UPGRADES = os.environ.get("RANDOM_UPGRADES", str(RANDOM_SELECTION)).lower() == "true" + +# Hunt mode: "missing", "upgrade", or "both" +HUNT_MODE = os.environ.get("HUNT_MODE", "both") + def refresh_settings(): """Refresh configuration settings from the settings manager.""" global HUNT_MISSING_SHOWS, HUNT_UPGRADE_EPISODES, SLEEP_DURATION global STATE_RESET_INTERVAL_HOURS, MONITORED_ONLY, RANDOM_SELECTION global SKIP_FUTURE_EPISODES, SKIP_SERIES_REFRESH + global API_TIMEOUT, DEBUG_MODE, COMMAND_WAIT_DELAY, COMMAND_WAIT_ATTEMPTS + global MINIMUM_DOWNLOAD_QUEUE_SIZE, RANDOM_MISSING, RANDOM_UPGRADES # Load settings directly from settings manager settings = settings_manager.get_all_settings() huntarr_settings = settings.get("huntarr", {}) + advanced_settings = settings.get("advanced", {}) # Update global variables with fresh values HUNT_MISSING_SHOWS = huntarr_settings.get("hunt_missing_shows", HUNT_MISSING_SHOWS) @@ -108,10 +116,31 @@ def refresh_settings(): SKIP_FUTURE_EPISODES = huntarr_settings.get("skip_future_episodes", SKIP_FUTURE_EPISODES) SKIP_SERIES_REFRESH = huntarr_settings.get("skip_series_refresh", SKIP_SERIES_REFRESH) + # Advanced settings + API_TIMEOUT = advanced_settings.get("api_timeout", API_TIMEOUT) + DEBUG_MODE = advanced_settings.get("debug_mode", DEBUG_MODE) + COMMAND_WAIT_DELAY = advanced_settings.get("command_wait_delay", COMMAND_WAIT_DELAY) + COMMAND_WAIT_ATTEMPTS = advanced_settings.get("command_wait_attempts", COMMAND_WAIT_ATTEMPTS) + MINIMUM_DOWNLOAD_QUEUE_SIZE = advanced_settings.get("minimum_download_queue_size", MINIMUM_DOWNLOAD_QUEUE_SIZE) + + # Get the specific random settings - default to RANDOM_SELECTION for backward compatibility + # but only if not explicitly set in the advanced settings + if "random_missing" in advanced_settings: + RANDOM_MISSING = advanced_settings.get("random_missing") + else: + RANDOM_MISSING = RANDOM_SELECTION + + if "random_upgrades" in advanced_settings: + RANDOM_UPGRADES = advanced_settings.get("random_upgrades") + else: + RANDOM_UPGRADES = RANDOM_SELECTION + # Log the refresh for debugging import logging logger = logging.getLogger("huntarr-sonarr") logger.debug(f"Settings refreshed: SLEEP_DURATION={SLEEP_DURATION}, HUNT_MISSING_SHOWS={HUNT_MISSING_SHOWS}") + logger.debug(f"Advanced settings refreshed: API_TIMEOUT={API_TIMEOUT}, DEBUG_MODE={DEBUG_MODE}") + logger.debug(f"Random settings: RANDOM_SELECTION={RANDOM_SELECTION}, RANDOM_MISSING={RANDOM_MISSING}, RANDOM_UPGRADES={RANDOM_UPGRADES}") def log_configuration(logger): """Log the current configuration settings""" @@ -126,10 +155,11 @@ def log_configuration(logger): logger.info(f"State Reset Interval: {STATE_RESET_INTERVAL_HOURS} hours") logger.info(f"Minimum Download Queue Size: {MINIMUM_DOWNLOAD_QUEUE_SIZE}") logger.info(f"MONITORED_ONLY={MONITORED_ONLY}, RANDOM_SELECTION={RANDOM_SELECTION}") + logger.info(f"RANDOM_MISSING={RANDOM_MISSING}, RANDOM_UPGRADES={RANDOM_UPGRADES}") logger.info(f"HUNT_MODE={HUNT_MODE}, SLEEP_DURATION={SLEEP_DURATION}s") logger.info(f"COMMAND_WAIT_DELAY={COMMAND_WAIT_DELAY}, COMMAND_WAIT_ATTEMPTS={COMMAND_WAIT_ATTEMPTS}") logger.info(f"SKIP_FUTURE_EPISODES={SKIP_FUTURE_EPISODES}, SKIP_SERIES_REFRESH={SKIP_SERIES_REFRESH}") - logger.info(f"ENABLE_WEB_UI={ENABLE_WEB_UI}") + logger.info(f"ENABLE_WEB_UI={ENABLE_WEB_UI}, DEBUG_MODE={DEBUG_MODE}") logger.debug(f"API_KEY={API_KEY}") # Initial refresh of settings diff --git a/missing.py b/missing.py index 86505352..973d5476 100644 --- a/missing.py +++ b/missing.py @@ -12,7 +12,8 @@ from config import ( HUNT_MISSING_SHOWS, MONITORED_ONLY, - RANDOM_SELECTION, + RANDOM_SELECTION, + RANDOM_MISSING, SKIP_FUTURE_EPISODES, SKIP_SERIES_REFRESH ) @@ -63,9 +64,13 @@ def process_missing_episodes() -> bool: shows_processed = 0 processing_done = False - # Optionally randomize show order - if RANDOM_SELECTION: + # Use the specific RANDOM_MISSING setting + # (no longer dependent on the master RANDOM_SELECTION setting) + if RANDOM_MISSING: + logger.info("Using random selection for missing shows (RANDOM_MISSING=true)") random.shuffle(shows_with_missing) + else: + logger.info("Using sequential selection for missing shows (RANDOM_MISSING=false)") # Get current date for future episode filtering current_date = datetime.datetime.now().date() diff --git a/settings_manager.py b/settings_manager.py index dfb9a435..3b8812e3 100644 --- a/settings_manager.py +++ b/settings_manager.py @@ -34,6 +34,15 @@ "random_selection": True, "skip_future_episodes": True, "skip_series_refresh": False + }, + "advanced": { + "api_timeout": 60, + "debug_mode": False, + "command_wait_delay": 1, + "command_wait_attempts": 600, + "minimum_download_queue_size": -1, + "random_missing": True, + "random_upgrades": True } } diff --git a/static/js/main.js b/static/js/main.js index 47bd3e64..e6b10e0b 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -11,17 +11,25 @@ document.addEventListener('DOMContentLoaded', function() { const themeToggle = document.getElementById('themeToggle'); const themeLabel = document.getElementById('themeLabel'); - // Settings form elements + // Settings form elements - Basic settings const huntMissingShowsInput = document.getElementById('hunt_missing_shows'); const huntUpgradeEpisodesInput = document.getElementById('hunt_upgrade_episodes'); const sleepDurationInput = document.getElementById('sleep_duration'); const sleepDurationHoursSpan = document.getElementById('sleep_duration_hours'); const stateResetIntervalInput = document.getElementById('state_reset_interval_hours'); const monitoredOnlyInput = document.getElementById('monitored_only'); - const randomSelectionInput = document.getElementById('random_selection'); + const randomMissingInput = document.getElementById('random_missing'); + const randomUpgradesInput = document.getElementById('random_upgrades'); const skipFutureEpisodesInput = document.getElementById('skip_future_episodes'); const skipSeriesRefreshInput = document.getElementById('skip_series_refresh'); + // Settings form elements - Advanced settings + const apiTimeoutInput = document.getElementById('api_timeout'); + const debugModeInput = document.getElementById('debug_mode'); + const commandWaitDelayInput = document.getElementById('command_wait_delay'); + const commandWaitAttemptsInput = document.getElementById('command_wait_attempts'); + const minimumDownloadQueueSizeInput = document.getElementById('minimum_download_queue_size'); + // Button elements for saving and resetting settings const saveSettingsButton = document.getElementById('saveSettings'); const resetSettingsButton = document.getElementById('resetSettings'); @@ -124,17 +132,28 @@ document.addEventListener('DOMContentLoaded', function() { .then(response => response.json()) .then(data => { const huntarr = data.huntarr || {}; + const advanced = data.advanced || {}; - // Fill form with current settings + // 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; sleepDurationInput.value = huntarr.sleep_duration || 900; updateSleepDurationDisplay(); stateResetIntervalInput.value = huntarr.state_reset_interval_hours || 168; monitoredOnlyInput.checked = huntarr.monitored_only !== false; - randomSelectionInput.checked = huntarr.random_selection !== false; skipFutureEpisodesInput.checked = huntarr.skip_future_episodes !== false; skipSeriesRefreshInput.checked = huntarr.skip_series_refresh === true; + + // Fill form with current settings - Advanced settings + apiTimeoutInput.value = advanced.api_timeout || 60; + debugModeInput.checked = advanced.debug_mode === true; + commandWaitDelayInput.value = advanced.command_wait_delay || 1; + commandWaitAttemptsInput.value = advanced.command_wait_attempts || 600; + minimumDownloadQueueSizeInput.value = advanced.minimum_download_queue_size || -1; + + // Handle random settings + randomMissingInput.checked = advanced.random_missing !== false; + randomUpgradesInput.checked = advanced.random_upgrades !== false; }) .catch(error => console.error('Error loading settings:', error)); } @@ -148,9 +167,17 @@ document.addEventListener('DOMContentLoaded', function() { sleep_duration: parseInt(sleepDurationInput.value) || 900, state_reset_interval_hours: parseInt(stateResetIntervalInput.value) || 168, monitored_only: monitoredOnlyInput.checked, - random_selection: randomSelectionInput.checked, skip_future_episodes: skipFutureEpisodesInput.checked, skip_series_refresh: skipSeriesRefreshInput.checked + }, + advanced: { + api_timeout: parseInt(apiTimeoutInput.value) || 60, + debug_mode: debugModeInput.checked, + command_wait_delay: parseInt(commandWaitDelayInput.value) || 1, + command_wait_attempts: parseInt(commandWaitAttemptsInput.value) || 600, + minimum_download_queue_size: parseInt(minimumDownloadQueueSizeInput.value) || -1, + random_missing: randomMissingInput.checked, + random_upgrades: randomUpgradesInput.checked } }; diff --git a/templates/index.html b/templates/index.html index 60aaabbf..7d79bdd1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -92,12 +92,20 @@
Only process monitored shows and episodes
Select shows and episodes randomly instead of sequentially
+Select missing shows randomly instead of sequentially
+Select upgrade episodes randomly instead of sequentially
Skip refreshing series metadata before processing (reduces disk activity)
Timeout in seconds for API requests to Sonarr (minimum 10 seconds)
+Enable detailed debug logging (takes effect immediately)
+Delay in seconds between checking for command status
+Number of attempts to check for command completion before giving up
+Minimum items in download queue before starting hunt. Set to -1 to disable check.
+