- Fix seriesMap key (use Sonarr internal id, not tvdbId) - Fix Sonarr tag resolution (use tag map like Radarr) - Use sourceTitle for history record matching - Fall back to embedded movie/series objects when API timeouts - Add includeMovie/includeSeries params to queue/history API calls - Add coverArt field to all download responses (TMDB poster URLs) - Add cover art display to frontend download cards - Fix user-summary route to use instance config and tag maps
188 lines
6.6 KiB
JavaScript
188 lines
6.6 KiB
JavaScript
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;
|