const axios = require('axios'); const cache = require('./cache'); const { getTorrents } = require('./qbittorrent'); const { getSABnzbdInstances, getSonarrInstances, getRadarrInstances } = require('./config'); const rawPollInterval = (process.env.POLL_INTERVAL || '').toLowerCase(); const POLL_INTERVAL = (rawPollInterval === 'off' || rawPollInterval === 'false' || rawPollInterval === 'disabled') ? 0 : (parseInt(process.env.POLL_INTERVAL, 10) || 5000); const POLLING_ENABLED = POLL_INTERVAL > 0; let polling = false; async function pollAllServices() { if (polling) { console.log('[Poller] Previous poll still running, skipping'); return; } polling = true; const start = Date.now(); try { const sabInstances = getSABnzbdInstances(); const sonarrInstances = getSonarrInstances(); const radarrInstances = getRadarrInstances(); // All fetches in parallel const [ sabQueues, sabHistories, sonarrTagsResults, sonarrQueues, sonarrHistories, sonarrSeriesResults, radarrQueues, radarrHistories, radarrMoviesResults, radarrTagsResults, qbittorrentTorrents ] = await Promise.all([ // SABnzbd Promise.all(sabInstances.map(inst => axios.get(`${inst.url}/api`, { params: { mode: 'queue', apikey: inst.apiKey, output: 'json' } }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { console.error(`[Poller] SABnzbd ${inst.id} queue error:`, err.message); return { instance: inst.id, data: { queue: { slots: [] } } }; }) )), Promise.all(sabInstances.map(inst => axios.get(`${inst.url}/api`, { params: { mode: 'history', apikey: inst.apiKey, output: 'json', limit: 20 } }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { console.error(`[Poller] SABnzbd ${inst.id} history error:`, err.message); return { instance: inst.id, data: { history: { slots: [] } } }; }) )), // Sonarr Promise.all(sonarrInstances.map(inst => axios.get(`${inst.url}/api/v3/tag`, { headers: { 'X-Api-Key': inst.apiKey } }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { console.error(`[Poller] Sonarr ${inst.id} tags error:`, err.message); return { instance: inst.id, data: [] }; }) )), Promise.all(sonarrInstances.map(inst => axios.get(`${inst.url}/api/v3/queue`, { headers: { 'X-Api-Key': inst.apiKey }, params: { includeSeries: true, includeEpisode: true } }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { console.error(`[Poller] Sonarr ${inst.id} queue error:`, err.message); return { instance: inst.id, data: { records: [] } }; }) )), Promise.all(sonarrInstances.map(inst => axios.get(`${inst.url}/api/v3/history`, { headers: { 'X-Api-Key': inst.apiKey }, params: { pageSize: 20, includeSeries: true, includeEpisode: true } }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { console.error(`[Poller] Sonarr ${inst.id} history error:`, err.message); return { instance: inst.id, data: { records: [] } }; }) )), Promise.all(sonarrInstances.map(inst => axios.get(`${inst.url}/api/v3/series`, { headers: { 'X-Api-Key': inst.apiKey } }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { console.error(`[Poller] Sonarr ${inst.id} series error:`, err.message); return { instance: inst.id, data: [] }; }) )), // Radarr Promise.all(radarrInstances.map(inst => axios.get(`${inst.url}/api/v3/queue`, { headers: { 'X-Api-Key': inst.apiKey }, params: { includeMovie: true } }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { console.error(`[Poller] Radarr ${inst.id} queue error:`, err.message); return { instance: inst.id, data: { records: [] } }; }) )), Promise.all(radarrInstances.map(inst => axios.get(`${inst.url}/api/v3/history`, { headers: { 'X-Api-Key': inst.apiKey }, params: { pageSize: 20, includeMovie: true } }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { console.error(`[Poller] Radarr ${inst.id} history error:`, err.message); return { instance: inst.id, data: { records: [] } }; }) )), Promise.all(radarrInstances.map(inst => axios.get(`${inst.url}/api/v3/movie`, { headers: { 'X-Api-Key': inst.apiKey } }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { console.error(`[Poller] Radarr ${inst.id} movies error:`, err.message); return { instance: inst.id, data: [] }; }) )), Promise.all(radarrInstances.map(inst => axios.get(`${inst.url}/api/v3/tag`, { headers: { 'X-Api-Key': inst.apiKey } }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { console.error(`[Poller] Radarr ${inst.id} tags error:`, err.message); return { instance: inst.id, data: [] }; }) )), // qBittorrent getTorrents().catch(err => { console.error(`[Poller] qBittorrent error:`, err.message); return []; }) ]); // When polling is active, TTL is 3x interval to avoid gaps between polls // When polling is disabled (on-demand), use 30s so data refreshes on next request after expiry const cacheTTL = POLLING_ENABLED ? POLL_INTERVAL * 3 : 30000; // SABnzbd const firstSabQueue = sabQueues[0] && sabQueues[0].data && sabQueues[0].data.queue; cache.set('poll:sab-queue', { slots: sabQueues.flatMap(q => (q.data.queue && q.data.queue.slots) || []), status: firstSabQueue && firstSabQueue.status, speed: firstSabQueue && firstSabQueue.speed, kbpersec: firstSabQueue && firstSabQueue.kbpersec }, cacheTTL); cache.set('poll:sab-history', { slots: sabHistories.flatMap(h => (h.data.history && h.data.history.slots) || []) }, cacheTTL); // Sonarr cache.set('poll:sonarr-tags', sonarrTagsResults, cacheTTL); cache.set('poll:sonarr-queue', { records: sonarrQueues.flatMap(q => q.data.records || []) }, cacheTTL); cache.set('poll:sonarr-history', { records: sonarrHistories.flatMap(h => h.data.records || []) }, cacheTTL); cache.set('poll:sonarr-series', sonarrSeriesResults.flatMap(s => { const inst = sonarrInstances.find(i => i.id === s.instance); return (s.data || []).map(item => ({ ...item, _instanceUrl: inst ? inst.url : null })); }), cacheTTL); // Radarr cache.set('poll:radarr-queue', { records: radarrQueues.flatMap(q => q.data.records || []) }, cacheTTL); cache.set('poll:radarr-history', { records: radarrHistories.flatMap(h => h.data.records || []) }, cacheTTL); cache.set('poll:radarr-movies', radarrMoviesResults.flatMap(m => { const inst = radarrInstances.find(i => i.id === m.instance); return (m.data || []).map(item => ({ ...item, _instanceUrl: inst ? inst.url : null })); }), cacheTTL); cache.set('poll:radarr-tags', radarrTagsResults.flatMap(t => t.data || []), cacheTTL); // qBittorrent cache.set('poll:qbittorrent', qbittorrentTorrents, cacheTTL); const elapsed = Date.now() - start; console.log(`[Poller] Poll complete in ${elapsed}ms`); } catch (err) { console.error(`[Poller] Poll error:`, err.message); } finally { polling = false; } } let intervalHandle = null; function startPoller() { if (!POLLING_ENABLED) { console.log(`[Poller] Background polling disabled (POLL_INTERVAL=${process.env.POLL_INTERVAL || 'not set'}). Data will be fetched on-demand.`); return; } console.log(`[Poller] Starting background poller (interval: ${POLL_INTERVAL}ms)`); // Run immediately, then on interval pollAllServices(); intervalHandle = setInterval(pollAllServices, POLL_INTERVAL); } function stopPoller() { if (intervalHandle) { clearInterval(intervalHandle); intervalHandle = null; console.log('[Poller] Stopped'); } } module.exports = { startPoller, stopPoller, pollAllServices, POLL_INTERVAL, POLLING_ENABLED };