- Fix seriesMap key (use Sonarr internal id, not tvdbId) - Fix Sonarr tag resolution (use tag map like Radarr) - Use sourceTitle for history record matching - Fall back to embedded movie/series objects when API timeouts - Add includeMovie/includeSeries params to queue/history API calls - Add coverArt field to all download responses (TMDB poster URLs) - Add cover art display to frontend download cards - Fix user-summary route to use instance config and tag maps
652 lines
27 KiB
JavaScript
652 lines
27 KiB
JavaScript
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();
|
|
console.log(`[Dashboard] Fetching downloads for authenticated user: ${user.name} (${username})`);
|
|
|
|
// 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 && userTag.toLowerCase() === username) {
|
|
userDownloads.push({
|
|
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
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 && userTag.toLowerCase() === username) {
|
|
userDownloads.push({
|
|
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
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} 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 && userTag.toLowerCase() === username) {
|
|
userDownloads.push({
|
|
type: 'series',
|
|
title: nzbName,
|
|
coverArt: getCoverArt(series),
|
|
status: slot.status,
|
|
size: slot.size,
|
|
completedAt: slot.completed_time,
|
|
seriesName: series.title,
|
|
episodeInfo: sonarrMatch
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 && userTag.toLowerCase() === username) {
|
|
userDownloads.push({
|
|
type: 'movie',
|
|
title: nzbName,
|
|
coverArt: getCoverArt(movie),
|
|
status: slot.status,
|
|
size: slot.size,
|
|
completedAt: slot.completed_time,
|
|
movieName: movie.title,
|
|
movieInfo: radarrMatch
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} 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 && 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;
|
|
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 && 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;
|
|
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 && 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;
|
|
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 && 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;
|
|
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,
|
|
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;
|