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

18
.env.example Normal file
View File

@@ -0,0 +1,18 @@
# Server Configuration
PORT=3001
# SABnzbd Configuration
SABNZBD_URL=http://localhost:8080
SABNZBD_API_KEY=your_sabnzbd_api_key
# Sonarr Configuration
SONARR_URL=http://localhost:8989
SONARR_API_KEY=your_sonarr_api_key
# Radarr Configuration
RADARR_URL=http://localhost:7878
RADARR_API_KEY=your_radarr_api_key
# Emby Configuration
EMBY_URL=http://localhost:8096
EMBY_API_KEY=your_emby_api_key

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
.env
dist/
build/
.DS_Store
*.log

136
README.md Normal file
View File

@@ -0,0 +1,136 @@
# Media Download Dashboard
A dashboard for tracking SABnzbd downloads matched to Sonarr/Radarr activity by user, with user identification via Emby server sessions.
## Features
- **User Identification**: Automatically identifies the current user from active Emby server sessions
- **Download Tracking**: Retrieves download details from SABnzbd
- **Service Integration**: Matches downloads to activity in Sonarr and Radarr
- **User Tag Matching**: Identifies requesting users from tags present against shows or movies
- **Personalized Dashboard**: Presents details of downloading shows/movies for the current user
## Prerequisites
- Node.js (v14 or higher)
- npm
- SABnzbd instance
- Sonarr instance
- Radarr instance
- Emby instance
## Installation
1. Clone the repository and navigate to the project directory
2. Install dependencies:
```bash
npm install
cd client && npm install
cd ..
```
3. Copy the example environment file and configure your services:
```bash
cp .env.example .env
```
Edit `.env` with your service URLs and API keys:
```
PORT=3001
SABNZBD_URL=http://localhost:8080
SABNZBD_API_KEY=your_sabnzbd_api_key
SONARR_URL=http://localhost:8989
SONARR_API_KEY=your_sonarr_api_key
RADARR_URL=http://localhost:7878
RADARR_API_KEY=your_radarr_api_key
EMBY_URL=http://localhost:8096
EMBY_API_KEY=your_emby_api_key
```
## Tagging Your Media
To enable user-specific download tracking, tag your shows and movies in Sonarr/Radarr:
1. In Sonarr/Radarr, go to your series/movie settings
2. Add a tag with the format: `user:username` (e.g., `user:john`)
3. Apply this tag to the series/movies you want to track for that user
The dashboard will match downloads to users based on these tags.
## Usage
Start the development servers:
```bash
npm run dev
```
This will start:
- Backend server on `http://localhost:3001`
- Frontend development server on `http://localhost:5173`
Open your browser and navigate to `http://localhost:5173`
## How It Works
1. **Session Detection**: The dashboard connects to Emby to detect active user sessions
2. **User Identification**: The current user is identified from their active Emby session
3. **Download Retrieval**: SABnzbd queue and history are retrieved
4. **Activity Matching**: Downloads are matched to Sonarr/Radarr queue and history
5. **Tag Matching**: The user tag from the series/movie is extracted and matched to the current user
6. **Display**: Only downloads matching the current user are displayed
## API Endpoints
### SABnzbd
- `GET /api/sabnzbd/queue` - Get current queue
- `GET /api/sabnzbd/history` - Get download history
### Sonarr
- `GET /api/sonarr/queue` - Get Sonarr queue
- `GET /api/sonarr/history` - Get Sonarr history
- `GET /api/sonarr/series` - Get all series with tags
- `GET /api/sonarr/series/:id` - Get specific series details
### Radarr
- `GET /api/radarr/queue` - Get Radarr queue
- `GET /api/radarr/history` - Get Radarr history
- `GET /api/radarr/movies` - Get all movies with tags
- `GET /api/radarr/movies/:id` - Get specific movie details
### Emby
- `GET /api/emby/sessions` - Get active sessions
- `GET /api/emby/users` - Get all users
- `GET /api/emby/users/:id` - Get specific user details
- `GET /api/emby/session/:sessionId/user` - Get user from session
### Dashboard
- `GET /api/dashboard/user-downloads/:sessionId` - Get downloads for current user
- `GET /api/dashboard/user-summary` - Get download summary for all users
## Production Build
Build the frontend for production:
```bash
npm run client:build
```
Start the production server:
```bash
npm run server:start
```
The frontend will be served from the backend server.
## Troubleshooting
- **"Failed to fetch Emby sessions"**: Ensure Emby is running and the API key is correct
- **"No downloads found"**: Ensure your media is tagged with `user:username` format
- **Download not showing**: Check that the download name matches between SABnzbd and Sonarr/Radarr
## License
MIT

12
client/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Media Download Dashboard</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

22
client/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "media-download-dashboard-client",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"axios": "^1.6.0"
},
"devDependencies": {
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@vitejs/plugin-react": "^4.2.0",
"vite": "^5.0.0"
}
}

284
client/src/App.css Normal file
View File

@@ -0,0 +1,284 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.app {
min-height: 100vh;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.app-header {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.app-header h1 {
color: #333;
font-size: 2rem;
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
background: #f0f0f0;
padding: 10px 20px;
border-radius: 20px;
}
.user-label {
color: #666;
font-weight: 500;
}
.user-name {
color: #667eea;
font-weight: bold;
font-size: 1.1rem;
}
.controls {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.controls label {
color: #333;
font-weight: 500;
}
.session-select {
flex: 1;
min-width: 200px;
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 1rem;
cursor: pointer;
}
.session-select:focus {
outline: none;
border-color: #667eea;
}
.refresh-btn {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
transition: background 0.3s;
}
.refresh-btn:hover {
background: #5568d3;
}
.error-message {
background: #fee;
color: #c33;
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
border-left: 4px solid #c33;
}
.loading {
text-align: center;
padding: 40px;
color: white;
font-size: 1.2rem;
}
.downloads-container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.downloads-container h2 {
color: #333;
margin-bottom: 20px;
font-size: 1.5rem;
}
.no-downloads {
text-align: center;
padding: 40px;
color: #666;
}
.no-downloads p {
margin: 10px 0;
}
.downloads-list {
display: grid;
gap: 20px;
}
.download-card {
border: 2px solid #e0e0e0;
border-radius: 10px;
padding: 20px;
transition: transform 0.2s, box-shadow 0.2s;
}
.download-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
}
.download-card.series {
border-left: 4px solid #667eea;
}
.download-card.movie {
border-left: 4px solid #f093fb;
}
.download-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.download-type {
padding: 5px 15px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 500;
}
.download-type.series {
background: #e8eaf6;
color: #667eea;
}
.download-type.movie {
background: #fce4ec;
color: #f093fb;
}
.download-status {
padding: 5px 15px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 500;
text-transform: capitalize;
}
.download-status.downloading {
background: #e8f5e9;
color: #4caf50;
}
.download-status.completed {
background: #e3f2fd;
color: #2196f3;
}
.download-status.failed {
background: #ffebee;
color: #f44336;
}
.download-title {
color: #333;
margin-bottom: 10px;
font-size: 1.2rem;
}
.download-series,
.download-movie {
color: #666;
margin-bottom: 15px;
font-style: italic;
}
.download-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
padding-top: 15px;
border-top: 1px solid #e0e0e0;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.detail-label {
color: #999;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-value {
color: #333;
font-weight: 500;
}
.app-footer {
margin-top: 20px;
text-align: center;
color: white;
font-size: 0.9rem;
}
.app-footer p {
opacity: 0.9;
}
@media (max-width: 768px) {
.app-header {
flex-direction: column;
align-items: flex-start;
}
.controls {
flex-direction: column;
align-items: stretch;
}
.download-details {
grid-template-columns: 1fr;
}
}

180
client/src/App.jsx Normal file
View File

@@ -0,0 +1,180 @@
import { useState, useEffect } from 'react';
import axios from 'axios';
import './App.css';
function App() {
const [sessionId, setSessionId] = useState('');
const [currentUser, setCurrentUser] = useState(null);
const [downloads, setDownloads] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [sessions, setSessions] = useState([]);
useEffect(() => {
fetchSessions();
}, []);
const fetchSessions = async () => {
try {
const response = await axios.get('/api/emby/sessions');
setSessions(response.data);
// Auto-select first active session
const activeSession = response.data.find(s => s.NowPlayingItem || s.Active);
if (activeSession) {
setSessionId(activeSession.Id);
fetchUserDownloads(activeSession.Id);
}
} catch (err) {
setError('Failed to fetch Emby sessions. Make sure Emby is running and configured.');
console.error(err);
}
};
const fetchUserDownloads = async (sessionId) => {
setLoading(true);
setError(null);
try {
const response = await axios.get(`/api/dashboard/user-downloads/${sessionId}`);
setCurrentUser(response.data.user);
setDownloads(response.data.downloads);
} catch (err) {
setError('Failed to fetch downloads. Make sure all services are configured.');
console.error(err);
} finally {
setLoading(false);
}
};
const handleSessionChange = (e) => {
const newSessionId = e.target.value;
setSessionId(newSessionId);
if (newSessionId) {
fetchUserDownloads(newSessionId);
}
};
const formatSize = (bytes) => {
if (!bytes) return 'N/A';
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
};
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
return new Date(dateString).toLocaleString();
};
return (
<div className="app">
<header className="app-header">
<h1>Media Download Dashboard</h1>
{currentUser && (
<div className="user-info">
<span className="user-label">Current User:</span>
<span className="user-name">{currentUser}</span>
</div>
)}
</header>
<div className="controls">
<label htmlFor="session-select">Select Emby Session:</label>
<select
id="session-select"
value={sessionId}
onChange={handleSessionChange}
className="session-select"
>
<option value="">-- Select Session --</option>
{sessions.map(session => (
<option key={session.Id} value={session.Id}>
{session.UserName} - {session.Client} {session.NowPlayingItem ? `(Playing: ${session.NowPlayingItem.Name})` : '(Idle)'}
</option>
))}
</select>
<button onClick={fetchSessions} className="refresh-btn">Refresh Sessions</button>
</div>
{error && (
<div className="error-message">
{error}
</div>
)}
{loading && (
<div className="loading">Loading downloads...</div>
)}
{!loading && !error && (
<div className="downloads-container">
<h2>Your Downloads</h2>
{downloads.length === 0 ? (
<div className="no-downloads">
<p>No downloads found for your user.</p>
<p>Make sure your shows and movies are tagged with "user:yourusername" in Sonarr/Radarr.</p>
</div>
) : (
<div className="downloads-list">
{downloads.map((download, index) => (
<div key={index} className={`download-card ${download.type}`}>
<div className="download-header">
<span className={`download-type ${download.type}`}>
{download.type === 'series' ? '📺 Series' : '🎬 Movie'}
</span>
<span className={`download-status ${download.status}`}>
{download.status}
</span>
</div>
<h3 className="download-title">{download.title}</h3>
{download.seriesName && (
<p className="download-series">Series: {download.seriesName}</p>
)}
{download.movieName && (
<p className="download-movie">Movie: {download.movieName}</p>
)}
<div className="download-details">
<div className="detail-item">
<span className="detail-label">Size:</span>
<span className="detail-value">{formatSize(download.size)}</span>
</div>
{download.progress && (
<div className="detail-item">
<span className="detail-label">Progress:</span>
<span className="detail-value">{download.progress}%</span>
</div>
)}
{download.speed && (
<div className="detail-item">
<span className="detail-label">Speed:</span>
<span className="detail-value">{download.speed}</span>
</div>
)}
{download.eta && (
<div className="detail-item">
<span className="detail-label">ETA:</span>
<span className="detail-value">{download.eta}</span>
</div>
)}
{download.completedAt && (
<div className="detail-item">
<span className="detail-label">Completed:</span>
<span className="detail-value">{formatDate(download.completedAt)}</span>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
)}
<footer className="app-footer">
<p>Ensure your media is tagged with "user:username" in Sonarr/Radarr to match downloads to users.</p>
</footer>
</div>
);
}
export default App;

10
client/src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './App.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

15
client/vite.config.js Normal file
View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true
}
}
}
})

34
package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "media-download-dashboard",
"version": "1.0.0",
"description": "Dashboard for tracking SABnzbd downloads matched to Sonarr/Radarr activity by user",
"main": "server/index.js",
"scripts": {
"dev": "concurrently \"npm run server:dev\" \"npm run client:dev\"",
"server:dev": "nodemon server/index.js",
"client:dev": "cd client && npm run dev",
"server:start": "node server/index.js",
"client:build": "cd client && npm run build",
"install:all": "npm install && cd client && npm install"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"axios": "^1.6.0",
"node-cron": "^3.0.3"
},
"devDependencies": {
"nodemon": "^3.0.1",
"concurrently": "^8.2.2"
},
"keywords": [
"sabnzbd",
"sonarr",
"radarr",
"emby",
"dashboard"
],
"author": "",
"license": "MIT"
}

29
server/index.js Normal file
View 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
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;

66
server/routes/emby.js Normal file
View 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
View 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
View 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
View 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;