Skip to content

Commit 66ab6b5

Browse files
committed
Add Prowlarr status monitoring and UI integration
- Implemented Prowlarr status polling in the frontend, refreshing every 30 seconds when the home section is active. - Added a new Prowlarr status card in the UI, displaying connection status, active indexers, total API calls, throttled indexers, and failed indexers. - Created backend API route to fetch Prowlarr statistics, including connection health and indexer status. - Enhanced error handling for Prowlarr data retrieval and updated UI elements for better user feedback.
1 parent 1836e40 commit 66ab6b5

File tree

3 files changed

+368
-1
lines changed

3 files changed

+368
-1
lines changed

frontend/static/js/new-main.js

Lines changed: 175 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ let huntarrUI = {
151151
// Setup Swaparr status polling (refresh every 30 seconds)
152152
this.setupSwaparrStatusPolling();
153153

154+
// Setup Prowlarr status polling (refresh every 30 seconds)
155+
this.setupProwlarrStatusPolling();
156+
154157
// Make dashboard visible after initialization to prevent FOUC
155158
setTimeout(() => {
156159
this.showDashboard();
@@ -2300,7 +2303,178 @@ let huntarrUI = {
23002303
}
23012304
}, 30000);
23022305
},
2303-
2306+
2307+
// Setup Prowlarr status polling
2308+
setupProwlarrStatusPolling: function() {
2309+
// Load initial status
2310+
this.loadProwlarrStatus();
2311+
2312+
// Set up polling to refresh Prowlarr status every 30 seconds
2313+
// Only poll when home section is active to reduce unnecessary requests
2314+
setInterval(() => {
2315+
if (this.currentSection === 'home') {
2316+
this.loadProwlarrStatus();
2317+
}
2318+
}, 30000);
2319+
},
2320+
2321+
// Load and update Prowlarr status card
2322+
loadProwlarrStatus: function() {
2323+
HuntarrUtils.fetchWithTimeout('./api/prowlarr/status')
2324+
.then(response => response.json())
2325+
.then(statusData => {
2326+
const prowlarrCard = document.getElementById('prowlarrStatusCard');
2327+
if (!prowlarrCard) return;
2328+
2329+
// Show/hide card based on whether Prowlarr is configured and enabled
2330+
if (statusData.configured && statusData.enabled) {
2331+
prowlarrCard.style.display = 'block';
2332+
2333+
// Update connection status
2334+
const statusElement = document.getElementById('prowlarrConnectionStatus');
2335+
if (statusElement) {
2336+
if (statusData.connected) {
2337+
statusElement.textContent = '🟢 Connected';
2338+
statusElement.className = 'status-badge success';
2339+
} else {
2340+
statusElement.textContent = '🔴 Disconnected';
2341+
statusElement.className = 'status-badge error';
2342+
}
2343+
}
2344+
2345+
// Load detailed stats if connected
2346+
if (statusData.connected) {
2347+
this.loadProwlarrStats();
2348+
} else {
2349+
// Show disconnected state
2350+
this.updateProwlarrStatsDisplay({
2351+
active_indexers: '--',
2352+
total_api_calls: '--',
2353+
throttled_indexers: '--',
2354+
failed_indexers: '--',
2355+
health_status: 'Disconnected'
2356+
});
2357+
}
2358+
2359+
// Setup refresh button
2360+
this.setupProwlarrRefreshButton();
2361+
2362+
} else {
2363+
prowlarrCard.style.display = 'none';
2364+
}
2365+
})
2366+
.catch(error => {
2367+
console.error('Error loading Prowlarr status:', error);
2368+
const prowlarrCard = document.getElementById('prowlarrStatusCard');
2369+
if (prowlarrCard) {
2370+
prowlarrCard.style.display = 'none';
2371+
}
2372+
});
2373+
},
2374+
2375+
// Load detailed Prowlarr statistics
2376+
loadProwlarrStats: function() {
2377+
HuntarrUtils.fetchWithTimeout('./api/prowlarr/stats')
2378+
.then(response => response.json())
2379+
.then(data => {
2380+
if (data.success) {
2381+
this.updateProwlarrStatsDisplay(data.stats);
2382+
} else {
2383+
console.error('Failed to load Prowlarr stats:', data.error);
2384+
this.updateProwlarrStatsDisplay({
2385+
active_indexers: '--',
2386+
total_api_calls: '--',
2387+
throttled_indexers: '--',
2388+
failed_indexers: '--',
2389+
health_status: 'Error loading stats'
2390+
});
2391+
}
2392+
})
2393+
.catch(error => {
2394+
console.error('Error loading Prowlarr stats:', error);
2395+
this.updateProwlarrStatsDisplay({
2396+
active_indexers: '--',
2397+
total_api_calls: '--',
2398+
throttled_indexers: '--',
2399+
failed_indexers: '--',
2400+
health_status: 'Connection error'
2401+
});
2402+
});
2403+
},
2404+
2405+
// Update Prowlarr stats display
2406+
updateProwlarrStatsDisplay: function(stats) {
2407+
// Update stat numbers
2408+
const activeElement = document.getElementById('prowlarr-active-indexers');
2409+
if (activeElement) activeElement.textContent = stats.active_indexers;
2410+
2411+
const callsElement = document.getElementById('prowlarr-total-calls');
2412+
if (callsElement) callsElement.textContent = this.formatLargeNumber(stats.total_api_calls);
2413+
2414+
const throttledElement = document.getElementById('prowlarr-throttled');
2415+
if (throttledElement) throttledElement.textContent = stats.throttled_indexers;
2416+
2417+
const failedElement = document.getElementById('prowlarr-failed');
2418+
if (failedElement) failedElement.textContent = stats.failed_indexers;
2419+
2420+
// Update health status
2421+
const healthElement = document.getElementById('prowlarr-health-status');
2422+
if (healthElement) {
2423+
healthElement.textContent = stats.health_status || 'Unknown';
2424+
2425+
// Add color coding based on health
2426+
if (stats.health_status && stats.health_status.includes('throttled')) {
2427+
healthElement.style.color = '#f59e0b'; // amber
2428+
} else if (stats.health_status && (stats.health_status.includes('failed') || stats.health_status.includes('disabled'))) {
2429+
healthElement.style.color = '#ef4444'; // red
2430+
} else if (stats.health_status && stats.health_status.includes('healthy')) {
2431+
healthElement.style.color = '#10b981'; // green
2432+
} else {
2433+
healthElement.style.color = '#9ca3af'; // gray
2434+
}
2435+
}
2436+
},
2437+
2438+
// Setup Prowlarr refresh button
2439+
setupProwlarrRefreshButton: function() {
2440+
const refreshButton = document.getElementById('refresh-prowlarr-data');
2441+
if (refreshButton) {
2442+
refreshButton.addEventListener('click', () => {
2443+
this.refreshProwlarrData();
2444+
});
2445+
}
2446+
},
2447+
2448+
// Refresh Prowlarr data
2449+
refreshProwlarrData: function() {
2450+
// Prevent multiple refreshes
2451+
if (this.prowlarrRefreshInProgress) {
2452+
return;
2453+
}
2454+
2455+
this.prowlarrRefreshInProgress = true;
2456+
2457+
// Update button to show refreshing state
2458+
const refreshButton = document.getElementById('refresh-prowlarr-data');
2459+
if (refreshButton) {
2460+
const originalText = refreshButton.innerHTML;
2461+
refreshButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Refreshing...';
2462+
refreshButton.disabled = true;
2463+
2464+
// Restore button after refresh
2465+
setTimeout(() => {
2466+
refreshButton.innerHTML = originalText;
2467+
refreshButton.disabled = false;
2468+
this.prowlarrRefreshInProgress = false;
2469+
}, 2000);
2470+
}
2471+
2472+
// Reload Prowlarr status and stats
2473+
this.loadProwlarrStatus();
2474+
2475+
// Show notification
2476+
this.showNotification('Prowlarr data refreshed', 'success');
2477+
},
23042478

23052479

23062480
// User

frontend/templates/components/home_section.html

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,75 @@ <h3><i class="fas fa-download"></i> Swaparr Status</h3>
580580
</div>
581581
</div>
582582

583+
<!-- Additional CSS for Prowlarr Status Card -->
584+
<style>
585+
.prowlarr-stats-grid {
586+
display: grid;
587+
grid-template-columns: 1fr;
588+
gap: 15px;
589+
width: 100%;
590+
}
591+
592+
.prowlarr-health-indicator {
593+
margin-top: 10px;
594+
padding: 8px 12px;
595+
background-color: rgba(32, 38, 50, 0.6);
596+
border-radius: 6px;
597+
border-left: 3px solid #6366f1;
598+
}
599+
600+
.health-status-text {
601+
font-size: 0.9em;
602+
font-weight: 500;
603+
color: #9ca3af;
604+
}
605+
</style>
606+
607+
<!-- Prowlarr Status Card -->
608+
<div class="card stats-card" id="prowlarrStatusCard" style="display: none;">
609+
<div class="card-header">
610+
<h3><i class="fas fa-search"></i> Prowlarr Monitor</h3>
611+
<button id="refresh-prowlarr-data" class="action-button success"><i class="fas fa-sync-alt"></i> Refresh</button>
612+
</div>
613+
<div class="media-stats-container">
614+
<div class="prowlarr-stats-grid">
615+
<!-- Prowlarr Stats Card -->
616+
<div class="app-stats-card prowlarr">
617+
<div class="status-container">
618+
<span id="prowlarrConnectionStatus" class="status-badge">⚫ Disconnected</span>
619+
</div>
620+
<div class="app-content">
621+
<div class="app-icon-wrapper">
622+
<img src="./static/images/app-icons/sonarr.png" alt="Prowlarr Logo" class="app-logo" style="filter: hue-rotate(60deg);">
623+
</div>
624+
<h4>Prowlarr</h4>
625+
</div>
626+
<div class="stats-numbers">
627+
<div class="stat-box">
628+
<div class="stat-number" id="prowlarr-active-indexers">--</div>
629+
<div class="stat-label">Active Indexers</div>
630+
</div>
631+
<div class="stat-box">
632+
<div class="stat-number" id="prowlarr-total-calls">--</div>
633+
<div class="stat-label">Total API Calls</div>
634+
</div>
635+
<div class="stat-box">
636+
<div class="stat-number" id="prowlarr-throttled">--</div>
637+
<div class="stat-label">Throttled Indexers</div>
638+
</div>
639+
<div class="stat-box">
640+
<div class="stat-number" id="prowlarr-failed">--</div>
641+
<div class="stat-label">Failed Indexers</div>
642+
</div>
643+
</div>
644+
<div class="prowlarr-health-indicator">
645+
<div id="prowlarr-health-status" class="health-status-text">Checking connection...</div>
646+
</div>
647+
</div>
648+
</div>
649+
</div>
650+
</div>
651+
583652
</div>
584653
</section>
585654

src/primary/apps/prowlarr_routes.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,130 @@ def get_status():
151151
prowlarr_logger.error(f"Error getting Prowlarr status: {str(e)}")
152152
return jsonify({"configured": False, "connected": False, "error": str(e)})
153153

154+
@prowlarr_bp.route('/stats', methods=['GET'])
155+
def get_prowlarr_stats():
156+
"""Get Prowlarr statistics for homepage monitoring"""
157+
try:
158+
settings = load_settings("prowlarr")
159+
160+
api_url = settings.get("api_url", "").strip()
161+
api_key = settings.get("api_key", "").strip()
162+
enabled = settings.get("enabled", True)
163+
164+
if not api_url or not api_key or not enabled:
165+
return jsonify({
166+
'success': False,
167+
'error': 'Prowlarr is not configured or enabled'
168+
}), 400
169+
170+
# Clean URL
171+
if not api_url.startswith(('http://', 'https://')):
172+
api_url = f'http://{api_url}'
173+
174+
headers = {'X-Api-Key': api_key}
175+
176+
# Initialize stats
177+
stats = {
178+
'connected': False,
179+
'active_indexers': 0,
180+
'total_indexers': 0,
181+
'throttled_indexers': 0,
182+
'failed_indexers': 0,
183+
'total_api_calls': 0,
184+
'version': 'Unknown',
185+
'health_status': 'Disconnected'
186+
}
187+
188+
try:
189+
# Get system status
190+
status_url = f"{api_url.rstrip('/')}/api/v1/system/status"
191+
status_response = requests.get(status_url, headers=headers, timeout=10)
192+
193+
if status_response.status_code == 200:
194+
status_data = status_response.json()
195+
stats['connected'] = True
196+
stats['version'] = status_data.get('version', 'Unknown')
197+
stats['health_status'] = 'Connected'
198+
199+
# Get indexers information
200+
try:
201+
indexers_url = f"{api_url.rstrip('/')}/api/v1/indexer"
202+
indexers_response = requests.get(indexers_url, headers=headers, timeout=10)
203+
204+
if indexers_response.status_code == 200:
205+
indexers = indexers_response.json()
206+
stats['total_indexers'] = len(indexers)
207+
208+
# Count active, throttled, and failed indexers
209+
active_count = 0
210+
throttled_count = 0
211+
failed_count = 0
212+
213+
for indexer in indexers:
214+
if indexer.get('enable', False):
215+
active_count += 1
216+
217+
# Check for throttling/rate limiting
218+
capabilities = indexer.get('capabilities', {})
219+
if capabilities.get('limitsexceeded', False):
220+
throttled_count += 1
221+
222+
# Check for failures - look for disabled indexers or low priority
223+
if not indexer.get('enable', False):
224+
failed_count += 1
225+
226+
stats['active_indexers'] = active_count
227+
stats['throttled_indexers'] = throttled_count
228+
stats['failed_indexers'] = failed_count
229+
230+
# Update health status based on indexer health
231+
if throttled_count > 0:
232+
stats['health_status'] = f'{throttled_count} indexer(s) throttled'
233+
elif failed_count > 0:
234+
stats['health_status'] = f'{failed_count} indexer(s) disabled'
235+
else:
236+
stats['health_status'] = 'All indexers healthy'
237+
238+
except Exception:
239+
# Indexer endpoint failed but system is connected
240+
stats['health_status'] = 'Indexer data unavailable'
241+
242+
# Get API history/usage statistics
243+
try:
244+
history_url = f"{api_url.rstrip('/')}/api/v1/history"
245+
history_response = requests.get(history_url, headers=headers, timeout=10, params={'pageSize': 1})
246+
247+
if history_response.status_code == 200:
248+
history_data = history_response.json()
249+
# Total records gives us approximate API call count
250+
stats['total_api_calls'] = history_data.get('totalRecords', 0)
251+
252+
except Exception:
253+
# History endpoint failed, keep default value
254+
pass
255+
256+
else:
257+
stats['health_status'] = f'Connection failed (HTTP {status_response.status_code})'
258+
259+
except requests.exceptions.Timeout:
260+
stats['health_status'] = 'Connection timeout'
261+
except requests.exceptions.ConnectionError:
262+
stats['health_status'] = 'Connection refused'
263+
except Exception as e:
264+
stats['health_status'] = f'Error: {str(e)[:50]}'
265+
266+
return jsonify({
267+
'success': True,
268+
'stats': stats
269+
})
270+
271+
except Exception as e:
272+
prowlarr_logger.error(f"Failed to get Prowlarr stats: {str(e)}")
273+
return jsonify({
274+
'success': False,
275+
'error': f'Failed to get Prowlarr stats: {str(e)}'
276+
}), 500
277+
154278
@prowlarr_bp.route('/test-connection', methods=['POST'])
155279
def test_connection_endpoint():
156280
"""Test connection to Prowlarr API instance"""

0 commit comments

Comments
 (0)