diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 04ea2f3..f4618a0 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -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." } } \ No newline at end of file diff --git a/_locales/en/messages.json b/_locales/en/messages.json index faa49c6..555b084 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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." } } \ No newline at end of file diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 92dbc94..f208a0e 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -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." } } \ No newline at end of file diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 4575828..b4db336 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -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." } } \ No newline at end of file diff --git a/_locales/it/messages.json b/_locales/it/messages.json index bf13777..102444b 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -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." } } \ No newline at end of file diff --git a/_locales/pt/messages.json b/_locales/pt/messages.json index 937b5d1..35a6ef3 100644 --- a/_locales/pt/messages.json +++ b/_locales/pt/messages.json @@ -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." } } \ No newline at end of file diff --git a/css/navbar.css b/css/navbar.css index c8a01df..e87050a 100644 --- a/css/navbar.css +++ b/css/navbar.css @@ -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; } -} \ No newline at end of file +} + +body.sidebar-collapsed .sidebar-nav { + transform: translateX(-100%); +} + +body.sidebar-collapsed #main-container { + padding-left: 0; +} diff --git a/js/api.js b/js/api.js index c421770..480b93c 100644 --- a/js/api.js +++ b/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) }; + } } \ No newline at end of file diff --git a/js/config.js b/js/config.js index a13cb2d..030e502 100644 --- a/js/config.js +++ b/js/config.js @@ -1,5 +1,5 @@ export const config = { defaultApiKey: '4e44d9029b1270a757cddc766a1bcb63', dbName: 'PlexDB', - dbVersion: 6, + dbVersion: 7, }; \ No newline at end of file diff --git a/js/db.js b/js/db.js index ef61075..3055a17 100644 --- a/js/db.js +++ b/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)); diff --git a/js/eventListeners.js b/js/eventListeners.js index 33add26..8c005c5 100644 --- a/js/eventListeners.js +++ b/js/eventListeners.js @@ -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; diff --git a/js/jellyfin.js b/js/jellyfin.js new file mode 100644 index 0000000..f6ae85a --- /dev/null +++ b/js/jellyfin.js @@ -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 = `${_('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 = `
${_('jellyfinConnecting', url)}
`; + + const authResult = await authenticateJellyfin(url, username, password); + + if (!authResult.success) { + statusDiv.innerHTML = `
${_('jellyfinAuthFailed', authResult.message)}
`; + showNotification(_('jellyfinAuthFailed', authResult.message), 'error'); + state.isScanningJellyfin = false; + scanBtn.innerHTML = originalBtnText; + scanBtn.disabled = false; + return; + } + + statusDiv.innerHTML = `
${_('jellyfinAuthSuccess')}
${_('jellyfinFetchingLibraries')}
`; + + const viewsResult = await fetchLibraryViews(url, authResult.userId, authResult.token); + + if (!viewsResult.success) { + statusDiv.innerHTML += `
${_('jellyfinFetchFailed', viewsResult.message)}
`; + 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 += `
${_('jellyfinNoMediaLibraries')}
`; + state.isScanningJellyfin = false; + scanBtn.innerHTML = originalBtnText; + scanBtn.disabled = false; + return; + } + + statusDiv.innerHTML += `
${_('jellyfinLibrariesFound', String(mediaLibraries.length))}
`; + + 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 += `
${_('jellyfinLibraryScanSuccess', [library.Name, String(result.value.items.length)])}
`; + } + } else { + const libraryName = result.reason?.libraryName || result.value?.libraryName || 'Unknown'; + statusDiv.innerHTML += `
${_('jellyfinLibraryScanFailed', libraryName)}
`; + } + } + + 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 += `
${message}
`; + 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; +} \ No newline at end of file diff --git a/js/main.js b/js/main.js index 0ac021d..e5e6422 100644 --- a/js/main.js +++ b/js/main.js @@ -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]; diff --git a/js/state.js b/js/state.js index bf3611a..5ffb200 100644 --- a/js/state.js +++ b/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, diff --git a/js/ui.js b/js/ui.js index 54bdc47..a096121 100644 --- a/js/ui.js +++ b/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); diff --git a/manifest.json b/manifest.json index defbfe4..cdf50c6 100644 --- a/manifest.json +++ b/manifest.json @@ -15,7 +15,8 @@ ], "host_permissions": [ "https://*.plex.tv/*", - "*://*:*/*" + "http://*/*", + "https://*/*" ], "background": { "service_worker": "js/background.js", diff --git a/plex.html b/plex.html index ea3ba70..a21b47b 100644 --- a/plex.html +++ b/plex.html @@ -288,6 +288,11 @@ __MSG_settingsTabPlex__ +