perf: cache slow-changing data (series, movies, tags) with 60s TTL
All checks were successful
Build and Push Docker Image / build (push) Successful in 27s

- 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:
2026-05-15 23:32:45 +01:00
parent eda9770f49
commit b04b52e3f1
2 changed files with 94 additions and 34 deletions

View File

@@ -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 [];