Skip to content
Merged
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
31 changes: 12 additions & 19 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,46 +1,39 @@
FROM python:3.9-slim
WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Install Flask for the web interface
RUN pip install --no-cache-dir flask

# Copy application files
COPY main.py config.py api.py state.py ./
COPY missing.py upgrade.py ./
COPY web_server.py ./
COPY utils/ ./utils/

# Create templates directory and copy index.html
RUN mkdir -p templates
COPY templates/ ./templates/

# Create required directories
RUN mkdir -p /tmp/huntarr-state
RUN mkdir -p /tmp/huntarr-logs

# Default environment variables
ENV API_KEY="your-api-key" \
API_URL="http://your-sonarr-address:8989" \
API_TIMEOUT="60" \
HUNT_MISSING_SHOWS=1 \
HUNT_UPGRADE_EPISODES=5 \
SLEEP_DURATION=900 \
STATE_RESET_INTERVAL_HOURS=168 \
RANDOM_SELECTION="true" \
MONITORED_ONLY="true" \
DEBUG_MODE="false" \
ENABLE_WEB_UI="true"

API_URL="http://your-sonarr-address:8989" \
API_TIMEOUT="60" \
HUNT_MISSING_SHOWS=1 \
HUNT_UPGRADE_EPISODES=0 \
SLEEP_DURATION=900 \
STATE_RESET_INTERVAL_HOURS=168 \
RANDOM_SELECTION="true" \
MONITORED_ONLY="true" \
DEBUG_MODE="false" \
ENABLE_WEB_UI="true" \
SKIP_FUTURE_EPISODES="true" \
SKIP_SERIES_REFRESH="false"
# Expose web interface port
EXPOSE 8988

# Add startup script that conditionally starts the web UI
COPY start.sh .
RUN chmod +x start.sh

# Run the startup script which will decide what to launch
CMD ["./start.sh"]
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ My 12-year-old daughter is passionate about singing, dancing, and exploring STEM
- 🔁 **State Tracking**: Remembers which shows and episodes have been processed to avoid duplicate searches
- ⚙️ **Configurable Reset Timer**: Automatically resets search history after a configurable period
- 📦 **Modular Design**: Modern codebase with separated concerns for easier maintenance
- 🌐 **Web Interface**: Real-time log viewer with day/night mode (new!)
- 🌐 **Web Interface**: Real-time log viewer with day/night mode
- 🔮 **Future Episode Skipping**: Skip processing episodes with future air dates
- 💾 **Reduced Disk Activity**: Option to skip series refresh before processing

## Indexers Approving of Huntarr:
* https://ninjacentral.co.za
Expand All @@ -72,11 +74,13 @@ My 12-year-old daughter is passionate about singing, dancing, and exploring STEM
2. **Missing Episodes**:
- Identifies shows with missing episodes
- Randomly selects shows to process (up to configurable limit)
- Refreshes metadata and triggers searches
- 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
- Skips episodes with future air dates (configurable)
4. **State Management**:
- Tracks which shows and episodes have been processed
- Automatically resets this tracking after a configurable time period
Expand Down Expand Up @@ -112,12 +116,14 @@ The following environment variables can be configured:
| `API_TIMEOUT` | Timeout in seconds for API requests to Sonarr | 60 |
| `MONITORED_ONLY` | Only process monitored shows/episodes | true |
| `HUNT_MISSING_SHOWS` | Maximum missing shows to process per cycle | 1 |
| `HUNT_UPGRADE_EPISODES` | Maximum upgrade episodes to process per cycle | 0 |
| `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 |
| `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 |
| `SKIP_FUTURE_EPISODES` | Skip processing episodes with future air dates (`true` or `false`) | true |
| `SKIP_SERIES_REFRESH` | Skip refreshing series metadata before processing (`true` or `false`) | false |

### Advanced Options (Optional)

Expand Down Expand Up @@ -167,6 +173,18 @@ The following environment variables can be configured:
- When set to `false`, the web interface will not start, saving resources.
- Default is `true` for convenient monitoring.

- **SKIP_FUTURE_EPISODES**
- When set to `true`, the script will skip processing episodes with future air dates.
- This helps avoid unnecessary searches for content that isn't available yet.
- Works for both missing episodes and quality upgrade processing.
- Default is `true` to optimize search efficiency.

- **SKIP_SERIES_REFRESH**
- When set to `true`, the script will skip refreshing series metadata before searching.
- This can significantly reduce disk activity on your Sonarr server.
- Default is `false` to maintain compatibility with previous behavior.
- Set to `true` if you notice excessive disk activity during Huntarr cycles.

- **COMMAND_WAIT_DELAY**
- Certain operations like refreshing and searching happen asynchronously.
- This is the delay in seconds between checking the status of these operations for completion.
Expand Down Expand Up @@ -242,6 +260,7 @@ docker run -d --name huntarr-sonarr \
-p 8988:8988 \ # Can be removed if ENABLE_WEB_UI=false
-e API_KEY="your-api-key" \
-e API_URL="http://your-sonarr-address:8989" \
-e API_TIMEOUT="60" \
-e MONITORED_ONLY="true" \
-e HUNT_MISSING_SHOWS="1" \
-e HUNT_UPGRADE_EPISODES="0" \
Expand All @@ -250,6 +269,8 @@ docker run -d --name huntarr-sonarr \
-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" \
huntarr/4sonarr:latest

# Optional advanced settings
Expand Down Expand Up @@ -288,6 +309,8 @@ services:
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"
Expand Down Expand Up @@ -320,6 +343,8 @@ docker run -d --name huntarr-sonarr \
-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" \
huntarr/4sonarr:latest

# Optional advanced settings
Expand Down Expand Up @@ -354,6 +379,9 @@ 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"
ExecStart=/usr/local/bin/huntarr.sh
Restart=on-failure
RestartSec=10
Expand All @@ -377,6 +405,8 @@ sudo systemctl start huntarr
- **Background Service**: Run it in the background to continuously maintain your library
- **Smart Rotation**: With state tracking, ensures all content gets attention over time
- **Real-time Monitoring**: Use the web interface to see what's happening at any time
- **Disk Usage Optimization**: Skip refreshing metadata to reduce disk wear and tear
- **Efficient Searching**: Skip processing episodes with future air dates to save resources

## Tips

Expand All @@ -389,6 +419,8 @@ sudo systemctl start huntarr
- **Port Conflicts**: If port 8988 is already in use, map to a different host port (e.g., `-p 9000:8988`)
- **Disable Web UI**: Set `ENABLE_WEB_UI=false` if you don't need the interface to save resources
- **Debugging Issues**: Enable `DEBUG_MODE=true` temporarily to see detailed logs when troubleshooting
- **Hard Drive Saving**: Enable `SKIP_SERIES_REFRESH=true` to reduce disk activity
- **Search Efficiency**: Keep `SKIP_FUTURE_EPISODES=true` to avoid searching for unavailable content

## Troubleshooting

Expand All @@ -399,6 +431,7 @@ sudo systemctl start huntarr
- **Logs**: Check the container logs with `docker logs huntarr-sonarr` if running in Docker
- **Debug Mode**: Enable `DEBUG_MODE=true` to see detailed API responses and process flow
- **State Files**: The script stores state in `/tmp/huntarr-state/` - 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`

---

Expand All @@ -408,4 +441,5 @@ 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!
[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!
9 changes: 9 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@
MINIMUM_DOWNLOAD_QUEUE_SIZE = -1
print(f"Warning: Invalid MINIMUM_DOWNLOAD_QUEUE_SIZE value, using default: {MINIMUM_DOWNLOAD_QUEUE_SIZE}")

# New Options

# Skip processing episodes with air dates in the future (default true)
SKIP_FUTURE_EPISODES = os.environ.get("SKIP_FUTURE_EPISODES", "true").lower() == "true"

# Skip refreshing series metadata before processing (default false)
SKIP_SERIES_REFRESH = os.environ.get("SKIP_SERIES_REFRESH", "false").lower() == "true"

# Selection Settings
RANDOM_SELECTION = os.environ.get("RANDOM_SELECTION", "true").lower() == "true"
MONITORED_ONLY = os.environ.get("MONITORED_ONLY", "true").lower() == "true"
Expand All @@ -92,5 +100,6 @@ def log_configuration(logger):
logger.info(f"MONITORED_ONLY={MONITORED_ONLY}, RANDOM_SELECTION={RANDOM_SELECTION}")
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.debug(f"API_KEY={API_KEY}")
64 changes: 55 additions & 9 deletions missing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@

import random
import time
import datetime
from typing import List
from utils.logger import logger
from config import HUNT_MISSING_SHOWS, MONITORED_ONLY, RANDOM_SELECTION
from config import (
HUNT_MISSING_SHOWS,
MONITORED_ONLY,
RANDOM_SELECTION,
SKIP_FUTURE_EPISODES,
SKIP_SERIES_REFRESH
)
from api import (
get_episodes_for_series,
refresh_series,
Expand Down Expand Up @@ -60,6 +67,9 @@ def process_missing_episodes() -> bool:
if RANDOM_SELECTION:
random.shuffle(shows_with_missing)

# Get current date for future episode filtering
current_date = datetime.datetime.now().date()

for show in shows_with_missing:
if shows_processed >= HUNT_MISSING_SHOWS:
break
Expand Down Expand Up @@ -88,16 +98,52 @@ def process_missing_episodes() -> bool:
logger.info(f"No missing monitored episodes found for '{show_title}' — skipping.")
continue

logger.info(f"Found {len(monitored_missing_episodes)} missing monitored episode(s) for '{show_title}'.")
# Skip future episodes if SKIP_FUTURE_EPISODES is enabled
if SKIP_FUTURE_EPISODES:
# Get episodes that don't have a future air date
current_or_past_episodes = []
future_episode_count = 0

for ep in monitored_missing_episodes:
air_date_str = ep.get("airDateUtc")

# If no air date, include it (can't determine if it's future)
if not air_date_str:
current_or_past_episodes.append(ep)
continue

try:
# Parse the UTC date string
air_date = datetime.datetime.fromisoformat(air_date_str.replace('Z', '+00:00')).date()
if air_date <= current_date:
current_or_past_episodes.append(ep)
else:
future_episode_count += 1
except (ValueError, TypeError):
# If date parsing fails, include it anyway
current_or_past_episodes.append(ep)

if future_episode_count > 0:
logger.info(f"Skipped {future_episode_count} future episodes for '{show_title}'")

monitored_missing_episodes = current_or_past_episodes

if not monitored_missing_episodes:
logger.info(f"All missing episodes for '{show_title}' are future episodes - skipping.")
continue

# Refresh the series
logger.info(f" - Refreshing series (ID: {series_id})...")
refresh_res = refresh_series(series_id)
if not refresh_res:
logger.warning(f"WARNING: Refresh command failed for {show_title}. Skipping.")
continue
logger.info(f"Found {len(monitored_missing_episodes)} missing monitored episode(s) for '{show_title}'.")

logger.info(f"Refresh command completed successfully.")
# Refresh the series only if SKIP_SERIES_REFRESH is not enabled
if not SKIP_SERIES_REFRESH:
logger.info(f" - Refreshing series (ID: {series_id})...")
refresh_res = refresh_series(series_id)
if not refresh_res:
logger.warning(f"WARNING: Refresh command failed for {show_title}. Skipping.")
continue
logger.info(f"Refresh command completed successfully.")
else:
logger.info(f" - Skipping series refresh (SKIP_SERIES_REFRESH=true)")

# Search specifically for these missing + monitored episodes
episode_ids = [ep["id"] for ep in monitored_missing_episodes]
Expand Down
44 changes: 35 additions & 9 deletions upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@

import random
import time
import datetime
from utils.logger import logger
from config import HUNT_UPGRADE_EPISODES, MONITORED_ONLY, RANDOM_SELECTION
from config import (
HUNT_UPGRADE_EPISODES,
MONITORED_ONLY,
RANDOM_SELECTION,
SKIP_FUTURE_EPISODES,
SKIP_SERIES_REFRESH
)
from api import get_cutoff_unmet, get_cutoff_unmet_total_pages, refresh_series, episode_search_episodes, sonarr_request
from state import load_processed_ids, save_processed_id, truncate_processed_list, PROCESSED_UPGRADE_FILE

Expand Down Expand Up @@ -35,6 +42,9 @@ def process_cutoff_upgrades() -> bool:
episodes_processed = 0
processing_done = False

# Get current date for future episode filtering
current_date = datetime.datetime.now().date()

page = 1
while True:
if episodes_processed >= HUNT_UPGRADE_EPISODES:
Expand Down Expand Up @@ -83,6 +93,20 @@ def process_cutoff_upgrades() -> bool:
else:
series_title = "Unknown Series"

# Skip future episodes if SKIP_FUTURE_EPISODES is enabled
if SKIP_FUTURE_EPISODES:
air_date_str = ep_obj.get("airDateUtc")
if air_date_str:
try:
# Parse the UTC date string
air_date = datetime.datetime.fromisoformat(air_date_str.replace('Z', '+00:00')).date()
if air_date > current_date:
logger.info(f"Skipping future episode '{series_title}' - S{season_num}E{ep_num} - '{ep_title}' (airs on {air_date})")
continue
except (ValueError, TypeError):
# If date parsing fails, proceed with the episode
pass

logger.info(f"Processing upgrade for \"{series_title}\" - S{season_num}E{ep_num} - \"{ep_title}\" (Episode ID: {episode_id})")

# If MONITORED_ONLY, ensure both series & episode are monitored
Expand All @@ -100,14 +124,16 @@ def process_cutoff_upgrades() -> bool:
logger.info("Skipping unmonitored episode or series.")
continue

# Refresh the series
logger.info(" - Refreshing series information...")
refresh_res = refresh_series(series_id)
if not refresh_res:
logger.warning("WARNING: Refresh command failed. Skipping this episode.")
continue

logger.info(f"Refresh command completed successfully.")
# Refresh the series only if SKIP_SERIES_REFRESH is not enabled
if not SKIP_SERIES_REFRESH:
logger.info(" - Refreshing series information...")
refresh_res = refresh_series(series_id)
if not refresh_res:
logger.warning("WARNING: Refresh command failed. Skipping this episode.")
continue
logger.info(f"Refresh command completed successfully.")
else:
logger.info(" - Skipping series refresh (SKIP_SERIES_REFRESH=true)")

# Search for the episode (upgrade)
logger.info(" - Searching for quality upgrade...")
Expand Down