commit 5d04d2796bc4efb2c4ebdbe130f4033c214efb36 Author: Gronod Date: Fri May 15 10:36:29 2026 +0100 Initial commit: Media Download Dashboard with SABnzbd, Sonarr, Radarr, and Emby integration diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4fa4f0a --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fdd8c5a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.env +dist/ +build/ +.DS_Store +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..4cde974 --- /dev/null +++ b/README.md @@ -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 diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..6893248 --- /dev/null +++ b/client/index.html @@ -0,0 +1,12 @@ + + + + + + Media Download Dashboard + + +
+ + + diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..8701624 --- /dev/null +++ b/client/package.json @@ -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" + } +} diff --git a/client/src/App.css b/client/src/App.css new file mode 100644 index 0000000..d6d1a25 --- /dev/null +++ b/client/src/App.css @@ -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; + } +} diff --git a/client/src/App.jsx b/client/src/App.jsx new file mode 100644 index 0000000..e328860 --- /dev/null +++ b/client/src/App.jsx @@ -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 ( +
+
+

Media Download Dashboard

+ {currentUser && ( +
+ Current User: + {currentUser} +
+ )} +
+ +
+ + + +
+ + {error && ( +
+ {error} +
+ )} + + {loading && ( +
Loading downloads...
+ )} + + {!loading && !error && ( +
+

Your Downloads

+ {downloads.length === 0 ? ( +
+

No downloads found for your user.

+

Make sure your shows and movies are tagged with "user:yourusername" in Sonarr/Radarr.

+
+ ) : ( +
+ {downloads.map((download, index) => ( +
+
+ + {download.type === 'series' ? '📺 Series' : '🎬 Movie'} + + + {download.status} + +
+

{download.title}

+ {download.seriesName && ( +

Series: {download.seriesName}

+ )} + {download.movieName && ( +

Movie: {download.movieName}

+ )} +
+
+ Size: + {formatSize(download.size)} +
+ {download.progress && ( +
+ Progress: + {download.progress}% +
+ )} + {download.speed && ( +
+ Speed: + {download.speed} +
+ )} + {download.eta && ( +
+ ETA: + {download.eta} +
+ )} + {download.completedAt && ( +
+ Completed: + {formatDate(download.completedAt)} +
+ )} +
+
+ ))} +
+ )} +
+ )} + +
+

Ensure your media is tagged with "user:username" in Sonarr/Radarr to match downloads to users.

+
+
+ ); +} + +export default App; diff --git a/client/src/main.jsx b/client/src/main.jsx new file mode 100644 index 0000000..6aee80b --- /dev/null +++ b/client/src/main.jsx @@ -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( + + + , +) diff --git a/client/vite.config.js b/client/vite.config.js new file mode 100644 index 0000000..0f5e78c --- /dev/null +++ b/client/vite.config.js @@ -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 + } + } + } +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..eeae0d3 --- /dev/null +++ b/package.json @@ -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" +} diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..f7ac702 --- /dev/null +++ b/server/index.js @@ -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}`); +}); diff --git a/server/routes/dashboard.js b/server/routes/dashboard.js new file mode 100644 index 0000000..743ba92 --- /dev/null +++ b/server/routes/dashboard.js @@ -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; diff --git a/server/routes/emby.js b/server/routes/emby.js new file mode 100644 index 0000000..6dab4da --- /dev/null +++ b/server/routes/emby.js @@ -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; diff --git a/server/routes/radarr.js b/server/routes/radarr.js new file mode 100644 index 0000000..083670f --- /dev/null +++ b/server/routes/radarr.js @@ -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; diff --git a/server/routes/sabnzbd.js b/server/routes/sabnzbd.js new file mode 100644 index 0000000..0120ee3 --- /dev/null +++ b/server/routes/sabnzbd.js @@ -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; diff --git a/server/routes/sonarr.js b/server/routes/sonarr.js new file mode 100644 index 0000000..68ea828 --- /dev/null +++ b/server/routes/sonarr.js @@ -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;