diff --git a/server/routes/dashboard.js b/server/routes/dashboard.js index 7afafec..8924e80 100644 --- a/server/routes/dashboard.js +++ b/server/routes/dashboard.js @@ -8,6 +8,9 @@ const { getSonarrInstances, getRadarrInstances } = require('../utils/config'); +const cache = require('../utils/cache'); + +const CACHE_TTL = 60 * 1000; // 60 seconds for slow-changing data (series, movies, tags) const EMBY_URL = process.env.EMBY_URL; const EMBY_API_KEY = process.env.EMBY_API_KEY; @@ -94,6 +97,19 @@ function getRadarrLink(movie) { return `${movie._instanceUrl}/movie/${movie.titleSlug}`; } +// Cached fetch helper - returns cached data if available, otherwise fetches and caches +async function cachedFetch(cacheKey, fetchFn, ttl = CACHE_TTL) { + const cached = cache.get(cacheKey); + if (cached) { + console.log(`[Dashboard] Cache HIT: ${cacheKey}`); + return cached; + } + console.log(`[Dashboard] Cache MISS: ${cacheKey}`); + const data = await fetchFn(); + cache.set(cacheKey, data, ttl); + return data; +} + // Get user downloads for authenticated user router.get('/user-downloads', async (req, res) => { try { @@ -139,14 +155,16 @@ router.get('/user-downloads', async (req, res) => { }) ); - // Fetch from all Sonarr instances - const sonarrTagsPromises = 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(`[Dashboard] Sonarr ${inst.id} tags error:`, err.message); - return { instance: inst.id, data: [] }; - }) + // Fetch from all Sonarr instances (tags cached) + const sonarrTagsPromise = cachedFetch('sonarr-tags', () => + 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(`[Dashboard] Sonarr ${inst.id} tags error:`, err.message); + return { instance: inst.id, data: [] }; + }) + )) ); const sonarrQueuePromises = sonarrInstances.map(inst => @@ -169,13 +187,15 @@ router.get('/user-downloads', async (req, res) => { }) ); - const sonarrSeriesPromises = 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(`[Dashboard] Sonarr ${inst.id} series error:`, err.message); - return { instance: inst.id, data: [] }; - }) + const sonarrSeriesPromise = cachedFetch('sonarr-series', () => + 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(`[Dashboard] Sonarr ${inst.id} series error:`, err.message); + return { instance: inst.id, data: [] }; + }) + )) ); // Fetch from all Radarr instances @@ -199,25 +219,29 @@ router.get('/user-downloads', async (req, res) => { }) ); - const radarrMoviesPromises = 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(`[Dashboard] Radarr ${inst.id} movies error:`, err.message); - return { instance: inst.id, data: [] }; - }) + const radarrMoviesPromise = cachedFetch('radarr-movies', () => + 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(`[Dashboard] Radarr ${inst.id} movies error:`, err.message); + return { instance: inst.id, data: [] }; + }) + )) ); - const radarrTagsPromises = 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(`[Dashboard] Radarr ${inst.id} tags error:`, err.message); - return { instance: inst.id, data: [] }; - }) + const radarrTagsPromise = cachedFetch('radarr-tags', () => + 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(`[Dashboard] Radarr ${inst.id} tags error:`, err.message); + return { instance: inst.id, data: [] }; + }) + )) ); - // Execute all requests + // Execute all requests (cached items resolve instantly on cache hit) const [ sabQueues, sabHistories, sonarrTagsResults, sonarrQueues, sonarrHistories, sonarrSeriesResults, radarrQueues, radarrHistories, radarrMoviesResults, radarrTagsResults, @@ -225,14 +249,14 @@ router.get('/user-downloads', async (req, res) => { ] = await Promise.all([ Promise.all(sabQueuePromises), Promise.all(sabHistoryPromises), - Promise.all(sonarrTagsPromises), + sonarrTagsPromise, Promise.all(sonarrQueuePromises), Promise.all(sonarrHistoryPromises), - Promise.all(sonarrSeriesPromises), + sonarrSeriesPromise, Promise.all(radarrQueuePromises), Promise.all(radarrHistoryPromises), - Promise.all(radarrMoviesPromises), - Promise.all(radarrTagsPromises), + radarrMoviesPromise, + radarrTagsPromise, getTorrents().catch(err => { console.error(`[Dashboard] qBittorrent error:`, err.message); return []; diff --git a/server/utils/cache.js b/server/utils/cache.js new file mode 100644 index 0000000..51970d1 --- /dev/null +++ b/server/utils/cache.js @@ -0,0 +1,36 @@ +const { logToFile } = require('./logger'); + +class MemoryCache { + constructor() { + this.store = new Map(); + } + + get(key) { + const entry = this.store.get(key); + if (!entry) return null; + if (Date.now() > entry.expiresAt) { + this.store.delete(key); + return null; + } + return entry.value; + } + + set(key, value, ttlMs) { + this.store.set(key, { + value, + expiresAt: Date.now() + ttlMs + }); + } + + invalidate(key) { + this.store.delete(key); + } + + clear() { + this.store.clear(); + } +} + +const cache = new MemoryCache(); + +module.exports = cache;