import { config } from './config.js'; import { state } from './state.js'; import { fetchWithTimeout } from './utils.js'; import { getFromDB } from './db.js'; import { _ } from './utils.js'; export async function fetchTMDB(endpoint, signal) { let tmdbLang = 'en-US'; const langMap = { 'es': 'es-ES', 'en': 'en-US', 'fr': 'fr-FR', 'de': 'de-DE' }; if (langMap[state.settings.language]) { tmdbLang = langMap[state.settings.language]; } const separator = endpoint.includes('?') ? '&' : '?'; const url = `https://api.themoviedb.org/3/${endpoint}${separator}language=${tmdbLang}&api_key=${state.settings.apiKey}`; const response = await fetch(url, { signal }); if (!response.ok) { const errorData = await response.json().catch(() => ({ status_message: "Unknown error" })); throw new Error(`HTTP error! status: ${response.status} - ${errorData.status_message}`); } return response.json(); } export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId) { const url = `${protocolo}://${ip}:${puerto}/library/metadata/${artistaId}/allLeaves?X-Plex-Token=${token}`; try { const response = await fetchWithTimeout(url, {}, 15000); if (!response.ok) throw new Error(`Failed to fetch tracks: ${response.status}`); const data = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(data, "text/xml"); if (xmlDoc.querySelector('parsererror')) throw new Error("Failed to parse track XML"); const tracks = Array.from(xmlDoc.querySelectorAll("Track")).map(track => { const part = track.querySelector("Part"); if (!part || !part.getAttribute("key")) return null; const fileKey = part.getAttribute("key"); const fileUrl = `${protocolo}://${ip}:${puerto}${fileKey}?X-Plex-Token=${token}`; const thumb = track.getAttribute("thumb"); const parentThumb = track.getAttribute("parentThumb"); const grandparentThumb = track.getAttribute("grandparentThumb"); let coverUrl = 'img/no-poster.png'; if (thumb) { coverUrl = `${protocolo}://${ip}:${puerto}${thumb}?X-Plex-Token=${token}`; } else if (parentThumb) { coverUrl = `${protocolo}://${ip}:${puerto}${parentThumb}?X-Plex-Token=${token}`; } else if (grandparentThumb) { coverUrl = `${protocolo}://${ip}:${puerto}${grandparentThumb}?X-Plex-Token=${token}`; } return { url: fileUrl, titulo: track.getAttribute("title") || 'Pista desconocida', album: track.getAttribute("parentTitle") || 'Álbum desconocido', artista: track.getAttribute("grandparentTitle") || 'Artista desconocido', cover: coverUrl, extension: part.getAttribute("container") || "mp3", id: track.getAttribute("ratingKey"), artistId: track.getAttribute("grandparentRatingKey") || artistaId, year: track.getAttribute("parentYear") || track.getAttribute("year"), genre: Array.from(track.querySelectorAll("Genre")).map(g => g.getAttribute('tag')).join(', ') || '', index: parseInt(track.getAttribute("index") || 0, 10), albumIndex: parseInt(track.getAttribute("parentIndex") || 0, 10) }; }).filter(track => track !== null); tracks.sort((a, b) => { if (a.albumIndex !== b.albumIndex) { return a.albumIndex - b.albumIndex; } return a.index - b.index; }); return tracks; } catch (error) { throw error; } } async function searchAndProcessServer(server, busqueda, tipoContenido) { const { ip, puerto, token, protocolo = 'http', nombre: serverName = 'Servidor Desconocido' } = server; if (!ip || !puerto || !token) return []; const plexSearchType = tipoContenido === 'movie' ? '1' : '2'; const searchUrl = `${protocolo}://${ip}:${puerto}/search?type=${plexSearchType}&query=${encodeURIComponent(busqueda)}&X-Plex-Token=${token}`; try { const response = await fetchWithTimeout(searchUrl, { headers: { 'Accept': 'application/xml' } }); if (!response.ok) return []; const data = await response.text(); const parser = new DOMParser(); const xml = parser.parseFromString(data, "text/xml"); if (xml.querySelector('parsererror')) return []; if (tipoContenido === 'movie') { return processMovieResults(xml, busqueda, protocolo, ip, puerto, token); } else { return await processShowResults(xml, busqueda, protocolo, ip, puerto, token); } } catch (error) { return []; } } function processMovieResults(xml, busqueda, protocolo, ip, puerto, token) { const videos = Array.from(xml.querySelectorAll("Video")); let videosToProcess = videos; const exactMatch = videos.find(v => v.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase()); if (exactMatch) { videosToProcess = [exactMatch]; } return videosToProcess.map(video => { const part = video.querySelector("Part"); if (!part || !part.getAttribute("key")) return null; const movieTitle = video.getAttribute("title") || busqueda; const movieYear = video.getAttribute("year"); const streamUrl = `${protocolo}://${ip}:${puerto}${part.getAttribute("key")}?X-Plex-Token=${token}`; const extinfName = `${movieTitle}${movieYear ? ` (${movieYear})` : ''}`; const logoUrl = video.getAttribute("thumb") ? `${protocolo}://${ip}:${puerto}${video.getAttribute("thumb")}?X-Plex-Token=${token}` : ''; const groupTitle = extinfName.replace(/"/g, "'"); return { url: streamUrl, title: extinfName, extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${logoUrl}" group-title="${groupTitle}",${extinfName}` }; }).filter(Boolean); } async function processShowResults(xml, busqueda, protocolo, ip, puerto, token) { const directories = Array.from(xml.querySelectorAll('Directory[type="show"]')); let directoryToProcess = directories.find(d => d.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase()); if (!directoryToProcess && directories.length > 0) { directoryToProcess = directories[0]; } if (directoryToProcess && directoryToProcess.getAttribute("ratingKey")) { const serieKey = directoryToProcess.getAttribute("ratingKey"); const leavesUrl = `${protocolo}://${ip}:${puerto}/library/metadata/${serieKey}/allLeaves?X-Plex-Token=${token}`; const leavesResponse = await fetchWithTimeout(leavesUrl, { headers: { 'Accept': 'application/xml' } }); if (leavesResponse.ok) { const leavesData = await leavesResponse.text(); const leavesXml = new DOMParser().parseFromString(leavesData, "text/xml"); if (!leavesXml.querySelector('parsererror')) { return processShowEpisodes(leavesXml, busqueda, protocolo, ip, puerto, token); } } } return []; } function processShowEpisodes(xml, busqueda, protocolo, ip, puerto, token) { const episodes = Array.from(xml.querySelectorAll("Video")); episodes.sort((a, b) => { const seasonA = parseInt(a.getAttribute("parentIndex") || 0, 10); const seasonB = parseInt(b.getAttribute("parentIndex") || 0, 10); if (seasonA !== seasonB) return seasonA - seasonB; const episodeA = parseInt(a.getAttribute("index") || 0, 10); const episodeB = parseInt(b.getAttribute("index") || 0, 10); return episodeA - episodeB; }); return episodes.map(episode => { const part = episode.querySelector("Part"); if (!part || !part.getAttribute("key")) return null; const serieTitulo = episode.getAttribute("grandparentTitle") || busqueda; const serieYear = episode.getAttribute("parentYear"); const seasonNum = episode.getAttribute("parentIndex") || 'S'; const episodeNum = episode.getAttribute("index") || 'E'; const episodeTitle = episode.getAttribute("title") || 'Episodio'; const streamUrl = `${protocolo}://${ip}:${puerto}${part.getAttribute("key")}?X-Plex-Token=${token}`; const groupTitle = `${serieTitulo}${serieYear ? ` (${serieYear})` : ''} - Temporada ${seasonNum}`.replace(/"/g, "'"); const extinfName = `${serieTitulo} T${seasonNum}E${episodeNum} ${episodeTitle}`; const logoUrl = episode.getAttribute("grandparentThumb") || episode.getAttribute("parentThumb") || episode.getAttribute("thumb"); const fullLogoUrl = logoUrl ? `${protocolo}://${ip}:${puerto}${logoUrl}?X-Plex-Token=${token}` : ''; return { url: streamUrl, title: extinfName, extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${fullLogoUrl}" group-title="${groupTitle}",${extinfName}` }; }).filter(Boolean); } export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) { if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') }; if (!state.db) return { success: false, streams: [], message: _('dbUnavailableForStreams') }; const servers = await getFromDB('conexiones_locales'); if (!servers || servers.length === 0) return { success: false, streams: [], message: _('noPlexServersForStreams') }; const searchTasks = servers.map(server => searchAndProcessServer(server, busqueda, tipoContenido)); const results = await Promise.allSettled(searchTasks); const allFoundStreams = results .filter(r => r.status === 'fulfilled' && Array.isArray(r.value)) .flatMap(r => r.value); const uniqueStreams = []; const seenUrls = new Set(); for (const stream of allFoundStreams) { if (!seenUrls.has(stream.url)) { uniqueStreams.push(stream); seenUrls.add(stream.url); } } if (tipoContenido === 'movie') { uniqueStreams.sort((a, b) => (a.title || '').localeCompare(b.title || '')); } if (uniqueStreams.length > 0) { return { success: true, streams: uniqueStreams }; } else { return { success: false, streams: [], message: _('notFoundOnServers', busqueda) }; } }