Files
Aniworld/src/server/web/static/js/index/socket-handler.js
2026-08-16 19:53:38 +02:00

487 lines
18 KiB
JavaScript

/**
* 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 '<div class="queue-item">' +
'<div class="queue-item-index">' + (index + 1) + '</div>' +
'<div class="queue-item-name">' + AniWorld.UI.escapeHtml(AniWorld.UI.getDisplayName(serie)) + '</div>' +
'<div class="queue-item-status">Waiting</div>' +
'</div>';
}).join('');
} else {
queueList.innerHTML = '<div class="queue-empty">No series in queue</div>';
}
}
/**
* Update current episode display
*/
function updateCurrentEpisode(data) {
const currentEpisode = document.getElementById('current-episode');
const progressFill = document.getElementById('current-progress-fill');
const progressText = document.getElementById('current-progress-text');
if (currentEpisode && data.episode) {
currentEpisode.textContent = data.episode + ' (' + data.episode_progress + ')';
}
if (data.overall_progress && progressFill && progressText) {
const parts = data.overall_progress.split('/');
const current = parseInt(parts[0]);
const total = parseInt(parts[1]);
const percent = total > 0 ? (current / total * 100).toFixed(1) : 0;
progressFill.style.width = percent + '%';
progressText.textContent = percent + '%';
}
}
/**
* Update download progress display
*/
function updateDownloadProgress(data) {
const queueProgress = document.getElementById('queue-progress');
if (queueProgress && data.completed_series && data.total_series) {
queueProgress.textContent = data.completed_series + '/' + data.total_series + ' series';
}
AniWorld.UI.showToast('Completed: ' + data.serie, 'success');
}
/**
* Get download state
*/
function getDownloadState() {
return {
isDownloading: isDownloading,
isPaused: isPaused
};
}
// Public API
return {
init: init,
updateConnectionStatus: updateConnectionStatus,
getDownloadState: getDownloadState
};
})();