diff --git a/Dockerfile b/Dockerfile index c2e402ab..e98dcd03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,27 @@ 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 state directory + +# 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" \ @@ -19,6 +32,15 @@ ENV API_KEY="your-api-key" \ STATE_RESET_INTERVAL_HOURS=168 \ RANDOM_SELECTION="true" \ MONITORED_ONLY="true" \ - DEBUG_MODE="false" -# Run the application -CMD ["python", "main.py"] \ No newline at end of file + DEBUG_MODE="false" \ + ENABLE_WEB_UI="true" + +# 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"] \ No newline at end of file diff --git a/README.md b/README.md index ffe3ac8d..a640ba77 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,18 @@ **NOTE**: This utilizes Sonarr API Version - `5`. Legacy name of this program: Sonarr Hunter. +--- + +**Change Log:** +Visit: https://github.com/plexguide/Huntarr-Sonarr/releases/ + ## Table of Contents - [Overview](#overview) - [Related Projects](#related-projects) - [Features](#features) - [How It Works](#how-it-works) - [Configuration Options](#configuration-options) +- [Web Interface](#web-interface) - [Installation Methods](#installation-methods) - [Docker Run](#docker-run) - [Docker Compose](#docker-compose) @@ -55,6 +61,7 @@ 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!) ## Indexers Approving of Huntarr: * https://ninjacentral.co.za @@ -110,8 +117,14 @@ The following environment variables can be configured: | `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 | + +### Advanced Options (Optional) + +| Variable | Description | Default | +|-------------------------------|-----------------------------------------------------------------------|---------------| | `COMMAND_WAIT_DELAY` | Delay in seconds between checking for command status | 1 | -| `COMMAND_WAIT_ATTEMPTS` | Number of attempts to check for command completeion before giving up | 600 | +| `COMMAND_WAIT_ATTEMPTS` | Number of attempts to check for command completion before giving up | 600 | | `MINIMUM_DOWNLOAD_QUEUE_SIZE` | Minimum number of items in the download queue before starting a hunt | -1 | ### Detailed Configuration Explanation @@ -149,6 +162,11 @@ The following environment variables can be configured: - When set to `true`, the script will output detailed debugging information about API responses and internal operations. - Useful for troubleshooting issues but can make logs verbose. +- **ENABLE_WEB_UI** + - When set to `true`, the web interface will be enabled on port 8988. + - When set to `false`, the web interface will not start, saving resources. + - Default is `true` for convenient monitoring. + - **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. @@ -163,6 +181,53 @@ The following environment variables can be configured: - This helps prevent overwhelming the queue with too many download requests at once and avoids creating a massive backlog of downloads. - Set to `-1` to disable this check. +## Web Interface + +Huntarr-Sonarr includes a real-time log viewer web interface that allows you to monitor its operation directly from your browser. + +### Features + +- **Real-time Log Updates**: Logs refresh automatically every second +- **Day/Night Mode**: Toggle between light and dark themes +- **Color-coded Log Entries**: Different log levels are displayed in different colors +- **Auto-scrolling**: Automatically scrolls to the latest log entries +- **Connection Status**: Shows whether the connection to the log stream is active + +### How to Access + +The web interface is available on port 8988. Simply navigate to: + +``` +http://YOUR_SERVER_IP:8988 +``` + +Or if you're accessing it locally: + +``` +http://localhost:8988 +``` + +### 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`. + +For example: +- `8988:8988` means "map port 8988 from the host to port 8988 in the container" + +If you want to use a different port on your host (e.g., 9000), you would use: +- `9000:8988` means "map port 9000 from the host to port 8988 in the container" + +You would then access the web interface at `http://YOUR_SERVER_IP:9000` + +### Enabling/Disabling the Web UI + +The web interface can be enabled or disabled using the `ENABLE_WEB_UI` environment variable: + +- `ENABLE_WEB_UI=true` - Enable the web interface (default) +- `ENABLE_WEB_UI=false` - Disable the web interface + +If you disable the web interface, you don't need to expose the port in your Docker configuration. + --- ## Installation Methods @@ -174,6 +239,7 @@ The simplest way to run Huntarr is via Docker: ```bash docker run -d --name huntarr-sonarr \ --restart always \ + -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 MONITORED_ONLY="true" \ @@ -183,10 +249,16 @@ docker run -d --name huntarr-sonarr \ -e RANDOM_SELECTION="true" \ -e STATE_RESET_INTERVAL_HOURS="168" \ -e DEBUG_MODE="false" \ + -e ENABLE_WEB_UI="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 should see new files downloading or you can type: +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: ```bash docker logs huntarr-sonarr ``` @@ -202,6 +274,8 @@ services: image: huntarr/4sonarr:latest container_name: huntarr-sonarr restart: always + ports: + - "8988:8988" # Can be removed if ENABLE_WEB_UI=false environment: API_KEY: "your-api-key" API_URL: "http://your-sonarr-address:8989" @@ -213,6 +287,12 @@ services: RANDOM_SELECTION: "true" STATE_RESET_INTERVAL_HOURS: "168" DEBUG_MODE: "false" + ENABLE_WEB_UI: "true" + + # Optional advanced settings + # COMMAND_WAIT_DELAY: "1" + # COMMAND_WAIT_ATTEMPTS: "600" + # MINIMUM_DOWNLOAD_QUEUE_SIZE: "-1" ``` Then run: @@ -228,6 +308,7 @@ Run this from Command Line in Unraid: ```bash docker run -d --name huntarr-sonarr \ --restart always \ + -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" \ @@ -238,7 +319,13 @@ docker run -d --name huntarr-sonarr \ -e RANDOM_SELECTION="true" \ -e STATE_RESET_INTERVAL_HOURS="168" \ -e DEBUG_MODE="false" \ + -e ENABLE_WEB_UI="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 @@ -289,6 +376,7 @@ sudo systemctl start huntarr - **New Show Setup**: Automatically find episodes for newly added shows - **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 ## Tips @@ -297,6 +385,9 @@ sudo systemctl start huntarr - **Batch Size Control**: Adjust `HUNT_MISSING_SHOWS` and `HUNT_UPGRADE_EPISODES` based on your indexer's rate limits - **Monitored Status**: Set `MONITORED_ONLY=false` if you want to download all missing episodes regardless of monitored status - **System Resources**: The script uses minimal resources and can run continuously on even low-powered systems +- **Web Interface**: Use the web interface to monitor progress instead of checking Docker logs +- **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 ## Troubleshooting @@ -304,33 +395,17 @@ sudo systemctl start huntarr - **API Key Issues**: Check that your API key is correct in Sonarr settings - **Connection Problems**: Ensure the Sonarr URL is accessible from where you're running the script - **Command Failures**: If search commands fail, try using the Sonarr UI to verify what commands are available in your version +- **Web Interface Not Loading**: Make sure port 8988 is exposed in your Docker configuration and not blocked by a firewall - **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 --- -**Change Log:** -- **v1**: Original code written -- **v2**: Optimized search -- **v3**: Variable names changed for docker optimization -- **v4**: Added monitored only tag -- **v5**: Added quality upgrade functionality to find episodes below cutoff quality -- **v6**: Added state tracking to prevent duplicate searches -- **v7**: Implemented configurable state reset timer -- **v8**: Added debug mode and improved error handling -- **v9**: Enhanced random selection mode for better distribution -- **v10**: Renamed from "Sonarr Hunter" to "Huntarr" -- **v11**: Complete modular refactoring for better maintainability -- **v12**: Improved variable naming with HUNT_ prefix -- **v13**: Enhanced state management and cycle processing - ---- - This script helps automate the tedious process of finding missing episodes and quality upgrades in your TV collection, running quietly in the background while respecting your indexers' rate limits. --- 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! \ No newline at end of file diff --git a/config.py b/config.py index fd2ad61b..2cf8e5f1 100644 --- a/config.py +++ b/config.py @@ -7,6 +7,9 @@ import os import logging +# Web UI Configuration +ENABLE_WEB_UI = os.environ.get("ENABLE_WEB_UI", "true").lower() == "true" + # API Configuration API_KEY = os.environ.get("API_KEY", "your-api-key") API_URL = os.environ.get("API_URL", "http://your-sonarr-address:8989") @@ -89,4 +92,5 @@ 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"ENABLE_WEB_UI={ENABLE_WEB_UI}") logger.debug(f"API_KEY={API_KEY}") \ No newline at end of file diff --git a/main.py b/main.py index 636f59f0..7545ec49 100644 --- a/main.py +++ b/main.py @@ -6,15 +6,39 @@ import time import sys +import os +import socket from utils.logger import logger -from config import HUNT_MODE, SLEEP_DURATION, MINIMUM_DOWNLOAD_QUEUE_SIZE, log_configuration +from config import HUNT_MODE, SLEEP_DURATION, MINIMUM_DOWNLOAD_QUEUE_SIZE, ENABLE_WEB_UI, log_configuration from missing import process_missing_episodes from upgrade import process_cutoff_upgrades from state import check_state_reset, calculate_reset_time from api import get_download_queue_size +def get_ip_address(): + """Get the host's IP address or hostname for display""" + try: + # Try to get the container's hostname + hostname = socket.gethostname() + # Try to get the container's IP + ip = socket.gethostbyname(hostname) + return ip + except: + return "YOUR_SERVER_IP" + def main_loop() -> None: """Main processing loop for Huntarr-Sonarr""" + + # Log welcome message for web interface + logger.info("=== Huntarr [Sonarr Edition] Starting ===") + + # Log web UI information if enabled + if ENABLE_WEB_UI: + server_ip = get_ip_address() + logger.info(f"Web interface available at http://{server_ip}:8988") + + logger.info("GitHub: https://github.com/plexguide/huntarr-sonarr") + while True: # Check if state files need to be reset check_state_reset() @@ -38,7 +62,7 @@ def main_loop() -> None: processing_done = True else: - logger.info(f"Download queue size ({download_queue_size}) is above the minimum threshold ({MINIMUM_DOWNLOAD_QUEUE_SIZE}). Skipped processing.") + logger.info(f"Download queue size ({download_queue_size}) is above the minimum threshold ({MINIMUM_DOWNLOAD_QUEUE_SIZE}). Skipped processing.") # Calculate time until the next reset calculate_reset_time() @@ -46,7 +70,24 @@ def main_loop() -> None: # Sleep at the end of the cycle only logger.info(f"Cycle complete. Sleeping {SLEEP_DURATION}s before next cycle...") logger.info("⭐ Tool Great? Donate @ https://donate.plex.one for Daughter's College Fund!") - time.sleep(SLEEP_DURATION) + + # Log web UI information if enabled + if ENABLE_WEB_UI: + server_ip = get_ip_address() + logger.info(f"Web interface available at http://{server_ip}:8988") + + # Sleep with progress updates for the web interface + sleep_start = time.time() + sleep_end = sleep_start + SLEEP_DURATION + + while time.time() < sleep_end: + # Sleep in smaller chunks for more responsive shutdown + time.sleep(min(10, sleep_end - time.time())) + + # Every minute, log the remaining sleep time for web interface visibility + if int((time.time() - sleep_start) % 60) == 0 and time.time() < sleep_end - 10: + remaining = int(sleep_end - time.time()) + logger.debug(f"Sleeping... {remaining}s remaining until next cycle") if __name__ == "__main__": # Log configuration settings diff --git a/requirements.txt b/requirements.txt index eed69883..730695f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -requests>=2.25.0 \ No newline at end of file +requests>=2.25.0 +flask>=2.0.0 \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100644 index 00000000..4da6ea4b --- /dev/null +++ b/start.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Startup script for Huntarr-Sonarr that conditionally starts the web UI + +# Convert to lowercase +ENABLE_WEB_UI=$(echo "${ENABLE_WEB_UI:-true}" | tr '[:upper:]' '[:lower:]') + +if [ "$ENABLE_WEB_UI" = "true" ]; then + echo "Starting with Web UI enabled on port 8988" + # Start both the web server and the main application + python web_server.py & + python main.py +else + echo "Web UI disabled, starting only the main application" + # Start only the main application + python main.py +fi \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 00000000..4d332297 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,318 @@ + + + + + + Huntarr-Sonarr Log Viewer + + + +
+

Huntarr-Sonarr Log Viewer

+

Visit the Huntarr [Sonarr Edition] GITHUB Page and click the ⭐ in the Upper Right!

+
+ +
+ 💰 Tool Great? Donate @ https://donate.plex.one for my Daughter's College Fund! +
+ +
+
Connected: Yes | Auto-refresh: Every 1 second
+
+
+ + Auto-scroll ON +
+
+ + Night Mode +
+
+
+ +
+ + + + + + \ No newline at end of file diff --git a/utils/.github/workflows/docker-image.yml b/utils/.github/workflows/docker-image.yml new file mode 100644 index 00000000..ea7c086d --- /dev/null +++ b/utils/.github/workflows/docker-image.yml @@ -0,0 +1,93 @@ +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 05128a62..7a1150c5 100644 --- a/utils/logger.py +++ b/utils/logger.py @@ -6,8 +6,14 @@ import logging 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") @@ -19,15 +25,21 @@ def setup_logger(): console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(logging.DEBUG if 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) + # Set format formatter = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) console_handler.setFormatter(formatter) + file_handler.setFormatter(formatter) - # Add handler to logger + # Add handlers to logger logger.addHandler(console_handler) + logger.addHandler(file_handler) return logger diff --git a/web_server.py b/web_server.py new file mode 100644 index 00000000..6ba8a09a --- /dev/null +++ b/web_server.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +Web server for Huntarr-Sonarr +Provides a web interface to view logs in real-time +""" + +import os +import time +import datetime +import pathlib +import socket +from flask import Flask, render_template, Response, stream_with_context +import logging +from config import ENABLE_WEB_UI + +# Check if web UI is enabled +if not ENABLE_WEB_UI: + print("Web UI is disabled. Exiting web server.") + exit(0) + +# Disable Flask default logging +log = logging.getLogger('werkzeug') +log.setLevel(logging.ERROR) + +# Create Flask app +app = Flask(__name__) + +# Log file location +LOG_FILE = "/tmp/huntarr-logs/huntarr.log" +LOG_DIR = pathlib.Path("/tmp/huntarr-logs") +LOG_DIR.mkdir(parents=True, exist_ok=True) + +@app.route('/') +def index(): + """Render the main page""" + return render_template('index.html') + +@app.route('/logs') +def stream_logs(): + """Stream logs to the client""" + def generate(): + # First get all existing logs + if os.path.exists(LOG_FILE): + with open(LOG_FILE, 'r') as f: + # Read the last 100 lines of the log file + lines = f.readlines()[-100:] + for line in lines: + yield f"data: {line}\n\n" + + # Then stream new logs as they appear + with open(LOG_FILE, 'r') as f: + # Move to the end of the file + f.seek(0, 2) + while True: + line = f.readline() + if line: + yield f"data: {line}\n\n" + else: + time.sleep(0.1) + + return Response(stream_with_context(generate()), + mimetype='text/event-stream') + +def get_ip_address(): + """Get the host's IP address or hostname for display""" + try: + hostname = socket.gethostname() + ip = socket.gethostbyname(hostname) + return ip + except: + return "localhost" + +if __name__ == "__main__": + # Create a basic log entry at startup + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ip_address = get_ip_address() + + with open(LOG_FILE, 'a') as f: + f.write(f"{timestamp} - huntarr-web - INFO - Web server starting on port 8988\n") + f.write(f"{timestamp} - huntarr-web - INFO - Web interface available at http://{ip_address}:8988\n") + + # Run the Flask app + app.run(host='0.0.0.0', port=8988, debug=False, threaded=True) \ No newline at end of file