Skip to content
Merged

Dev #54

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
72 changes: 45 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

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

---

Expand All @@ -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!
13 changes: 10 additions & 3 deletions api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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
{
Expand All @@ -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
Expand Down
42 changes: 36 additions & 6 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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"""
Expand All @@ -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
Expand Down
11 changes: 8 additions & 3 deletions missing.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from config import (
HUNT_MISSING_SHOWS,
MONITORED_ONLY,
RANDOM_SELECTION,
RANDOM_SELECTION,
RANDOM_MISSING,
SKIP_FUTURE_EPISODES,
SKIP_SERIES_REFRESH
)
Expand Down Expand Up @@ -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()
Expand Down
9 changes: 9 additions & 0 deletions settings_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
Loading