generate m3u
This commit is contained in:
parent
50b9d82f0f
commit
aa3622db2d
@ -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." }
|
||||
}
|
@ -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." }
|
||||
}
|
@ -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." }
|
||||
}
|
@ -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." }
|
||||
}
|
@ -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." }
|
||||
}
|
@ -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." }
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
243
css/m3u-generator.css
Normal file
243
css/m3u-generator.css
Normal 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;
|
||||
}
|
||||
}
|
10
css/main.css
10
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 */
|
||||
|
||||
@ -21,3 +22,12 @@
|
||||
.hero:not(.loading) .hero-content {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Custom styles for settings modal descriptions */
|
||||
#settingsModal .text-muted {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
#main-view {
|
||||
padding-top: var(--topbar-height);
|
||||
}
|
||||
|
@ -195,7 +195,7 @@ body.light-theme .sidebar-nav {
|
||||
max-width: none;
|
||||
}
|
||||
#main-view {
|
||||
padding-top: 5rem;
|
||||
padding-top: var(--topbar-height);
|
||||
}
|
||||
}
|
||||
|
||||
|
11
js/db.js
11
js/db.js
@ -266,3 +266,14 @@ 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,
|
||||
}));
|
||||
}
|
@ -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(); });
|
||||
|
165
js/m3u-generator.js
Normal file
165
js/m3u-generator.js
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
48
js/plex.js
48
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;
|
||||
|
9
js/ui.js
9
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')) {
|
||||
|
45
plex.html
45
plex.html
@ -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-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" href="#" id="nav-m3u-generator"><i class="fas fa-list-ul"></i><span>__MSG_navM3uGenerator__</span></a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@ -231,6 +232,49 @@
|
||||
<div id="history-list"></div>
|
||||
</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;">
|
||||
<h3 class="section-subtitle mt-4">__MSG_consoleTitle__</h3>
|
||||
<div id="consoleOutput"></div>
|
||||
@ -720,6 +764,7 @@
|
||||
<script src="js/i18n.js"></script>
|
||||
<script type="module" src="js/main.js"></script>
|
||||
<script type="module" src="js/activityViewer.js"></script>
|
||||
<script type="module" src="js/m3u-generator.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user