diff --git a/js/constants.js b/js/constants.js new file mode 100644 index 0000000..3d1aeaa --- /dev/null +++ b/js/constants.js @@ -0,0 +1,20 @@ +export const API_URLS = { + TMDB_BASE: 'https://api.themoviedb.org/3', + TMDB_IMAGE_BASE: 'https://image.tmdb.org/t/p', + PLEX_TV: 'https://plex.tv/api/resources', + YOUTUBE_EMBED: 'https://www.youtube.com/embed/', + IMDB_TITLE: 'https://www.imdb.com/title/' +}; + +export const CONFIG = { + DEFAULT_API_KEY: '4e44d9029b1270a757cddc766a1bcb63', + DB_NAME: 'PlexDB', + DB_VERSION: 6 +}; + +export const STORAGE_KEYS = { + USER_HISTORY: 'cineplex_userHistory', + USER_PREFERENCES: 'cineplex_userPreferences', + FAVORITES: 'cineplex_favorites', + RECOMMENDATIONS_CACHE: 'cineplex_recommendations' +}; \ No newline at end of file diff --git a/js/db.js b/js/db.js index 8d99409..a7c5ff1 100644 --- a/js/db.js +++ b/js/db.js @@ -268,12 +268,27 @@ export async function importDatabase(file) { } export async function getServers() { - const connections = await getFromDB('conexiones_locales'); - return connections.map(conn => ({ + const plexConnections = await getFromDB('conexiones_locales'); + const jellyfinConnections = await getFromDB('jellyfin_settings'); + + const plexServers = plexConnections.map(conn => ({ id: conn.id, name: conn.nombre, + type: 'plex', accessToken: conn.token, publicUrl: conn.protocolo + '://' + conn.ip + ':' + conn.puerto, localUrl: conn.protocolo + '://' + conn.ip + ':' + conn.puerto, })); + + const jellyfinServers = jellyfinConnections.map(conn => ({ + id: conn.id, + name: conn.username || 'Jellyfin Server', + type: 'jellyfin', + accessToken: conn.apiKey, + userId: conn.userId, + publicUrl: conn.url, + localUrl: conn.url, + })); + + return [...plexServers, ...jellyfinServers]; } \ No newline at end of file diff --git a/js/jellyfin.js b/js/jellyfin.js index f6ae85a..f5ad524 100644 --- a/js/jellyfin.js +++ b/js/jellyfin.js @@ -32,7 +32,7 @@ async function authenticateJellyfin(url, username, password) { } } -async function fetchLibraryViews(url, userId, apiKey) { +export async function fetchLibraryViews(url, userId, apiKey) { const viewsUrl = `${url}/Users/${userId}/Views`; try { const response = await fetch(viewsUrl, { @@ -49,8 +49,8 @@ async function fetchLibraryViews(url, userId, apiKey) { } -async function fetchItemsFromLibrary(url, userId, apiKey, library) { - const itemsUrl = `${url}/Users/${userId}/Items?ParentId=${library.Id}&recursive=true&fields=ProductionYear&includeItemTypes=Movie,Series`; +export async function fetchItemsFromLibrary(url, userId, apiKey, library) { + const itemsUrl = `${url}/Users/${userId}/Items?ParentId=${library.Id}&recursive=true&fields=ProductionYear,RunTimeTicks,SeriesName,ParentIndexNumber,ImageTags&includeItemTypes=Movie,Series`; try { const response = await fetch(itemsUrl, { @@ -67,7 +67,10 @@ async function fetchItemsFromLibrary(url, userId, apiKey, library) { title: item.Name, year: item.ProductionYear, type: item.Type, - posterTag: item.ImageTags?.Primary, + duration: item.RunTimeTicks, + seriesTitle: item.SeriesName, + seasonNumber: item.ParentIndexNumber, + thumb: item.ImageTags?.Primary ? `${url}/Items/${item.Id}/Images/Primary?tag=${item.ImageTags.Primary}` : '' })); return { success: true, items, libraryName: library.Name, libraryId: library.Id }; diff --git a/js/m3u-generator.js b/js/m3u-generator.js index ba0e092..1d8c6de 100644 --- a/js/m3u-generator.js +++ b/js/m3u-generator.js @@ -1,5 +1,6 @@ import { getServers } from './db.js'; -import { fetchLibraries, fetchLibraryContents } from './plex.js'; +import { fetchLibraries as fetchPlexLibraries, fetchLibraryContents as fetchPlexLibraryContents } from './plex.js'; +import { fetchLibraryViews as fetchJellyfinLibraries, fetchItemsFromLibrary as fetchJellyfinLibraryContents } from './jellyfin.js'; import { showNotification, _ } from './utils.js'; document.addEventListener('DOMContentLoaded', () => { @@ -22,6 +23,10 @@ document.addEventListener('DOMContentLoaded', () => { gsap.to(m3uConfigPanel, { autoAlpha: 1, y: 0, duration: 0.8, ease: "power3.out" }); gsap.to(m3uInfoPanel, { autoAlpha: 1, y: 0, duration: 0.8, ease: "power3.out", delay: 0.2 }); + await loadServers(); + } + + async function loadServers() { try { const servers = await getServers(); m3uServerSelect.innerHTML = ``; @@ -29,6 +34,7 @@ document.addEventListener('DOMContentLoaded', () => { const option = document.createElement('option'); option.value = server.id; option.textContent = server.name; + option.dataset.type = server.type; m3uServerSelect.appendChild(option); }); } catch (error) { @@ -37,7 +43,9 @@ document.addEventListener('DOMContentLoaded', () => { } m3uServerSelect.addEventListener('change', async () => { - const serverId = m3uServerSelect.value; + const selectedOption = m3uServerSelect.options[m3uServerSelect.selectedIndex]; + const serverId = selectedOption.value; + const serverType = selectedOption.dataset.type; m3uLibrariesContainer.innerHTML = ''; downloadM3uBtn.disabled = true; @@ -57,7 +65,20 @@ document.addEventListener('DOMContentLoaded', () => { const server = servers.find(s => s.id == serverId); if (!server) return; - const libraries = await fetchLibraries(server.accessToken, server.publicUrl, server.localUrl); + let libraries = []; + if (serverType === 'plex') { + libraries = await fetchPlexLibraries(server.accessToken, server.publicUrl, server.localUrl); + } else if (serverType === 'jellyfin') { + const jellyfinLibraries = await fetchJellyfinLibraries(server.publicUrl, server.userId, server.accessToken); + if(jellyfinLibraries.success) { + libraries = jellyfinLibraries.views.map(lib => ({ + key: lib.Id, + title: lib.Name, + type: lib.CollectionType === 'movies' ? 'movie' : (lib.CollectionType === 'tvshows' ? 'show' : 'music') + })); + } + } + const checkboxes = []; libraries.forEach(library => { if (library.type === 'movie' || library.type === 'show' || library.type === 'music') { @@ -93,7 +114,9 @@ document.addEventListener('DOMContentLoaded', () => { }); downloadM3uBtn.addEventListener('click', async () => { - const serverId = m3uServerSelect.value; + const selectedOption = m3uServerSelect.options[m3uServerSelect.selectedIndex]; + const serverId = selectedOption.value; + const serverType = selectedOption.dataset.type; const selectedLibraries = Array.from(m3uLibrariesContainer.querySelectorAll('input:checked')).map(input => input.value); if (!serverId || selectedLibraries.length === 0) { @@ -117,7 +140,24 @@ document.addEventListener('DOMContentLoaded', () => { const libraryElement = m3uLibrariesContainer.querySelector(`#library-${libraryKey}`); const libraryType = libraryElement.getAttribute('data-type'); const libraryTitle = libraryElement ? libraryElement.nextElementSibling.textContent.trim() : ''; - const items = await fetchLibraryContents(server.accessToken, server.publicUrl, server.localUrl, libraryKey, libraryType, 60000); + + let items = []; + if (serverType === 'plex') { + items = await fetchPlexLibraryContents(server.accessToken, server.publicUrl, server.localUrl, libraryKey, libraryType, 60000); + } else if (serverType === 'jellyfin') { + const jellyfinItems = await fetchJellyfinLibraryContents(server.publicUrl, server.userId, server.accessToken, { Id: libraryKey }); + if(jellyfinItems.success) { + items = jellyfinItems.items.map(item => ({ + title: item.title, + duration: item.duration ? item.duration / 10000 : -1, + seriesTitle: item.seriesTitle, + seasonNumber: item.seasonNumber, + thumb: item.thumb, + url: `${server.publicUrl}/Videos/${item.id}/stream?static=true&api_key=${server.accessToken}` + })); + } + } + items.forEach(item => { const duration = item.duration ? Math.round(item.duration / 1000) : -1; const groupTitle = item.seriesTitle && item.seasonNumber @@ -178,4 +218,6 @@ document.addEventListener('DOMContentLoaded', () => { initializeM3uGenerator(); } } + + document.addEventListener('actualizacion', loadServers); }); \ No newline at end of file diff --git a/js/ui.js b/js/ui.js index 53e0a04..3f77472 100644 --- a/js/ui.js +++ b/js/ui.js @@ -1079,6 +1079,7 @@ export async function generateStatistics() { const allTokens = await getFromDB('tokens'); const allConnections = await getFromDB('conexiones_locales'); + const allJellyfinConnections = await getFromDB('jellyfin_settings'); const filteredTokens = selectedToken === 'all' ? allTokens @@ -1088,8 +1089,10 @@ export async function generateStatistics() { ? allConnections : allConnections.filter(c => c.tokenPrincipal === selectedToken); + const totalServers = filteredConnections.length + (selectedToken === 'all' ? allJellyfinConnections.length : 0); + animateValue('total-tokens', 0, filteredTokens.length, 1000); - animateValue('total-servers', 0, filteredConnections.length, 1000); + animateValue('total-servers', 0, totalServers, 1000); updateTokenDetailsCard(selectedToken, filteredConnections);