/** * AniWorld - Series Manager Module * * Manages series data, filtering, sorting, and rendering. * * Dependencies: constants.js, api-client.js, ui-utils.js */ var AniWorld = window.AniWorld || {}; AniWorld.SeriesManager = (function() { 'use strict'; const API = AniWorld.Constants.API; // State let seriesData = []; let filteredSeriesData = []; let showMissingOnly = false; let sortAlphabetical = false; /** * Initialize the series manager */ function init() { bindEvents(); } /** * Bind UI events for filtering and sorting */ function bindEvents() { const missingOnlyBtn = document.getElementById('show-missing-only'); if (missingOnlyBtn) { missingOnlyBtn.addEventListener('click', toggleMissingOnlyFilter); } const sortBtn = document.getElementById('sort-alphabetical'); if (sortBtn) { sortBtn.addEventListener('click', toggleAlphabeticalSort); } } /** * Load series from API * @returns {Promise} Array of series data */ async function loadSeries() { try { AniWorld.UI.showLoading(); const response = await AniWorld.ApiClient.get(API.ANIME_LIST); if (!response) { return []; } const data = await response.json(); // Check if response has the expected format if (Array.isArray(data)) { // API returns array of AnimeSummary objects seriesData = data.map(function(anime) { // Count total missing episodes from the episode dictionary const episodeDict = anime.missing_episodes || {}; const totalMissing = Object.values(episodeDict).reduce( function(sum, episodes) { return sum + (Array.isArray(episodes) ? episodes.length : 0); }, 0 ); return { key: anime.key, name: anime.name, site: anime.site, folder: anime.folder, episodeDict: episodeDict, missing_episodes: totalMissing, has_missing: anime.has_missing || totalMissing > 0, has_nfo: anime.has_nfo || false, nfo_created_at: anime.nfo_created_at || null, nfo_updated_at: anime.nfo_updated_at || null, tmdb_id: anime.tmdb_id || null, tvdb_id: anime.tvdb_id || null, loading_status: anime.loading_status || 'completed', episodes_loaded: anime.episodes_loaded !== false, nfo_loaded: anime.nfo_loaded !== false, logo_loaded: anime.logo_loaded !== false, images_loaded: anime.images_loaded !== false }; }); } else if (data.status === 'success') { // Legacy format support seriesData = data.series; } else { AniWorld.UI.showToast('Error loading series: ' + (data.message || 'Unknown error'), 'error'); return []; } applyFiltersAndSort(); renderSeries(); return seriesData; } catch (error) { console.error('Error loading series:', error); AniWorld.UI.showToast('Failed to load series', 'error'); return []; } finally { AniWorld.UI.hideLoading(); } } /** * Toggle missing episodes only filter */ function toggleMissingOnlyFilter() { showMissingOnly = !showMissingOnly; const button = document.getElementById('show-missing-only'); button.setAttribute('data-active', showMissingOnly); button.classList.toggle('active', showMissingOnly); const icon = button.querySelector('i'); const text = button.querySelector('span'); if (showMissingOnly) { icon.className = 'fas fa-filter-circle-xmark'; text.textContent = 'Show All Series'; } else { icon.className = 'fas fa-filter'; text.textContent = 'Missing Episodes Only'; } applyFiltersAndSort(); renderSeries(); // Clear selection when filter changes if (AniWorld.SelectionManager) { AniWorld.SelectionManager.clearSelection(); } } /** * Toggle alphabetical sorting */ function toggleAlphabeticalSort() { sortAlphabetical = !sortAlphabetical; const button = document.getElementById('sort-alphabetical'); button.setAttribute('data-active', sortAlphabetical); button.classList.toggle('active', sortAlphabetical); const icon = button.querySelector('i'); const text = button.querySelector('span'); if (sortAlphabetical) { icon.className = 'fas fa-sort-alpha-up'; text.textContent = 'Default Sort'; } else { icon.className = 'fas fa-sort-alpha-down'; text.textContent = 'A-Z Sort'; } applyFiltersAndSort(); renderSeries(); } /** * Apply current filters and sorting to series data */ function applyFiltersAndSort() { let filtered = seriesData.slice(); // Sort based on the current sorting mode filtered.sort(function(a, b) { if (sortAlphabetical) { // Pure alphabetical sorting return AniWorld.UI.getDisplayName(a).localeCompare(AniWorld.UI.getDisplayName(b)); } else { // Default sorting: missing episodes first (descending), then by name if (a.missing_episodes > 0 && b.missing_episodes === 0) return -1; if (a.missing_episodes === 0 && b.missing_episodes > 0) return 1; // If both have missing episodes, sort by count (descending) if (a.missing_episodes > 0 && b.missing_episodes > 0) { if (a.missing_episodes !== b.missing_episodes) { return b.missing_episodes - a.missing_episodes; } } // For series with same missing episode status, maintain stable order return 0; } }); // Apply missing episodes filter if (showMissingOnly) { filtered = filtered.filter(function(serie) { return serie.missing_episodes > 0; }); } filteredSeriesData = filtered; } /** * Render series cards in the grid */ function renderSeries() { const grid = document.getElementById('series-grid'); const dataToRender = filteredSeriesData.length > 0 ? filteredSeriesData : (seriesData.length > 0 ? seriesData : []); if (dataToRender.length === 0) { const message = showMissingOnly ? 'No series with missing episodes found.' : 'No series found. Try searching for anime or rescanning your directory.'; grid.innerHTML = '
' + '' + '

' + message + '

' + '
'; return; } grid.innerHTML = dataToRender.map(function(serie) { return createSerieCard(serie); }).join(''); // Bind checkbox events grid.querySelectorAll('.series-checkbox').forEach(function(checkbox) { checkbox.addEventListener('change', function(e) { if (AniWorld.SelectionManager) { AniWorld.SelectionManager.toggleSerieSelection(e.target.dataset.key, e.target.checked); } }); }); // Bind NFO button events grid.querySelectorAll('.nfo-create-btn').forEach(function(btn) { btn.addEventListener('click', function(e) { e.stopPropagation(); const seriesKey = e.currentTarget.dataset.key; if (AniWorld.NFOManager) { AniWorld.NFOManager.createNFO(seriesKey).then(function() { // Reload series to reflect new NFO status loadSeries(); }).catch(function(error) { console.error('Error creating NFO:', error); }); } }); }); grid.querySelectorAll('.nfo-view-btn').forEach(function(btn) { btn.addEventListener('click', function(e) { e.stopPropagation(); const seriesKey = e.currentTarget.dataset.key; if (AniWorld.NFOManager) { AniWorld.NFOManager.showNFOModal(seriesKey); } }); }); grid.querySelectorAll('.nfo-refresh-btn').forEach(function(btn) { btn.addEventListener('click', function(e) { e.stopPropagation(); const seriesKey = e.currentTarget.dataset.key; if (AniWorld.NFOManager) { AniWorld.NFOManager.refreshNFO(seriesKey).then(function() { // Reload series to reflect updated NFO loadSeries(); }).catch(function(error) { console.error('Error refreshing NFO:', error); }); } }); }); } /** * Get loading indicator HTML for a series * @param {Object} serie - Series data object * @returns {string} HTML string */ function getLoadingIndicatorHTML(serie) { const statusMessages = { 'pending': 'Queued for loading...', 'loading_episodes': 'Loading episodes...', 'loading_nfo': 'Generating NFO metadata...', 'loading_logo': 'Downloading logo...', 'loading_images': 'Downloading images...' }; const statusMessage = statusMessages[serie.loading_status] || 'Loading...'; return '
' + '
' + ' ' + statusMessage + '' + '
' + getProgressItemsHTML(serie) + '
' + '
' + '
'; } /** * Get progress items HTML * @param {Object} serie - Series data object * @returns {string} HTML string */ function getProgressItemsHTML(serie) { const items = [ { key: 'episodes_loaded', label: 'Episodes' }, { key: 'nfo_loaded', label: 'NFO' }, { key: 'logo_loaded', label: 'Logo' }, { key: 'images_loaded', label: 'Images' } ]; return items.map(function(item) { const isLoaded = serie[item.key]; return '
' + '' + (isLoaded ? '✓' : '⋯') + '' + '' + item.label + '' + '
'; }).join(''); } /** * Create HTML for a series card * @param {Object} serie - Series data object * @returns {string} HTML string */ function createSerieCard(serie) { const isSelected = AniWorld.SelectionManager ? AniWorld.SelectionManager.isSelected(serie.key) : false; const hasMissingEpisodes = serie.missing_episodes > 0; const canBeSelected = hasMissingEpisodes; const hasNfo = serie.has_nfo || false; const isLoading = serie.loading_status && serie.loading_status !== 'completed' && serie.loading_status !== 'failed'; // Debug logging for troubleshooting if (serie.key === 'so-im-a-spider-so-what') { console.log('[createSerieCard] Spider series:', { key: serie.key, missing_episodes: serie.missing_episodes, missing_episodes_type: typeof serie.missing_episodes, episodeDict: serie.episodeDict, hasMissingEpisodes: hasMissingEpisodes, has_missing: serie.has_missing }); } return '
' + '
' + '' + '
' + '

' + AniWorld.UI.escapeHtml(AniWorld.UI.getDisplayName(serie)) + '

' + '
' + AniWorld.UI.escapeHtml(serie.folder) + '
' + '
' + '
' + (hasMissingEpisodes ? '' : '') + (hasNfo ? '' : '') + '
' + '
' + '
' + '
' + '' + '' + (hasMissingEpisodes ? serie.missing_episodes + ' missing episodes' : 'Complete') + '' + '
' + '' + serie.site + '' + '
' + (isLoading ? getLoadingIndicatorHTML(serie) : '') + '
' + (hasNfo ? '' + '' : '') + '
' + '
'; } /** * Get all series data * @returns {Array} Series data array */ function getSeriesData() { return seriesData; } /** * Get filtered series data * @returns {Array} Filtered series data array */ function getFilteredSeriesData() { return filteredSeriesData; } /** * Find a series by key * @param {string} key - Series key * @returns {Object|undefined} Series object or undefined */ function findByKey(key) { return seriesData.find(function(s) { return s.key === key; }); } /** * Update series loading status from WebSocket event * @param {Object} data - Loading status data */ function updateSeriesLoadingStatus(data) { const seriesKey = data.series_id || data.series_key; if (!seriesKey) return; // Find series in data const serie = findByKey(seriesKey); if (!serie) return; // Update series data serie.loading_status = data.status || data.loading_status; if (data.progress) { serie.episodes_loaded = data.progress.episodes || false; serie.nfo_loaded = data.progress.nfo || false; serie.logo_loaded = data.progress.logo || false; serie.images_loaded = data.progress.images || false; } // Update the specific card in the DOM const seriesCard = document.querySelector('[data-series-id="' + seriesKey + '"]'); if (seriesCard) { // If loading completed, reload to get fresh data if (data.status === 'completed') { loadSeries(); } else { // Update loading indicator const loadingIndicator = seriesCard.querySelector('.loading-indicator'); if (loadingIndicator) { loadingIndicator.innerHTML = '
' + ' ' + (data.message || 'Loading...') + '' + '
' + getProgressItemsHTML(serie) + '
' + '
'; } else if (serie.loading_status !== 'completed' && serie.loading_status !== 'failed') { // Add loading indicator if it doesn't exist const statsDiv = seriesCard.querySelector('.series-stats'); if (statsDiv) { statsDiv.insertAdjacentHTML('afterend', getLoadingIndicatorHTML(serie)); } } // Add loading class to card seriesCard.classList.add('loading'); } } } /** * Update a single series from WebSocket data * @param {Object} updatedData - Updated series data from WebSocket */ function updateSingleSeries(updatedData) { if (!updatedData || !updatedData.key) { console.warn('Invalid series update data:', updatedData); return; } console.log('[updateSingleSeries] Received data:', updatedData); console.log('[updateSingleSeries] missing_episodes type:', typeof updatedData.missing_episodes); console.log('[updateSingleSeries] missing_episodes value:', updatedData.missing_episodes); // Count total missing episodes from the episode dictionary const episodeDict = updatedData.missing_episodes || {}; console.log('[updateSingleSeries] episodeDict:', episodeDict); const totalMissing = Object.values(episodeDict).reduce( function(sum, episodes) { return sum + (Array.isArray(episodes) ? episodes.length : 0); }, 0 ); console.log('[updateSingleSeries] totalMissing calculated:', totalMissing); // Transform WebSocket data to match our internal format const transformedSerie = { key: updatedData.key, name: updatedData.name, site: updatedData.site || 'aniworld.to', folder: updatedData.folder, episodeDict: episodeDict, missing_episodes: totalMissing, has_missing: updatedData.has_missing || totalMissing > 0, has_nfo: updatedData.has_nfo || false, nfo_created_at: updatedData.nfo_created_at || null, nfo_updated_at: updatedData.nfo_updated_at || null, tmdb_id: updatedData.tmdb_id || null, tvdb_id: updatedData.tvdb_id || null, loading_status: updatedData.loading_status || 'completed', episodes_loaded: updatedData.episodes_loaded !== false, nfo_loaded: updatedData.nfo_loaded !== false, logo_loaded: updatedData.logo_loaded !== false, images_loaded: updatedData.images_loaded !== false }; console.log('[updateSingleSeries] Transformed serie:', transformedSerie); // Find existing series in our data const existingIndex = seriesData.findIndex(function(s) { return s.key === updatedData.key; }); if (existingIndex >= 0) { // Update existing series seriesData[existingIndex] = transformedSerie; console.log('Updated existing series:', updatedData.key, transformedSerie); } else { // Add new series seriesData.push(transformedSerie); console.log('Added new series:', updatedData.key, transformedSerie); } // Reapply filters and re-render applyFiltersAndSort(); renderSeries(); } // Public API return { init: init, loadSeries: loadSeries, renderSeries: renderSeries, applyFiltersAndSort: applyFiltersAndSort, getSeriesData: getSeriesData, getFilteredSeriesData: getFilteredSeriesData, findByKey: findByKey, updateSeriesLoadingStatus: updateSeriesLoadingStatus, updateSingleSeries: updateSingleSeries }; })();