487 lines
18 KiB
JavaScript
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
|
|
};
|
|
})();
|