Skip to content

Commit d1a2889

Browse files
authored
Merge pull request #7 from plexguide/dev
Dev
2 parents 197b897 + 5d2aba2 commit d1a2889

File tree

11 files changed

+618
-467
lines changed

11 files changed

+618
-467
lines changed

Dockerfile

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,24 @@
1-
# Start from a lightweight Python image
2-
FROM python:3.10-slim
3-
4-
# Set default environment variables (non-sensitive only!)
5-
# NOTE: We removed API_KEY to avoid the Dockerfile secrets warning
6-
ENV API_URL="http://your-radarr-address:7878" \
7-
SEARCH_TYPE="both" \
8-
MAX_MISSING="1" \
9-
MAX_UPGRADES="5" \
10-
SLEEP_DURATION="900" \
1+
FROM python:3.9-slim
2+
WORKDIR /app
3+
# Install dependencies
4+
COPY requirements.txt .
5+
RUN pip install --no-cache-dir -r requirements.txt
6+
# Copy application files
7+
COPY main.py config.py api.py state.py ./
8+
COPY missing.py upgrade.py ./
9+
COPY utils/ ./utils/
10+
# Create state directory
11+
RUN mkdir -p /tmp/huntarr-state
12+
# Default environment variables
13+
ENV API_KEY="your-api-key" \
14+
API_URL="http://your-radarr-address:7878" \
15+
API_TIMEOUT="60" \
16+
HUNT_MISSING_MOVIES=1 \
17+
HUNT_UPGRADE_MOVIES=5 \
18+
SLEEP_DURATION=900 \
19+
STATE_RESET_INTERVAL_HOURS=168 \
1120
RANDOM_SELECTION="true" \
1221
MONITORED_ONLY="true" \
13-
STATE_RESET_INTERVAL_HOURS="168" \
1422
DEBUG_MODE="false"
15-
16-
# Create a directory for our script and state files
17-
RUN mkdir -p /app && mkdir -p /tmp/huntarr-radarr-state
18-
19-
# Switch working directory
20-
WORKDIR /app
21-
22-
# Install any Python dependencies you need (requests is needed by huntarr.py)
23-
RUN pip install --no-cache-dir requests
24-
25-
# Copy the Python script into the container
26-
COPY huntarr.py /app/huntarr.py
27-
28-
# Make the script executable (optional but good practice)
29-
RUN chmod +x /app/huntarr.py
30-
31-
# The script’s entrypoint. It will run your `huntarr.py` when the container starts.
32-
ENTRYPOINT ["python", "huntarr.py"]
23+
# Run the application
24+
CMD ["python", "main.py"]

api.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Radarr API Helper Functions
4+
Handles all communication with the Radarr API
5+
"""
6+
7+
import requests
8+
import time
9+
import datetime
10+
from typing import List, Dict, Any, Optional, Union
11+
from utils.logger import logger, debug_log
12+
from config import API_KEY, API_URL, API_TIMEOUT, MONITORED_ONLY, SKIP_FUTURE_RELEASES
13+
14+
# Create a session for reuse
15+
session = requests.Session()
16+
17+
def radarr_request(endpoint: str, method: str = "GET", data: Dict = None) -> Optional[Union[Dict, List]]:
18+
"""
19+
Make a request to the Radarr API (v3).
20+
`endpoint` should be something like 'movie', 'command', etc.
21+
"""
22+
url = f"{API_URL}/api/v3/{endpoint}"
23+
headers = {
24+
"X-Api-Key": API_KEY,
25+
"Content-Type": "application/json"
26+
}
27+
28+
try:
29+
if method.upper() == "GET":
30+
response = session.get(url, headers=headers, timeout=API_TIMEOUT)
31+
elif method.upper() == "POST":
32+
response = session.post(url, headers=headers, json=data, timeout=API_TIMEOUT)
33+
else:
34+
logger.error(f"Unsupported HTTP method: {method}")
35+
return None
36+
37+
response.raise_for_status()
38+
return response.json()
39+
except requests.exceptions.RequestException as e:
40+
logger.error(f"API request error: {e}")
41+
return None
42+
43+
def get_movies() -> List[Dict]:
44+
"""Get all movies from Radarr (full list)"""
45+
result = radarr_request("movie")
46+
if result:
47+
debug_log("Raw movies API response sample:", result[:2] if len(result) > 2 else result)
48+
return result or []
49+
50+
def get_cutoff_unmet() -> List[Dict]:
51+
"""
52+
Directly query Radarr for only those movies where the quality cutoff is not met.
53+
This is the most reliable way for big libraries. Optionally filter by monitored.
54+
"""
55+
query = "movie?qualityCutoffNotMet=true"
56+
if MONITORED_ONLY:
57+
# Append &monitored=true to the querystring
58+
query += "&monitored=true"
59+
60+
# Perform the request
61+
result = radarr_request(query, method="GET")
62+
return result or []
63+
64+
def get_missing_movies() -> List[Dict]:
65+
"""
66+
Get a list of movies that are missing files.
67+
Filters based on MONITORED_ONLY setting and optionally
68+
excludes future releases.
69+
"""
70+
movies = get_movies()
71+
72+
if not movies:
73+
return []
74+
75+
missing_movies = []
76+
77+
# Get current date in ISO format (YYYY-MM-DD) for date comparison
78+
current_date = datetime.datetime.now().strftime("%Y-%m-%d")
79+
80+
for movie in movies:
81+
# Skip if not missing a file
82+
if movie.get('hasFile'):
83+
continue
84+
85+
# Apply monitored filter if needed
86+
if MONITORED_ONLY and not movie.get('monitored'):
87+
continue
88+
89+
# Skip future releases if enabled
90+
if SKIP_FUTURE_RELEASES:
91+
# Check physical, digital, and cinema release dates
92+
physical_release = movie.get('physicalRelease')
93+
digital_release = movie.get('digitalRelease')
94+
in_cinemas = movie.get('inCinemas')
95+
96+
# Use the earliest available release date for comparison
97+
release_date = None
98+
if physical_release:
99+
release_date = physical_release
100+
elif digital_release:
101+
release_date = digital_release
102+
elif in_cinemas:
103+
release_date = in_cinemas
104+
105+
# Skip if release date exists and is in the future
106+
if release_date and release_date > current_date:
107+
logger.debug(f"Skipping future release '{movie.get('title')}' with date {release_date}")
108+
continue
109+
110+
missing_movies.append(movie)
111+
112+
return missing_movies
113+
114+
def refresh_movie(movie_id: int) -> Optional[Dict]:
115+
"""Refresh a movie by ID"""
116+
data = {
117+
"name": "RefreshMovie",
118+
"movieIds": [movie_id]
119+
}
120+
return radarr_request("command", method="POST", data=data)
121+
122+
def movie_search(movie_id: int) -> Optional[Dict]:
123+
"""Search for a movie by ID"""
124+
data = {
125+
"name": "MoviesSearch",
126+
"movieIds": [movie_id]
127+
}
128+
return radarr_request("command", method="POST", data=data)
129+
130+
def rescan_movie(movie_id: int) -> Optional[Dict]:
131+
"""Rescan movie files"""
132+
data = {
133+
"name": "RescanMovie",
134+
"movieIds": [movie_id]
135+
}
136+
return radarr_request("command", method="POST", data=data)

config.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Configuration module for Huntarr-Radarr
4+
Handles all environment variables and configuration settings
5+
"""
6+
7+
import os
8+
import logging
9+
10+
# API Configuration
11+
API_KEY = os.environ.get("API_KEY", "your-api-key")
12+
API_URL = os.environ.get("API_URL", "http://your-radarr-address:7878")
13+
14+
# API timeout in seconds
15+
try:
16+
API_TIMEOUT = int(os.environ.get("API_TIMEOUT", "60"))
17+
except ValueError:
18+
API_TIMEOUT = 60
19+
print(f"Warning: Invalid API_TIMEOUT value, using default: {API_TIMEOUT}")
20+
21+
# Missing Content Settings
22+
try:
23+
HUNT_MISSING_MOVIES = int(os.environ.get("HUNT_MISSING_MOVIES", "1"))
24+
except ValueError:
25+
HUNT_MISSING_MOVIES = 1
26+
print(f"Warning: Invalid HUNT_MISSING_MOVIES value, using default: {HUNT_MISSING_MOVIES}")
27+
28+
# Upgrade Settings
29+
try:
30+
HUNT_UPGRADE_MOVIES = int(os.environ.get("HUNT_UPGRADE_MOVIES", "5"))
31+
except ValueError:
32+
HUNT_UPGRADE_MOVIES = 5
33+
print(f"Warning: Invalid HUNT_UPGRADE_MOVIES value, using default: {HUNT_UPGRADE_MOVIES}")
34+
35+
# Sleep duration in seconds after completing one full cycle (default 15 minutes)
36+
try:
37+
SLEEP_DURATION = int(os.environ.get("SLEEP_DURATION", "900"))
38+
except ValueError:
39+
SLEEP_DURATION = 900
40+
print(f"Warning: Invalid SLEEP_DURATION value, using default: {SLEEP_DURATION}")
41+
42+
# Reset processed state file after this many hours (default 168 hours = 1 week)
43+
try:
44+
STATE_RESET_INTERVAL_HOURS = int(os.environ.get("STATE_RESET_INTERVAL_HOURS", "168"))
45+
except ValueError:
46+
STATE_RESET_INTERVAL_HOURS = 168
47+
print(f"Warning: Invalid STATE_RESET_INTERVAL_HOURS value, using default: {STATE_RESET_INTERVAL_HOURS}")
48+
49+
# Selection Settings
50+
RANDOM_SELECTION = os.environ.get("RANDOM_SELECTION", "true").lower() == "true"
51+
MONITORED_ONLY = os.environ.get("MONITORED_ONLY", "true").lower() == "true"
52+
SKIP_FUTURE_RELEASES = os.environ.get("SKIP_FUTURE_RELEASES", "true").lower() == "true"
53+
54+
# Hunt mode: "missing", "upgrade", or "both"
55+
HUNT_MODE = os.environ.get("HUNT_MODE", "both")
56+
57+
# Debug Settings
58+
DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true"
59+
60+
def log_configuration(logger):
61+
"""Log the current configuration settings"""
62+
logger.info("=== Huntarr [Radarr Edition] Starting ===")
63+
logger.info(f"API URL: {API_URL}")
64+
logger.info(f"API Timeout: {API_TIMEOUT}s")
65+
logger.info(f"Missing Content Configuration: HUNT_MISSING_MOVIES={HUNT_MISSING_MOVIES}")
66+
logger.info(f"Upgrade Configuration: HUNT_UPGRADE_MOVIES={HUNT_UPGRADE_MOVIES}")
67+
logger.info(f"State Reset Interval: {STATE_RESET_INTERVAL_HOURS} hours")
68+
logger.info(f"MONITORED_ONLY={MONITORED_ONLY}, RANDOM_SELECTION={RANDOM_SELECTION}")
69+
logger.info(f"SKIP_FUTURE_RELEASES={SKIP_FUTURE_RELEASES}")
70+
logger.info(f"HUNT_MODE={HUNT_MODE}, SLEEP_DURATION={SLEEP_DURATION}s")
71+
logger.debug(f"API_KEY={API_KEY}")

0 commit comments

Comments
 (0)