const express = require('express'); const axios = require('axios'); const router = express.Router(); const { getTorrents, mapTorrentToDownload } = require('../utils/qbittorrent'); const { getSABnzbdInstances, getSonarrInstances, getRadarrInstances } = require('../utils/config'); const EMBY_URL = process.env.EMBY_URL; const EMBY_API_KEY = process.env.EMBY_API_KEY; // Helper function to extract poster/cover art URL from a movie or series object function getCoverArt(item) { if (!item || !item.images) return null; const poster = item.images.find(img => img.coverType === 'poster'); if (poster) return poster.remoteUrl || poster.url || null; // Fallback to fanart if no poster const fanart = item.images.find(img => img.coverType === 'fanart'); return fanart ? (fanart.remoteUrl || fanart.url || null) : null; } // Helper function to extract user tag from series/movie // For Radarr: tags is array of IDs, tagMap is id -> label mapping // For Sonarr: tags is array of objects with label property function extractUserTag(tags, tagMap) { if (!tags || tags.length === 0) return null; // If tagMap provided (Radarr), look up label by ID if (tagMap) { for (const tagId of tags) { const label = tagMap.get(tagId); if (label) return label; } return null; } // Sonarr style - tags are objects with label const userTag = tags.find(tag => tag && tag.label); return userTag ? userTag.label : null; } // Get user downloads for authenticated user router.get('/user-downloads', async (req, res) => { try { // Get authenticated user from cookie const userCookie = req.cookies.emby_user; if (!userCookie) { return res.status(401).json({ error: 'Not authenticated' }); } const user = JSON.parse(userCookie); const username = user.name.toLowerCase(); const isAdmin = !!user.isAdmin; const showAll = isAdmin && req.query.showAll === 'true'; console.log(`[Dashboard] Fetching downloads for authenticated user: ${user.name} (${username}), isAdmin: ${isAdmin}, showAll: ${showAll}`); // Get all service instances const sabInstances = getSABnzbdInstances(); const sonarrInstances = getSonarrInstances(); const radarrInstances = getRadarrInstances(); console.log(`[Dashboard] Fetching data from all services...`); console.log(`[Dashboard] SABnzbd instances: ${sabInstances.length}`); console.log(`[Dashboard] Sonarr instances: ${sonarrInstances.length}`); console.log(`[Dashboard] Radarr instances: ${radarrInstances.length}`); // Fetch from all SABnzbd instances const sabQueuePromises = 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(`[Dashboard] SABnzbd ${inst.id} queue error:`, err.message); return { instance: inst.id, data: { queue: { slots: [] } } }; }) ); const sabHistoryPromises = sabInstances.map(inst => axios.get(`${inst.url}/api`, { params: { mode: 'history', apikey: inst.apiKey, output: 'json', limit: 100 } }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { console.error(`[Dashboard] SABnzbd ${inst.id} history error:`, err.message); return { instance: inst.id, data: { history: { slots: [] } } }; }) ); // 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: [] }; }) ); const sonarrQueuePromises = 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(`[Dashboard] Sonarr ${inst.id} queue error:`, err.message); return { instance: inst.id, data: { records: [] } }; }) ); const sonarrHistoryPromises = sonarrInstances.map(inst => axios.get(`${inst.url}/api/v3/history`, { headers: { 'X-Api-Key': inst.apiKey }, params: { pageSize: 100, includeSeries: true, includeEpisode: true } }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { console.error(`[Dashboard] Sonarr ${inst.id} history error:`, err.message); return { instance: inst.id, data: { records: [] } }; }) ); 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: [] }; }) ); // Fetch from all Radarr instances const radarrQueuePromises = 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(`[Dashboard] Radarr ${inst.id} queue error:`, err.message); return { instance: inst.id, data: { records: [] } }; }) ); const radarrHistoryPromises = radarrInstances.map(inst => axios.get(`${inst.url}/api/v3/history`, { headers: { 'X-Api-Key': inst.apiKey }, params: { pageSize: 100, includeMovie: true } }).then(res => ({ instance: inst.id, data: res.data })).catch(err => { console.error(`[Dashboard] Radarr ${inst.id} history error:`, err.message); return { instance: inst.id, data: { records: [] } }; }) ); 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 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: [] }; }) ); // Execute all requests const [ sabQueues, sabHistories, sonarrTagsResults, sonarrQueues, sonarrHistories, sonarrSeriesResults, radarrQueues, radarrHistories, radarrMoviesResults, radarrTagsResults, qbittorrentTorrents ] = await Promise.all([ Promise.all(sabQueuePromises), Promise.all(sabHistoryPromises), Promise.all(sonarrTagsPromises), Promise.all(sonarrQueuePromises), Promise.all(sonarrHistoryPromises), Promise.all(sonarrSeriesPromises), Promise.all(radarrQueuePromises), Promise.all(radarrHistoryPromises), Promise.all(radarrMoviesPromises), Promise.all(radarrTagsPromises), getTorrents().catch(err => { console.error(`[Dashboard] qBittorrent error:`, err.message); return []; }) ]); // Aggregate data from all instances const firstSabQueue = sabQueues[0] && sabQueues[0].data && sabQueues[0].data.queue; const sabnzbdQueue = { data: { queue: { slots: sabQueues.flatMap(q => (q.data.queue && q.data.queue.slots) || []), status: firstSabQueue && firstSabQueue.status, speed: firstSabQueue && firstSabQueue.speed, kbpersec: firstSabQueue && firstSabQueue.kbpersec } } }; const sabnzbdHistory = { data: { history: { slots: sabHistories.flatMap(h => (h.data.history && h.data.history.slots) || []) } } }; const sonarrQueue = { data: { records: sonarrQueues.flatMap(q => q.data.records || []) } }; const sonarrHistory = { data: { records: sonarrHistories.flatMap(h => h.data.records || []) } }; const sonarrSeries = { data: sonarrSeriesResults.flatMap(s => s.data || []) }; const radarrQueue = { data: { records: radarrQueues.flatMap(q => q.data.records || []) } }; const radarrHistory = { data: { records: radarrHistories.flatMap(h => h.data.records || []) } }; const radarrMovies = { data: radarrMoviesResults.flatMap(m => m.data || []) }; const radarrTags = { data: radarrTagsResults.flatMap(t => t.data || []) }; console.log(`[Dashboard] Data fetched successfully`); console.log(`[Dashboard] Sonarr series: ${sonarrSeries.data.length}`); console.log(`[Dashboard] Radarr movies: ${radarrMovies.data.length}`); console.log(`[Dashboard] Radarr queue records: ${radarrQueue.data.records.length}`); console.log(`[Dashboard] Radarr history records: ${radarrHistory.data.records.length}`); console.log(`[Dashboard] SABnzbd queue slots: ${sabnzbdQueue.data.queue ? sabnzbdQueue.data.queue.slots.length : 0}`); console.log(`[Dashboard] SABnzbd history slots: ${sabnzbdHistory.data.history ? sabnzbdHistory.data.history.slots.length : 0}`); console.log(`[Dashboard] qBittorrent torrents: ${qbittorrentTorrents.length}`); console.log(`[Dashboard] Radarr queue:`, JSON.stringify(radarrQueue.data.records)); console.log(`[Dashboard] Radarr history:`, JSON.stringify(radarrHistory.data.records)); // Create maps for quick lookup const seriesMap = new Map(sonarrSeries.data.map(s => [s.id, s])); const moviesMap = new Map(radarrMovies.data.map(m => [m.id, m])); // Create tag maps (id -> label) const sonarrTagMap = new Map(sonarrTagsResults.flatMap(t => t.data || []).map(t => [t.id, t.label])); const radarrTagMap = new Map(radarrTags.data.map(t => [t.id, t.label])); console.log(`[Dashboard] Radarr tags:`, JSON.stringify(radarrTags.data)); console.log(`[Dashboard] Movies map keys:`, Array.from(moviesMap.keys()).slice(0, 10)); console.log(`[Dashboard] Looking for movieId: 2962`); console.log(`[Dashboard] Movie 2962:`, JSON.stringify(moviesMap.get(2962))); console.log(`[Dashboard] Sample movie structure:`, JSON.stringify(radarrMovies.data[0])); // Match SABnzbd downloads to Sonarr/Radarr activity const userDownloads = []; // Process SABnzbd queue const queueStatus = sabnzbdQueue.data.queue ? sabnzbdQueue.data.queue.status : null; const queueSpeed = sabnzbdQueue.data.queue ? sabnzbdQueue.data.queue.speed : null; const queueKbpersec = sabnzbdQueue.data.queue ? sabnzbdQueue.data.queue.kbpersec : null; console.log(`[Dashboard] Queue status: ${queueStatus}, speed: ${queueSpeed}, kbpersec: ${queueKbpersec}`); // Helper to determine status and speed function getSlotStatusAndSpeed(slot) { // If whole queue is paused, everything is paused with 0 speed if (queueStatus === 'Paused') { return { status: 'Paused', speed: '0' }; } // Use slot's actual status and queue speed return { status: slot.status || 'Unknown', speed: queueSpeed || queueKbpersec || '0' }; } if (sabnzbdQueue.data.queue && sabnzbdQueue.data.queue.slots) { for (const slot of sabnzbdQueue.data.queue.slots) { try { const nzbName = slot.filename || slot.nzbname; if (!nzbName) { console.log(`[Dashboard] Skipping slot with no filename/nzbname`); continue; } const slotState = getSlotStatusAndSpeed(slot); console.log(`[Dashboard] Slot ${nzbName}: status=${slotState.status}, speed=${slotState.speed}`); const nzbNameLower = nzbName.toLowerCase(); // Try to match with Sonarr const sonarrMatch = sonarrQueue.data.records.find(r => { const rTitle = (r.title || r.sourceTitle || '').toLowerCase(); return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle)); }); if (sonarrMatch && sonarrMatch.seriesId) { const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series; if (series) { const userTag = extractUserTag(series.tags, sonarrTagMap); if (userTag && (showAll || userTag.toLowerCase() === username)) { const dlObj = { type: 'series', title: nzbName, coverArt: getCoverArt(series), status: slotState.status, progress: slot.percentage, mb: slot.mb, mbmissing: slot.mbmissing, size: slot.size, speed: slotState.speed, eta: slot.timeleft, seriesName: series.title, episodeInfo: sonarrMatch, userTag: userTag }; if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; } userDownloads.push(dlObj); } } } // Try to match with Radarr const radarrMatch = radarrQueue.data.records.find(r => { const rTitle = (r.title || r.sourceTitle || '').toLowerCase(); return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle)); }); if (radarrMatch && radarrMatch.movieId) { const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie; if (movie) { const userTag = extractUserTag(movie.tags, radarrTagMap); if (userTag && (showAll || userTag.toLowerCase() === username)) { const dlObj = { type: 'movie', title: nzbName, coverArt: getCoverArt(movie), status: slotState.status, progress: slot.percentage, mb: slot.mb, mbmissing: slot.mbmissing, size: slot.size, speed: slotState.speed, eta: slot.timeleft, movieName: movie.title, movieInfo: radarrMatch, userTag: userTag }; if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = movie.path || null; } userDownloads.push(dlObj); } } } } catch (err) { console.error(`[Dashboard] Error processing slot:`, err.message); console.error(`[Dashboard] Slot data:`, JSON.stringify(slot)); } } } // Process SABnzbd history if (sabnzbdHistory.data.history && sabnzbdHistory.data.history.slots) { for (const slot of sabnzbdHistory.data.history.slots) { try { const nzbName = slot.name || slot.nzb_name || slot.nzbname; if (!nzbName) { console.log(`[Dashboard] Skipping history slot with no name/nzb_name/nzbname`); continue; } const nzbNameLower = nzbName.toLowerCase(); // Try to match with Sonarr history const sonarrMatch = sonarrHistory.data.records.find(r => { const rTitle = (r.sourceTitle || r.title || '').toLowerCase(); return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle)); }); if (sonarrMatch && sonarrMatch.seriesId) { const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series; if (series) { const userTag = extractUserTag(series.tags, sonarrTagMap); if (userTag && (showAll || userTag.toLowerCase() === username)) { const dlObj = { type: 'series', title: nzbName, coverArt: getCoverArt(series), status: slot.status, size: slot.size, completedAt: slot.completed_time, seriesName: series.title, episodeInfo: sonarrMatch, userTag: userTag }; if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; } userDownloads.push(dlObj); } } } // Try to match with Radarr history const radarrMatch = radarrHistory.data.records.find(r => { const rTitle = (r.sourceTitle || r.title || '').toLowerCase(); return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle)); }); if (radarrMatch && radarrMatch.movieId) { const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie; if (movie) { const userTag = extractUserTag(movie.tags, radarrTagMap); if (userTag && (showAll || userTag.toLowerCase() === username)) { const dlObj = { type: 'movie', title: nzbName, coverArt: getCoverArt(movie), status: slot.status, size: slot.size, completedAt: slot.completed_time, movieName: movie.title, movieInfo: radarrMatch, userTag: userTag }; if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = movie.path || null; } userDownloads.push(dlObj); } } } } catch (err) { console.error(`[Dashboard] Error processing history slot:`, err.message); console.error(`[Dashboard] History slot data:`, JSON.stringify(slot)); } } } // Debug: show what queue records look like and which movies/series are tagged for this user console.log(`[Dashboard] Sonarr queue titles:`, sonarrQueue.data.records.map(r => ({ title: r.title, seriesId: r.seriesId }))); console.log(`[Dashboard] Radarr queue titles:`, radarrQueue.data.records.map(r => ({ title: r.title, movieId: r.movieId }))); console.log(`[Dashboard] Sonarr history titles:`, sonarrHistory.data.records.slice(0, 10).map(r => ({ title: r.title, seriesId: r.seriesId }))); console.log(`[Dashboard] Radarr history titles:`, radarrHistory.data.records.slice(0, 10).map(r => ({ title: r.title, movieId: r.movieId }))); console.log(`[Dashboard] Sonarr tag map:`, Array.from(sonarrTagMap.entries())); console.log(`[Dashboard] Radarr tag map:`, Array.from(radarrTagMap.entries())); // Show movies/series tagged for this user const userMovies = radarrMovies.data.filter(m => { const tag = extractUserTag(m.tags, radarrTagMap); return tag && tag.toLowerCase() === username; }); const userSeries = sonarrSeries.data.filter(s => { const tag = extractUserTag(s.tags, sonarrTagMap); return tag && tag.toLowerCase() === username; }); console.log(`[Dashboard] Movies tagged for ${username}:`, userMovies.map(m => m.title)); console.log(`[Dashboard] Series tagged for ${username}:`, userSeries.map(s => s.title)); // Process qBittorrent torrents - match to user-tagged Sonarr/Radarr activity console.log(`[Dashboard] Processing ${qbittorrentTorrents.length} qBittorrent torrents for user ${username}`); for (const torrent of qbittorrentTorrents) { try { const torrentName = torrent.name || ''; const torrentNameLower = torrentName.toLowerCase(); if (!torrentName) continue; console.log(`[Dashboard] Checking torrent "${torrentName}"`); // Try to match with Sonarr queue (user-tagged series) const sonarrMatch = sonarrQueue.data.records.find(r => { const rTitle = (r.title || r.sourceTitle || '').toLowerCase(); return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle)); }); if (sonarrMatch && sonarrMatch.seriesId) { const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series; if (series) { const userTag = extractUserTag(series.tags, sonarrTagMap); if (userTag && (showAll || userTag.toLowerCase() === username)) { console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr series "${series.title}"`); const download = mapTorrentToDownload(torrent); download.type = 'series'; download.coverArt = getCoverArt(series); download.seriesName = series.title; download.episodeInfo = sonarrMatch; download.userTag = userTag; if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = series.path || null; } userDownloads.push(download); continue; // Skip to next torrent } } } // Try to match with Radarr queue (user-tagged movies) const radarrMatch = radarrQueue.data.records.find(r => { const rTitle = (r.title || r.sourceTitle || '').toLowerCase(); return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle)); }); if (radarrMatch && radarrMatch.movieId) { const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie; if (movie) { const userTag = extractUserTag(movie.tags, radarrTagMap); if (userTag && (showAll || userTag.toLowerCase() === username)) { console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr movie "${movie.title}"`); const download = mapTorrentToDownload(torrent); download.type = 'movie'; download.coverArt = getCoverArt(movie); download.movieName = movie.title; download.movieInfo = radarrMatch; download.userTag = userTag; if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = movie.path || null; } userDownloads.push(download); continue; // Skip to next torrent } } } // Try to match with Sonarr history const sonarrHistoryMatch = sonarrHistory.data.records.find(r => { const rTitle = (r.sourceTitle || r.title || '').toLowerCase(); return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle)); }); if (sonarrHistoryMatch && sonarrHistoryMatch.seriesId) { const series = seriesMap.get(sonarrHistoryMatch.seriesId) || sonarrHistoryMatch.series; if (series) { const userTag = extractUserTag(series.tags, sonarrTagMap); if (userTag && (showAll || userTag.toLowerCase() === username)) { console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr history "${series.title}"`); const download = mapTorrentToDownload(torrent); download.type = 'series'; download.coverArt = getCoverArt(series); download.seriesName = series.title; download.episodeInfo = sonarrHistoryMatch; download.userTag = userTag; if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = series.path || null; } userDownloads.push(download); continue; } } } // Try to match with Radarr history const radarrHistoryMatch = radarrHistory.data.records.find(r => { const rTitle = (r.sourceTitle || r.title || '').toLowerCase(); return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle)); }); if (radarrHistoryMatch && radarrHistoryMatch.movieId) { const movie = moviesMap.get(radarrHistoryMatch.movieId) || radarrHistoryMatch.movie; if (movie) { const userTag = extractUserTag(movie.tags, radarrTagMap); if (userTag && (showAll || userTag.toLowerCase() === username)) { console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr history "${movie.title}"`); const download = mapTorrentToDownload(torrent); download.type = 'movie'; download.coverArt = getCoverArt(movie); download.movieName = movie.title; download.movieInfo = radarrHistoryMatch; download.userTag = userTag; if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = movie.path || null; } userDownloads.push(download); continue; } } } } catch (err) { console.error(`[Dashboard] Error processing torrent:`, err.message); console.error(`[Dashboard] Torrent data:`, JSON.stringify(torrent)); } } console.log(`[Dashboard] Found ${userDownloads.length} downloads for user ${username}`); console.log(`[Dashboard] Sending ${userDownloads.length} downloads`); if (userDownloads.length > 0) { console.log(`[Dashboard] First download:`, JSON.stringify(userDownloads[0])); } res.json({ user: user.name, isAdmin: isAdmin, downloads: userDownloads }); } catch (error) { console.error(`[Dashboard] Error fetching user downloads:`, error.message); console.error(`[Dashboard] Full error:`, error); res.status(500).json({ error: 'Failed to fetch user downloads', details: error.message }); } }); // Get all users with their download counts router.get('/user-summary', async (req, res) => { try { const sonarrInstances = getSonarrInstances(); const radarrInstances = getRadarrInstances(); // Get all Emby users const usersResponse = await axios.get(`${EMBY_URL}/Users`, { headers: { 'X-MediaBrowser-Token': EMBY_API_KEY } }); // Get all series, movies, and tags from all instances const [sonarrSeriesResults, sonarrTagsResults, radarrMoviesResults, radarrTagsResults] = await Promise.all([ Promise.all(sonarrInstances.map(inst => axios.get(`${inst.url}/api/v3/series`, { headers: { 'X-Api-Key': inst.apiKey } }).then(r => r.data).catch(() => []) )), Promise.all(sonarrInstances.map(inst => axios.get(`${inst.url}/api/v3/tag`, { headers: { 'X-Api-Key': inst.apiKey } }).then(r => r.data).catch(() => []) )), Promise.all(radarrInstances.map(inst => axios.get(`${inst.url}/api/v3/movie`, { headers: { 'X-Api-Key': inst.apiKey } }).then(r => r.data).catch(() => []) )), Promise.all(radarrInstances.map(inst => axios.get(`${inst.url}/api/v3/tag`, { headers: { 'X-Api-Key': inst.apiKey } }).then(r => r.data).catch(() => []) )) ]); const allSeries = sonarrSeriesResults.flat(); const sonarrTagMap = new Map(sonarrTagsResults.flat().map(t => [t.id, t.label])); const allMovies = radarrMoviesResults.flat(); const radarrTagMap = new Map(radarrTagsResults.flat().map(t => [t.id, t.label])); // Count downloads per user const userDownloads = {}; usersResponse.data.forEach(user => { userDownloads[user.Name.toLowerCase()] = { username: user.Name, seriesCount: 0, movieCount: 0 }; }); // Process series tags allSeries.forEach(series => { const userTag = extractUserTag(series.tags, sonarrTagMap); if (userTag) { const username = userTag.toLowerCase(); if (userDownloads[username]) { userDownloads[username].seriesCount++; } } }); // Process movie tags allMovies.forEach(movie => { const userTag = extractUserTag(movie.tags, radarrTagMap); if (userTag) { const username = userTag.toLowerCase(); if (userDownloads[username]) { userDownloads[username].movieCount++; } } }); res.json(Object.values(userDownloads)); } catch (error) { res.status(500).json({ error: 'Failed to fetch user summary', details: error.message }); } }); module.exports = router;