Jellyfin integration. Movies and TV shows only.
This commit is contained in:
parent
74886c0c8c
commit
e988ff15c8
@ -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." }
|
||||
}
|
@ -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." }
|
||||
}
|
@ -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." }
|
||||
}
|
@ -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." }
|
||||
}
|
@ -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." }
|
||||
}
|
@ -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." }
|
||||
}
|
@ -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
104
js/api.js
@ -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) };
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
export const config = {
|
||||
defaultApiKey: '4e44d9029b1270a757cddc766a1bcb63',
|
||||
dbName: 'PlexDB',
|
||||
dbVersion: 6,
|
||||
dbVersion: 7,
|
||||
};
|
11
js/db.js
11
js/db.js
@ -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));
|
||||
|
@ -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
209
js/jellyfin.js
Normal 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;
|
||||
}
|
@ -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];
|
||||
|
11
js/state.js
11
js/state.js
@ -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
105
js/ui.js
@ -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);
|
||||
|
@ -15,7 +15,8 @@
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://*.plex.tv/*",
|
||||
"*://*:*/*"
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "js/background.js",
|
||||
|
27
plex.html
27
plex.html
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user