Initial commit: Media Download Dashboard with SABnzbd, Sonarr, Radarr, and Emby integration

This commit is contained in:
2026-05-15 10:36:29 +01:00
commit 5d04d2796b
16 changed files with 1219 additions and 0 deletions

252
server/routes/dashboard.js Normal file
View File

@@ -0,0 +1,252 @@
const express = require('express');
const axios = require('axios');
const router = express.Router();
const SABNZBD_URL = process.env.SABNZBD_URL;
const SABNZBD_API_KEY = process.env.SABNZBD_API_KEY;
const SONARR_URL = process.env.SONARR_URL;
const SONARR_API_KEY = process.env.SONARR_API_KEY;
const RADARR_URL = process.env.RADARR_URL;
const RADARR_API_KEY = process.env.RADARR_API_KEY;
const EMBY_URL = process.env.EMBY_URL;
const EMBY_API_KEY = process.env.EMBY_API_KEY;
// Helper function to extract user tag from series/movie
function extractUserTag(tags) {
if (!tags) return null;
const userTag = tags.find(tag => tag.label && tag.label.startsWith('user:'));
return userTag ? userTag.label.replace('user:', '') : null;
}
// Get user downloads for current session
router.get('/user-downloads/:sessionId', async (req, res) => {
try {
// Get current user from Emby session
const sessionResponse = await axios.get(`${EMBY_URL}/Sessions`, {
headers: { 'X-MediaBrowser-Token': EMBY_API_KEY }
});
const session = sessionResponse.data.find(s => s.Id === req.params.sessionId);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
const userResponse = await axios.get(`${EMBY_URL}/Users/${session.UserId}`, {
headers: { 'X-MediaBrowser-Token': EMBY_API_KEY }
});
const currentUser = userResponse.data;
const username = currentUser.Name.toLowerCase();
// Get SABnzbd queue and history
const [sabnzbdQueue, sabnzbdHistory, sonarrQueue, sonarrHistory, radarrQueue, radarrHistory, sonarrSeries, radarrMovies] = await Promise.all([
axios.get(`${SABNZBD_URL}/api`, {
params: { mode: 'queue', apikey: SABNZBD_API_KEY, output: 'json' }
}),
axios.get(`${SABNZBD_URL}/api`, {
params: { mode: 'history', apikey: SABNZBD_API_KEY, output: 'json', limit: 100 }
}),
axios.get(`${SONARR_URL}/api/v3/queue`, {
headers: { 'X-Api-Key': SONARR_API_KEY }
}).catch(() => ({ data: { records: [] } })),
axios.get(`${SONARR_URL}/api/v3/history`, {
headers: { 'X-Api-Key': SONARR_API_KEY },
params: { pageSize: 100 }
}).catch(() => ({ data: { records: [] } })),
axios.get(`${RADARR_URL}/api/v3/queue`, {
headers: { 'X-Api-Key': RADARR_API_KEY }
}).catch(() => ({ data: { records: [] } })),
axios.get(`${RADARR_URL}/api/v3/history`, {
headers: { 'X-Api-Key': RADARR_API_KEY },
params: { pageSize: 100 }
}).catch(() => ({ data: { records: [] } })),
axios.get(`${SONARR_URL}/api/v3/series`, {
headers: { 'X-Api-Key': SONARR_API_KEY }
}).catch(() => ({ data: [] })),
axios.get(`${RADARR_URL}/api/v3/movie`, {
headers: { 'X-Api-Key': RADARR_API_KEY }
}).catch(() => ({ data: [] }))
]);
// Create maps for quick lookup
const seriesMap = new Map(sonarrSeries.data.map(s => [s.tvdbId || s.id, s]));
const moviesMap = new Map(radarrMovies.data.map(m => [m.tmdbId || m.id, m]));
// Match SABnzbd downloads to Sonarr/Radarr activity
const userDownloads = [];
// Process SABnzbd queue
if (sabnzbdQueue.data.queue && sabnzbdQueue.data.queue.slots) {
for (const slot of sabnznzbdQueue.data.queue.slots) {
const nzbName = slot.nzbname.toLowerCase();
// Try to match with Sonarr
const sonarrMatch = sonarrQueue.data.records.find(r =>
r.title.toLowerCase().includes(nzbName) || nzbName.includes(r.title.toLowerCase())
);
if (sonarrMatch && sonarrMatch.seriesId) {
const series = seriesMap.get(sonarrMatch.seriesId);
if (series) {
const userTag = extractUserTag(series.tags);
if (userTag && userTag.toLowerCase() === username) {
userDownloads.push({
type: 'series',
title: slot.nzbname,
status: 'downloading',
progress: slot.percentage,
size: slot.size,
speed: slot.speed,
eta: slot.eta,
seriesName: series.title,
episodeInfo: sonarrMatch
});
}
}
}
// Try to match with Radarr
const radarrMatch = radarrQueue.data.records.find(r =>
r.title.toLowerCase().includes(nzbName) || nzbName.includes(r.title.toLowerCase())
);
if (radarrMatch && radarrMatch.movieId) {
const movie = moviesMap.get(radarrMatch.movieId);
if (movie) {
const userTag = extractUserTag(movie.tags);
if (userTag && userTag.toLowerCase() === username) {
userDownloads.push({
type: 'movie',
title: slot.nzbname,
status: 'downloading',
progress: slot.percentage,
size: slot.size,
speed: slot.speed,
eta: slot.eta,
movieName: movie.title,
movieInfo: radarrMatch
});
}
}
}
}
}
// Process SABnzbd history
if (sabnzbdHistory.data.history && sabnzbdHistory.data.history.slots) {
for (const slot of sabnzbdHistory.data.history.slots) {
const nzbName = slot.nzbname.toLowerCase();
// Try to match with Sonarr history
const sonarrMatch = sonarrHistory.data.records.find(r =>
r.title.toLowerCase().includes(nzbName) || nzbName.includes(r.title.toLowerCase())
);
if (sonarrMatch && sonarrMatch.seriesId) {
const series = seriesMap.get(sonarrMatch.seriesId);
if (series) {
const userTag = extractUserTag(series.tags);
if (userTag && userTag.toLowerCase() === username) {
userDownloads.push({
type: 'series',
title: slot.nzbname,
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 =>
r.title.toLowerCase().includes(nzbName) || nzbName.includes(r.title.toLowerCase())
);
if (radarrMatch && radarrMatch.movieId) {
const movie = moviesMap.get(radarrMatch.movieId);
if (movie) {
const userTag = extractUserTag(movie.tags);
if (userTag && userTag.toLowerCase() === username) {
userDownloads.push({
type: 'movie',
title: slot.nzbname,
status: slot.status,
size: slot.size,
completedAt: slot.completed_time,
movieName: movie.title,
movieInfo: radarrMatch
});
}
}
}
}
}
res.json({
user: currentUser.Name,
downloads: userDownloads
});
} catch (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 {
// Get all Emby users
const usersResponse = await axios.get(`${EMBY_URL}/Users`, {
headers: { 'X-MediaBrowser-Token': EMBY_API_KEY }
});
// Get all series and movies with tags
const [sonarrSeries, radarrMovies] = await Promise.all([
axios.get(`${SONARR_URL}/api/v3/series`, {
headers: { 'X-Api-Key': SONARR_API_KEY }
}).catch(() => ({ data: [] })),
axios.get(`${RADARR_URL}/api/v3/movie`, {
headers: { 'X-Api-Key': RADARR_API_KEY }
}).catch(() => ({ data: [] }))
]);
// Count downloads per user
const userDownloads = {};
usersResponse.data.forEach(user => {
userDownloads[user.Name.toLowerCase()] = {
username: user.Name,
seriesCount: 0,
movieCount: 0
};
});
// Process series tags
sonarrSeries.data.forEach(series => {
const userTag = extractUserTag(series.tags);
if (userTag) {
const username = userTag.toLowerCase();
if (userDownloads[username]) {
userDownloads[username].seriesCount++;
}
}
});
// Process movie tags
radarrMovies.data.forEach(movie => {
const userTag = extractUserTag(movie.tags);
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;