Files
sofarr/client/src/App.jsx
Gronod 8c4cc20551
All checks were successful
Build and Push Docker Image / build (push) Successful in 48s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m21s
CI / Security audit (push) Successful in 1m47s
CI / Tests & coverage (push) Successful in 2m1s
Add MIT copyright headers to all source files
2026-05-19 09:07:42 +01:00

189 lines
6.7 KiB
JavaScript

// Copyright (c) 2026 Gordon Bolton. MIT License.
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}`}>
{download.coverArt && (
<div className="download-cover">
<img src={download.coverArt} alt={download.movieName || download.seriesName || download.title} />
</div>
)}
<div className="download-info">
<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>
)}
</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;