import { state } from './state.js'; import { getFromDB, clearStore, addItemsToStore } from './db.js'; import { logToConsole, emitirEventoActualizacion, mostrarSpinner, ocultarSpinner, showNotification, fetchWithTimeout, TimeoutError, _ } from './utils.js'; function mapSectionTypeToObjectName(sectionType) { switch (sectionType) { case 'movie': return 'movies'; case 'show': return 'series'; case 'artist': return 'artists'; case 'photo': return 'photos'; default: return null; } } async function fetchPlexResources(token, signal) { const response = await fetchWithTimeout(`https://plex.tv/api/resources?X-Plex-Token=${token}`, { signal }, 10000); if (!response.ok) throw new Error(`Error ${response.status} obteniendo recursos (token ${token.substring(0, 4)})`); const data = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(data, "application/xml"); if (xmlDoc.querySelector('parsererror')) throw new Error('Error parseando XML de recursos Plex'); return xmlDoc.querySelectorAll('Device[product="Plex Media Server"]'); } async function fetchSectionContent(url, signal, timeout = 7000) { const response = await fetchWithTimeout(url, { signal }, timeout); if (!response.ok) throw new Error(`Error ${response.status} obteniendo contenido`); const data = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(data, "text/xml"); if (xmlDoc.querySelector('parsererror')) throw new Error('Error parseando XML de contenido'); return xmlDoc; } async function parseAndStoreSectionItems(contentXml, storeName, serverData) { const type = storeName === 'movies' ? 'movie' : (storeName === 'series' ? 'show' : (storeName === 'artists' ? 'artist' : 'photo')); let items; if (type === 'movie' || type === 'show') { const itemSelector = type === 'movie' ? 'Video' : 'Directory'; items = Array.from(contentXml.querySelectorAll(itemSelector)).map(el => ({ title: el.getAttribute('title'), year: el.getAttribute('year'), genre: el.querySelector('Genre')?.getAttribute('tag') || _('noGenre') })); } else if (type === 'artist' || type === 'photo') { items = Array.from(contentXml.querySelectorAll('Directory')).map(el => ({ id: el.getAttribute('ratingKey'), title: el.getAttribute('title'), thumb: el.getAttribute('thumb') })); } const validItems = items.filter(i => i && i.title); logToConsole(` Encontrados ${validItems.length} items válidos en "${serverData.sectionTitle}".`); if (validItems.length > 0) { const entry = { ip: serverData.ip, puerto: serverData.puerto, token: serverData.token, protocolo: serverData.protocolo, serverName: serverData.nombre, titulos: validItems, tokenPrincipal: serverData.tokenPrincipal }; await addItemsToStore(storeName, [entry]); } } async function processServer(device, token, tipos, signal) { const serverName = device.getAttribute('name') || 'Servidor Sin Nombre'; const accessToken = device.getAttribute('accessToken'); if (!accessToken) { throw new Error(`Servidor: ${serverName} - Sin token de acceso.`); } const connections = Array.from(device.querySelectorAll('Connection')); const connection = connections.find(c => c.getAttribute('local') === '0') || connections.find(c => c.getAttribute('local') === '1'); if (!connection) { throw new Error(`Servidor: ${serverName} - Sin conexión válida.`); } const protocol = connection.getAttribute('protocol'); const address = connection.getAttribute('address'); const port = connection.getAttribute('port'); if (!address || !port) { throw new Error(`Servidor: ${serverName} - IP o Puerto no encontrados.`); } logToConsole(` -> Procesando servidor: ${serverName} (${address}:${port})`); const connectionData = { ip: address, puerto: port, token: accessToken, protocolo: protocol, tokenPrincipal: token, nombre: serverName }; await addItemsToStore('conexiones_locales', [connectionData]); const sectionsUrl = `${protocol}://${address}:${port}/library/sections?X-Plex-Token=${accessToken}`; const sectionsXml = await fetchSectionContent(sectionsUrl, signal); const directories = sectionsXml.querySelectorAll('Directory'); logToConsole(` Encontradas ${directories.length} secciones en ${serverName}.`); const sectionPromises = Array.from(directories).map(async (dir) => { const type = dir.getAttribute('type'); const storeName = mapSectionTypeToObjectName(type); if (storeName && tipos.includes(storeName)) { const sectionId = dir.getAttribute('key'); const sectionTitle = dir.getAttribute('title'); logToConsole(` Procesando sección: "${sectionTitle}" (Tipo: ${type})`); const contentUrl = `${protocol}://${address}:${port}/library/sections/${sectionId}/all?X-Plex-Token=${accessToken}`; try { const contentXml = await fetchSectionContent(contentUrl, signal); const serverData = { ...connectionData, sectionTitle }; await parseAndStoreSectionItems(contentXml, storeName, serverData); } catch (e) { if (e instanceof TimeoutError) { logToConsole(` [REINTENTO PENDIENTE] La sección "${sectionTitle}" tardó demasiado en responder.`); state.plexScanRetryQueue.push({ url: contentUrl, storeName: storeName, serverData: { ...connectionData, sectionTitle } }); } else { logToConsole(` Error procesando sección "${sectionTitle}": ${e.message}`); } } } }); await Promise.all(sectionPromises); } async function processRetryQueue(signal) { if (state.plexScanRetryQueue.length === 0) { logToConsole(_('noRetriesPending')); return; } logToConsole(_('startingRetryPhase', state.plexScanRetryQueue.length)); const queue = [...state.plexScanRetryQueue]; state.plexScanRetryQueue = []; for (const item of queue) { if (signal.aborted) { logToConsole(_('retryPhaseCancelled')); break; } const { url, storeName, serverData } = item; logToConsole(_('retyingSection', serverData.sectionTitle)); try { const contentXml = await fetchSectionContent(url, signal, 30000); await parseAndStoreSectionItems(contentXml, storeName, serverData); logToConsole(_('retrySuccess', serverData.sectionTitle)); } catch (e) { logToConsole(_('retryError', [serverData.sectionTitle, e.message])); } } logToConsole(_('retryPhaseFinished')); } export async function startPlexScan(tipos) { if (state.isScanningPlex) { showNotification(_('plexScanInProgress'), "warning"); return; } state.isScanningPlex = true; state.plexScanAbortController = new AbortController(); state.plexScanRetryQueue = []; const signal = state.plexScanAbortController.signal; mostrarSpinner(); logToConsole(_('plexScanStarting')); try { const tokensData = await getFromDB('tokens'); const tokens = tokensData.map(item => item.token).filter(Boolean); if (tokens.length === 0) throw new Error(_('noPlexTokens')); logToConsole(_('clearingSections', tipos.join(', '))); await Promise.all(tipos.map(tipo => clearStore(tipo))); await clearStore('conexiones_locales'); logToConsole(_('sectionsCleared')); const tokenPromises = tokens.map(async (token) => { if (signal.aborted) return; try { const devices = await fetchPlexResources(token, signal); logToConsole(_('tokenFoundServers', [token.substring(0,4), devices.length])); const serverPromises = Array.from(devices).map(device => processServer(device, token, tipos, signal)); await Promise.allSettled(serverPromises); } catch (e) { logToConsole(_('errorProcessingToken', [token.substring(0,4), e.message])); } }); await Promise.all(tokenPromises); logToConsole(_('initialScanPhaseComplete')); await processRetryQueue(signal); logToConsole(_('plexScanFinished')); showNotification(_('plexScanFinished'), 'success'); emitirEventoActualizacion(); } catch (error) { if (error.name !== 'AbortError') { logToConsole(_('plexScanFatalError', error.message)); showNotification(_('errorDuringScan', error.message), 'error'); } else { logToConsole(_('scanCancelled')); showNotification(_('scanCancelledInfo'), 'info'); } } finally { state.isScanningPlex = false; state.plexScanAbortController = null; ocultarSpinner(); } }