234 lines
10 KiB
JavaScript
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) };
|
|
}
|
|
} |