CinePlex/js/api.js
2025-07-02 14:16:25 +02:00

234 lines
10 KiB
JavaScript

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) };
}
}