diff --git a/_locales/de/messages.json b/_locales/de/messages.json index baab331..53d978e 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -30,6 +30,7 @@ "photosBreadcrumbHome": { "message": "Alben" }, "selectServer": { "message": "Einen Server auswählen" }, "loading": { "message": "Lädt..." }, + "loadingLibraries": { "message": "Bibliotheken werden geladen..." }, "photosEmptyState": { "message": "Keine Alben oder Fotos gefunden." }, "photosEmptyStateSub": { "message": "Bitte wähle einen Server aus oder stelle sicher, dass du eine Fotobibliothek in Plex hast." }, "statsTitle": { "message": "Bibliotheksstatistiken" }, @@ -333,5 +334,17 @@ "activityCopied": { "message": "Kennung in die Zwischenablage kopiert!" }, "activityCopyError": { "message": "Fehler beim Kopieren der Kennung." }, "noProvidersFound": { "message": "Keine Anbieter gefunden." }, - "availableOnPlex": { "message": "Auf Plex verfügbar" } + "availableOnPlex": { "message": "Auf Plex verfügbar" }, + "navM3uGenerator": { "message": "M3U-Generator" }, + "m3uGeneratorTitle": { "message": "M3U-Wiedergabelisten-Generator" }, + "selectAServer": { "message": "Wählen Sie einen Server aus..." }, + "downloadM3u": { "message": "M3U herunterladen" }, + "m3uGenerator": { "message": "M3U-Generator" }, + "selectServer": { "message": "Server auswählen" }, + "selectLibraries": { "message": "Bibliotheken auswählen" }, + "howToUse": { "message": "Anleitung" }, + "m3uInstruction1": { "message": "Wählen Sie einen Server aus der Liste aus." }, + "m3uInstruction2": { "message": "Wählen Sie eine oder mehrere Bibliotheken aus, die Sie einschließen möchten." }, + "m3uInstruction3": { "message": "Klicken Sie auf die Download-Schaltfläche." }, + "m3uInstruction4": { "message": "Importieren Sie die .m3u-Datei in Ihren kompatiblen Player." } } \ No newline at end of file diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f5ef7bf..9bc31ef 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -30,6 +30,7 @@ "photosBreadcrumbHome": { "message": "Albums" }, "selectServer": { "message": "Select a server" }, "loading": { "message": "Loading..." }, + "loadingLibraries": { "message": "Loading libraries..." }, "photosEmptyState": { "message": "No albums or photos found." }, "photosEmptyStateSub": { "message": "Please select a server or make sure you have a photo library in Plex." }, "statsTitle": { "message": "Library Statistics" }, @@ -330,5 +331,17 @@ "activityCopied": { "message": "Identifier copied to clipboard!" }, "activityCopyError": { "message": "Failed to copy identifier." }, "noProvidersFound": { "message": "No providers found." }, - "availableOnPlex": { "message": "Available on Plex" } + "availableOnPlex": { "message": "Available on Plex" }, + "navM3uGenerator": { "message": "M3U Generator" }, + "m3uGeneratorTitle": { "message": "M3U Playlist Generator" }, + "selectAServer": { "message": "Select a server..." }, + "downloadM3u": { "message": "Download M3U" }, + "m3uGenerator": { "message": "M3U Generator" }, + "selectServer": { "message": "Select Server" }, + "selectLibraries": { "message": "Select Libraries" }, + "howToUse": { "message": "How to Use" }, + "m3uInstruction1": { "message": "Choose a server from the list." }, + "m3uInstruction2": { "message": "Select one or more libraries to include." }, + "m3uInstruction3": { "message": "Click the download button." }, + "m3uInstruction4": { "message": "Import the .m3u file into your compatible player." } } \ No newline at end of file diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 115468d..0a29e0d 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -30,6 +30,7 @@ "photosBreadcrumbHome": { "message": "Álbumes" }, "selectServer": { "message": "Selecciona un servidor" }, "loading": { "message": "Cargando..." }, + "loadingLibraries": { "message": "Cargando bibliotecas..." }, "photosEmptyState": { "message": "No se encontraron álbumes ni fotos." }, "photosEmptyStateSub": { "message": "Por favor, selecciona un servidor o asegúrate de tener una biblioteca de fotos en Plex." }, "statsTitle": { "message": "Estadísticas de la Biblioteca" }, @@ -330,5 +331,17 @@ "activityCopied": { "message": "¡Identificador copiado al portapapeles!" }, "activityCopyError": { "message": "Error al copiar el identificador." }, "noProvidersFound": { "message": "No se encontraron proveedores." }, - "availableOnPlex": { "message": "Disponible en Plex" } + "availableOnPlex": { "message": "Disponible en Plex" }, + "navM3uGenerator": { "message": "Generador M3U" }, + "m3uGeneratorTitle": { "message": "Generador de Listas M3U" }, + "selectAServer": { "message": "Selecciona un servidor..." }, + "downloadM3u": { "message": "Descargar M3U" }, + "m3uGenerator": { "message": "Generador M3U" }, + "selectServer": { "message": "Seleccionar Servidor" }, + "selectLibraries": { "message": "Seleccionar Bibliotecas" }, + "howToUse": { "message": "Cómo Usar" }, + "m3uInstruction1": { "message": "Elige un servidor de la lista." }, + "m3uInstruction2": { "message": "Selecciona una o más bibliotecas para incluir." }, + "m3uInstruction3": { "message": "Haz clic en el botón de descarga." }, + "m3uInstruction4": { "message": "Importa el archivo .m3u en tu reproductor compatible." } } \ No newline at end of file diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 2f73aa2..fc9f7bb 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -30,6 +30,7 @@ "photosBreadcrumbHome": { "message": "Albums" }, "selectServer": { "message": "Sélectionnez un serveur" }, "loading": { "message": "Chargement..." }, + "loadingLibraries": { "message": "Chargement des bibliothèques..." }, "photosEmptyState": { "message": "Aucun album ou photo trouvé." }, "photosEmptyStateSub": { "message": "Veuillez sélectionner un serveur ou vous assurer d'avoir une bibliothèque de photos dans Plex." }, "statsTitle": { "message": "Statistiques de la Bibliothèque" }, @@ -330,5 +331,17 @@ "activityCopied": { "message": "Identifiant copié dans le presse-papiers !" }, "activityCopyError": { "message": "Échec de la copie de l'identifiant." }, "noProvidersFound": { "message": "Aucun fournisseur trouvé." }, - "availableOnPlex": { "message": "Disponible sur Plex" } + "availableOnPlex": { "message": "Disponible sur Plex" }, + "navM3uGenerator": { "message": "Générateur M3U" }, + "m3uGeneratorTitle": { "message": "Générateur de Listes de Lecture M3U" }, + "selectAServer": { "message": "Sélectionnez un serveur..." }, + "downloadM3u": { "message": "Télécharger M3U" }, + "m3uGenerator": { "message": "Générateur M3U" }, + "selectServer": { "message": "Sélectionner le Serveur" }, + "selectLibraries": { "message": "Sélectionner les Bibliothèques" }, + "howToUse": { "message": "Comment Utiliser" }, + "m3uInstruction1": { "message": "Choisissez un serveur dans la liste." }, + "m3uInstruction2": { "message": "Sélectionnez une ou plusieurs bibliothèques à inclure." }, + "m3uInstruction3": { "message": "Cliquez sur le bouton de téléchargement." }, + "m3uInstruction4": { "message": "Importez le fichier .m3u dans votre lecteur compatible." } } \ No newline at end of file diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 81930ad..c868873 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -30,6 +30,7 @@ "photosBreadcrumbHome": { "message": "Album" }, "selectServer": { "message": "Seleziona un server" }, "loading": { "message": "Caricamento in corso..." }, + "loadingLibraries": { "message": "Caricamento librerie in corso..." }, "photosEmptyState": { "message": "Nessun album o foto trovati." }, "photosEmptyStateSub": { "message": "Seleziona un server o assicurati di avere una libreria di foto su Plex." }, "statsTitle": { "message": "Statistiche della Libreria" }, @@ -330,5 +331,17 @@ "activityCopied": { "message": "Identificatore copiato negli appunti!" }, "activityCopyError": { "message": "Copia dell'identificatore non riuscita." }, "noProvidersFound": { "message": "Nessun fornitore trovato." }, - "availableOnPlex": { "message": "Disponibile su Plex" } + "availableOnPlex": { "message": "Disponibile su Plex" }, + "navM3uGenerator": { "message": "Generatore M3U" }, + "m3uGeneratorTitle": { "message": "Generatore di Playlist M3U" }, + "selectAServer": { "message": "Seleziona un server..." }, + "downloadM3u": { "message": "Scarica M3U" }, + "m3uGenerator": { "message": "Generatore M3U" }, + "selectServer": { "message": "Seleziona Server" }, + "selectLibraries": { "message": "Seleziona Librerie" }, + "howToUse": { "message": "Come Usare" }, + "m3uInstruction1": { "message": "Scegli un server dalla lista." }, + "m3uInstruction2": { "message": "Seleziona una o più librerie da includere." }, + "m3uInstruction3": { "message": "Clicca sul pulsante di download." }, + "m3uInstruction4": { "message": "Importa il file .m3u nel tuo lettore compatibile." } } \ No newline at end of file diff --git a/_locales/pt/messages.json b/_locales/pt/messages.json index 0b75c9e..5946e1f 100644 --- a/_locales/pt/messages.json +++ b/_locales/pt/messages.json @@ -30,6 +30,7 @@ "photosBreadcrumbHome": { "message": "Álbuns" }, "selectServer": { "message": "Selecione um servidor" }, "loading": { "message": "Carregando..." }, + "loadingLibraries": { "message": "Carregando bibliotecas..." }, "photosEmptyState": { "message": "Nenhum álbum ou foto encontrado." }, "photosEmptyStateSub": { "message": "Por favor, selecione um servidor ou certifique-se de que você tem uma biblioteca de fotos no Plex." }, "statsTitle": { "message": "Estatísticas da Biblioteca" }, @@ -330,5 +331,17 @@ "activityCopied": { "message": "Identificador copiado para a área de transferência!" }, "activityCopyError": { "message": "Falha ao copiar o identificador." }, "noProvidersFound": { "message": "Nenhum provedor encontrado." }, - "availableOnPlex": { "message": "Disponível no Plex" } + "availableOnPlex": { "message": "Disponível no Plex" }, + "navM3uGenerator": { "message": "Gerador M3U" }, + "m3uGeneratorTitle": { "message": "Gerador de Lista de Reprodução M3U" }, + "selectAServer": { "message": "Selecione um servidor..." }, + "downloadM3u": { "message": "Baixar M3U" }, + "m3uGenerator": { "message": "Gerador M3U" }, + "selectServer": { "message": "Selecionar Servidor" }, + "selectLibraries": { "message": "Selecionar Bibliotecas" }, + "howToUse": { "message": "Como Usar" }, + "m3uInstruction1": { "message": "Escolha um servidor da lista." }, + "m3uInstruction2": { "message": "Selecione uma ou mais bibliotecas para incluir." }, + "m3uInstruction3": { "message": "Clique no botão de download." }, + "m3uInstruction4": { "message": "Importe o arquivo .m3u para o seu reprodutor compatível." } } \ No newline at end of file diff --git a/css/base.css b/css/base.css index c47a966..15e6e5d 100644 --- a/css/base.css +++ b/css/base.css @@ -55,7 +55,7 @@ body { min-height: 100vh; overflow-x: hidden; position: relative; - padding-top: var(--topbar-height); + /* padding-top: var(--topbar-height); */ transition: background-color 0.3s, color 0.3s; } diff --git a/css/m3u-generator.css b/css/m3u-generator.css new file mode 100644 index 0000000..3af3163 --- /dev/null +++ b/css/m3u-generator.css @@ -0,0 +1,243 @@ +/* M3U Generator Section */ + +.m3u-animated-item { + opacity: 0; + transform: translateY(20px); +} + +#m3u-generator-section { + max-width: 1400px; + margin: 0 auto; +} + +.m3u-container { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 2.5rem; + margin-top: 2rem; +} + +.m3u-config-panel { + background: var(--glass); + border: 1px solid var(--glass-border); + border-radius: var(--border-radius-lg); + padding: 2rem; + box-shadow: var(--shadow); +} + +.m3u-step { + margin-bottom: 2.5rem; +} + +.m3u-step:last-child { + margin-bottom: 0; +} + +.m3u-step-header { + display: flex; + align-items: center; + margin-bottom: 1.5rem; +} + +.m3u-step-number { + font-size: 1.2rem; + font-weight: 700; + color: var(--primary); + background: var(--gradient); + border-radius: 50%; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 1rem; +} + +.m3u-step-title { + font-family: 'Orbitron', sans-serif; + font-size: 1.4rem; + font-weight: 600; + color: var(--text-primary); +} + +#m3u-server-select { + width: 100%; +} + +#m3u-libraries-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1rem; + max-height: 400px; + overflow-y: auto; + padding-right: 1rem; +} + +#m3u-libraries-container::-webkit-scrollbar { + width: 8px; +} + +#m3u-libraries-container::-webkit-scrollbar-track { + background: transparent; +} + +#m3u-libraries-container::-webkit-scrollbar-thumb { + background-color: var(--glass-border); + border-radius: 8px; +} + +#m3u-libraries-container .form-check { + background: rgba(0, 0, 0, 0.15); + padding: 0.8rem 1rem; + border-radius: var(--border-radius-md); + transition: background-color 0.2s; + display: flex; + align-items: center; + cursor: pointer; +} + +#m3u-libraries-container .form-check:hover { + background: rgba(0, 0, 0, 0.25); +} + +#m3u-libraries-container .form-check-input { + width: 1.1em; + height: 1.1em; + margin-right: 0.8rem; + background-color: var(--secondary); + border: 1px solid var(--glass-border); + flex-shrink: 0; /* Prevent shrinking */ +} + +#m3u-libraries-container .form-check-label { + font-weight: 500; + cursor: pointer; + color: var(--text-secondary); + font-size: 0.9rem; + line-height: 1.2; /* Ensure consistent line height */ +} + +#m3u-libraries-container .form-check-input:checked { + background-color: var(--accent); + border-color: var(--accent); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%230a0a0f' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e"); +} + +#m3u-libraries-container .form-check-label { + font-weight: 500; + cursor: pointer; + color: var(--text-secondary); + font-size: 0.9rem; +} + +#m3u-libraries-container .form-check-label i { + color: var(--text-secondary); + width: 20px; + text-align: center; + margin-right: 0.5rem; + transition: color 0.2s; +} + +#m3u-libraries-container .form-check-input:checked + .form-check-label i { + color: var(--accent); +} + +.m3u-info-panel { + background: var(--card-bg); + border-radius: var(--border-radius-lg); + padding: 2rem; + text-align: center; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.m3u-info-title { + font-family: 'Orbitron', sans-serif; + font-size: 1.4rem; + margin-bottom: 1.5rem; + color: white; /* Changed to white for better visibility */ +} + +.m3u-instructions { + text-align: left; + margin-left: 1.5rem; + color: var(--text-secondary); + font-size: 0.95rem; + margin-bottom: 2rem; +} + +.m3u-instructions li { + margin-bottom: 1rem; +} + +#download-m3u-btn { + width: 100%; + padding: 1rem; + font-size: 1rem; + border-radius: var(--border-radius-md); + transition: all 0.3s ease; /* Added for animation */ +} + +#download-m3u-btn:hover { + transform: translateY(-3px); /* Added for hover effect */ + box-shadow: var(--shadow-lg); /* Added for hover effect */ +} + +#download-m3u-btn span { + margin-left: 0.5rem; +} + +/* Loading state for libraries */ +#m3u-libraries-loader { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 150px; + color: var(--text-secondary); +} + +#m3u-libraries-loader .spinner-border { + width: 3rem; + height: 3rem; + margin-bottom: 1rem; +} + +/* Light Theme */ +.light-theme .m3u-config-panel { + background: var(--secondary); +} + +.light-theme #m3u-libraries-container .form-check { + background: rgba(0, 0, 0, 0.03); +} + +.light-theme #m3u-libraries-container .form-check:hover { + background: rgba(0, 0, 0, 0.06); +} + +.light-theme #m3u-libraries-container .form-check-input { + background-color: #e9ecef; + border-color: #ced4da; +} + +.light-theme #m3u-libraries-container .form-check-input:checked { + background-color: var(--accent-dark); + border-color: var(--accent-dark); +} + +.light-theme #m3u-libraries-container .form-check-label i { + color: var(--text-secondary); +} + +.light-theme #m3u-libraries-container .form-check-input:checked + .form-check-label i { + color: var(--accent-dark); +} + +/* Responsive */ +@media (max-width: 992px) { + .m3u-container { + grid-template-columns: 1fr; + } +} diff --git a/css/main.css b/css/main.css index 763e34e..d5b8557 100644 --- a/css/main.css +++ b/css/main.css @@ -11,6 +11,7 @@ @import url('music-player.css'); @import url('photos.css'); @import url('activity-viewer.css'); +@import url('m3u-generator.css'); /* Styles to manage hero loading state and content section visibility */ @@ -20,4 +21,13 @@ .hero:not(.loading) .hero-content { opacity: 1; -} \ No newline at end of file +} + +/* Custom styles for settings modal descriptions */ +#settingsModal .text-muted { + color: white !important; +} + +#main-view { + padding-top: var(--topbar-height); +} diff --git a/css/navbar.css b/css/navbar.css index 90e1b4f..8d3cf96 100644 --- a/css/navbar.css +++ b/css/navbar.css @@ -195,7 +195,7 @@ body.light-theme .sidebar-nav { max-width: none; } #main-view { - padding-top: 5rem; + padding-top: var(--topbar-height); } } diff --git a/js/db.js b/js/db.js index 3055a17..8d99409 100644 --- a/js/db.js +++ b/js/db.js @@ -265,4 +265,15 @@ export async function importDatabase(file) { } }; reader.readAsText(file); +} + +export async function getServers() { + const connections = await getFromDB('conexiones_locales'); + return connections.map(conn => ({ + id: conn.id, + name: conn.nombre, + accessToken: conn.token, + publicUrl: conn.protocolo + '://' + conn.ip + ':' + conn.puerto, + localUrl: conn.protocolo + '://' + conn.ip + ':' + conn.puerto, + })); } \ No newline at end of file diff --git a/js/eventListeners.js b/js/eventListeners.js index 968f164..f323d15 100644 --- a/js/eventListeners.js +++ b/js/eventListeners.js @@ -53,6 +53,7 @@ export function setupEventListeners() { document.getElementById('nav-favorites').addEventListener('click', (e) => { e.preventDefault(); switchView('favorites'); }); document.getElementById('nav-history').addEventListener('click', (e) => { e.preventDefault(); switchView('history'); }); document.getElementById('nav-recommendations').addEventListener('click', (e) => { e.preventDefault(); switchView('recommendations'); }); + document.getElementById('nav-m3u-generator').addEventListener('click', (e) => { e.preventDefault(); switchView('m3u-generator'); }); document.getElementById('reset-view-btn').addEventListener('click', (e) => { e.preventDefault(); resetView(); }); document.getElementById('footer-logo-btn').addEventListener('click', (e) => { e.preventDefault(); resetView(); }); diff --git a/js/m3u-generator.js b/js/m3u-generator.js new file mode 100644 index 0000000..089d619 --- /dev/null +++ b/js/m3u-generator.js @@ -0,0 +1,165 @@ +import { getServers } from './db.js'; +import { fetchLibraries, fetchLibraryContents } from './plex.js'; +import { showNotification, _ } from './utils.js'; + +document.addEventListener('DOMContentLoaded', () => { + const m3uServerSelect = document.getElementById('m3u-server-select'); + const m3uLibrariesContainer = document.getElementById('m3u-libraries-container'); + const m3uLibrariesLoader = document.getElementById('m3u-libraries-loader'); + const m3uLibrariesStep = document.getElementById('m3u-libraries-step'); + const m3uConfigPanel = document.querySelector('.m3u-config-panel'); + const m3uInfoPanel = document.querySelector('.m3u-info-panel'); + const downloadM3uBtn = document.getElementById('download-m3u-btn'); + let isInitialized = false; + + // GSAP animations + gsap.set([m3uConfigPanel, m3uInfoPanel], { autoAlpha: 0, y: 50 }); + + async function initializeM3uGenerator() { + if (isInitialized) return; + isInitialized = true; + + 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 }); + + try { + const servers = await getServers(); + m3uServerSelect.innerHTML = ``; + servers.forEach(server => { + const option = document.createElement('option'); + option.value = server.id; + option.textContent = server.name; + m3uServerSelect.appendChild(option); + }); + } catch (error) { + console.error('Error loading servers for M3U generator:', error); + } + } + + m3uServerSelect.addEventListener('change', async () => { + const serverId = m3uServerSelect.value; + m3uLibrariesContainer.innerHTML = ''; + downloadM3uBtn.disabled = true; + + if (!serverId) { + gsap.to(m3uLibrariesStep, { autoAlpha: 0, height: 0, duration: 0.3, onComplete: () => m3uLibrariesStep.style.display = 'none' }); + return; + } + + m3uLibrariesStep.style.display = 'block'; + gsap.fromTo(m3uLibrariesStep, { autoAlpha: 0, height: 0 }, { autoAlpha: 1, height: 'auto', duration: 0.5, ease: "power3.out" }); + m3uLibrariesContainer.innerHTML = ''; // Clear previous libraries + m3uLibrariesLoader.style.display = 'block'; // Show loader + downloadM3uBtn.disabled = true; + + try { + const servers = await getServers(); + const server = servers.find(s => s.id == serverId); + if (!server) return; + + const libraries = await fetchLibraries(server.accessToken, server.publicUrl, server.localUrl); + const checkboxes = []; + libraries.forEach(library => { + if (library.type === 'movie' || library.type === 'show' || library.type === 'music') { + const checkbox = document.createElement('div'); + checkbox.classList.add('form-check'); + checkbox.innerHTML = ` + + + `; + m3uLibrariesContainer.appendChild(checkbox); + checkboxes.push(checkbox); + + // Make the entire form-check div clickable + checkbox.addEventListener('click', () => { + const input = checkbox.querySelector('.form-check-input'); + input.checked = !input.checked; + // Update download button state + downloadM3uBtn.disabled = m3uLibrariesContainer.querySelectorAll('input:checked').length === 0; + }); + } + }); + gsap.from(checkboxes, { opacity: 0, y: 20, stagger: 0.05, duration: 0.3 }); + downloadM3uBtn.disabled = m3uLibrariesContainer.querySelectorAll('input:checked').length === 0; + } catch (error) { + console.error('Error fetching libraries:', error); + showNotification('Error fetching libraries.', 'error'); + } finally { + m3uLibrariesLoader.style.display = 'none'; // Hide loader + } + }); + + downloadM3uBtn.addEventListener('click', async () => { + const serverId = m3uServerSelect.value; + const selectedLibraries = Array.from(m3uLibrariesContainer.querySelectorAll('input:checked')).map(input => input.value); + + if (!serverId || selectedLibraries.length === 0) { + showNotification('Please select a server and at least one library.', 'error'); + return; + } + + downloadM3uBtn.disabled = true; + downloadM3uBtn.innerHTML = ` Generating...`; + + try { + const servers = await getServers(); + const server = servers.find(s => s.id == serverId); + if (!server) return; + + let m3uContent = '#EXTM3U\n'; + for (const libraryKey of selectedLibraries) { + 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); + items.forEach(item => { + const duration = item.duration ? Math.round(item.duration / 1000) : -1; + const groupTitle = item.seriesTitle && item.seasonNumber + ? `group-title="${item.seriesTitle} - Season ${item.seasonNumber}"` + : `group-title="${libraryTitle}"`; + const tvgLogo = item.thumb ? `tvg-logo="${item.thumb}"` : ''; + m3uContent += `#EXTINF:${duration} ${tvgLogo} ${groupTitle},${item.title}\n`; + m3uContent += `${item.url}\n`; + }); + } + + const blob = new Blob([m3uContent], { type: 'audio/x-mpegurl;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'playlist.m3u'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + showNotification('M3U playlist downloaded successfully.', 'success'); + } catch (error) { + console.error('Error generating M3U file:', error); + showNotification('Error generating M3U file.', 'error'); + } finally { + downloadM3uBtn.disabled = false; + downloadM3uBtn.innerHTML = ` __MSG_downloadM3u__`; + } + }); + + const navLink = document.getElementById('nav-m3u-generator'); + if (navLink) { + const observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + if (mutation.attributeName === 'class' && mutation.target.classList.contains('active')) { + initializeM3uGenerator(); + } + }); + }); + observer.observe(navLink, { attributes: true }); + + // Initial check in case it's already active + if (navLink.classList.contains('active')) { + initializeM3uGenerator(); + } + } +}); \ No newline at end of file diff --git a/js/plex.js b/js/plex.js index 83353fb..a6a07aa 100644 --- a/js/plex.js +++ b/js/plex.js @@ -32,6 +32,54 @@ async function fetchSectionContent(url, signal, timeout = 7000) { return xmlDoc; } +export async function fetchLibraries(accessToken, publicUrl, localUrl) { + const url = `${localUrl || publicUrl}/library/sections?X-Plex-Token=${accessToken}`; + const xmlDoc = await fetchSectionContent(url, new AbortController().signal); + return Array.from(xmlDoc.querySelectorAll('Directory')).map(dir => ({ + key: dir.getAttribute('key'), + title: dir.getAttribute('title'), + type: dir.getAttribute('type'), + })); +} + +export async function fetchLibraryContents(accessToken, publicUrl, localUrl, libraryKey, libraryType) { + const endpoint = libraryType === 'show' ? 'allLeaves' : 'all'; + const url = `${localUrl || publicUrl}/library/sections/${libraryKey}/${endpoint}?X-Plex-Token=${accessToken}`; + const contentXml = await fetchSectionContent(url, new AbortController().signal); + const items = []; + const baseUrl = localUrl || publicUrl; + + const processItems = (selector, type) => { + contentXml.querySelectorAll(selector).forEach(el => { + const media = el.querySelector('Media Part'); + if (media) { + const mediaUrl = `${baseUrl}${media.getAttribute('key')}?X-Plex-Token=${accessToken}`; + let thumbUrl = el.getAttribute('thumb') || el.getAttribute('parentThumb') || el.getAttribute('grandparentThumb'); + if (thumbUrl) { + thumbUrl = `${baseUrl}/photo/:/transcode?width=400&height=600&minSize=1&upscale=1&url=${encodeURIComponent(thumbUrl)}&X-Plex-Token=${accessToken}`; + } + + const finalTitle = el.getAttribute('title'); + + items.push({ + title: finalTitle, + duration: el.getAttribute('duration'), + url: mediaUrl, + thumb: thumbUrl, + seriesTitle: el.getAttribute('type') === 'episode' ? el.getAttribute('grandparentTitle') : null, + seasonNumber: el.getAttribute('type') === 'episode' ? el.getAttribute('parentIndex') : null, + grandparentTitle: el.getAttribute('grandparentTitle') || (type === 'music' ? el.getAttribute('parentTitle') : null), + }); + } + }); + }; + + processItems('Video', 'video'); // Handles both movies and episodes + processItems('Track', 'music'); // Handles music + + return items; +} + async function parseAndStoreSectionItems(contentXml, storeName, serverData) { const type = storeName === 'movies' ? 'movie' : (storeName === 'series' ? 'show' : (storeName === 'artists' ? 'artist' : 'photo')); let items; diff --git a/js/ui.js b/js/ui.js index 7dd9304..53e0a04 100644 --- a/js/ui.js +++ b/js/ui.js @@ -91,6 +91,7 @@ export function resetView() { document.getElementById('recommendations-section').style.display = 'none'; document.getElementById('photos-section').style.display = 'none'; document.getElementById('providers-section').style.display = 'none'; + document.getElementById('m3u-generator-section').style.display = 'none'; // Show hero if enabled if (heroSection) { @@ -175,7 +176,7 @@ export function switchView(viewType) { document.getElementById('search-input').value = ''; state.lastScrollPosition = 0; - const allSections = ['content-section', 'stats-section', 'history-section', 'recommendations-section', 'photos-section', 'providers-section']; + const allSections = ['content-section', 'stats-section', 'history-section', 'recommendations-section', 'photos-section', 'providers-section', 'm3u-generator-section']; allSections.forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; @@ -235,6 +236,9 @@ export function switchView(viewType) { case 'providers': document.getElementById('providers-section').style.display = 'block'; break; + case 'm3u-generator': + document.getElementById('m3u-generator-section').style.display = 'block'; + break; } updateActiveNav(viewType); @@ -266,6 +270,9 @@ export function switchView(viewType) { case 'providers': loadProviders(); break; + case 'm3u-generator': + // The m3u-generator.js file handles its own initialization + break; } if (document.getElementById('item-details-view').classList.contains('active')) { diff --git a/plex.html b/plex.html index aa22a13..af092b1 100644 --- a/plex.html +++ b/plex.html @@ -54,6 +54,7 @@
  • __MSG_navHistory__
  • __MSG_navRecommendations__
  • __MSG_navMusic__
  • +
  • __MSG_navM3uGenerator__
  • @@ -231,6 +232,49 @@
    +
    +
    +

    __MSG_m3uGenerator__

    +
    +
    +
    +
    +
    + 1 +

    __MSG_selectServer__

    +
    + +
    + +
    +
    +

    __MSG_howToUse__

    +
      +
    1. __MSG_m3uInstruction1__
    2. +
    3. __MSG_m3uInstruction2__
    4. +
    5. __MSG_m3uInstruction3__
    6. +
    7. __MSG_m3uInstruction4__
    8. +
    + +
    +
    +
    +