diff --git a/README.md b/README.md
index 1b3710b1..897c9c88 100644
--- a/README.md
+++ b/README.md
@@ -205,6 +205,7 @@ Learn more about **Cleanuperr** at [https://github.com/flmorg/cleanuperr](https:
- **API Rate Limits**: Configure hourly API caps to prevent rate limiting by your indexers
- **Universal Timeouts**: All apps use consistent 120s timeouts for reliable command completion
- **Monitored Only**: Filter searches to focus only on content you've marked as monitored
+- **Notifications**: Configure Apprise notifications to get alerts when media is processed
## Troubleshooting
diff --git a/frontend/static/js/settings_forms.js b/frontend/static/js/settings_forms.js
index 7dde0a22..5062c4f5 100644
--- a/frontend/static/js/settings_forms.js
+++ b/frontend/static/js/settings_forms.js
@@ -1062,6 +1062,21 @@ const SettingsForms = {
settings.ssl_verify = getInputValue('#ssl_verify', true);
settings.stateful_management_hours = getInputValue('#stateful_management_hours', 168);
+ // Notification settings
+ settings.enable_notifications = getInputValue('#enable_notifications', false);
+ settings.notification_level = container.querySelector('#notification_level')?.value || 'info';
+
+ // Process apprise URLs (split by newline)
+ const appriseUrlsText = container.querySelector('#apprise_urls')?.value || '';
+ settings.apprise_urls = appriseUrlsText.split('\n')
+ .map(url => url.trim())
+ .filter(url => url.length > 0);
+
+ settings.notify_on_missing = getInputValue('#notify_on_missing', true);
+ settings.notify_on_upgrade = getInputValue('#notify_on_upgrade', true);
+ settings.notification_include_instance = getInputValue('#notification_include_instance', true);
+ settings.notification_include_app = getInputValue('#notification_include_app', true);
+
// Handle the auth_mode dropdown
const authMode = container.querySelector('#auth_mode')?.value || 'login';
@@ -1327,6 +1342,66 @@ const SettingsForms = {
Base URL path for reverse proxy (e.g., '/huntarr'). Leave empty for root path. Requires restart.
+
+
+
Notifications
+
+
+
+
Enable sending notifications via Apprise
+
+
+
+
+
Minimum level of events that will trigger notifications
+
+
+
+
+
+
Send notifications when missing media is processed
+
+
+
+
+
Send notifications when media is upgraded
+
+
+
+
+
Include instance name in notification messages
+
+
+
+
+
Include app name (Sonarr, Radarr, etc.) in notification messages
+
+
`;
// Get hours input and days span elements once
diff --git a/requirements.txt b/requirements.txt
index 419b6dee..8c3a852d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,4 +4,5 @@ waitress==2.1.2
bcrypt==4.1.2
qrcode[pil]==7.4.2 # Added qrcode with PIL support
pyotp==2.9.0 # Added pyotp
-pywin32==306; sys_platform == 'win32' # For Windows service support
\ No newline at end of file
+pywin32==306; sys_platform == 'win32' # For Windows service support
+apprise==1.6.0 # Added for notification support
\ No newline at end of file
diff --git a/src/primary/default_configs/general.json b/src/primary/default_configs/general.json
index 6cd5021d..5e71e829 100644
--- a/src/primary/default_configs/general.json
+++ b/src/primary/default_configs/general.json
@@ -6,6 +6,11 @@
"check_for_updates": true,
"enable_notifications": false,
"notification_level": "info",
+ "apprise_urls": [],
+ "notify_on_missing": true,
+ "notify_on_upgrade": true,
+ "notification_include_instance": true,
+ "notification_include_app": true,
"local_access_bypass": false,
"proxy_auth_bypass": false,
"stateful_management_hours": 168,
diff --git a/src/primary/history_manager.py b/src/primary/history_manager.py
index 2cd939e3..ffb84059 100644
--- a/src/primary/history_manager.py
+++ b/src/primary/history_manager.py
@@ -120,6 +120,15 @@ def add_history_entry(app_type, entry_data):
json.dump(history_data, f, indent=2)
logger.info(f"Added history entry for {app_type}-{instance_name}: {entry_data['name']}")
+
+ # Send notification about this history entry
+ try:
+ # Import here to avoid circular imports
+ from src.primary.notification_manager import send_history_notification
+ send_history_notification(entry)
+ except Exception as e:
+ logger.error(f"Failed to send notification for history entry: {e}")
+
return entry
def get_history(app_type, search_query=None, page=1, page_size=20):
diff --git a/src/primary/notification_manager.py b/src/primary/notification_manager.py
new file mode 100644
index 00000000..23303b87
--- /dev/null
+++ b/src/primary/notification_manager.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+"""
+Notification manager for Huntarr
+Handles sending notifications via Apprise
+"""
+
+import logging
+import json
+from typing import Dict, Any, Optional, List
+
+# Lazy import Apprise to avoid startup issues if the package is not installed
+apprise_import_error = None
+try:
+ import apprise
+except ImportError as e:
+ apprise_import_error = str(e)
+
+# Create a logger for the notification manager
+logger = logging.getLogger(__name__)
+
+# Import the settings manager
+from src.primary.settings_manager import get_setting, load_settings
+
+def get_notification_config():
+ """
+ Get the notification configuration from general settings
+
+ Returns:
+ dict: The notification configuration
+ """
+ general_settings = load_settings('general')
+ notification_config = {
+ 'enabled': general_settings.get('enable_notifications', False),
+ 'level': general_settings.get('notification_level', 'info'),
+ 'apprise_urls': general_settings.get('apprise_urls', []),
+ 'notify_on_missing': general_settings.get('notify_on_missing', True),
+ 'notify_on_upgrade': general_settings.get('notify_on_upgrade', True),
+ 'include_instance_name': general_settings.get('notification_include_instance', True),
+ 'include_app_name': general_settings.get('notification_include_app', True)
+ }
+
+ return notification_config
+
+def create_apprise_object():
+ """
+ Create and configure an Apprise object with the URLs from settings
+
+ Returns:
+ apprise.Apprise: Configured Apprise object or None if there was an error
+ """
+ if apprise_import_error:
+ logger.error(f"Apprise is not available: {apprise_import_error}")
+ return None
+
+ config = get_notification_config()
+
+ if not config['enabled'] or not config['apprise_urls']:
+ return None
+
+ try:
+ # Create an Apprise instance
+ apobj = apprise.Apprise()
+
+ # Add all the URLs to our Apprise object
+ for url in config['apprise_urls']:
+ if url and url.strip():
+ added = apobj.add(url.strip())
+ if added:
+ logger.debug(f"Added Apprise URL: {url[:15]}...")
+ else:
+ logger.warning(f"Failed to add Apprise URL: {url[:15]}...")
+
+ return apobj
+ except Exception as e:
+ logger.error(f"Error creating Apprise object: {e}")
+ return None
+
+def send_notification(title, message, level='info', attach=None):
+ """
+ Send a notification via Apprise
+
+ Args:
+ title (str): The notification title
+ message (str): The notification message
+ level (str): The notification level (info, success, warning, error)
+ attach (str, optional): Path to a file to attach
+
+ Returns:
+ bool: True if notification was sent successfully, False otherwise
+ """
+ if apprise_import_error:
+ logger.error(f"Cannot send notification, Apprise is not available: {apprise_import_error}")
+ return False
+
+ config = get_notification_config()
+
+ if not config['enabled']:
+ logger.debug("Notifications are disabled in settings")
+ return False
+
+ # Check if the notification level is high enough to send
+ levels = {
+ 'debug': 0,
+ 'info': 1,
+ 'success': 1,
+ 'warning': 2,
+ 'error': 3
+ }
+
+ if levels.get(level, 0) < levels.get(config['level'], 1):
+ logger.debug(f"Notification level {level} is below configured level {config['level']}")
+ return False
+
+ # Create Apprise object
+ apobj = create_apprise_object()
+ if not apobj:
+ return False
+
+ # Set notification type based on level
+ notify_type = apprise.NotifyType.INFO
+
+ if level == 'success':
+ notify_type = apprise.NotifyType.SUCCESS
+ elif level == 'warning':
+ notify_type = apprise.NotifyType.WARNING
+ elif level == 'error':
+ notify_type = apprise.NotifyType.FAILURE
+
+ try:
+ # Send notification
+ result = apobj.notify(
+ body=message,
+ title=title,
+ notify_type=notify_type,
+ attach=attach
+ )
+
+ logger.info(f"Notification sent (level={level}): {title}")
+ return result
+
+ except Exception as e:
+ logger.error(f"Failed to send notification: {e}")
+ return False
+
+def send_history_notification(entry_data, operation_type=None):
+ """
+ Send a notification about a history entry
+
+ Args:
+ entry_data (dict): The history entry data
+ operation_type (str, optional): Override the operation type
+
+ Returns:
+ bool: True if notification was sent successfully, False otherwise
+ """
+ config = get_notification_config()
+
+ if not config['enabled']:
+ return False
+
+ # Skip if we shouldn't notify on this operation type
+ op_type = operation_type or entry_data.get('operation_type', 'missing')
+ if op_type == 'missing' and not config.get('notify_on_missing', True):
+ return False
+ if op_type == 'upgrade' and not config.get('notify_on_upgrade', True):
+ return False
+
+ # Determine notification level based on operation type
+ level = 'info'
+ if op_type == 'error':
+ level = 'error'
+ elif op_type == 'upgrade':
+ level = 'success'
+
+ # Build notification title
+ title_parts = ["Huntarr"]
+
+ if config.get('include_app_name', True) and 'app_type' in entry_data:
+ app_type = entry_data['app_type']
+ # Capitalize app name
+ title_parts.append(app_type.capitalize())
+
+ if config.get('include_instance_name', True) and 'instance_name' in entry_data:
+ title_parts.append(f"({entry_data['instance_name']})")
+
+ title = " ".join(title_parts)
+
+ # Build notification message
+ if op_type == 'missing':
+ message = f"Added Missing: {entry_data.get('processed_info', 'Unknown')}"
+ elif op_type == 'upgrade':
+ message = f"Added Upgrade: {entry_data.get('processed_info', 'Unknown')}"
+ elif op_type == 'error':
+ message = f"Error Processing: {entry_data.get('processed_info', 'Unknown')}"
+ else:
+ message = f"{op_type.capitalize()}: {entry_data.get('processed_info', 'Unknown')}"
+
+ # Send the notification
+ return send_notification(title, message, level=level)
+
+# Example usage (for testing)
+if __name__ == "__main__":
+ import sys
+ logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S')
+
+ # Test notification
+ result = send_notification(
+ title="Huntarr Test Notification",
+ message="This is a test notification from Huntarr",
+ level="info"
+ )
+
+ logger.info(f"Notification result: {result}")
\ No newline at end of file
diff --git a/version.txt b/version.txt
index bf993904..19300b7b 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-7.0.7
\ No newline at end of file
+7.0.8
\ No newline at end of file