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 @@

Processing Options

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

@@ -116,6 +124,38 @@

Processing Options

Skip refreshing series metadata before processing (reduces disk activity)

+ +
+

Advanced Settings

+
+ + +

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.

+
+
diff --git a/upgrade.py b/upgrade.py index d702bdfb..9ef38998 100644 --- a/upgrade.py +++ b/upgrade.py @@ -12,6 +12,7 @@ HUNT_UPGRADE_EPISODES, MONITORED_ONLY, RANDOM_SELECTION, + RANDOM_UPGRADES, SKIP_FUTURE_EPISODES, SKIP_SERIES_REFRESH ) @@ -45,29 +46,47 @@ def process_cutoff_upgrades() -> bool: # Get current date for future episode filtering current_date = datetime.datetime.now().date() - page = 1 + # Use the specific RANDOM_UPGRADES setting + # (no longer dependent on the master RANDOM_SELECTION setting) + should_use_random = RANDOM_UPGRADES + + if should_use_random: + logger.info("Using random selection for quality upgrades (RANDOM_UPGRADES=true)") + else: + logger.info("Using sequential selection for quality upgrades (RANDOM_UPGRADES=false)") + page = 1 + while True: if episodes_processed >= HUNT_UPGRADE_EPISODES: logger.info(f"Reached HUNT_UPGRADE_EPISODES={HUNT_UPGRADE_EPISODES} for this cycle.") break - # If random selection, pick a random page each iteration - if RANDOM_SELECTION and total_pages > 1: + # If random selection is enabled, pick a random page each iteration + if should_use_random and total_pages > 1: page = random.randint(1, total_pages) + # If sequential and we've reached the end, we're done + elif not should_use_random and page > total_pages: + break logger.info(f"Retrieving cutoff-unmet episodes (page={page} of {total_pages})...") cutoff_data = get_cutoff_unmet(page) if not cutoff_data or "records" not in cutoff_data: logger.error(f"ERROR: Unable to retrieve cutoffโ€“unmet data from Sonarr on page {page}.") - break + + # In sequential mode, try the next page + if not should_use_random: + page += 1 + continue + else: + break episodes = cutoff_data["records"] total_eps = len(episodes) logger.info(f"Found {total_eps} episodes on page {page} that need quality upgrades.") - # Randomize or sequential indices + # Randomize or sequential indices within the page indices = list(range(total_eps)) - if RANDOM_SELECTION: + if should_use_random: random.shuffle(indices) for idx in indices: @@ -149,16 +168,12 @@ def process_cutoff_upgrades() -> bool: logger.warning(f"WARNING: Search command failed for episode ID {episode_id}.") continue - # Move to the next page if not random - if not RANDOM_SELECTION: + # Move to the next page if using sequential mode + if not should_use_random: page += 1 - if page > total_pages: - break - else: - # In random mode, we just handle one random page this iteration, - # then either break or keep looping until we hit HUNT_UPGRADE_EPISODES. - pass - + # In random mode, we just handle one random page this iteration, + # then check if we've processed enough episodes or continue to another random page + logger.info(f"Completed processing {episodes_processed} upgrade episodes for this cycle.") truncate_processed_list(PROCESSED_UPGRADE_FILE) diff --git a/utils/.github/workflows/docker-image.yml b/utils/.github/workflows/docker-image.yml deleted file mode 100644 index ea7c086d..00000000 --- a/utils/.github/workflows/docker-image.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: Docker Build and Push -on: - push: - branches: - - main - - dev - tags: - - "*" # This will trigger on any tag push - pull_request: - branches: - - main -jobs: - build-and-push: - runs-on: ubuntu-latest - steps: - # 1) Check out your repository code with full depth - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - # 2) List files to verify huntarr.py is present - - name: List files in directory - run: ls -la - - # 3) Set up QEMU for multi-architecture builds - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - with: - platforms: arm64,amd64 - - # 4) Set up Docker Buildx - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - # 5) Log in to Docker Hub - - name: Log in to Docker Hub - if: github.event_name != 'pull_request' - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - # 6) Extract version from tag if it's a tag push - - name: Extract version from tag - if: startsWith(github.ref, 'refs/tags/') - id: get_version - run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - - # 7a) Build & Push if on 'main' branch - - name: Build and Push (main) - if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - uses: docker/build-push-action@v3 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - tags: | - huntarr/4sonarr:latest - huntarr/4sonarr:${{ github.sha }} - - # 7b) Build & Push if on 'dev' branch - - name: Build and Push (dev) - if: github.ref == 'refs/heads/dev' && github.event_name != 'pull_request' - uses: docker/build-push-action@v3 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - tags: | - huntarr/4sonarr:dev - huntarr/4sonarr:${{ github.sha }} - - # 7c) Build & Push if it's a tag/release - - name: Build and Push (release) - if: startsWith(github.ref, 'refs/tags/') && github.event_name != 'pull_request' - uses: docker/build-push-action@v3 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - tags: | - huntarr/4sonarr:${{ steps.get_version.outputs.VERSION }} - huntarr/4sonarr:latest - - # 7d) Just build on pull requests - - name: Build (PR) - if: github.event_name == 'pull_request' - uses: docker/build-push-action@v3 - with: - context: . - push: false - platforms: linux/amd64,linux/arm64 \ No newline at end of file diff --git a/utils/logger.py b/utils/logger.py index 7a1150c5..15b42357 100644 --- a/utils/logger.py +++ b/utils/logger.py @@ -7,27 +7,51 @@ import sys import os import pathlib -from config import DEBUG_MODE # Create log directory LOG_DIR = pathlib.Path("/tmp/huntarr-logs") LOG_DIR.mkdir(parents=True, exist_ok=True) LOG_FILE = LOG_DIR / "huntarr.log" -def setup_logger(): - """Configure and return the application logger""" - logger = logging.getLogger("huntarr-sonarr") +# Global logger instance +logger = None + +def setup_logger(debug_mode=None): + """Configure and return the application logger + + Args: + debug_mode (bool, optional): Override the DEBUG_MODE from config. Defaults to None. + + Returns: + logging.Logger: The configured logger + """ + global logger - # Set the log level based on DEBUG_MODE - logger.setLevel(logging.DEBUG if DEBUG_MODE else logging.INFO) + # Get DEBUG_MODE from config, but only if we haven't been given a value + if debug_mode is None: + from config import DEBUG_MODE as CONFIG_DEBUG_MODE + use_debug_mode = CONFIG_DEBUG_MODE + else: + use_debug_mode = debug_mode + + if logger is None: + # First-time setup + logger = logging.getLogger("huntarr-sonarr") + else: + # Reset handlers to avoid duplicates + for handler in logger.handlers[:]: + logger.removeHandler(handler) + + # Set the log level based on use_debug_mode + logger.setLevel(logging.DEBUG if use_debug_mode else logging.INFO) # Create console handler console_handler = logging.StreamHandler(sys.stdout) - console_handler.setLevel(logging.DEBUG if DEBUG_MODE else logging.INFO) + console_handler.setLevel(logging.DEBUG if use_debug_mode else logging.INFO) # Create file handler for the web interface file_handler = logging.FileHandler(LOG_FILE) - file_handler.setLevel(logging.DEBUG if DEBUG_MODE else logging.INFO) + file_handler.setLevel(logging.DEBUG if use_debug_mode else logging.INFO) # Set format formatter = logging.Formatter( @@ -41,14 +65,17 @@ def setup_logger(): logger.addHandler(console_handler) logger.addHandler(file_handler) + if use_debug_mode: + logger.debug("Debug logging enabled") + return logger -# Create the logger instance +# Create the logger instance on module import logger = setup_logger() def debug_log(message: str, data: object = None) -> None: """Log debug messages with optional data.""" - if DEBUG_MODE: + if logger.level <= logging.DEBUG: logger.debug(f"{message}") if data is not None: try: diff --git a/web_server.py b/web_server.py index 5a8e6908..8459a5ec 100644 --- a/web_server.py +++ b/web_server.py @@ -14,8 +14,9 @@ import logging from config import ENABLE_WEB_UI import settings_manager +from utils.logger import setup_logger -# Check if web UI is enabled +# Check if web UI is disabled if not ENABLE_WEB_UI: print("Web UI is disabled. Exiting web server.") exit(0) @@ -89,20 +90,37 @@ def update_settings(): if "huntarr" in data: old_settings = settings_manager.get_setting("huntarr", None, {}) for key, value in data["huntarr"].items(): - old_value = old_settings.get(key, "Default") # Use "Default" instead of None for display + old_value = old_settings.get(key) if old_value != value: - changes_log.append(f"Changed {key} from {old_value} to {value}") + # Remove the "from Default" text - just log the new value + changes_log.append(f"Changed {key} to {value}") settings_manager.update_setting("huntarr", key, value) # Update UI settings if "ui" in data: old_settings = settings_manager.get_setting("ui", None, {}) for key, value in data["ui"].items(): - old_value = old_settings.get(key, "Default") # Use "Default" instead of None for display + old_value = old_settings.get(key) if old_value != value: - changes_log.append(f"Changed UI.{key} from {old_value} to {value}") + # Remove the "from Default" text - just log the new value + changes_log.append(f"Changed UI.{key} to {value}") settings_manager.update_setting("ui", key, value) + # Update advanced settings + if "advanced" in data: + old_settings = settings_manager.get_setting("advanced", None, {}) + for key, value in data["advanced"].items(): + old_value = old_settings.get(key) + if old_value != value: + changes_log.append(f"Changed advanced.{key} to {value}") + settings_manager.update_setting("advanced", key, value) + + # Special handling for debug_mode setting + if key == "debug_mode" and old_value != value: + # Reconfigure the logger with new debug mode setting + setup_logger(value) + changes_log.append(f"Reconfigured logger with DEBUG_MODE={value}") + # Write changes to log file if changes_log: with open(LOG_FILE, 'a') as f: @@ -145,10 +163,11 @@ def update_theme(): if "dark_mode" in data and old_value != data["dark_mode"]: settings_manager.update_setting("ui", "dark_mode", data["dark_mode"]) - # Log the theme change + # Log the theme change - simplified to remove "from X" text timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") with open(LOG_FILE, 'a') as f: - f.write(f"{timestamp} - huntarr-web - INFO - Changed theme from {'Dark' if old_value else 'Light'} to {'Dark' if data['dark_mode'] else 'Light'} Mode\n") + new_mode = 'Dark' if data['dark_mode'] else 'Light' + f.write(f"{timestamp} - huntarr-web - INFO - Changed theme to {new_mode} Mode\n") return jsonify({"success": True}) except Exception as e: