perf: cache slow-changing data (series, movies, tags) with 60s TTL
- Add MemoryCache utility with get/set/invalidate/clear - Cache Sonarr series, Sonarr tags, Radarr movies, Radarr tags - 60-second TTL - first request fetches, subsequent requests served from cache - Queue, history, and torrent data remain uncached (changes frequently) - On cache hit, these 4 heavy API calls resolve instantly
This commit is contained in:
@@ -8,6 +8,9 @@ const {
|
|||||||
getSonarrInstances,
|
getSonarrInstances,
|
||||||
getRadarrInstances
|
getRadarrInstances
|
||||||
} = require('../utils/config');
|
} = 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_URL = process.env.EMBY_URL;
|
||||||
const EMBY_API_KEY = process.env.EMBY_API_KEY;
|
const EMBY_API_KEY = process.env.EMBY_API_KEY;
|
||||||
@@ -94,6 +97,19 @@ function getRadarrLink(movie) {
|
|||||||
return `${movie._instanceUrl}/movie/${movie.titleSlug}`;
|
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
|
// Get user downloads for authenticated user
|
||||||
router.get('/user-downloads', async (req, res) => {
|
router.get('/user-downloads', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -139,14 +155,16 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch from all Sonarr instances
|
// Fetch from all Sonarr instances (tags cached)
|
||||||
const sonarrTagsPromises = sonarrInstances.map(inst =>
|
const sonarrTagsPromise = cachedFetch('sonarr-tags', () =>
|
||||||
axios.get(`${inst.url}/api/v3/tag`, {
|
Promise.all(sonarrInstances.map(inst =>
|
||||||
headers: { 'X-Api-Key': inst.apiKey }
|
axios.get(`${inst.url}/api/v3/tag`, {
|
||||||
}).then(res => ({ instance: inst.id, data: res.data })).catch(err => {
|
headers: { 'X-Api-Key': inst.apiKey }
|
||||||
console.error(`[Dashboard] Sonarr ${inst.id} tags error:`, err.message);
|
}).then(res => ({ instance: inst.id, data: res.data })).catch(err => {
|
||||||
return { instance: inst.id, data: [] };
|
console.error(`[Dashboard] Sonarr ${inst.id} tags error:`, err.message);
|
||||||
})
|
return { instance: inst.id, data: [] };
|
||||||
|
})
|
||||||
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
const sonarrQueuePromises = sonarrInstances.map(inst =>
|
const sonarrQueuePromises = sonarrInstances.map(inst =>
|
||||||
@@ -169,13 +187,15 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const sonarrSeriesPromises = sonarrInstances.map(inst =>
|
const sonarrSeriesPromise = cachedFetch('sonarr-series', () =>
|
||||||
axios.get(`${inst.url}/api/v3/series`, {
|
Promise.all(sonarrInstances.map(inst =>
|
||||||
headers: { 'X-Api-Key': inst.apiKey }
|
axios.get(`${inst.url}/api/v3/series`, {
|
||||||
}).then(res => ({ instance: inst.id, data: res.data })).catch(err => {
|
headers: { 'X-Api-Key': inst.apiKey }
|
||||||
console.error(`[Dashboard] Sonarr ${inst.id} series error:`, err.message);
|
}).then(res => ({ instance: inst.id, data: res.data })).catch(err => {
|
||||||
return { instance: inst.id, data: [] };
|
console.error(`[Dashboard] Sonarr ${inst.id} series error:`, err.message);
|
||||||
})
|
return { instance: inst.id, data: [] };
|
||||||
|
})
|
||||||
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch from all Radarr instances
|
// Fetch from all Radarr instances
|
||||||
@@ -199,25 +219,29 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const radarrMoviesPromises = radarrInstances.map(inst =>
|
const radarrMoviesPromise = cachedFetch('radarr-movies', () =>
|
||||||
axios.get(`${inst.url}/api/v3/movie`, {
|
Promise.all(radarrInstances.map(inst =>
|
||||||
headers: { 'X-Api-Key': inst.apiKey }
|
axios.get(`${inst.url}/api/v3/movie`, {
|
||||||
}).then(res => ({ instance: inst.id, data: res.data })).catch(err => {
|
headers: { 'X-Api-Key': inst.apiKey }
|
||||||
console.error(`[Dashboard] Radarr ${inst.id} movies error:`, err.message);
|
}).then(res => ({ instance: inst.id, data: res.data })).catch(err => {
|
||||||
return { instance: inst.id, data: [] };
|
console.error(`[Dashboard] Radarr ${inst.id} movies error:`, err.message);
|
||||||
})
|
return { instance: inst.id, data: [] };
|
||||||
|
})
|
||||||
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
const radarrTagsPromises = radarrInstances.map(inst =>
|
const radarrTagsPromise = cachedFetch('radarr-tags', () =>
|
||||||
axios.get(`${inst.url}/api/v3/tag`, {
|
Promise.all(radarrInstances.map(inst =>
|
||||||
headers: { 'X-Api-Key': inst.apiKey }
|
axios.get(`${inst.url}/api/v3/tag`, {
|
||||||
}).then(res => ({ instance: inst.id, data: res.data })).catch(err => {
|
headers: { 'X-Api-Key': inst.apiKey }
|
||||||
console.error(`[Dashboard] Radarr ${inst.id} tags error:`, err.message);
|
}).then(res => ({ instance: inst.id, data: res.data })).catch(err => {
|
||||||
return { instance: inst.id, data: [] };
|
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 [
|
const [
|
||||||
sabQueues, sabHistories, sonarrTagsResults, sonarrQueues, sonarrHistories, sonarrSeriesResults,
|
sabQueues, sabHistories, sonarrTagsResults, sonarrQueues, sonarrHistories, sonarrSeriesResults,
|
||||||
radarrQueues, radarrHistories, radarrMoviesResults, radarrTagsResults,
|
radarrQueues, radarrHistories, radarrMoviesResults, radarrTagsResults,
|
||||||
@@ -225,14 +249,14 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
Promise.all(sabQueuePromises),
|
Promise.all(sabQueuePromises),
|
||||||
Promise.all(sabHistoryPromises),
|
Promise.all(sabHistoryPromises),
|
||||||
Promise.all(sonarrTagsPromises),
|
sonarrTagsPromise,
|
||||||
Promise.all(sonarrQueuePromises),
|
Promise.all(sonarrQueuePromises),
|
||||||
Promise.all(sonarrHistoryPromises),
|
Promise.all(sonarrHistoryPromises),
|
||||||
Promise.all(sonarrSeriesPromises),
|
sonarrSeriesPromise,
|
||||||
Promise.all(radarrQueuePromises),
|
Promise.all(radarrQueuePromises),
|
||||||
Promise.all(radarrHistoryPromises),
|
Promise.all(radarrHistoryPromises),
|
||||||
Promise.all(radarrMoviesPromises),
|
radarrMoviesPromise,
|
||||||
Promise.all(radarrTagsPromises),
|
radarrTagsPromise,
|
||||||
getTorrents().catch(err => {
|
getTorrents().catch(err => {
|
||||||
console.error(`[Dashboard] qBittorrent error:`, err.message);
|
console.error(`[Dashboard] qBittorrent error:`, err.message);
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
36
server/utils/cache.js
Normal file
36
server/utils/cache.js
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user