generate m3u

This commit is contained in:
Filipinos 2025-07-26 18:53:03 +02:00
parent 50b9d82f0f
commit aa3622db2d
16 changed files with 618 additions and 10 deletions

View File

@ -30,6 +30,7 @@
"photosBreadcrumbHome": { "message": "Alben" }, "photosBreadcrumbHome": { "message": "Alben" },
"selectServer": { "message": "Einen Server auswählen" }, "selectServer": { "message": "Einen Server auswählen" },
"loading": { "message": "Lädt..." }, "loading": { "message": "Lädt..." },
"loadingLibraries": { "message": "Bibliotheken werden geladen..." },
"photosEmptyState": { "message": "Keine Alben oder Fotos gefunden." }, "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." }, "photosEmptyStateSub": { "message": "Bitte wähle einen Server aus oder stelle sicher, dass du eine Fotobibliothek in Plex hast." },
"statsTitle": { "message": "Bibliotheksstatistiken" }, "statsTitle": { "message": "Bibliotheksstatistiken" },
@ -333,5 +334,17 @@
"activityCopied": { "message": "Kennung in die Zwischenablage kopiert!" }, "activityCopied": { "message": "Kennung in die Zwischenablage kopiert!" },
"activityCopyError": { "message": "Fehler beim Kopieren der Kennung." }, "activityCopyError": { "message": "Fehler beim Kopieren der Kennung." },
"noProvidersFound": { "message": "Keine Anbieter gefunden." }, "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." }
} }

View File

@ -30,6 +30,7 @@
"photosBreadcrumbHome": { "message": "Albums" }, "photosBreadcrumbHome": { "message": "Albums" },
"selectServer": { "message": "Select a server" }, "selectServer": { "message": "Select a server" },
"loading": { "message": "Loading..." }, "loading": { "message": "Loading..." },
"loadingLibraries": { "message": "Loading libraries..." },
"photosEmptyState": { "message": "No albums or photos found." }, "photosEmptyState": { "message": "No albums or photos found." },
"photosEmptyStateSub": { "message": "Please select a server or make sure you have a photo library in Plex." }, "photosEmptyStateSub": { "message": "Please select a server or make sure you have a photo library in Plex." },
"statsTitle": { "message": "Library Statistics" }, "statsTitle": { "message": "Library Statistics" },
@ -330,5 +331,17 @@
"activityCopied": { "message": "Identifier copied to clipboard!" }, "activityCopied": { "message": "Identifier copied to clipboard!" },
"activityCopyError": { "message": "Failed to copy identifier." }, "activityCopyError": { "message": "Failed to copy identifier." },
"noProvidersFound": { "message": "No providers found." }, "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." }
} }

View File

@ -30,6 +30,7 @@
"photosBreadcrumbHome": { "message": "Álbumes" }, "photosBreadcrumbHome": { "message": "Álbumes" },
"selectServer": { "message": "Selecciona un servidor" }, "selectServer": { "message": "Selecciona un servidor" },
"loading": { "message": "Cargando..." }, "loading": { "message": "Cargando..." },
"loadingLibraries": { "message": "Cargando bibliotecas..." },
"photosEmptyState": { "message": "No se encontraron álbumes ni fotos." }, "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." }, "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" }, "statsTitle": { "message": "Estadísticas de la Biblioteca" },
@ -330,5 +331,17 @@
"activityCopied": { "message": "¡Identificador copiado al portapapeles!" }, "activityCopied": { "message": "¡Identificador copiado al portapapeles!" },
"activityCopyError": { "message": "Error al copiar el identificador." }, "activityCopyError": { "message": "Error al copiar el identificador." },
"noProvidersFound": { "message": "No se encontraron proveedores." }, "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." }
} }

View File

@ -30,6 +30,7 @@
"photosBreadcrumbHome": { "message": "Albums" }, "photosBreadcrumbHome": { "message": "Albums" },
"selectServer": { "message": "Sélectionnez un serveur" }, "selectServer": { "message": "Sélectionnez un serveur" },
"loading": { "message": "Chargement..." }, "loading": { "message": "Chargement..." },
"loadingLibraries": { "message": "Chargement des bibliothèques..." },
"photosEmptyState": { "message": "Aucun album ou photo trouvé." }, "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." }, "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" }, "statsTitle": { "message": "Statistiques de la Bibliothèque" },
@ -330,5 +331,17 @@
"activityCopied": { "message": "Identifiant copié dans le presse-papiers !" }, "activityCopied": { "message": "Identifiant copié dans le presse-papiers !" },
"activityCopyError": { "message": "Échec de la copie de l'identifiant." }, "activityCopyError": { "message": "Échec de la copie de l'identifiant." },
"noProvidersFound": { "message": "Aucun fournisseur trouvé." }, "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." }
} }

View File

@ -30,6 +30,7 @@
"photosBreadcrumbHome": { "message": "Album" }, "photosBreadcrumbHome": { "message": "Album" },
"selectServer": { "message": "Seleziona un server" }, "selectServer": { "message": "Seleziona un server" },
"loading": { "message": "Caricamento in corso..." }, "loading": { "message": "Caricamento in corso..." },
"loadingLibraries": { "message": "Caricamento librerie in corso..." },
"photosEmptyState": { "message": "Nessun album o foto trovati." }, "photosEmptyState": { "message": "Nessun album o foto trovati." },
"photosEmptyStateSub": { "message": "Seleziona un server o assicurati di avere una libreria di foto su Plex." }, "photosEmptyStateSub": { "message": "Seleziona un server o assicurati di avere una libreria di foto su Plex." },
"statsTitle": { "message": "Statistiche della Libreria" }, "statsTitle": { "message": "Statistiche della Libreria" },
@ -330,5 +331,17 @@
"activityCopied": { "message": "Identificatore copiato negli appunti!" }, "activityCopied": { "message": "Identificatore copiato negli appunti!" },
"activityCopyError": { "message": "Copia dell'identificatore non riuscita." }, "activityCopyError": { "message": "Copia dell'identificatore non riuscita." },
"noProvidersFound": { "message": "Nessun fornitore trovato." }, "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." }
} }

View File

@ -30,6 +30,7 @@
"photosBreadcrumbHome": { "message": "Álbuns" }, "photosBreadcrumbHome": { "message": "Álbuns" },
"selectServer": { "message": "Selecione um servidor" }, "selectServer": { "message": "Selecione um servidor" },
"loading": { "message": "Carregando..." }, "loading": { "message": "Carregando..." },
"loadingLibraries": { "message": "Carregando bibliotecas..." },
"photosEmptyState": { "message": "Nenhum álbum ou foto encontrado." }, "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." }, "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" }, "statsTitle": { "message": "Estatísticas da Biblioteca" },
@ -330,5 +331,17 @@
"activityCopied": { "message": "Identificador copiado para a área de transferência!" }, "activityCopied": { "message": "Identificador copiado para a área de transferência!" },
"activityCopyError": { "message": "Falha ao copiar o identificador." }, "activityCopyError": { "message": "Falha ao copiar o identificador." },
"noProvidersFound": { "message": "Nenhum provedor encontrado." }, "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." }
} }

View File

@ -55,7 +55,7 @@ body {
min-height: 100vh; min-height: 100vh;
overflow-x: hidden; overflow-x: hidden;
position: relative; position: relative;
padding-top: var(--topbar-height); /* padding-top: var(--topbar-height); */
transition: background-color 0.3s, color 0.3s; transition: background-color 0.3s, color 0.3s;
} }

243
css/m3u-generator.css Normal file
View File

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

View File

@ -11,6 +11,7 @@
@import url('music-player.css'); @import url('music-player.css');
@import url('photos.css'); @import url('photos.css');
@import url('activity-viewer.css'); @import url('activity-viewer.css');
@import url('m3u-generator.css');
/* Styles to manage hero loading state and content section visibility */ /* Styles to manage hero loading state and content section visibility */
@ -20,4 +21,13 @@
.hero:not(.loading) .hero-content { .hero:not(.loading) .hero-content {
opacity: 1; opacity: 1;
} }
/* Custom styles for settings modal descriptions */
#settingsModal .text-muted {
color: white !important;
}
#main-view {
padding-top: var(--topbar-height);
}

View File

@ -195,7 +195,7 @@ body.light-theme .sidebar-nav {
max-width: none; max-width: none;
} }
#main-view { #main-view {
padding-top: 5rem; padding-top: var(--topbar-height);
} }
} }

View File

@ -265,4 +265,15 @@ export async function importDatabase(file) {
} }
}; };
reader.readAsText(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,
}));
} }

View File

@ -53,6 +53,7 @@ export function setupEventListeners() {
document.getElementById('nav-favorites').addEventListener('click', (e) => { e.preventDefault(); switchView('favorites'); }); 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-history').addEventListener('click', (e) => { e.preventDefault(); switchView('history'); });
document.getElementById('nav-recommendations').addEventListener('click', (e) => { e.preventDefault(); switchView('recommendations'); }); 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('reset-view-btn').addEventListener('click', (e) => { e.preventDefault(); resetView(); });
document.getElementById('footer-logo-btn').addEventListener('click', (e) => { e.preventDefault(); resetView(); }); document.getElementById('footer-logo-btn').addEventListener('click', (e) => { e.preventDefault(); resetView(); });

165
js/m3u-generator.js Normal file
View File

@ -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 = `<option value="">${_('selectAServer')}</option>`;
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 = `
<input class="form-check-input" type="checkbox" value="${library.key}" id="library-${library.key}" data-type="${library.type}">
<label class="form-check-label" for="library-${library.key}">
<i class="fas ${library.type === 'movie' ? 'fa-film' : (library.type === 'show' ? 'fa-tv' : 'fa-music')} me-2"></i>
${library.title}
</label>
`;
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 = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 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 = `<i class="fas fa-download"></i> __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();
}
}
});

View File

@ -32,6 +32,54 @@ async function fetchSectionContent(url, signal, timeout = 7000) {
return xmlDoc; 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) { async function parseAndStoreSectionItems(contentXml, storeName, serverData) {
const type = storeName === 'movies' ? 'movie' : (storeName === 'series' ? 'show' : (storeName === 'artists' ? 'artist' : 'photo')); const type = storeName === 'movies' ? 'movie' : (storeName === 'series' ? 'show' : (storeName === 'artists' ? 'artist' : 'photo'));
let items; let items;

View File

@ -91,6 +91,7 @@ export function resetView() {
document.getElementById('recommendations-section').style.display = 'none'; document.getElementById('recommendations-section').style.display = 'none';
document.getElementById('photos-section').style.display = 'none'; document.getElementById('photos-section').style.display = 'none';
document.getElementById('providers-section').style.display = 'none'; document.getElementById('providers-section').style.display = 'none';
document.getElementById('m3u-generator-section').style.display = 'none';
// Show hero if enabled // Show hero if enabled
if (heroSection) { if (heroSection) {
@ -175,7 +176,7 @@ export function switchView(viewType) {
document.getElementById('search-input').value = ''; document.getElementById('search-input').value = '';
state.lastScrollPosition = 0; 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 => { allSections.forEach(id => {
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) el.style.display = 'none'; if (el) el.style.display = 'none';
@ -235,6 +236,9 @@ export function switchView(viewType) {
case 'providers': case 'providers':
document.getElementById('providers-section').style.display = 'block'; document.getElementById('providers-section').style.display = 'block';
break; break;
case 'm3u-generator':
document.getElementById('m3u-generator-section').style.display = 'block';
break;
} }
updateActiveNav(viewType); updateActiveNav(viewType);
@ -266,6 +270,9 @@ export function switchView(viewType) {
case 'providers': case 'providers':
loadProviders(); loadProviders();
break; break;
case 'm3u-generator':
// The m3u-generator.js file handles its own initialization
break;
} }
if (document.getElementById('item-details-view').classList.contains('active')) { if (document.getElementById('item-details-view').classList.contains('active')) {

View File

@ -54,6 +54,7 @@
<li><a class="nav-link" href="#" id="nav-history"><i class="fas fa-history"></i><span>__MSG_navHistory__</span></a></li> <li><a class="nav-link" href="#" id="nav-history"><i class="fas fa-history"></i><span>__MSG_navHistory__</span></a></li>
<li><a class="nav-link" href="#" id="nav-recommendations"><i class="fas fa-magic"></i><span>__MSG_navRecommendations__</span></a></li> <li><a class="nav-link" href="#" id="nav-recommendations"><i class="fas fa-magic"></i><span>__MSG_navRecommendations__</span></a></li>
<li><a class="nav-link d-lg-none" href="#" id="openMusicPlayerMobile"><i class="fas fa-music"></i><span>__MSG_navMusic__</span></a></li> <li><a class="nav-link d-lg-none" href="#" id="openMusicPlayerMobile"><i class="fas fa-music"></i><span>__MSG_navMusic__</span></a></li>
<li><a class="nav-link" href="#" id="nav-m3u-generator"><i class="fas fa-list-ul"></i><span>__MSG_navM3uGenerator__</span></a></li>
</ul> </ul>
</nav> </nav>
@ -231,6 +232,49 @@
<div id="history-list"></div> <div id="history-list"></div>
</section> </section>
<section id="m3u-generator-section" class="content-section">
<div class="section-header">
<h2 class="section-title">__MSG_m3uGenerator__</h2>
</div>
<div class="m3u-container">
<div class="m3u-config-panel m3u-animated-item">
<div class="m3u-step">
<div class="m3u-step-header">
<span class="m3u-step-number">1</span>
<h3 class="m3u-step-title">__MSG_selectServer__</h3>
</div>
<select id="m3u-server-select" class="filter-select"></select>
</div>
<div id="m3u-libraries-step" class="m3u-step" style="display: none;">
<div class="m3u-step-header">
<span class="m3u-step-number">2</span>
<h3 class="m3u-step-title">__MSG_selectLibraries__</h3>
</div>
<div id="m3u-libraries-container"></div>
<div id="m3u-libraries-loader" class="text-center py-4" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">__MSG_loading__</span>
</div>
<p class="mt-3 lead">__MSG_loadingLibraries__</p>
</div>
</div>
</div>
<div class="m3u-info-panel m3u-animated-item">
<h3 class="m3u-info-title">__MSG_howToUse__</h3>
<ol class="m3u-instructions">
<li>__MSG_m3uInstruction1__</li>
<li>__MSG_m3uInstruction2__</li>
<li>__MSG_m3uInstruction3__</li>
<li>__MSG_m3uInstruction4__</li>
</ol>
<button id="download-m3u-btn" class="btn btn-primary" disabled>
<i class="fas fa-download"></i>
<span>__MSG_downloadM3u__</span>
</button>
</div>
</div>
</section>
<div id="consoleOutputContainer" class="mt-5" style="display: none;"> <div id="consoleOutputContainer" class="mt-5" style="display: none;">
<h3 class="section-subtitle mt-4">__MSG_consoleTitle__</h3> <h3 class="section-subtitle mt-4">__MSG_consoleTitle__</h3>
<div id="consoleOutput"></div> <div id="consoleOutput"></div>
@ -720,6 +764,7 @@
<script src="js/i18n.js"></script> <script src="js/i18n.js"></script>
<script type="module" src="js/main.js"></script> <script type="module" src="js/main.js"></script>
<script type="module" src="js/activityViewer.js"></script> <script type="module" src="js/activityViewer.js"></script>
<script type="module" src="js/m3u-generator.js"></script>
</body> </body>