Initial commit: Media Download Dashboard with SABnzbd, Sonarr, Radarr, and Emby integration
This commit is contained in:
29
server/index.js
Normal file
29
server/index.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
require('dotenv').config();
|
||||
|
||||
const sabnzbdRoutes = require('./routes/sabnzbd');
|
||||
const sonarrRoutes = require('./routes/sonarr');
|
||||
const radarrRoutes = require('./routes/radarr');
|
||||
const embyRoutes = require('./routes/emby');
|
||||
const dashboardRoutes = require('./routes/dashboard');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.use('/api/sabnzbd', sabnzbdRoutes);
|
||||
app.use('/api/sonarr', sonarrRoutes);
|
||||
app.use('/api/radarr', radarrRoutes);
|
||||
app.use('/api/emby', embyRoutes);
|
||||
app.use('/api/dashboard', dashboardRoutes);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.json({ message: 'Media Download Dashboard API' });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
252
server/routes/dashboard.js
Normal file
252
server/routes/dashboard.js
Normal 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;
|
||||
66
server/routes/emby.js
Normal file
66
server/routes/emby.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const express = require('express');
|
||||
const axios = require('axios');
|
||||
const router = express.Router();
|
||||
|
||||
const EMBY_URL = process.env.EMBY_URL;
|
||||
const EMBY_API_KEY = process.env.EMBY_API_KEY;
|
||||
|
||||
// Get active sessions
|
||||
router.get('/sessions', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${EMBY_URL}/Sessions`, {
|
||||
headers: { 'X-MediaBrowser-Token': EMBY_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Emby sessions', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get user by ID
|
||||
router.get('/users/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${EMBY_URL}/Users/${req.params.id}`, {
|
||||
headers: { 'X-MediaBrowser-Token': EMBY_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch user details', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get all users
|
||||
router.get('/users', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${EMBY_URL}/Users`, {
|
||||
headers: { 'X-MediaBrowser-Token': EMBY_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch users', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get current user by session ID
|
||||
router.get('/session/:sessionId/user', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${EMBY_URL}/Sessions`, {
|
||||
headers: { 'X-MediaBrowser-Token': EMBY_API_KEY }
|
||||
});
|
||||
|
||||
const session = response.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 }
|
||||
});
|
||||
|
||||
res.json(userResponse.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch user from session', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
57
server/routes/radarr.js
Normal file
57
server/routes/radarr.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const express = require('express');
|
||||
const axios = require('axios');
|
||||
const router = express.Router();
|
||||
|
||||
const RADARR_URL = process.env.RADARR_URL;
|
||||
const RADARR_API_KEY = process.env.RADARR_API_KEY;
|
||||
|
||||
// Get queue
|
||||
router.get('/queue', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${RADARR_URL}/api/v3/queue`, {
|
||||
headers: { 'X-Api-Key': RADARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Radarr queue', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get history
|
||||
router.get('/history', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${RADARR_URL}/api/v3/history`, {
|
||||
headers: { 'X-Api-Key': RADARR_API_KEY },
|
||||
params: { pageSize: req.query.pageSize || 50 }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Radarr history', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get movie details
|
||||
router.get('/movies/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${RADARR_URL}/api/v3/movie/${req.params.id}`, {
|
||||
headers: { 'X-Api-Key': RADARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch movie details', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get all movies with tags
|
||||
router.get('/movies', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${RADARR_URL}/api/v3/movie`, {
|
||||
headers: { 'X-Api-Key': RADARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch movies', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
41
server/routes/sabnzbd.js
Normal file
41
server/routes/sabnzbd.js
Normal file
@@ -0,0 +1,41 @@
|
||||
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;
|
||||
|
||||
// Get current queue
|
||||
router.get('/queue', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${SABNZBD_URL}/api`, {
|
||||
params: {
|
||||
mode: 'queue',
|
||||
apikey: SABNZBD_API_KEY,
|
||||
output: 'json'
|
||||
}
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch SABnzbd queue', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get history
|
||||
router.get('/history', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${SABNZBD_URL}/api`, {
|
||||
params: {
|
||||
mode: 'history',
|
||||
apikey: SABNZBD_API_KEY,
|
||||
output: 'json',
|
||||
limit: req.query.limit || 50
|
||||
}
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch SABnzbd history', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
57
server/routes/sonarr.js
Normal file
57
server/routes/sonarr.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const express = require('express');
|
||||
const axios = require('axios');
|
||||
const router = express.Router();
|
||||
|
||||
const SONARR_URL = process.env.SONARR_URL;
|
||||
const SONARR_API_KEY = process.env.SONARR_API_KEY;
|
||||
|
||||
// Get queue
|
||||
router.get('/queue', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${SONARR_URL}/api/v3/queue`, {
|
||||
headers: { 'X-Api-Key': SONARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Sonarr queue', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get history
|
||||
router.get('/history', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${SONARR_URL}/api/v3/history`, {
|
||||
headers: { 'X-Api-Key': SONARR_API_KEY },
|
||||
params: { pageSize: req.query.pageSize || 50 }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Sonarr history', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get series details
|
||||
router.get('/series/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${SONARR_URL}/api/v3/series/${req.params.id}`, {
|
||||
headers: { 'X-Api-Key': SONARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch series details', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get all series with tags
|
||||
router.get('/series', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${SONARR_URL}/api/v3/series`, {
|
||||
headers: { 'X-Api-Key': SONARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch series', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user