/** * AniWorld - Socket Handler Module for Index Page * * Handles WebSocket events specific to the index page. * * Dependencies: constants.js, websocket-client.js, ui-utils.js, scan-manager.js, series-manager.js */ var AniWorld = window.AniWorld || {}; AniWorld.IndexSocketHandler = (function() { 'use strict'; const WS_EVENTS = AniWorld.Constants.WS_EVENTS; // State let isDownloading = false; let isPaused = false; let localization = null; /** * Initialize socket handler * @param {Object} localizationObj - Localization object */ function init(localizationObj) { localization = localizationObj; setupSocketHandlers(); } /** * Get localized text */ function getText(key) { if (localization && localization.getText) { return localization.getText(key); } // Fallback text const fallbacks = { 'connected-server': 'Connected to server', 'disconnected-server': 'Disconnected from server', 'download-completed': 'Download completed', 'download-failed': 'Download failed', 'paused': 'Paused', 'downloading': 'Downloading...', 'connected': 'Connected', 'disconnected': 'Disconnected' }; return fallbacks[key] || key; } /** * Set up WebSocket event handlers */ function setupSocketHandlers() { const socket = AniWorld.WebSocketClient.getSocket(); if (!socket) { console.warn('Socket not available for handler setup'); return; } // Connection events socket.on('connect', function() { AniWorld.UI.showToast(getText('connected-server'), 'success'); updateConnectionStatus(true); AniWorld.ScanManager.checkActiveScanStatus(); }); socket.on('disconnect', function() { AniWorld.UI.showToast(getText('disconnected-server'), 'warning'); updateConnectionStatus(false); }); // Scan events socket.on(WS_EVENTS.SCAN_STARTED, function(data) { console.log('Scan started:', data); AniWorld.ScanManager.showScanProgressOverlay(data); AniWorld.ScanManager.updateProcessStatus('rescan', true); }); socket.on(WS_EVENTS.SCAN_PROGRESS, function(data) { console.log('Scan progress:', data); AniWorld.ScanManager.updateScanProgressOverlay(data); }); // Handle both legacy and new scan complete events const handleScanComplete = function(data) { console.log('Scan completed:', data); AniWorld.ScanManager.hideScanProgressOverlay(data); AniWorld.UI.showToast('Scan completed successfully', 'success'); AniWorld.ScanManager.updateProcessStatus('rescan', false); AniWorld.SeriesManager.loadSeries(); }; socket.on(WS_EVENTS.SCAN_COMPLETED, handleScanComplete); socket.on(WS_EVENTS.SCAN_COMPLETE, handleScanComplete); // Handle scan errors const handleScanError = function(data) { AniWorld.ConfigManager.hideStatus(); AniWorld.UI.showToast('Scan error: ' + (data.message || data.error), 'error'); AniWorld.ScanManager.updateProcessStatus('rescan', false, true); }; socket.on(WS_EVENTS.SCAN_ERROR, handleScanError); socket.on(WS_EVENTS.SCAN_FAILED, handleScanError); // Scheduled scan events socket.on(WS_EVENTS.SCHEDULED_RESCAN_STARTED, function() { AniWorld.UI.showToast('Scheduled rescan started', 'info'); AniWorld.ScanManager.updateProcessStatus('rescan', true); }); socket.on(WS_EVENTS.SCHEDULED_RESCAN_COMPLETED, function(data) { AniWorld.UI.showToast('Scheduled rescan completed successfully', 'success'); AniWorld.ScanManager.updateProcessStatus('rescan', false); AniWorld.SeriesManager.loadSeries(); }); socket.on(WS_EVENTS.SCHEDULED_RESCAN_ERROR, function(data) { AniWorld.UI.showToast('Scheduled rescan error: ' + data.error, 'error'); AniWorld.ScanManager.updateProcessStatus('rescan', false, true); }); socket.on(WS_EVENTS.SCHEDULED_RESCAN_SKIPPED, function(data) { AniWorld.UI.showToast('Scheduled rescan skipped: ' + data.reason, 'warning'); }); socket.on(WS_EVENTS.AUTO_DOWNLOAD_STARTED, function(data) { AniWorld.UI.showToast('Auto-download started after scheduled rescan', 'info'); AniWorld.ScanManager.updateProcessStatus('download', true); }); socket.on(WS_EVENTS.AUTO_DOWNLOAD_ERROR, function(data) { AniWorld.UI.showToast('Auto-download error: ' + data.error, 'error'); AniWorld.ScanManager.updateProcessStatus('download', false, true); }); // Series events socket.on(WS_EVENTS.SERIES_UPDATED, function(data) { console.log('Series updated:', data); // NOTE: websocket-client.js strips the outer {type, data, ...} wrapper // before emitting, so `data` here is the inner series data object // (e.g. {key, name, missing_episodes, ...}) — NOT {type, data, ...}. // AniWorld.SeriesManager.updateSingleSeries() expects this flat object. if (data && data.key && AniWorld.SeriesManager && AniWorld.SeriesManager.updateSingleSeries) { AniWorld.SeriesManager.updateSingleSeries(data); } else { // Fallback to full reload if data is incomplete console.warn('Incomplete series update data, falling back to full reload', data); if (AniWorld.SeriesManager && AniWorld.SeriesManager.loadSeries) { AniWorld.SeriesManager.loadSeries(); } } }); // Series loading events socket.on(WS_EVENTS.SERIES_LOADING_UPDATE, function(data) { console.log('Series loading update:', data); if (AniWorld.SeriesManager && AniWorld.SeriesManager.updateSeriesLoadingStatus) { AniWorld.SeriesManager.updateSeriesLoadingStatus(data); } }); // Series deleted event — remove the card from the UI socket.on(WS_EVENTS.SERIES_DELETED, function(data) { console.info('[SocketHandler] Series deleted:', data); if (data && data.key && AniWorld.SeriesManager && AniWorld.SeriesManager.removeSeries) { AniWorld.SeriesManager.removeSeries(data.key); AniWorld.UI.showToast('Series deleted: ' + (data.name || data.key), 'success'); } }); // Download events socket.on(WS_EVENTS.DOWNLOAD_STARTED, function(data) { isDownloading = true; isPaused = false; AniWorld.ScanManager.updateProcessStatus('download', true); showDownloadQueue(data); showStatus('Starting download of ' + data.total_series + ' series...', true, true); }); socket.on(WS_EVENTS.DOWNLOAD_PROGRESS, function(data) { let status = ''; let percent = 0; if (data.progress !== undefined) { percent = data.progress; status = 'Downloading: ' + percent.toFixed(1) + '%'; if (data.speed_mbps && data.speed_mbps > 0) { status += ' (' + data.speed_mbps.toFixed(1) + ' Mbps)'; } if (data.eta_seconds && data.eta_seconds > 0) { const eta = AniWorld.UI.formatETA(data.eta_seconds); status += ' - ETA: ' + eta; } } else if (data.total_bytes) { percent = ((data.downloaded_bytes || 0) / data.total_bytes * 100); status = 'Downloading: ' + percent.toFixed(1) + '%'; } else if (data.downloaded_mb !== undefined) { status = 'Downloaded: ' + data.downloaded_mb.toFixed(1) + ' MB'; } else { status = 'Downloading: ' + (data.percent || '0%'); } if (percent > 0) { updateProgress(percent, status); } else { updateStatus(status); } }); socket.on(WS_EVENTS.DOWNLOAD_COMPLETED, function(data) { isDownloading = false; isPaused = false; hideDownloadQueue(); AniWorld.ConfigManager.hideStatus(); AniWorld.UI.showToast(getText('download-completed'), 'success'); AniWorld.SeriesManager.loadSeries(); AniWorld.SelectionManager.clearSelection(); }); socket.on(WS_EVENTS.DOWNLOAD_ERROR, function(data) { isDownloading = false; isPaused = false; hideDownloadQueue(); AniWorld.ConfigManager.hideStatus(); AniWorld.UI.showToast(getText('download-failed') + ': ' + data.message, 'error'); }); // Download queue events socket.on(WS_EVENTS.DOWNLOAD_QUEUE_COMPLETED, function() { AniWorld.ScanManager.updateProcessStatus('download', false); AniWorld.UI.showToast('All downloads completed!', 'success'); }); socket.on(WS_EVENTS.DOWNLOAD_STOP_REQUESTED, function() { AniWorld.UI.showToast('Stopping downloads...', 'info'); }); socket.on(WS_EVENTS.DOWNLOAD_STOPPED, function() { AniWorld.ScanManager.updateProcessStatus('download', false); AniWorld.UI.showToast('Downloads stopped', 'success'); }); socket.on(WS_EVENTS.DOWNLOAD_QUEUE_UPDATE, function(data) { updateDownloadQueue(data); }); socket.on(WS_EVENTS.DOWNLOAD_EPISODE_UPDATE, function(data) { updateCurrentEpisode(data); }); socket.on(WS_EVENTS.DOWNLOAD_SERIES_COMPLETED, function(data) { updateDownloadProgress(data); }); // Download control events socket.on(WS_EVENTS.DOWNLOAD_PAUSED, function() { isPaused = true; updateStatus(getText('paused')); }); socket.on(WS_EVENTS.DOWNLOAD_RESUMED, function() { isPaused = false; updateStatus(getText('downloading')); }); socket.on(WS_EVENTS.DOWNLOAD_CANCELLED, function() { isDownloading = false; isPaused = false; hideDownloadQueue(); AniWorld.ConfigManager.hideStatus(); AniWorld.UI.showToast('Download cancelled', 'warning'); }); // NFO events socket.on('nfo_creating', function(data) { console.log('NFO creation started:', data); AniWorld.UI.showToast( 'Creating NFO for ' + (data.series_name || data.series_key), 'info' ); }); socket.on('nfo_completed', function(data) { console.log('NFO creation completed:', data); AniWorld.UI.showToast( 'NFO created for ' + (data.series_name || data.series_key), 'success' ); // Reload series to reflect new NFO status if (AniWorld.SeriesManager && AniWorld.SeriesManager.loadSeries) { AniWorld.SeriesManager.loadSeries(); } }); socket.on('nfo_failed', function(data) { console.error('NFO creation failed:', data); AniWorld.UI.showToast( 'NFO creation failed: ' + (data.message || data.error || 'Unknown error'), 'error' ); }); } /** * Update connection status display */ function updateConnectionStatus(connected) { const indicator = document.getElementById('connection-status-display'); if (indicator) { const statusIndicator = indicator.querySelector('.status-indicator'); const statusText = indicator.querySelector('.status-text'); if (connected) { statusIndicator.classList.add('connected'); statusText.textContent = getText('connected'); } else { statusIndicator.classList.remove('connected'); statusText.textContent = getText('disconnected'); } } } /** * Show status panel */ function showStatus(message, showProgress, showControls) { showProgress = showProgress || false; showControls = showControls || false; const panel = document.getElementById('status-panel'); const messageEl = document.getElementById('status-message'); const progressContainer = document.getElementById('progress-container'); const controlsContainer = document.getElementById('download-controls'); messageEl.textContent = message; progressContainer.classList.toggle('hidden', !showProgress); controlsContainer.classList.toggle('hidden', !showControls); if (showProgress) { updateProgress(0); } panel.classList.remove('hidden'); } /** * Update status message */ function updateStatus(message) { document.getElementById('status-message').textContent = message; } /** * Update progress bar */ function updateProgress(percent, message) { const fill = document.getElementById('progress-fill'); const text = document.getElementById('progress-text'); fill.style.width = percent + '%'; text.textContent = message || percent + '%'; } /** * Show download queue */ function showDownloadQueue(data) { const queueSection = document.getElementById('download-queue-section'); const queueProgress = document.getElementById('queue-progress'); queueProgress.textContent = '0/' + data.total_series + ' series'; updateDownloadQueue({ queue: data.queue || [], current_downloading: null, stats: { completed_series: 0, total_series: data.total_series } }); queueSection.classList.remove('hidden'); } /** * Hide download queue */ function hideDownloadQueue() { const queueSection = document.getElementById('download-queue-section'); const currentDownload = document.getElementById('current-download'); queueSection.classList.add('hidden'); currentDownload.classList.add('hidden'); } /** * Update download queue display */ function updateDownloadQueue(data) { const queueList = document.getElementById('queue-list'); const currentDownload = document.getElementById('current-download'); const queueProgress = document.getElementById('queue-progress'); // Update overall progress if (data.stats) { queueProgress.textContent = data.stats.completed_series + '/' + data.stats.total_series + ' series'; } // Update current downloading if (data.current_downloading) { currentDownload.classList.remove('hidden'); document.getElementById('current-serie-name').textContent = AniWorld.UI.getDisplayName(data.current_downloading); document.getElementById('current-episode').textContent = data.current_downloading.missing_episodes + ' episodes remaining'; } else { currentDownload.classList.add('hidden'); } // Update queue list if (data.queue && data.queue.length > 0) { queueList.innerHTML = data.queue.map(function(serie, index) { return '