Jellyfin integration. Movies and TV shows only.

This commit is contained in:
Filipinos 2025-07-11 12:10:50 +02:00
parent 74886c0c8c
commit e988ff15c8
17 changed files with 640 additions and 50 deletions

View File

@ -58,6 +58,7 @@
"settingsTitleFull": { "message": "Einstellungen und Konfiguration" },
"settingsTabGeneral": { "message": "Allgemein" },
"settingsTabPlex": { "message": "Plex" },
"settingsTabJellyfin": { "message": "Jellyfin" },
"settingsTabPhpGen": { "message": "PHP-Generator" },
"settingsTabData": { "message": "Daten" },
"settingsApiServer": { "message": "API- und Serverkonfiguration" },
@ -80,6 +81,12 @@
"settingsPlexTokens": { "message": "Plex-Tokens" },
"settingsPlexTokensDesc": { "message": "Bearbeite die Liste der Plex-Tokens (JSON-Format)." },
"settingsSaveTokens": { "message": "Tokens speichern" },
"settingsJellyfinTitle": { "message": "Jellyfin-Einstellungen" },
"settingsJellyfinDesc": { "message": "Füge die Daten deines Jellyfin-Servers hinzu, um dessen Inhalt zu scannen." },
"jellyfinUrlLabel": { "message": "Jellyfin Server-URL" },
"jellyfinUserLabel": { "message": "Benutzername" },
"jellyfinPassLabel": { "message": "Passwort" },
"jellyfinConnectAndScan": { "message": "Verbinden und Scannen" },
"settingsPhpGenTitle": { "message": "PHP-Server-Skript-Generator" },
"settingsPhpFileOptions": { "message": "Dateioptionen" },
"settingsPhpSavePathLabel": { "message": "Speicherpfad auf dem Server" },
@ -286,5 +293,26 @@
"errorParsingPlexXml": { "message": "Fehler beim Parsen von Plex-XML." },
"untitled": { "message": "Ohne Titel" },
"itemCount": { "message": "$count$ Elemente", "placeholders": { "count": { "content": "$1" } } },
"noPhotoServers": { "message": "Keine Foto-Server" }
"noPhotoServers": { "message": "Keine Foto-Server" },
"jellyfinScanInProgress": { "message": "Jellyfin-Scan läuft bereits." },
"jellyfinScanning": { "message": "Scanne Jellyfin..." },
"jellyfinMissingCredentials": { "message": "Bitte vervollständige die Jellyfin-URL und den Benutzernamen." },
"jellyfinConnecting": { "message": "Verbinde mit Jellyfin unter: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinAuthFailed": { "message": "Jellyfin-Authentifizierung fehlgeschlagen: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinAuthSuccess": { "message": "Jellyfin-Authentifizierung erfolgreich." },
"jellyfinFetchingLibraries": { "message": "Bibliotheken werden abgerufen..." },
"jellyfinFetchFailed": { "message": "Fehler beim Abrufen der Bibliotheken: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinNoMediaLibraries": { "message": "Keine Film- oder Serienbibliotheken auf Jellyfin gefunden." },
"jellyfinLibrariesFound": { "message": "$count$ Medienbibliothek(en) gefunden.", "placeholders": { "count": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Erfolg] '$libraryName gescannt, $count$ Titel hinzugefügt.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"jellyfinLibraryScanFailed": { "message": "Fehler beim Scannen der Bibliothek '$libraryName.", "placeholders": { "libraryName": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Jellyfin-Scan abgeschlossen. $movies$ Filme und $series$ Serien hinzugefügt.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Jellyfin-Anmeldeinformationen nicht konfiguriert." },
"notFoundOnJellyfin": { "message": "\"$query$\" auf Jellyfin nicht gefunden.", "placeholders": { "query": { "content": "$1" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" auf keinem Server gefunden.", "placeholders": { "query": { "content": "$1" } } },
"localOnPlex": { "message": "Auf Plex" },
"searchOnPlex": { "message": "Auf Plex suchen" },
"jellyfinTitle": { "message": "Jellyfin-Inhalt" },
"noJellyfinContent": { "message": "Kein Jellyfin-Inhalt gefunden." },
"noJellyfinContentSub": { "message": "Stelle sicher, dass du deinen Jellyfin-Server in den Einstellungen gescannt hast." }
}

View File

@ -58,6 +58,7 @@
"settingsTitleFull": { "message": "Settings and Configuration" },
"settingsTabGeneral": { "message": "General" },
"settingsTabPlex": { "message": "Plex" },
"settingsTabJellyfin": { "message": "Jellyfin" },
"settingsTabPhpGen": { "message": "PHP Generator" },
"settingsTabData": { "message": "Data" },
"settingsApiServer": { "message": "API and Server Configuration" },
@ -80,6 +81,12 @@
"settingsPlexTokens": { "message": "Plex Tokens" },
"settingsPlexTokensDesc": { "message": "Edit the list of Plex tokens (JSON format)." },
"settingsSaveTokens": { "message": "Save Tokens" },
"settingsJellyfinTitle": { "message": "Jellyfin Settings" },
"settingsJellyfinDesc": { "message": "Add your Jellyfin server details to scan its content." },
"jellyfinUrlLabel": { "message": "Jellyfin Server URL" },
"jellyfinUserLabel": { "message": "Username" },
"jellyfinPassLabel": { "message": "Password" },
"jellyfinConnectAndScan": { "message": "Connect and Scan" },
"settingsPhpGenTitle": { "message": "PHP Server Script Generator" },
"settingsPhpFileOptions": { "message": "File Options" },
"settingsPhpSavePathLabel": { "message": "Save Path on Server" },
@ -286,5 +293,26 @@
"errorParsingPlexXml": { "message": "Error parsing Plex XML." },
"untitled": { "message": "Untitled" },
"itemCount": { "message": "$count$ items", "placeholders": { "count": { "content": "$1" } } },
"noPhotoServers": { "message": "No photo servers" }
"noPhotoServers": { "message": "No photo servers" },
"jellyfinScanInProgress": { "message": "Jellyfin scan is already in progress." },
"jellyfinScanning": { "message": "Scanning Jellyfin..." },
"jellyfinMissingCredentials": { "message": "Please complete the Jellyfin URL and username." },
"jellyfinConnecting": { "message": "Connecting to Jellyfin at: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinAuthFailed": { "message": "Jellyfin authentication failed: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinAuthSuccess": { "message": "Jellyfin authentication successful." },
"jellyfinFetchingLibraries": { "message": "Fetching libraries..." },
"jellyfinFetchFailed": { "message": "Failed to fetch libraries: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinNoMediaLibraries": { "message": "No movie or series libraries found on Jellyfin." },
"jellyfinLibrariesFound": { "message": "$count$ media library(s) found.", "placeholders": { "count": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Success] Scanned '$libraryName, added $count$ titles.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"jellyfinLibraryScanFailed": { "message": "Failed to scan library '$libraryName." , "placeholders": { "libraryName": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Jellyfin scan completed. Added $movies$ movies and $series$ series.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Jellyfin credentials not configured." },
"notFoundOnJellyfin": { "message": "\"$query$\" not found on Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" not found on any server.", "placeholders": { "query": { "content": "$1" } } },
"localOnPlex": { "message": "On Plex" },
"searchOnPlex": { "message": "Search on Plex" },
"jellyfinTitle": { "message": "Jellyfin Content" },
"noJellyfinContent": { "message": "No Jellyfin content found." },
"noJellyfinContentSub": { "message": "Make sure you have scanned your Jellyfin server in the settings." }
}

View File

@ -58,6 +58,7 @@
"settingsTitleFull": { "message": "Ajustes y Configuración" },
"settingsTabGeneral": { "message": "General" },
"settingsTabPlex": { "message": "Plex" },
"settingsTabJellyfin": { "message": "Jellyfin" },
"settingsTabPhpGen": { "message": "Generador PHP" },
"settingsTabData": { "message": "Datos" },
"settingsApiServer": { "message": "Configuración de API y Servidor" },
@ -80,6 +81,12 @@
"settingsPlexTokens": { "message": "Tokens de Plex" },
"settingsPlexTokensDesc": { "message": "Edita la lista de tokens de Plex (formato JSON)." },
"settingsSaveTokens": { "message": "Guardar Tokens" },
"settingsJellyfinTitle": { "message": "Configuración de Jellyfin" },
"settingsJellyfinDesc": { "message": "Añade los datos de tu servidor Jellyfin para escanear su contenido." },
"jellyfinUrlLabel": { "message": "URL del Servidor Jellyfin" },
"jellyfinUserLabel": { "message": "Nombre de Usuario" },
"jellyfinPassLabel": { "message": "Contraseña" },
"jellyfinConnectAndScan": { "message": "Conectar y Escanear" },
"settingsPhpGenTitle": { "message": "Generador de Script PHP para el Servidor" },
"settingsPhpFileOptions": { "message": "Opciones del Archivo" },
"settingsPhpSavePathLabel": { "message": "Ruta de Guardado en el Servidor" },
@ -273,7 +280,7 @@
"invalidStreamInfo": {"message": "Información inválida."},
"dbUnavailableForStreams": {"message": "Base de datos local no disponible."},
"noPlexServersForStreams": {"message": "No hay servidores Plex."},
"notFoundOnServers": {"message": "No se encontró \"$query$\" en los servidores.", "placeholders": {"query": {"content": "$1"}}},
"notFoundOnServers": {"message": "No se encontró \"$query$\" en los servidores de Plex.", "placeholders": {"query": {"content": "$1"}}},
"relativeTime_justNow": { "message": "Ahora mismo" },
"relativeTime_minutesAgo": { "message": "Hace $count$ minutos", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_hoursAgo": { "message": "Hace $count$ horas", "placeholders": { "count": { "content": "$1" } } },
@ -286,5 +293,26 @@
"errorParsingPlexXml": { "message": "Error al analizar el XML de Plex." },
"untitled": { "message": "Sin título" },
"itemCount": { "message": "$count$ elementos", "placeholders": { "count": { "content": "$1" } } },
"noPhotoServers": { "message": "No hay servidores de fotos" }
"noPhotoServers": { "message": "No hay servidores de fotos" },
"jellyfinScanInProgress": { "message": "El escaneo Jellyfin ya está en curso." },
"jellyfinScanning": { "message": "Escaneando Jellyfin..." },
"jellyfinMissingCredentials": { "message": "Por favor, completa la URL y el usuario de Jellyfin." },
"jellyfinConnecting": { "message": "Conectando a Jellyfin en: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinAuthFailed": { "message": "Autenticación Jellyfin fallida: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinAuthSuccess": { "message": "Autenticación Jellyfin exitosa." },
"jellyfinFetchingLibraries": { "message": "Obteniendo bibliotecas..." },
"jellyfinFetchFailed": { "message": "Error al obtener bibliotecas: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinNoMediaLibraries": { "message": "No se encontraron bibliotecas de películas o series en Jellyfin." },
"jellyfinLibrariesFound": { "message": "$count$ biblioteca(s) de medios encontrada(s).", "placeholders": { "count": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Éxito] '$libraryName$' escaneada, $count$ títulos añadidos.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"jellyfinLibraryScanFailed": { "message": "Error al escanear la biblioteca '$libraryName$'.", "placeholders": { "libraryName": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Escaneo Jellyfin completado. Añadidas $movies$ películas y $series$ series.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Credenciales de Jellyfin no configuradas." },
"notFoundOnJellyfin": { "message": "No se encontró \"$query$\" en Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
"notFoundOnAnyServer": { "message": "No se encontró \"$query$\" en ningún servidor.", "placeholders": { "query": { "content": "$1" } } },
"localOnPlex": { "message": "En Plex" },
"searchOnPlex": { "message": "Buscar en Plex" },
"jellyfinTitle": { "message": "Contenido de Jellyfin" },
"noJellyfinContent": { "message": "No se encontró contenido de Jellyfin." },
"noJellyfinContentSub": { "message": "Asegúrate de haber escaneado tu servidor Jellyfin en los ajustes." }
}

View File

@ -58,6 +58,7 @@
"settingsTitleFull": { "message": "Paramètres et Configuration" },
"settingsTabGeneral": { "message": "Général" },
"settingsTabPlex": { "message": "Plex" },
"settingsTabJellyfin": { "message": "Jellyfin" },
"settingsTabPhpGen": { "message": "Générateur PHP" },
"settingsTabData": { "message": "Données" },
"settingsApiServer": { "message": "Configuration API et Serveur" },
@ -80,6 +81,12 @@
"settingsPlexTokens": { "message": "Tokens Plex" },
"settingsPlexTokensDesc": { "message": "Modifiez la liste des tokens Plex (format JSON)." },
"settingsSaveTokens": { "message": "Sauvegarder les Tokens" },
"settingsJellyfinTitle": { "message": "Paramètres Jellyfin" },
"settingsJellyfinDesc": { "message": "Ajoutez les informations de votre serveur Jellyfin pour scanner son contenu." },
"jellyfinUrlLabel": { "message": "URL du serveur Jellyfin" },
"jellyfinUserLabel": { "message": "Nom d'utilisateur" },
"jellyfinPassLabel": { "message": "Mot de passe" },
"jellyfinConnectAndScan": { "message": "Connecter et Scanner" },
"settingsPhpGenTitle": { "message": "Générateur de Script PHP pour Serveur" },
"settingsPhpFileOptions": { "message": "Options du Fichier" },
"settingsPhpSavePathLabel": { "message": "Chemin de Sauvegarde sur le Serveur" },
@ -286,5 +293,26 @@
"errorParsingPlexXml": { "message": "Erreur d'analyse du XML de Plex." },
"untitled": { "message": "Sans titre" },
"itemCount": { "message": "$count$ éléments", "placeholders": { "count": { "content": "$1" } } },
"noPhotoServers": { "message": "Aucun serveur de photos" }
"noPhotoServers": { "message": "Aucun serveur de photos" },
"jellyfinScanInProgress": { "message": "Le scan Jellyfin est déjà en cours." },
"jellyfinScanning": { "message": "Scan de Jellyfin en cours..." },
"jellyfinMissingCredentials": { "message": "Veuillez compléter l'URL et le nom d'utilisateur de Jellyfin." },
"jellyfinConnecting": { "message": "Connexion à Jellyfin à : $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinAuthFailed": { "message": "Échec de l'authentification Jellyfin : $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinAuthSuccess": { "message": "Authentification Jellyfin réussie." },
"jellyfinFetchingLibraries": { "message": "Récupération des bibliothèques..." },
"jellyfinFetchFailed": { "message": "Échec de la récupération des bibliothèques : $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinNoMediaLibraries": { "message": "Aucune bibliothèque de films ou de séries trouvée sur Jellyfin." },
"jellyfinLibrariesFound": { "message": "$count$ bibliothèque(s) multimédia(s) trouvée(s).", "placeholders": { "count": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Succès] '$libraryName scannée, $count$ titres ajoutés.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"jellyfinLibraryScanFailed": { "message": "Échec du scan de la bibliothèque '$libraryName.", "placeholders": { "libraryName": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Scan Jellyfin terminé. $movies$ films et $series$ séries ajoutés.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Identifiants Jellyfin non configurés." },
"notFoundOnJellyfin": { "message": "\"$query$\" non trouvé sur Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" non trouvé sur aucun serveur.", "placeholders": { "query": { "content": "$1" } } },
"localOnPlex": { "message": "Sur Plex" },
"searchOnPlex": { "message": "Rechercher sur Plex" },
"jellyfinTitle": { "message": "Contenu Jellyfin" },
"noJellyfinContent": { "message": "Aucun contenu Jellyfin trouvé." },
"noJellyfinContentSub": { "message": "Assurez-vous d'avoir scanné votre serveur Jellyfin dans les paramètres." }
}

View File

@ -58,6 +58,7 @@
"settingsTitleFull": { "message": "Impostazioni e Configurazione" },
"settingsTabGeneral": { "message": "Generale" },
"settingsTabPlex": { "message": "Plex" },
"settingsTabJellyfin": { "message": "Jellyfin" },
"settingsTabPhpGen": { "message": "Generatore PHP" },
"settingsTabData": { "message": "Dati" },
"settingsApiServer": { "message": "Configurazione API e Server" },
@ -80,6 +81,12 @@
"settingsPlexTokens": { "message": "Token Plex" },
"settingsPlexTokensDesc": { "message": "Modifica la lista dei token Plex (formato JSON)." },
"settingsSaveTokens": { "message": "Salva Token" },
"settingsJellyfinTitle": { "message": "Impostazioni Jellyfin" },
"settingsJellyfinDesc": { "message": "Aggiungi i dati del tuo server Jellyfin per scansionarne il contenuto." },
"jellyfinUrlLabel": { "message": "URL Server Jellyfin" },
"jellyfinUserLabel": { "message": "Nome utente" },
"jellyfinPassLabel": { "message": "Password" },
"jellyfinConnectAndScan": { "message": "Connetti e Scansiona" },
"settingsPhpGenTitle": { "message": "Generatore di Script PHP per Server" },
"settingsPhpFileOptions": { "message": "Opzioni File" },
"settingsPhpSavePathLabel": { "message": "Percorso di salvataggio sul server" },
@ -286,5 +293,26 @@
"errorParsingPlexXml": { "message": "Errore nell'analisi dell'XML di Plex." },
"untitled": { "message": "Senza titolo" },
"itemCount": { "message": "$count$ elementi", "placeholders": { "count": { "content": "$1" } } },
"noPhotoServers": { "message": "Nessun server di foto" }
"noPhotoServers": { "message": "Nessun server di foto" },
"jellyfinScanInProgress": { "message": "Scansione Jellyfin già in corso." },
"jellyfinScanning": { "message": "Scansione di Jellyfin in corso..." },
"jellyfinMissingCredentials": { "message": "Per favore, completa l'URL e il nome utente di Jellyfin." },
"jellyfinConnecting": { "message": "Connessione a Jellyfin a: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinAuthFailed": { "message": "Autenticazione Jellyfin fallita: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinAuthSuccess": { "message": "Autenticazione Jellyfin riuscita." },
"jellyfinFetchingLibraries": { "message": "Recupero delle librerie..." },
"jellyfinFetchFailed": { "message": "Recupero delle librerie fallito: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinNoMediaLibraries": { "message": "Nessuna libreria di film o serie trovata su Jellyfin." },
"jellyfinLibrariesFound": { "message": "$count$ libreria(e) multimediale(i) trovata(e).", "placeholders": { "count": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Successo] Scansionata '$libraryName, aggiunti $count$ titoli.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"jellyfinLibraryScanFailed": { "message": "Scansione della libreria '$libraryName fallita.", "placeholders": { "libraryName": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Scansione Jellyfin completata. Aggiunti $movies$ film e $series$ serie.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Credenziali di Jellyfin non configurate." },
"notFoundOnJellyfin": { "message": "\"$query$\" non trovato su Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" non trovato su nessun server.", "placeholders": { "query": { "content": "$1" } } },
"localOnPlex": { "message": "Su Plex" },
"searchOnPlex": { "message": "Cerca su Plex" },
"jellyfinTitle": { "message": "Contenuto Jellyfin" },
"noJellyfinContent": { "message": "Nessun contenuto Jellyfin trovato." },
"noJellyfinContentSub": { "message": "Assicurati di aver scansionato il tuo server Jellyfin nelle impostazioni." }
}

View File

@ -58,6 +58,7 @@
"settingsTitleFull": { "message": "Configurações e Ajustes" },
"settingsTabGeneral": { "message": "Geral" },
"settingsTabPlex": { "message": "Plex" },
"settingsTabJellyfin": { "message": "Jellyfin" },
"settingsTabPhpGen": { "message": "Gerador de PHP" },
"settingsTabData": { "message": "Dados" },
"settingsApiServer": { "message": "Configuração de API e Servidor" },
@ -80,6 +81,12 @@
"settingsPlexTokens": { "message": "Tokens do Plex" },
"settingsPlexTokensDesc": { "message": "Edite a lista de tokens do Plex (formato JSON)." },
"settingsSaveTokens": { "message": "Salvar Tokens" },
"settingsJellyfinTitle": { "message": "Configurações do Jellyfin" },
"settingsJellyfinDesc": { "message": "Adicione os detalhes do seu servidor Jellyfin para analisar seu conteúdo." },
"jellyfinUrlLabel": { "message": "URL do Servidor Jellyfin" },
"jellyfinUserLabel": { "message": "Nome de usuário" },
"jellyfinPassLabel": { "message": "Senha" },
"jellyfinConnectAndScan": { "message": "Conectar e Analisar" },
"settingsPhpGenTitle": { "message": "Gerador de Script PHP para Servidor" },
"settingsPhpFileOptions": { "message": "Opções de Arquivo" },
"settingsPhpSavePathLabel": { "message": "Caminho para Salvar no Servidor" },
@ -286,5 +293,26 @@
"errorParsingPlexXml": { "message": "Erro ao analisar o XML do Plex." },
"untitled": { "message": "Sem título" },
"itemCount": { "message": "$count$ itens", "placeholders": { "count": { "content": "$1" } } },
"noPhotoServers": { "message": "Nenhum servidor de fotos" }
"noPhotoServers": { "message": "Nenhum servidor de fotos" },
"jellyfinScanInProgress": { "message": "A análise do Jellyfin já está em andamento." },
"jellyfinScanning": { "message": "Analisando Jellyfin..." },
"jellyfinMissingCredentials": { "message": "Por favor, complete a URL e o nome de usuário do Jellyfin." },
"jellyfinConnecting": { "message": "Conectando ao Jellyfin em: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinAuthFailed": { "message": "A autenticação do Jellyfin falhou: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinAuthSuccess": { "message": "Autenticação do Jellyfin bem-sucedida." },
"jellyfinFetchingLibraries": { "message": "Buscando bibliotecas..." },
"jellyfinFetchFailed": { "message": "Falha ao buscar bibliotecas: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinNoMediaLibraries": { "message": "Nenhuma biblioteca de filmes ou séries encontrada no Jellyfin." },
"jellyfinLibrariesFound": { "message": "$count$ biblioteca(s) de mídia encontrada(s).", "placeholders": { "count": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Sucesso] Análise de '$libraryName concluída, $count$ títulos adicionados.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"jellyfinLibraryScanFailed": { "message": "Falha ao analisar a biblioteca '$libraryName.", "placeholders": { "libraryName": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Análise do Jellyfin concluída. Adicionados $movies$ filmes e $series$ séries.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Credenciais do Jellyfin não configuradas." },
"notFoundOnJellyfin": { "message": "\"$query$\" não encontrado no Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" não encontrado em nenhum servidor.", "placeholders": { "query": { "content": "$1" } } },
"localOnPlex": { "message": "No Plex" },
"searchOnPlex": { "message": "Pesquisar no Plex" },
"jellyfinTitle": { "message": "Conteúdo do Jellyfin" },
"noJellyfinContent": { "message": "Nenhum conteúdo do Jellyfin encontrado." },
"noJellyfinContentSub": { "message": "Certifique-se de que você analisou seu servidor Jellyfin nas configurações." }
}

View File

@ -155,9 +155,9 @@ body.light-theme .sidebar-nav {
}
@media (min-width: 992px) {
#sidebar-toggle {
/* #sidebar-toggle {
display: none;
}
} */
.sidebar-nav {
transform: translateX(0);
}
@ -215,4 +215,12 @@ body.light-theme .sidebar-nav {
height: 36px;
font-size: 1.1rem;
}
}
}
body.sidebar-collapsed .sidebar-nav {
transform: translateX(-100%);
}
body.sidebar-collapsed #main-container {
padding-left: 0;
}

104
js/api.js
View File

@ -220,4 +220,108 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
} else {
return { success: false, streams: [], message: _('notFoundOnServers', busqueda) };
}
}
export async function fetchAllStreamsFromJellyfin(busqueda, tipoContenido) {
if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') };
const { url, userId, apiKey } = state.jellyfinSettings;
if (!url || !userId || !apiKey) return { success: false, streams: [], message: _('noJellyfinCredentials') };
const jellyfinSearchType = tipoContenido === 'movie' ? 'Movie' : 'Series';
const searchUrl = `${url}/Users/${userId}/Items?searchTerm=${encodeURIComponent(busqueda)}&IncludeItemTypes=${jellyfinSearchType}&Recursive=true`;
try {
const response = await fetch(searchUrl, { headers: { 'X-Emby-Token': apiKey } });
if (!response.ok) throw new Error(`Error buscando en Jellyfin: ${response.status}`);
const searchData = await response.json();
if (!searchData.Items || searchData.Items.length === 0) {
return { success: false, streams: [], message: _('notFoundOnJellyfin', busqueda) };
}
const item = searchData.Items.find(i => i.Name.toLowerCase() === busqueda.toLowerCase()) || searchData.Items[0];
const itemId = item.Id;
const itemName = item.Name;
const itemYear = item.ProductionYear;
const posterTag = item.ImageTags?.Primary;
const posterUrl = posterTag ? `${url}/Items/${itemId}/Images/Primary?tag=${posterTag}` : '';
let streams = [];
if (item.Type === 'Movie') {
const streamUrl = `${url}/Videos/${itemId}/stream?api_key=${apiKey}`;
const extinfName = `${itemName}${itemYear ? ` (${itemYear})` : ''}`;
const groupTitle = extinfName.replace(/"/g, "'");
streams.push({
url: streamUrl,
title: extinfName,
extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${posterUrl}" group-title="${groupTitle}",${extinfName}`
});
} else if (item.Type === 'Series') {
const episodesUrl = `${url}/Shows/${itemId}/Episodes?userId=${userId}`;
const episodesResponse = await fetch(episodesUrl, { headers: { 'X-Emby-Token': apiKey } });
if (!episodesResponse.ok) throw new Error(`Error obteniendo episodios: ${episodesResponse.status}`);
const episodesData = await episodesResponse.json();
const sortedEpisodes = episodesData.Items.sort((a,b) => {
if (a.ParentIndexNumber !== b.ParentIndexNumber) return (a.ParentIndexNumber || 0) - (b.ParentIndexNumber || 0);
return (a.IndexNumber || 0) - (b.IndexNumber || 0);
});
sortedEpisodes.forEach(ep => {
const streamUrl = `${url}/Videos/${ep.Id}/stream?api_key=${apiKey}`;
const seasonNum = ep.ParentIndexNumber || 'S';
const episodeNum = ep.IndexNumber || 'E';
const episodeTitle = ep.Name || 'Episodio';
const groupTitle = `${itemName} - Temporada ${seasonNum}`.replace(/"/g, "'");
const extinfName = `${itemName} T${seasonNum}E${episodeNum} ${episodeTitle}`;
streams.push({
url: streamUrl,
title: extinfName,
extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${posterUrl}" group-title="${groupTitle}",${extinfName}`
});
});
}
return { success: true, streams };
} catch (error) {
console.error("Error fetching streams from Jellyfin:", error);
return { success: false, streams: [], message: error.message };
}
}
export async function fetchAllAvailableStreams(title, type) {
const plexPromise = fetchAllStreamsFromPlex(title, type);
const jellyfinPromise = fetchAllStreamsFromJellyfin(title, type);
const results = await Promise.allSettled([plexPromise, jellyfinPromise]);
let allStreams = [];
const errorMessages = [];
results.forEach((result, index) => {
const sourceName = index === 0 ? 'Plex' : 'Jellyfin';
if (result.status === 'fulfilled' && result.value.success) {
allStreams.push(...result.value.streams);
} else if (result.status === 'fulfilled' && !result.value.success) {
if (result.value.message !== _('noPlexServersForStreams') && result.value.message !== _('noJellyfinCredentials')) {
errorMessages.push(`${sourceName}: ${result.value.message}`);
}
} else if (result.status === 'rejected') {
errorMessages.push(`${sourceName}: ${result.reason.message}`);
}
});
const uniqueStreamsMap = new Map(allStreams.map(stream => [stream.url, stream]));
const uniqueStreams = Array.from(uniqueStreamsMap.values());
if (uniqueStreams.length > 0) {
return { success: true, streams: uniqueStreams, message: `Found ${uniqueStreams.length} streams.` };
} else {
return { success: false, streams: [], message: errorMessages.join('; ') || _('notFoundOnAnyServer', title) };
}
}

View File

@ -1,5 +1,5 @@
export const config = {
defaultApiKey: '4e44d9029b1270a757cddc766a1bcb63',
dbName: 'PlexDB',
dbVersion: 6,
dbVersion: 7,
};

View File

@ -13,14 +13,17 @@ export function initDB() {
request.onupgradeneeded = e => {
state.db = e.target.result;
const transaction = e.target.transaction;
const storesToCreate = ['movies', 'series', 'artists', 'photos', 'tokens', 'conexiones_locales', 'settings'];
const storesToCreate = ['movies', 'series', 'artists', 'photos', 'tokens', 'conexiones_locales', 'settings', 'jellyfin_settings', 'jellyfin_movies', 'jellyfin_series'];
storesToCreate.forEach(storeName => {
if (!state.db.objectStoreNames.contains(storeName)) {
let storeOptions;
if (storeName === 'settings') {
if (['settings', 'jellyfin_settings'].includes(storeName)) {
storeOptions = { keyPath: 'id' };
} else {
} else if (['jellyfin_movies', 'jellyfin_series'].includes(storeName)) {
storeOptions = { keyPath: 'libraryId' };
}
else {
storeOptions = { keyPath: 'id', autoIncrement: true };
}
const store = state.db.createObjectStore(storeName, storeOptions);
@ -126,7 +129,7 @@ export function addItemsToStore(storeName, items) {
export async function clearContentData() {
showNotification(_("deletingContentData"), "info");
mostrarSpinner();
const storesToDelete = ['movies', 'series', 'artists', 'photos', 'conexiones_locales'];
const storesToDelete = ['movies', 'series', 'artists', 'photos', 'conexiones_locales', 'jellyfin_movies', 'jellyfin_series'];
try {
if (!state.db) throw new Error(_("dbNotAvailable"));
const storesPresent = storesToDelete.filter(name => state.db.objectStoreNames.contains(name));

View File

@ -3,6 +3,7 @@ import { switchView, resetView, showMainView, showItemDetails, applyFilters, sea
import { debounce, showNotification, _ } from './utils.js';
import { clearContentData, loadTokensToEditor, saveTokensFromEditor, exportDatabase, importDatabase } from './db.js';
import { startPlexScan } from './plex.js';
import { startJellyfinScan } from './jellyfin.js';
import { Equalizer } from './equalizer.js';
async function handleDatabaseUpdate() {
@ -28,9 +29,16 @@ async function handleDatabaseUpdate() {
}
export function setupEventListeners() {
const savedSidebarState = localStorage.getItem('sidebarCollapsed');
if (savedSidebarState === 'true') {
document.body.classList.add('sidebar-collapsed');
} else {
document.body.classList.remove('sidebar-collapsed');
}
document.getElementById('sidebar-toggle').addEventListener('click', () => {
document.getElementById('sidebar-nav').classList.toggle('open');
document.getElementById('main-container').classList.toggle('sidebar-open');
document.body.classList.toggle('sidebar-collapsed');
const isCollapsed = document.body.classList.contains('sidebar-collapsed');
localStorage.setItem('sidebarCollapsed', isCollapsed);
});
document.getElementById('nav-movies').addEventListener('click', (e) => { e.preventDefault(); switchView('movies'); });
@ -109,6 +117,8 @@ export function setupEventListeners() {
}
});
document.getElementById('jellyfinScanBtn').addEventListener('click', startJellyfinScan);
document.getElementById('clearDataBtn').addEventListener('click', () => {
if (confirm(_('confirmClearContent'))) {
clearContentData();
@ -235,7 +245,7 @@ function handleMainViewClick(e) {
handlePhotoGridClick(photoCard);
return;
}
const card = e.target.closest('.item-card');
if (!card) return;

209
js/jellyfin.js Normal file
View File

@ -0,0 +1,209 @@
import { state } from './state.js';
import { addItemsToStore, clearStore } from './db.js';
import { showNotification, _, emitirEventoActualizacion } from './utils.js';
async function authenticateJellyfin(url, username, password) {
const authUrl = `${url}/Users/AuthenticateByName`;
const body = JSON.stringify({
Username: username,
Pw: password
});
try {
const response = await fetch(authUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Emby-Authorization': 'MediaBrowser Client="CinePlex", Device="Chrome", DeviceId="cineplex-jellyfin-integration", Version="1.0"'
},
body: body
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.AuthenticationResult?.ErrorMessage || `Error ${response.status}`);
}
const data = await response.json();
return { success: true, token: data.AccessToken, userId: data.User.Id };
} catch (error) {
return { success: false, message: error.message };
}
}
async function fetchLibraryViews(url, userId, apiKey) {
const viewsUrl = `${url}/Users/${userId}/Views`;
try {
const response = await fetch(viewsUrl, {
headers: {
'X-Emby-Token': apiKey
}
});
if (!response.ok) throw new Error(`Error ${response.status} fetching library views`);
const data = await response.json();
return { success: true, views: data.Items };
} catch (error) {
return { success: false, message: error.message };
}
}
async function fetchItemsFromLibrary(url, userId, apiKey, library) {
const itemsUrl = `${url}/Users/${userId}/Items?ParentId=${library.Id}&recursive=true&fields=ProductionYear&includeItemTypes=Movie,Series`;
try {
const response = await fetch(itemsUrl, {
headers: {
'X-Emby-Token': apiKey
}
});
if (!response.ok) throw new Error(`Error ${response.status}`);
const data = await response.json();
const items = data.Items.map(item => ({
id: item.Id,
title: item.Name,
year: item.ProductionYear,
type: item.Type,
posterTag: item.ImageTags?.Primary,
}));
return { success: true, items, libraryName: library.Name, libraryId: library.Id };
} catch (error) {
return { success: false, message: error.message, libraryName: library.Name, libraryId: library.Id };
}
}
export async function startJellyfinScan() {
if (state.isScanningJellyfin) {
showNotification(_('jellyfinScanInProgress'), 'warning');
return;
}
state.isScanningJellyfin = true;
const statusDiv = document.getElementById('jellyfinScanStatus');
const scanBtn = document.getElementById('jellyfinScanBtn');
const originalBtnText = scanBtn.innerHTML;
scanBtn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${_('jellyfinScanning')}`;
scanBtn.disabled = true;
const urlInput = document.getElementById('jellyfinServerUrl');
const usernameInput = document.getElementById('jellyfinUsername');
const passwordInput = document.getElementById('jellyfinPassword');
let url = urlInput.value.trim();
const username = usernameInput.value.trim();
const password = passwordInput.value;
if (!url || !username) {
showNotification(_('jellyfinMissingCredentials'), 'error');
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
return;
}
url = url.replace(/\/web\/.*$/, '').replace(/\/$/, '');
statusDiv.innerHTML = `<div class="text-info">${_('jellyfinConnecting', url)}</div>`;
const authResult = await authenticateJellyfin(url, username, password);
if (!authResult.success) {
statusDiv.innerHTML = `<div class="text-danger">${_('jellyfinAuthFailed', authResult.message)}</div>`;
showNotification(_('jellyfinAuthFailed', authResult.message), 'error');
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
return;
}
statusDiv.innerHTML = `<div class="text-success">${_('jellyfinAuthSuccess')}</div><div class="text-info">${_('jellyfinFetchingLibraries')}</div>`;
const viewsResult = await fetchLibraryViews(url, authResult.userId, authResult.token);
if (!viewsResult.success) {
statusDiv.innerHTML += `<div class="text-danger">${_('jellyfinFetchFailed', viewsResult.message)}</div>`;
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
return;
}
const mediaLibraries = viewsResult.views.filter(v => v.CollectionType === 'movies' || v.CollectionType === 'tvshows');
if (mediaLibraries.length === 0) {
statusDiv.innerHTML += `<div class="text-warning">${_('jellyfinNoMediaLibraries')}</div>`;
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
return;
}
statusDiv.innerHTML += `<div class="text-info">${_('jellyfinLibrariesFound', String(mediaLibraries.length))}</div>`;
await clearStore('jellyfin_movies');
await clearStore('jellyfin_series');
let totalMovies = 0;
let totalSeries = 0;
const scanPromises = mediaLibraries.map(library =>
fetchItemsFromLibrary(url, authResult.userId, authResult.token, library)
);
const results = await Promise.allSettled(scanPromises);
for (const result of results) {
if (result.status === 'fulfilled' && result.value.success) {
const library = mediaLibraries.find(lib => lib.Id === result.value.libraryId);
if (library) {
const storeName = library.CollectionType === 'movies' ? 'jellyfin_movies' : 'jellyfin_series';
const dbEntry = {
serverUrl: url,
libraryId: library.Id,
libraryName: library.Name,
titulos: result.value.items,
};
await addItemsToStore(storeName, [dbEntry]);
if (storeName === 'jellyfin_movies') {
totalMovies += result.value.items.length;
} else {
totalSeries += result.value.items.length;
}
statusDiv.innerHTML += `<div class="text-success-secondary">${_('jellyfinLibraryScanSuccess', [library.Name, String(result.value.items.length)])}</div>`;
}
} else {
const libraryName = result.reason?.libraryName || result.value?.libraryName || 'Unknown';
statusDiv.innerHTML += `<div class="text-warning">${_('jellyfinLibraryScanFailed', libraryName)}</div>`;
}
}
const newSettings = {
id: 'jellyfin_credentials',
url: url,
username: username,
password: password,
apiKey: authResult.token,
userId: authResult.userId
};
await addItemsToStore('jellyfin_settings', [newSettings]);
state.jellyfinSettings = newSettings;
const message = _('jellyfinScanSuccess', [String(totalMovies), String(totalSeries)]);
statusDiv.innerHTML += `<div class="text-success mt-2">${message}</div>`;
showNotification(message, 'success');
setTimeout(() => {
const modalInstance = bootstrap.Modal.getInstance(document.getElementById('settingsModal'));
if(modalInstance) modalInstance.hide();
emitirEventoActualizacion();
}, 2000);
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
}

View File

@ -18,6 +18,12 @@ async function loadSettings() {
if (!state.settings.apiKey) {
state.settings.apiKey = config.defaultApiKey;
}
const jellyfinSettingsData = await getFromDB('jellyfin_settings');
if (jellyfinSettingsData && jellyfinSettingsData.length > 0) {
state.jellyfinSettings = { ...state.jellyfinSettings, ...jellyfinSettingsData[0] };
}
} catch (error) {
console.error("Could not load settings from DB, using defaults.", error);
state.settings.language = chrome.i18n.getUILanguage().split('-')[0];

View File

@ -15,6 +15,16 @@ export const state = {
phpFilename: 'CinePlex_Playlist.m3u',
phpFileAction: 'append',
},
jellyfinSettings: {
id: 'jellyfin_credentials',
url: '',
username: '',
password: '',
apiKey: '',
userId: '',
},
jellyfinMovies: [],
jellyfinSeries: [],
localMovies: [],
localSeries: [],
localArtists: [],
@ -32,6 +42,7 @@ export const state = {
isAddingStream: false,
isDownloadingM3U: false,
isScanningPlex: false,
isScanningJellyfin: false,
musicPlayer: null,
currentContentFetchController: null,
plexScanAbortController: null,

105
js/ui.js
View File

@ -1,5 +1,5 @@
import { state } from './state.js';
import { fetchTMDB, fetchAllStreamsFromPlex } from './api.js';
import { fetchTMDB, fetchAllAvailableStreams } from './api.js';
import { showNotification, getRelativeTime, fetchWithTimeout, _ } from './utils.js';
import { getFromDB, addItemsToStore } from './db.js';
@ -7,7 +7,7 @@ let charts = {};
export async function loadInitialContent() {
await Promise.all([loadGenres(), loadYears()]);
resetView(); // Show hero-only view first
resetView();
setupScrollEffects();
}
@ -30,11 +30,21 @@ export function initializeUserData() {
export async function loadLocalContent() {
if (!state.db) return;
try {
const [movies, series, artists, photos] = await Promise.all([getFromDB('movies'), getFromDB('series'), getFromDB('artists'), getFromDB('photos')]);
const [movies, series, artists, photos, jfMovies, jfSeries] = await Promise.all([
getFromDB('movies'),
getFromDB('series'),
getFromDB('artists'),
getFromDB('photos'),
getFromDB('jellyfin_movies'),
getFromDB('jellyfin_series')
]);
state.localMovies = movies;
state.localSeries = series;
state.localArtists = artists;
state.localPhotos = photos;
state.jellyfinMovies = jfMovies;
state.jellyfinSeries = jfSeries;
} catch (error) {
showNotification(_("errorLoadingLocalContent"), "error");
}
@ -331,16 +341,30 @@ export async function loadContent(append = false) {
}
}
function buscarContenidoLocal(title, type) {
if (!title || !type) return null;
function isContentAvailableLocally(title, type) {
if (!title || !type) return false;
const normalizedTitle = title.toLowerCase().trim();
const source = type === 'movie' ? state.localMovies : state.localSeries;
if (!Array.isArray(source)) return null;
return source.find(server =>
server && Array.isArray(server.titulos) &&
server.titulos.some(t => t && typeof t.title === 'string' && t.title.toLowerCase().trim() === normalizedTitle)
) || null;
const plexSource = type === 'movie' ? state.localMovies : state.localSeries;
if (Array.isArray(plexSource)) {
const foundInPlex = plexSource.some(server =>
server && Array.isArray(server.titulos) &&
server.titulos.some(t => t && typeof t.title === 'string' && t.title.toLowerCase().trim() === normalizedTitle)
);
if (foundInPlex) return true;
}
const jellyfinType = type === 'movie' ? 'Movie' : 'Series';
const jellyfinSource = type === 'movie' ? state.jellyfinMovies : state.jellyfinSeries;
if (Array.isArray(jellyfinSource)) {
const foundInJellyfin = jellyfinSource.some(library =>
library && Array.isArray(library.titulos) &&
library.titulos.some(t => t && typeof t.title === 'string' && t.title.toLowerCase().trim() === normalizedTitle && t.type === jellyfinType)
);
if (foundInJellyfin) return true;
}
return false;
}
@ -369,7 +393,7 @@ function renderGrid(items, append = false) {
const releaseDate = isMovie ? item.release_date : item.first_air_date;
const year = releaseDate ? releaseDate.slice(0, 4) : 'N/A';
const posterPath = item.poster_path ? `https://image.tmdb.org/t/p/w500${item.poster_path}` : 'img/no-poster.png';
const isAvailable = !!buscarContenidoLocal(title, itemType);
const isAvailable = isContentAvailableLocally(title, itemType);
const isFavorite = state.favorites.some(fav => fav.id === item.id && fav.type === itemType);
const voteAvg = item.vote_average ? item.vote_average.toFixed(1) : 'N/A';
const ratingClass = voteAvg >= 7.5 ? 'rating-good' : (voteAvg >= 5.0 ? 'rating-ok' : 'rating-bad');
@ -553,7 +577,7 @@ async function renderItemDetails(item) {
const voteAverage = item.vote_average ? item.vote_average.toFixed(1) : 'N/A';
const genres = item.genres || [];
const trailer = item.videos?.results?.find(v => v.site === 'YouTube' && v.type === 'Trailer');
const isAvailable = !!buscarContenidoLocal(title, state.currentItemType);
const isAvailable = isContentAvailableLocally(title, state.currentItemType);
const isFavorite = state.favorites.some(fav => fav.id === item.id && fav.type === state.currentItemType);
const imdbId = item.external_ids?.imdb_id;
@ -765,7 +789,7 @@ export function displayHistory() {
const fragment = document.createDocumentFragment();
[...state.userHistory].sort((a,b) => b.timestamp - a.timestamp).forEach(item => {
const posterUrl = item.poster ? `https://image.tmdb.org/t/p/w92${item.poster}` : 'img/no-poster.png';
const isAvailable = !!buscarContenidoLocal(item.title, item.type);
const isAvailable = isContentAvailableLocally(item.title, item.type);
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
historyItem.dataset.id = item.id;
@ -921,22 +945,24 @@ export async function generateStatistics() {
try {
const selectedToken = document.getElementById('stats-token-filter').value;
const filterByToken = (data) => {
if (selectedToken === 'all') return data;
return data.filter(server => server.tokenPrincipal === selectedToken);
};
const filteredMovies = filterByToken(state.localMovies);
const filteredSeries = filterByToken(state.localSeries);
const filteredArtists = filterByToken(state.localArtists);
const filteredPlexMovies = selectedToken === 'all' ? state.localMovies : state.localMovies.filter(server => server.tokenPrincipal === selectedToken);
const filteredPlexSeries = selectedToken === 'all' ? state.localSeries : state.localSeries.filter(server => server.tokenPrincipal === selectedToken);
const filteredPlexArtists = selectedToken === 'all' ? state.localArtists : state.localArtists.filter(server => server.tokenPrincipal === selectedToken);
const plexMovieItems = filteredPlexMovies.flatMap(s => s.titulos);
const plexSeriesItems = filteredPlexSeries.flatMap(s => s.titulos);
const plexArtistItems = filteredPlexArtists.flatMap(s => s.titulos);
const jellyfinMovieItems = state.jellyfinMovies.flatMap(lib => lib.titulos);
const jellyfinSeriesItems = state.jellyfinSeries.flatMap(lib => lib.titulos);
const allMovieItems = [...plexMovieItems, ...jellyfinMovieItems];
const allSeriesItems = [...plexSeriesItems, ...jellyfinSeriesItems];
const allArtistItems = [...plexArtistItems];
const allMovieItems = filteredMovies.flatMap(s => s.titulos);
const allSeriesItems = filteredSeries.flatMap(s => s.titulos);
const uniqueMovieTitles = new Set(allMovieItems.map(item => item.title));
const uniqueSeriesTitles = new Set(allSeriesItems.map(item => item.title));
const uniqueArtists = new Set(filteredArtists.flatMap(s => s.titulos.map(t => t.title)));
const uniqueArtists = new Set(allArtistItems.map(item => item.title));
animateValue('total-movies', 0, uniqueMovieTitles.size, 1000);
animateValue('total-series', 0, uniqueSeriesTitles.size, 1000);
animateValue('total-artists', 0, uniqueArtists.size, 1000);
@ -1233,7 +1259,7 @@ function updateHeroContent(item) {
const type = item.title ? 'movie' : 'tv';
const title = item.title || item.name;
const isAvailable = !!buscarContenidoLocal(title, type);
const isAvailable = isContentAvailableLocally(title, type);
if (heroTitle) heroTitle.textContent = title;
if (heroSubtitle) heroSubtitle.textContent = item.overview.substring(0, 200) + (item.overview.length > 200 ? '...' : '');
@ -1277,7 +1303,7 @@ export async function addStreamToList(title, type, buttonElement = null) {
showNotification(_('searchingStreams', title), 'info');
try {
const streamData = await fetchAllStreamsFromPlex(title, type);
const streamData = await fetchAllAvailableStreams(title, type);
if (!streamData.success || streamData.streams.length === 0) throw new Error(streamData.message);
showNotification(_('sendingStreams', String(streamData.streams.length)), 'info');
@ -1316,7 +1342,8 @@ export async function downloadM3U(title, type, buttonElement = null) {
showNotification(_('generatingM3U', title), "info");
try {
const streamData = await fetchAllStreamsFromPlex(title, type);
const streamData = await fetchAllAvailableStreams(title, type);
if (!streamData.success || streamData.streams.length === 0) throw new Error(streamData.message);
let m3uContent = "#EXTM3U\n";
@ -1408,6 +1435,10 @@ export function openSettingsModal() {
document.getElementById('lightModeToggle').checked = state.settings.theme === 'light';
document.getElementById('showHeroToggle').checked = state.settings.showHero;
document.getElementById('jellyfinServerUrl').value = state.jellyfinSettings.url || '';
document.getElementById('jellyfinUsername').value = state.jellyfinSettings.username || '';
document.getElementById('jellyfinPassword').value = state.jellyfinSettings.password || '';
document.getElementById('phpSecretKeyCheck').checked = state.settings.phpUseSecretKey;
document.getElementById('phpSecretKey').value = state.settings.phpSecretKey || '';
document.getElementById('phpSavePath').value = state.settings.phpSavePath || '';
@ -1438,9 +1469,21 @@ export async function saveSettings() {
};
state.settings = { ...state.settings, ...newSettings };
const newJellyfinSettings = {
id: 'jellyfin_credentials',
url: document.getElementById('jellyfinServerUrl').value.trim(),
username: document.getElementById('jellyfinUsername').value.trim(),
password: document.getElementById('jellyfinPassword').value
};
state.jellyfinSettings = { ...state.jellyfinSettings, ...newJellyfinSettings };
try {
await addItemsToStore('settings', [state.settings]);
await Promise.all([
addItemsToStore('settings', [state.settings]),
addItemsToStore('jellyfin_settings', [state.jellyfinSettings])
]);
showNotification(_('settingsSavedSuccess'), 'success');
applyTheme(state.settings.theme);
applyHeroVisibility(state.settings.showHero);

View File

@ -15,7 +15,8 @@
],
"host_permissions": [
"https://*.plex.tv/*",
"*://*:*/*"
"http://*/*",
"https://*/*"
],
"background": {
"service_worker": "js/background.js",

View File

@ -288,6 +288,11 @@
<i class="fas fa-server me-2"></i>__MSG_settingsTabPlex__
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="jellyfin-tab" data-bs-toggle="tab" data-bs-target="#jellyfin" type="button" role="tab" aria-controls="jellyfin" aria-selected="false">
<i class="fas fa-database me-2"></i>__MSG_settingsTabJellyfin__
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="php-gen-tab" data-bs-toggle="tab" data-bs-target="#php-gen" type="button" role="tab" aria-controls="php-gen" aria-selected="false">
<i class="fab fa-php me-2"></i>__MSG_settingsTabPhpGen__
@ -359,6 +364,28 @@
</div>
</div>
</div>
<div class="tab-pane fade" id="jellyfin" role="tabpanel" aria-labelledby="jellyfin-tab">
<h5 class="mb-3">__MSG_settingsJellyfinTitle__</h5>
<p class="small text-muted mb-3">__MSG_settingsJellyfinDesc__</p>
<div class="mb-3">
<label for="jellyfinServerUrl" class="form-label">__MSG_jellyfinUrlLabel__</label>
<input type="url" class="form-control" id="jellyfinServerUrl" placeholder="http://192.168.1.10:8096">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="jellyfinUsername" class="form-label">__MSG_jellyfinUserLabel__</label>
<input type="text" class="form-control" id="jellyfinUsername">
</div>
<div class="col-md-6 mb-3">
<label for="jellyfinPassword" class="form-label">__MSG_jellyfinPassLabel__</label>
<input type="password" class="form-control" id="jellyfinPassword">
</div>
</div>
<div class="d-grid">
<button type="button" class="btn btn-primary" id="jellyfinScanBtn"><i class="fas fa-search-plus me-2"></i>__MSG_jellyfinConnectAndScan__</button>
</div>
<div id="jellyfinScanStatus" class="mt-3"></div>
</div>
<div class="tab-pane fade" id="php-gen" role="tabpanel" aria-labelledby="php-gen-tab">
<h5 class="mb-3">__MSG_settingsPhpGenTitle__</h5>
<div class="row">