let currentUser = null; let downloads = []; let refreshInterval = null; let currentRefreshRate = 5000; // default 5 seconds // Check authentication on load document.addEventListener('DOMContentLoaded', () => { checkAuthentication(); document.getElementById('login-form').addEventListener('submit', handleLogin); document.getElementById('logout-btn').addEventListener('click', handleLogout); document.getElementById('refresh-rate').addEventListener('change', handleRefreshRateChange); }); function startAutoRefresh() { if (refreshInterval) clearInterval(refreshInterval); if (currentRefreshRate > 0) { refreshInterval = setInterval(() => fetchUserDownloads(false), currentRefreshRate); } } function handleRefreshRateChange(e) { const rate = parseInt(e.target.value); currentRefreshRate = rate; startAutoRefresh(); } function stopAutoRefresh() { if (refreshInterval) { clearInterval(refreshInterval); refreshInterval = null; } } async function checkAuthentication() { try { const response = await fetch('/api/auth/me'); const data = await response.json(); if (data.authenticated) { currentUser = data.user; showDashboard(); fetchUserDownloads(true); startAutoRefresh(); } else { showLogin(); } } catch (err) { console.error('Authentication check failed:', err); showLogin(); } } async function handleLogin(e) { e.preventDefault(); const username = document.getElementById('username').value; const password = document.getElementById('password').value; try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await response.json(); if (data.success) { currentUser = data.user; showDashboard(); fetchUserDownloads(true); startAutoRefresh(); } else { showLoginError(data.error || 'Login failed'); } } catch (err) { showLoginError('Login failed. Please try again.'); console.error(err); } } async function handleLogout() { try { stopAutoRefresh(); await fetch('/api/auth/logout', { method: 'POST' }); currentUser = null; downloads = []; showLogin(); } catch (err) { console.error('Logout failed:', err); } } function showLogin() { document.getElementById('login-container').style.display = 'flex'; document.getElementById('dashboard-container').style.display = 'none'; hideLoginError(); } function showDashboard() { document.getElementById('login-container').style.display = 'none'; document.getElementById('dashboard-container').style.display = 'block'; document.getElementById('currentUser').textContent = currentUser.name || '-'; } function showLoginError(message) { const errorDiv = document.getElementById('login-error'); errorDiv.textContent = message; errorDiv.style.display = 'block'; } function hideLoginError() { const errorDiv = document.getElementById('login-error'); errorDiv.style.display = 'none'; } async function fetchUserDownloads(isInitialLoad = false) { if (isInitialLoad) { showLoading(); } hideError(); try { const response = await fetch('/api/dashboard/user-downloads'); const data = await response.json(); currentUser = data.user; downloads = data.downloads; // Debug: log first download to see what fields are present if (downloads.length > 0) { console.log('[Dashboard] Download data:', JSON.stringify(downloads[0])); } document.getElementById('currentUser').textContent = currentUser || '-'; renderDownloads(); } catch (err) { showError('Failed to fetch downloads. Make sure all services are configured.'); console.error(err); } finally { if (isInitialLoad) { hideLoading(); } } } function renderDownloads() { const downloadsList = document.getElementById('downloads-list'); const noDownloads = document.getElementById('no-downloads'); if (downloads.length === 0) { noDownloads.style.display = 'block'; downloadsList.innerHTML = ''; return; } noDownloads.style.display = 'none'; // Get existing cards const existingCards = new Map(); downloadsList.querySelectorAll('.download-card').forEach(card => { existingCards.set(card.dataset.id, card); }); // Track which downloads we've processed const processedIds = new Set(); downloads.forEach(download => { const id = download.title; processedIds.add(id); const existingCard = existingCards.get(id); if (existingCard) { // Update existing card updateDownloadCard(existingCard, download); } else { // Create new card const card = createDownloadCard(download); downloadsList.appendChild(card); } }); // Remove cards for downloads that no longer exist existingCards.forEach((card, id) => { if (!processedIds.has(id)) { card.remove(); } }); } function updateDownloadCard(card, download) { // Update status const statusEl = card.querySelector('.download-status'); if (statusEl && statusEl.textContent !== download.status) { statusEl.textContent = download.status; statusEl.className = `download-status ${download.status}`; } // Update progress bar and missing pieces const progressContainer = card.querySelector('.progress-container'); if (progressContainer && download.progress !== undefined) { const progressBar = progressContainer.querySelector('.progress-bar'); const progressText = progressContainer.querySelector('.progress-text'); const missingText = progressContainer.querySelector('.missing-text'); if (progressBar) { const downloaded = progressBar.querySelector('.downloaded'); if (downloaded) { downloaded.style.width = download.progress + '%'; } } if (progressText) { progressText.textContent = download.progress + '%'; } if (missingText) { const totalMb = parseFloat(download.mb) || parseFloat(download.size); const missingMb = parseFloat(download.mbmissing) || 0; if (missingMb > 0 && totalMb > 0) { missingText.textContent = `(missing ${missingMb.toFixed(1)} of ${totalMb.toFixed(1)} MB)`; } else { missingText.textContent = ''; } } } // Update speed const speedEl = card.querySelector('.detail-item[data-label="Speed"] .detail-value'); if (speedEl && download.speed !== undefined) { speedEl.textContent = download.speed; } // Update ETA const etaEl = card.querySelector('.detail-item[data-label="ETA"] .detail-value'); if (etaEl && download.eta !== undefined) { etaEl.textContent = download.eta; } // Update qBittorrent-specific fields if (download.qbittorrent) { const seedsEl = card.querySelector('.detail-item[data-label="Seeds"] .detail-value'); if (seedsEl && download.seeds !== undefined) { seedsEl.textContent = download.seeds; } const peersEl = card.querySelector('.detail-item[data-label="Peers"] .detail-value'); if (peersEl && download.peers !== undefined) { peersEl.textContent = download.peers; } const availabilityEl = card.querySelector('.detail-item[data-label="Availability"] .detail-value'); if (availabilityEl && download.availability !== undefined) { availabilityEl.textContent = `${download.availability}%`; } } } function createDownloadCard(download) { const card = document.createElement('div'); card.className = `download-card ${download.type}`; card.dataset.id = download.title; // Cover art if (download.coverArt) { const coverDiv = document.createElement('div'); coverDiv.className = 'download-cover'; const coverImg = document.createElement('img'); coverImg.src = download.coverArt; coverImg.alt = download.movieName || download.seriesName || download.title; coverImg.loading = 'lazy'; coverDiv.appendChild(coverImg); card.appendChild(coverDiv); } // Info wrapper const infoDiv = document.createElement('div'); infoDiv.className = 'download-info'; const header = document.createElement('div'); header.className = 'download-header'; const type = document.createElement('span'); type.className = `download-type ${download.type}`; if (download.type === 'series') { type.textContent = '📺 Series'; } else if (download.type === 'movie') { type.textContent = '🎬 Movie'; } else if (download.type === 'torrent') { const instName = download.instanceName ? ` (${download.instanceName})` : ''; type.textContent = `📥 Torrent${instName}`; } else { type.textContent = download.type; } const status = document.createElement('span'); status.className = `download-status ${download.status}`; status.textContent = download.status; header.appendChild(type); header.appendChild(status); const title = document.createElement('h3'); title.className = 'download-title'; title.textContent = download.title; infoDiv.appendChild(header); infoDiv.appendChild(title); if (download.seriesName) { const series = document.createElement('p'); series.className = 'download-series'; series.textContent = `Series: ${download.seriesName}`; infoDiv.appendChild(series); } if (download.movieName) { const movie = document.createElement('p'); movie.className = 'download-movie'; movie.textContent = `Movie: ${download.movieName}`; infoDiv.appendChild(movie); } const details = document.createElement('div'); details.className = 'download-details'; const size = createDetailItem('Size', formatSize(download.size)); details.appendChild(size); if (download.progress !== undefined) { const progressItem = document.createElement('div'); progressItem.className = 'detail-item progress-item'; progressItem.dataset.label = 'Progress'; const labelSpan = document.createElement('span'); labelSpan.className = 'detail-label'; labelSpan.textContent = 'Progress'; const valueDiv = document.createElement('div'); valueDiv.className = 'progress-container'; // Progress bar with segments const totalMb = parseFloat(download.mb) || parseFloat(download.size); const missingMb = parseFloat(download.mbmissing) || 0; const downloadedMb = totalMb - missingMb; const progressPercent = parseFloat(download.progress) || 0; const missingPercent = totalMb > 0 ? (missingMb / totalMb) * 100 : 0; const progressBar = document.createElement('div'); progressBar.className = 'progress-bar'; // Downloaded portion (green) if (progressPercent > 0) { const downloaded = document.createElement('div'); downloaded.className = 'progress-segment downloaded'; downloaded.style.width = progressPercent + '%'; progressBar.appendChild(downloaded); } valueDiv.appendChild(progressBar); // Text showing percentage const progressText = document.createElement('span'); progressText.className = 'progress-text'; progressText.textContent = download.progress + '%'; valueDiv.appendChild(progressText); // Missing pieces text if (missingMb > 0 && totalMb > 0) { const missingText = document.createElement('span'); missingText.className = 'missing-text'; missingText.textContent = `(missing ${missingMb.toFixed(1)} of ${totalMb.toFixed(1)} MB)`; valueDiv.appendChild(missingText); } progressItem.appendChild(labelSpan); progressItem.appendChild(valueDiv); details.appendChild(progressItem); } if (download.speed) { const speed = createDetailItem('Speed', download.speed); details.appendChild(speed); } if (download.eta) { const eta = createDetailItem('ETA', download.eta); details.appendChild(eta); } // qBittorrent-specific fields if (download.qbittorrent) { if (download.seeds !== undefined) { const seeds = createDetailItem('Seeds', download.seeds); details.appendChild(seeds); } if (download.peers !== undefined) { const peers = createDetailItem('Peers', download.peers); details.appendChild(peers); } if (download.availability !== undefined) { const availability = createDetailItem('Availability', `${download.availability}%`); details.appendChild(availability); } } if (download.completedAt) { const completed = createDetailItem('Completed', formatDate(download.completedAt)); details.appendChild(completed); } infoDiv.appendChild(details); card.appendChild(infoDiv); return card; } function createDetailItem(label, value) { const item = document.createElement('div'); item.className = 'detail-item'; item.dataset.label = label; const labelSpan = document.createElement('span'); labelSpan.className = 'detail-label'; labelSpan.textContent = label; const valueSpan = document.createElement('span'); valueSpan.className = 'detail-value'; valueSpan.textContent = value; item.appendChild(labelSpan); item.appendChild(valueSpan); return item; } function formatSize(size) { if (!size) return 'N/A'; // If already a formatted string (e.g., "21.5 GB"), return as-is if (typeof size === 'string') { return size; } // If it's a number (bytes), format it const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(size) / Math.log(1024)); return Math.round(size / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; } function formatDate(dateString) { if (!dateString) return 'N/A'; return new Date(dateString).toLocaleString(); } function showError(message) { const errorDiv = document.getElementById('error-message'); errorDiv.textContent = message; errorDiv.style.display = 'block'; } function hideError() { const errorDiv = document.getElementById('error-message'); errorDiv.style.display = 'none'; } function showLoading() { const loading = document.getElementById('loading'); loading.style.display = 'block'; } function hideLoading() { const loading = document.getElementById('loading'); loading.style.display = 'none'; }