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" },
 | 
					    "settingsTitleFull": { "message": "Einstellungen und Konfiguration" },
 | 
				
			||||||
    "settingsTabGeneral": { "message": "Allgemein" },
 | 
					    "settingsTabGeneral": { "message": "Allgemein" },
 | 
				
			||||||
    "settingsTabPlex": { "message": "Plex" },
 | 
					    "settingsTabPlex": { "message": "Plex" },
 | 
				
			||||||
 | 
					    "settingsTabJellyfin": { "message": "Jellyfin" },
 | 
				
			||||||
    "settingsTabPhpGen": { "message": "PHP-Generator" },
 | 
					    "settingsTabPhpGen": { "message": "PHP-Generator" },
 | 
				
			||||||
    "settingsTabData": { "message": "Daten" },
 | 
					    "settingsTabData": { "message": "Daten" },
 | 
				
			||||||
    "settingsApiServer": { "message": "API- und Serverkonfiguration" },
 | 
					    "settingsApiServer": { "message": "API- und Serverkonfiguration" },
 | 
				
			||||||
@ -80,6 +81,12 @@
 | 
				
			|||||||
    "settingsPlexTokens": { "message": "Plex-Tokens" },
 | 
					    "settingsPlexTokens": { "message": "Plex-Tokens" },
 | 
				
			||||||
    "settingsPlexTokensDesc": { "message": "Bearbeite die Liste der Plex-Tokens (JSON-Format)." },
 | 
					    "settingsPlexTokensDesc": { "message": "Bearbeite die Liste der Plex-Tokens (JSON-Format)." },
 | 
				
			||||||
    "settingsSaveTokens": { "message": "Tokens speichern" },
 | 
					    "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" },
 | 
					    "settingsPhpGenTitle": { "message": "PHP-Server-Skript-Generator" },
 | 
				
			||||||
    "settingsPhpFileOptions": { "message": "Dateioptionen" },
 | 
					    "settingsPhpFileOptions": { "message": "Dateioptionen" },
 | 
				
			||||||
    "settingsPhpSavePathLabel": { "message": "Speicherpfad auf dem Server" },
 | 
					    "settingsPhpSavePathLabel": { "message": "Speicherpfad auf dem Server" },
 | 
				
			||||||
@ -286,5 +293,26 @@
 | 
				
			|||||||
    "errorParsingPlexXml": { "message": "Fehler beim Parsen von Plex-XML." },
 | 
					    "errorParsingPlexXml": { "message": "Fehler beim Parsen von Plex-XML." },
 | 
				
			||||||
    "untitled": { "message": "Ohne Titel" },
 | 
					    "untitled": { "message": "Ohne Titel" },
 | 
				
			||||||
    "itemCount": { "message": "$count$ Elemente", "placeholders": { "count": { "content": "$1" } } },
 | 
					    "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" },
 | 
					    "settingsTitleFull": { "message": "Settings and Configuration" },
 | 
				
			||||||
    "settingsTabGeneral": { "message": "General" },
 | 
					    "settingsTabGeneral": { "message": "General" },
 | 
				
			||||||
    "settingsTabPlex": { "message": "Plex" },
 | 
					    "settingsTabPlex": { "message": "Plex" },
 | 
				
			||||||
 | 
					    "settingsTabJellyfin": { "message": "Jellyfin" },
 | 
				
			||||||
    "settingsTabPhpGen": { "message": "PHP Generator" },
 | 
					    "settingsTabPhpGen": { "message": "PHP Generator" },
 | 
				
			||||||
    "settingsTabData": { "message": "Data" },
 | 
					    "settingsTabData": { "message": "Data" },
 | 
				
			||||||
    "settingsApiServer": { "message": "API and Server Configuration" },
 | 
					    "settingsApiServer": { "message": "API and Server Configuration" },
 | 
				
			||||||
@ -80,6 +81,12 @@
 | 
				
			|||||||
    "settingsPlexTokens": { "message": "Plex Tokens" },
 | 
					    "settingsPlexTokens": { "message": "Plex Tokens" },
 | 
				
			||||||
    "settingsPlexTokensDesc": { "message": "Edit the list of Plex tokens (JSON format)." },
 | 
					    "settingsPlexTokensDesc": { "message": "Edit the list of Plex tokens (JSON format)." },
 | 
				
			||||||
    "settingsSaveTokens": { "message": "Save Tokens" },
 | 
					    "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" },
 | 
					    "settingsPhpGenTitle": { "message": "PHP Server Script Generator" },
 | 
				
			||||||
    "settingsPhpFileOptions": { "message": "File Options" },
 | 
					    "settingsPhpFileOptions": { "message": "File Options" },
 | 
				
			||||||
    "settingsPhpSavePathLabel": { "message": "Save Path on Server" },
 | 
					    "settingsPhpSavePathLabel": { "message": "Save Path on Server" },
 | 
				
			||||||
@ -286,5 +293,26 @@
 | 
				
			|||||||
    "errorParsingPlexXml": { "message": "Error parsing Plex XML." },
 | 
					    "errorParsingPlexXml": { "message": "Error parsing Plex XML." },
 | 
				
			||||||
    "untitled": { "message": "Untitled" },
 | 
					    "untitled": { "message": "Untitled" },
 | 
				
			||||||
    "itemCount": { "message": "$count$ items", "placeholders": { "count": { "content": "$1" } } },
 | 
					    "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" },
 | 
					    "settingsTitleFull": { "message": "Ajustes y Configuración" },
 | 
				
			||||||
    "settingsTabGeneral": { "message": "General" },
 | 
					    "settingsTabGeneral": { "message": "General" },
 | 
				
			||||||
    "settingsTabPlex": { "message": "Plex" },
 | 
					    "settingsTabPlex": { "message": "Plex" },
 | 
				
			||||||
 | 
					    "settingsTabJellyfin": { "message": "Jellyfin" },
 | 
				
			||||||
    "settingsTabPhpGen": { "message": "Generador PHP" },
 | 
					    "settingsTabPhpGen": { "message": "Generador PHP" },
 | 
				
			||||||
    "settingsTabData": { "message": "Datos" },
 | 
					    "settingsTabData": { "message": "Datos" },
 | 
				
			||||||
    "settingsApiServer": { "message": "Configuración de API y Servidor" },
 | 
					    "settingsApiServer": { "message": "Configuración de API y Servidor" },
 | 
				
			||||||
@ -80,6 +81,12 @@
 | 
				
			|||||||
    "settingsPlexTokens": { "message": "Tokens de Plex" },
 | 
					    "settingsPlexTokens": { "message": "Tokens de Plex" },
 | 
				
			||||||
    "settingsPlexTokensDesc": { "message": "Edita la lista de tokens de Plex (formato JSON)." },
 | 
					    "settingsPlexTokensDesc": { "message": "Edita la lista de tokens de Plex (formato JSON)." },
 | 
				
			||||||
    "settingsSaveTokens": { "message": "Guardar Tokens" },
 | 
					    "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" },
 | 
					    "settingsPhpGenTitle": { "message": "Generador de Script PHP para el Servidor" },
 | 
				
			||||||
    "settingsPhpFileOptions": { "message": "Opciones del Archivo" },
 | 
					    "settingsPhpFileOptions": { "message": "Opciones del Archivo" },
 | 
				
			||||||
    "settingsPhpSavePathLabel": { "message": "Ruta de Guardado en el Servidor" },
 | 
					    "settingsPhpSavePathLabel": { "message": "Ruta de Guardado en el Servidor" },
 | 
				
			||||||
@ -273,7 +280,7 @@
 | 
				
			|||||||
    "invalidStreamInfo": {"message": "Información inválida."},
 | 
					    "invalidStreamInfo": {"message": "Información inválida."},
 | 
				
			||||||
    "dbUnavailableForStreams": {"message": "Base de datos local no disponible."},
 | 
					    "dbUnavailableForStreams": {"message": "Base de datos local no disponible."},
 | 
				
			||||||
    "noPlexServersForStreams": {"message": "No hay servidores Plex."},
 | 
					    "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_justNow": { "message": "Ahora mismo" },
 | 
				
			||||||
    "relativeTime_minutesAgo": { "message": "Hace $count$ minutos", "placeholders": { "count": { "content": "$1" } } },
 | 
					    "relativeTime_minutesAgo": { "message": "Hace $count$ minutos", "placeholders": { "count": { "content": "$1" } } },
 | 
				
			||||||
    "relativeTime_hoursAgo": { "message": "Hace $count$ horas", "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." },
 | 
					    "errorParsingPlexXml": { "message": "Error al analizar el XML de Plex." },
 | 
				
			||||||
    "untitled": { "message": "Sin título" },
 | 
					    "untitled": { "message": "Sin título" },
 | 
				
			||||||
    "itemCount": { "message": "$count$ elementos", "placeholders": { "count": { "content": "$1" } } },
 | 
					    "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" },
 | 
					    "settingsTitleFull": { "message": "Paramètres et Configuration" },
 | 
				
			||||||
    "settingsTabGeneral": { "message": "Général" },
 | 
					    "settingsTabGeneral": { "message": "Général" },
 | 
				
			||||||
    "settingsTabPlex": { "message": "Plex" },
 | 
					    "settingsTabPlex": { "message": "Plex" },
 | 
				
			||||||
 | 
					    "settingsTabJellyfin": { "message": "Jellyfin" },
 | 
				
			||||||
    "settingsTabPhpGen": { "message": "Générateur PHP" },
 | 
					    "settingsTabPhpGen": { "message": "Générateur PHP" },
 | 
				
			||||||
    "settingsTabData": { "message": "Données" },
 | 
					    "settingsTabData": { "message": "Données" },
 | 
				
			||||||
    "settingsApiServer": { "message": "Configuration API et Serveur" },
 | 
					    "settingsApiServer": { "message": "Configuration API et Serveur" },
 | 
				
			||||||
@ -80,6 +81,12 @@
 | 
				
			|||||||
    "settingsPlexTokens": { "message": "Tokens Plex" },
 | 
					    "settingsPlexTokens": { "message": "Tokens Plex" },
 | 
				
			||||||
    "settingsPlexTokensDesc": { "message": "Modifiez la liste des tokens Plex (format JSON)." },
 | 
					    "settingsPlexTokensDesc": { "message": "Modifiez la liste des tokens Plex (format JSON)." },
 | 
				
			||||||
    "settingsSaveTokens": { "message": "Sauvegarder les Tokens" },
 | 
					    "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" },
 | 
					    "settingsPhpGenTitle": { "message": "Générateur de Script PHP pour Serveur" },
 | 
				
			||||||
    "settingsPhpFileOptions": { "message": "Options du Fichier" },
 | 
					    "settingsPhpFileOptions": { "message": "Options du Fichier" },
 | 
				
			||||||
    "settingsPhpSavePathLabel": { "message": "Chemin de Sauvegarde sur le Serveur" },
 | 
					    "settingsPhpSavePathLabel": { "message": "Chemin de Sauvegarde sur le Serveur" },
 | 
				
			||||||
@ -286,5 +293,26 @@
 | 
				
			|||||||
    "errorParsingPlexXml": { "message": "Erreur d'analyse du XML de Plex." },
 | 
					    "errorParsingPlexXml": { "message": "Erreur d'analyse du XML de Plex." },
 | 
				
			||||||
    "untitled": { "message": "Sans titre" },
 | 
					    "untitled": { "message": "Sans titre" },
 | 
				
			||||||
    "itemCount": { "message": "$count$ éléments", "placeholders": { "count": { "content": "$1" } } },
 | 
					    "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" },
 | 
					    "settingsTitleFull": { "message": "Impostazioni e Configurazione" },
 | 
				
			||||||
    "settingsTabGeneral": { "message": "Generale" },
 | 
					    "settingsTabGeneral": { "message": "Generale" },
 | 
				
			||||||
    "settingsTabPlex": { "message": "Plex" },
 | 
					    "settingsTabPlex": { "message": "Plex" },
 | 
				
			||||||
 | 
					    "settingsTabJellyfin": { "message": "Jellyfin" },
 | 
				
			||||||
    "settingsTabPhpGen": { "message": "Generatore PHP" },
 | 
					    "settingsTabPhpGen": { "message": "Generatore PHP" },
 | 
				
			||||||
    "settingsTabData": { "message": "Dati" },
 | 
					    "settingsTabData": { "message": "Dati" },
 | 
				
			||||||
    "settingsApiServer": { "message": "Configurazione API e Server" },
 | 
					    "settingsApiServer": { "message": "Configurazione API e Server" },
 | 
				
			||||||
@ -80,6 +81,12 @@
 | 
				
			|||||||
    "settingsPlexTokens": { "message": "Token Plex" },
 | 
					    "settingsPlexTokens": { "message": "Token Plex" },
 | 
				
			||||||
    "settingsPlexTokensDesc": { "message": "Modifica la lista dei token Plex (formato JSON)." },
 | 
					    "settingsPlexTokensDesc": { "message": "Modifica la lista dei token Plex (formato JSON)." },
 | 
				
			||||||
    "settingsSaveTokens": { "message": "Salva Token" },
 | 
					    "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" },
 | 
					    "settingsPhpGenTitle": { "message": "Generatore di Script PHP per Server" },
 | 
				
			||||||
    "settingsPhpFileOptions": { "message": "Opzioni File" },
 | 
					    "settingsPhpFileOptions": { "message": "Opzioni File" },
 | 
				
			||||||
    "settingsPhpSavePathLabel": { "message": "Percorso di salvataggio sul server" },
 | 
					    "settingsPhpSavePathLabel": { "message": "Percorso di salvataggio sul server" },
 | 
				
			||||||
@ -286,5 +293,26 @@
 | 
				
			|||||||
    "errorParsingPlexXml": { "message": "Errore nell'analisi dell'XML di Plex." },
 | 
					    "errorParsingPlexXml": { "message": "Errore nell'analisi dell'XML di Plex." },
 | 
				
			||||||
    "untitled": { "message": "Senza titolo" },
 | 
					    "untitled": { "message": "Senza titolo" },
 | 
				
			||||||
    "itemCount": { "message": "$count$ elementi", "placeholders": { "count": { "content": "$1" } } },
 | 
					    "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" },
 | 
					    "settingsTitleFull": { "message": "Configurações e Ajustes" },
 | 
				
			||||||
    "settingsTabGeneral": { "message": "Geral" },
 | 
					    "settingsTabGeneral": { "message": "Geral" },
 | 
				
			||||||
    "settingsTabPlex": { "message": "Plex" },
 | 
					    "settingsTabPlex": { "message": "Plex" },
 | 
				
			||||||
 | 
					    "settingsTabJellyfin": { "message": "Jellyfin" },
 | 
				
			||||||
    "settingsTabPhpGen": { "message": "Gerador de PHP" },
 | 
					    "settingsTabPhpGen": { "message": "Gerador de PHP" },
 | 
				
			||||||
    "settingsTabData": { "message": "Dados" },
 | 
					    "settingsTabData": { "message": "Dados" },
 | 
				
			||||||
    "settingsApiServer": { "message": "Configuração de API e Servidor" },
 | 
					    "settingsApiServer": { "message": "Configuração de API e Servidor" },
 | 
				
			||||||
@ -80,6 +81,12 @@
 | 
				
			|||||||
    "settingsPlexTokens": { "message": "Tokens do Plex" },
 | 
					    "settingsPlexTokens": { "message": "Tokens do Plex" },
 | 
				
			||||||
    "settingsPlexTokensDesc": { "message": "Edite a lista de tokens do Plex (formato JSON)." },
 | 
					    "settingsPlexTokensDesc": { "message": "Edite a lista de tokens do Plex (formato JSON)." },
 | 
				
			||||||
    "settingsSaveTokens": { "message": "Salvar Tokens" },
 | 
					    "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" },
 | 
					    "settingsPhpGenTitle": { "message": "Gerador de Script PHP para Servidor" },
 | 
				
			||||||
    "settingsPhpFileOptions": { "message": "Opções de Arquivo" },
 | 
					    "settingsPhpFileOptions": { "message": "Opções de Arquivo" },
 | 
				
			||||||
    "settingsPhpSavePathLabel": { "message": "Caminho para Salvar no Servidor" },
 | 
					    "settingsPhpSavePathLabel": { "message": "Caminho para Salvar no Servidor" },
 | 
				
			||||||
@ -286,5 +293,26 @@
 | 
				
			|||||||
    "errorParsingPlexXml": { "message": "Erro ao analisar o XML do Plex." },
 | 
					    "errorParsingPlexXml": { "message": "Erro ao analisar o XML do Plex." },
 | 
				
			||||||
    "untitled": { "message": "Sem título" },
 | 
					    "untitled": { "message": "Sem título" },
 | 
				
			||||||
    "itemCount": { "message": "$count$ itens", "placeholders": { "count": { "content": "$1" } } },
 | 
					    "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) {
 | 
					@media (min-width: 992px) {
 | 
				
			||||||
    #sidebar-toggle {
 | 
					    /* #sidebar-toggle {
 | 
				
			||||||
        display: none;
 | 
					        display: none;
 | 
				
			||||||
    }
 | 
					    } */
 | 
				
			||||||
    .sidebar-nav {
 | 
					    .sidebar-nav {
 | 
				
			||||||
        transform: translateX(0);
 | 
					        transform: translateX(0);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -216,3 +216,11 @@ body.light-theme .sidebar-nav {
 | 
				
			|||||||
        font-size: 1.1rem;
 | 
					        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
									
									
									
									
									
								
							@ -221,3 +221,107 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
 | 
				
			|||||||
        return { success: false, streams: [], message: _('notFoundOnServers', busqueda) };
 | 
					        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 = {
 | 
					export const config = {
 | 
				
			||||||
    defaultApiKey: '4e44d9029b1270a757cddc766a1bcb63',
 | 
					    defaultApiKey: '4e44d9029b1270a757cddc766a1bcb63',
 | 
				
			||||||
    dbName: 'PlexDB',
 | 
					    dbName: 'PlexDB',
 | 
				
			||||||
    dbVersion: 6,
 | 
					    dbVersion: 7,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
							
								
								
									
										11
									
								
								js/db.js
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								js/db.js
									
									
									
									
									
								
							@ -13,14 +13,17 @@ export function initDB() {
 | 
				
			|||||||
        request.onupgradeneeded = e => {
 | 
					        request.onupgradeneeded = e => {
 | 
				
			||||||
            state.db = e.target.result;
 | 
					            state.db = e.target.result;
 | 
				
			||||||
            const transaction = e.target.transaction;
 | 
					            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 => {
 | 
					            storesToCreate.forEach(storeName => {
 | 
				
			||||||
                if (!state.db.objectStoreNames.contains(storeName)) {
 | 
					                if (!state.db.objectStoreNames.contains(storeName)) {
 | 
				
			||||||
                    let storeOptions;
 | 
					                    let storeOptions;
 | 
				
			||||||
                    if (storeName === 'settings') {
 | 
					                    if (['settings', 'jellyfin_settings'].includes(storeName)) {
 | 
				
			||||||
                        storeOptions = { keyPath: 'id' };
 | 
					                        storeOptions = { keyPath: 'id' };
 | 
				
			||||||
                    } else {
 | 
					                    } else if (['jellyfin_movies', 'jellyfin_series'].includes(storeName)) {
 | 
				
			||||||
 | 
					                        storeOptions = { keyPath: 'libraryId' };
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    else {
 | 
				
			||||||
                        storeOptions = { keyPath: 'id', autoIncrement: true };
 | 
					                        storeOptions = { keyPath: 'id', autoIncrement: true };
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    const store = state.db.createObjectStore(storeName, storeOptions);
 | 
					                    const store = state.db.createObjectStore(storeName, storeOptions);
 | 
				
			||||||
@ -126,7 +129,7 @@ export function addItemsToStore(storeName, items) {
 | 
				
			|||||||
export async function clearContentData() {
 | 
					export async function clearContentData() {
 | 
				
			||||||
    showNotification(_("deletingContentData"), "info");
 | 
					    showNotification(_("deletingContentData"), "info");
 | 
				
			||||||
    mostrarSpinner();
 | 
					    mostrarSpinner();
 | 
				
			||||||
    const storesToDelete = ['movies', 'series', 'artists', 'photos', 'conexiones_locales'];
 | 
					    const storesToDelete = ['movies', 'series', 'artists', 'photos', 'conexiones_locales', 'jellyfin_movies', 'jellyfin_series'];
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        if (!state.db) throw new Error(_("dbNotAvailable"));
 | 
					        if (!state.db) throw new Error(_("dbNotAvailable"));
 | 
				
			||||||
        const storesPresent = storesToDelete.filter(name => state.db.objectStoreNames.contains(name));
 | 
					        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 { debounce, showNotification, _ } from './utils.js';
 | 
				
			||||||
import { clearContentData, loadTokensToEditor, saveTokensFromEditor, exportDatabase, importDatabase } from './db.js';
 | 
					import { clearContentData, loadTokensToEditor, saveTokensFromEditor, exportDatabase, importDatabase } from './db.js';
 | 
				
			||||||
import { startPlexScan } from './plex.js';
 | 
					import { startPlexScan } from './plex.js';
 | 
				
			||||||
 | 
					import { startJellyfinScan } from './jellyfin.js';
 | 
				
			||||||
import { Equalizer } from './equalizer.js';
 | 
					import { Equalizer } from './equalizer.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function handleDatabaseUpdate() {
 | 
					async function handleDatabaseUpdate() {
 | 
				
			||||||
@ -28,9 +29,16 @@ async function handleDatabaseUpdate() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function setupEventListeners() {
 | 
					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-toggle').addEventListener('click', () => {
 | 
				
			||||||
        document.getElementById('sidebar-nav').classList.toggle('open');
 | 
					        document.body.classList.toggle('sidebar-collapsed');
 | 
				
			||||||
        document.getElementById('main-container').classList.toggle('sidebar-open');
 | 
					        const isCollapsed = document.body.classList.contains('sidebar-collapsed');
 | 
				
			||||||
 | 
					        localStorage.setItem('sidebarCollapsed', isCollapsed);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    document.getElementById('nav-movies').addEventListener('click', (e) => { e.preventDefault(); switchView('movies'); });
 | 
					    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', () => {
 | 
					    document.getElementById('clearDataBtn').addEventListener('click', () => {
 | 
				
			||||||
        if (confirm(_('confirmClearContent'))) {
 | 
					        if (confirm(_('confirmClearContent'))) {
 | 
				
			||||||
            clearContentData();
 | 
					            clearContentData();
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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) {
 | 
					        if (!state.settings.apiKey) {
 | 
				
			||||||
            state.settings.apiKey = config.defaultApiKey;
 | 
					            state.settings.apiKey = config.defaultApiKey;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const jellyfinSettingsData = await getFromDB('jellyfin_settings');
 | 
				
			||||||
 | 
					        if (jellyfinSettingsData && jellyfinSettingsData.length > 0) {
 | 
				
			||||||
 | 
					            state.jellyfinSettings = { ...state.jellyfinSettings, ...jellyfinSettingsData[0] };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
        console.error("Could not load settings from DB, using defaults.", error);
 | 
					        console.error("Could not load settings from DB, using defaults.", error);
 | 
				
			||||||
        state.settings.language = chrome.i18n.getUILanguage().split('-')[0];
 | 
					        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',
 | 
					        phpFilename: 'CinePlex_Playlist.m3u',
 | 
				
			||||||
        phpFileAction: 'append',
 | 
					        phpFileAction: 'append',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    jellyfinSettings: {
 | 
				
			||||||
 | 
					        id: 'jellyfin_credentials',
 | 
				
			||||||
 | 
					        url: '',
 | 
				
			||||||
 | 
					        username: '',
 | 
				
			||||||
 | 
					        password: '',
 | 
				
			||||||
 | 
					        apiKey: '',
 | 
				
			||||||
 | 
					        userId: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    jellyfinMovies: [],
 | 
				
			||||||
 | 
					    jellyfinSeries: [],
 | 
				
			||||||
    localMovies: [],
 | 
					    localMovies: [],
 | 
				
			||||||
    localSeries: [],
 | 
					    localSeries: [],
 | 
				
			||||||
    localArtists: [],
 | 
					    localArtists: [],
 | 
				
			||||||
@ -32,6 +42,7 @@ export const state = {
 | 
				
			|||||||
    isAddingStream: false,
 | 
					    isAddingStream: false,
 | 
				
			||||||
    isDownloadingM3U: false,
 | 
					    isDownloadingM3U: false,
 | 
				
			||||||
    isScanningPlex: false,
 | 
					    isScanningPlex: false,
 | 
				
			||||||
 | 
					    isScanningJellyfin: false,
 | 
				
			||||||
    musicPlayer: null,
 | 
					    musicPlayer: null,
 | 
				
			||||||
    currentContentFetchController: null,
 | 
					    currentContentFetchController: null,
 | 
				
			||||||
    plexScanAbortController: null,
 | 
					    plexScanAbortController: null,
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										97
									
								
								js/ui.js
									
									
									
									
									
								
							
							
						
						
									
										97
									
								
								js/ui.js
									
									
									
									
									
								
							@ -1,5 +1,5 @@
 | 
				
			|||||||
import { state } from './state.js';
 | 
					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 { showNotification, getRelativeTime, fetchWithTimeout, _ } from './utils.js';
 | 
				
			||||||
import { getFromDB, addItemsToStore } from './db.js';
 | 
					import { getFromDB, addItemsToStore } from './db.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -7,7 +7,7 @@ let charts = {};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export async function loadInitialContent() {
 | 
					export async function loadInitialContent() {
 | 
				
			||||||
    await Promise.all([loadGenres(), loadYears()]);
 | 
					    await Promise.all([loadGenres(), loadYears()]);
 | 
				
			||||||
    resetView(); // Show hero-only view first
 | 
					    resetView(); 
 | 
				
			||||||
    setupScrollEffects();
 | 
					    setupScrollEffects();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -30,11 +30,21 @@ export function initializeUserData() {
 | 
				
			|||||||
export async function loadLocalContent() {
 | 
					export async function loadLocalContent() {
 | 
				
			||||||
    if (!state.db) return;
 | 
					    if (!state.db) return;
 | 
				
			||||||
    try {
 | 
					    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.localMovies = movies;
 | 
				
			||||||
        state.localSeries = series;
 | 
					        state.localSeries = series;
 | 
				
			||||||
        state.localArtists = artists;
 | 
					        state.localArtists = artists;
 | 
				
			||||||
        state.localPhotos = photos;
 | 
					        state.localPhotos = photos;
 | 
				
			||||||
 | 
					        state.jellyfinMovies = jfMovies;
 | 
				
			||||||
 | 
					        state.jellyfinSeries = jfSeries;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
        showNotification(_("errorLoadingLocalContent"), "error");
 | 
					        showNotification(_("errorLoadingLocalContent"), "error");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -331,16 +341,30 @@ export async function loadContent(append = false) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function buscarContenidoLocal(title, type) {
 | 
					function isContentAvailableLocally(title, type) {
 | 
				
			||||||
    if (!title || !type) return null;
 | 
					    if (!title || !type) return false;
 | 
				
			||||||
    const normalizedTitle = title.toLowerCase().trim();
 | 
					    const normalizedTitle = title.toLowerCase().trim();
 | 
				
			||||||
    const source = type === 'movie' ? state.localMovies : state.localSeries;
 | 
					 | 
				
			||||||
    if (!Array.isArray(source)) return null;
 | 
					 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    return source.find(server => 
 | 
					    const plexSource = type === 'movie' ? state.localMovies : state.localSeries;
 | 
				
			||||||
 | 
					    if (Array.isArray(plexSource)) {
 | 
				
			||||||
 | 
					        const foundInPlex = plexSource.some(server => 
 | 
				
			||||||
            server && Array.isArray(server.titulos) && 
 | 
					            server && Array.isArray(server.titulos) && 
 | 
				
			||||||
            server.titulos.some(t => t && typeof t.title === 'string' && t.title.toLowerCase().trim() === normalizedTitle)
 | 
					            server.titulos.some(t => t && typeof t.title === 'string' && t.title.toLowerCase().trim() === normalizedTitle)
 | 
				
			||||||
    ) || null;
 | 
					        );
 | 
				
			||||||
 | 
					        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 releaseDate = isMovie ? item.release_date : item.first_air_date;
 | 
				
			||||||
        const year = releaseDate ? releaseDate.slice(0, 4) : 'N/A';
 | 
					        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 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 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 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');
 | 
					        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 voteAverage = item.vote_average ? item.vote_average.toFixed(1) : 'N/A';
 | 
				
			||||||
    const genres = item.genres || [];
 | 
					    const genres = item.genres || [];
 | 
				
			||||||
    const trailer = item.videos?.results?.find(v => v.site === 'YouTube' && v.type === 'Trailer');
 | 
					    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 isFavorite = state.favorites.some(fav => fav.id === item.id && fav.type === state.currentItemType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const imdbId = item.external_ids?.imdb_id;
 | 
					    const imdbId = item.external_ids?.imdb_id;
 | 
				
			||||||
@ -765,7 +789,7 @@ export function displayHistory() {
 | 
				
			|||||||
    const fragment = document.createDocumentFragment();
 | 
					    const fragment = document.createDocumentFragment();
 | 
				
			||||||
    [...state.userHistory].sort((a,b) => b.timestamp - a.timestamp).forEach(item => {
 | 
					    [...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 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');
 | 
					        const historyItem = document.createElement('div');
 | 
				
			||||||
        historyItem.className = 'history-item';
 | 
					        historyItem.className = 'history-item';
 | 
				
			||||||
        historyItem.dataset.id = item.id;
 | 
					        historyItem.dataset.id = item.id;
 | 
				
			||||||
@ -921,22 +945,24 @@ export async function generateStatistics() {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
        const selectedToken = document.getElementById('stats-token-filter').value;
 | 
					        const selectedToken = document.getElementById('stats-token-filter').value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const filterByToken = (data) => {
 | 
					        const filteredPlexMovies = selectedToken === 'all' ? state.localMovies : state.localMovies.filter(server => server.tokenPrincipal === selectedToken);
 | 
				
			||||||
            if (selectedToken === 'all') return data;
 | 
					        const filteredPlexSeries = selectedToken === 'all' ? state.localSeries : state.localSeries.filter(server => server.tokenPrincipal === selectedToken);
 | 
				
			||||||
            return data.filter(server => server.tokenPrincipal === selectedToken);
 | 
					        const filteredPlexArtists = selectedToken === 'all' ? state.localArtists : state.localArtists.filter(server => server.tokenPrincipal === selectedToken);
 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const filteredMovies = filterByToken(state.localMovies);
 | 
					        const plexMovieItems = filteredPlexMovies.flatMap(s => s.titulos);
 | 
				
			||||||
        const filteredSeries = filterByToken(state.localSeries);
 | 
					        const plexSeriesItems = filteredPlexSeries.flatMap(s => s.titulos);
 | 
				
			||||||
        const filteredArtists = filterByToken(state.localArtists);
 | 
					        const plexArtistItems = filteredPlexArtists.flatMap(s => s.titulos);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const allMovieItems = filteredMovies.flatMap(s => s.titulos);
 | 
					        const jellyfinMovieItems = state.jellyfinMovies.flatMap(lib => lib.titulos);
 | 
				
			||||||
        const allSeriesItems = filteredSeries.flatMap(s => s.titulos);
 | 
					        const jellyfinSeriesItems = state.jellyfinSeries.flatMap(lib => lib.titulos);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const allMovieItems = [...plexMovieItems, ...jellyfinMovieItems];
 | 
				
			||||||
 | 
					        const allSeriesItems = [...plexSeriesItems, ...jellyfinSeriesItems];
 | 
				
			||||||
 | 
					        const allArtistItems = [...plexArtistItems];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const uniqueMovieTitles = new Set(allMovieItems.map(item => item.title));
 | 
					        const uniqueMovieTitles = new Set(allMovieItems.map(item => item.title));
 | 
				
			||||||
        const uniqueSeriesTitles = new Set(allSeriesItems.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-movies', 0, uniqueMovieTitles.size, 1000);
 | 
				
			||||||
        animateValue('total-series', 0, uniqueSeriesTitles.size, 1000);
 | 
					        animateValue('total-series', 0, uniqueSeriesTitles.size, 1000);
 | 
				
			||||||
        animateValue('total-artists', 0, uniqueArtists.size, 1000);
 | 
					        animateValue('total-artists', 0, uniqueArtists.size, 1000);
 | 
				
			||||||
@ -1233,7 +1259,7 @@ function updateHeroContent(item) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const type = item.title ? 'movie' : 'tv';
 | 
					    const type = item.title ? 'movie' : 'tv';
 | 
				
			||||||
    const title = item.title || item.name;
 | 
					    const title = item.title || item.name;
 | 
				
			||||||
    const isAvailable = !!buscarContenidoLocal(title, type);
 | 
					    const isAvailable = isContentAvailableLocally(title, type);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if (heroTitle) heroTitle.textContent = title;
 | 
					    if (heroTitle) heroTitle.textContent = title;
 | 
				
			||||||
    if (heroSubtitle) heroSubtitle.textContent = item.overview.substring(0, 200) + (item.overview.length > 200 ? '...' : '');
 | 
					    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');
 | 
					    showNotification(_('searchingStreams', title), 'info');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    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);
 | 
					        if (!streamData.success || streamData.streams.length === 0) throw new Error(streamData.message);
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        showNotification(_('sendingStreams', String(streamData.streams.length)), 'info');
 | 
					        showNotification(_('sendingStreams', String(streamData.streams.length)), 'info');
 | 
				
			||||||
@ -1316,7 +1342,8 @@ export async function downloadM3U(title, type, buttonElement = null) {
 | 
				
			|||||||
    showNotification(_('generatingM3U', title), "info");
 | 
					    showNotification(_('generatingM3U', title), "info");
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    try {
 | 
					    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);
 | 
					        if (!streamData.success || streamData.streams.length === 0) throw new Error(streamData.message);
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        let m3uContent = "#EXTM3U\n";
 | 
					        let m3uContent = "#EXTM3U\n";
 | 
				
			||||||
@ -1408,6 +1435,10 @@ export function openSettingsModal() {
 | 
				
			|||||||
    document.getElementById('lightModeToggle').checked = state.settings.theme === 'light';
 | 
					    document.getElementById('lightModeToggle').checked = state.settings.theme === 'light';
 | 
				
			||||||
    document.getElementById('showHeroToggle').checked = state.settings.showHero;
 | 
					    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('phpSecretKeyCheck').checked = state.settings.phpUseSecretKey;
 | 
				
			||||||
    document.getElementById('phpSecretKey').value = state.settings.phpSecretKey || '';
 | 
					    document.getElementById('phpSecretKey').value = state.settings.phpSecretKey || '';
 | 
				
			||||||
    document.getElementById('phpSavePath').value = state.settings.phpSavePath || '';
 | 
					    document.getElementById('phpSavePath').value = state.settings.phpSavePath || '';
 | 
				
			||||||
@ -1439,8 +1470,20 @@ export async function saveSettings() {
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    state.settings = { ...state.settings, ...newSettings };
 | 
					    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 {
 | 
					    try {
 | 
				
			||||||
        await addItemsToStore('settings', [state.settings]);
 | 
					        await Promise.all([
 | 
				
			||||||
 | 
					            addItemsToStore('settings', [state.settings]),
 | 
				
			||||||
 | 
					            addItemsToStore('jellyfin_settings', [state.jellyfinSettings])
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        showNotification(_('settingsSavedSuccess'), 'success');
 | 
					        showNotification(_('settingsSavedSuccess'), 'success');
 | 
				
			||||||
        applyTheme(state.settings.theme);
 | 
					        applyTheme(state.settings.theme);
 | 
				
			||||||
        applyHeroVisibility(state.settings.showHero);
 | 
					        applyHeroVisibility(state.settings.showHero);
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,8 @@
 | 
				
			|||||||
    ],
 | 
					    ],
 | 
				
			||||||
    "host_permissions": [
 | 
					    "host_permissions": [
 | 
				
			||||||
        "https://*.plex.tv/*",
 | 
					        "https://*.plex.tv/*",
 | 
				
			||||||
        "*://*:*/*"
 | 
					        "http://*/*",
 | 
				
			||||||
 | 
					        "https://*/*"
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    "background": {
 | 
					    "background": {
 | 
				
			||||||
        "service_worker": "js/background.js",
 | 
					        "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__
 | 
					                                <i class="fas fa-server me-2"></i>__MSG_settingsTabPlex__
 | 
				
			||||||
                            </button>
 | 
					                            </button>
 | 
				
			||||||
                        </li>
 | 
					                        </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">
 | 
					                        <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">
 | 
					                             <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__
 | 
					                                <i class="fab fa-php me-2"></i>__MSG_settingsTabPhpGen__
 | 
				
			||||||
@ -359,6 +364,28 @@
 | 
				
			|||||||
                                </div>
 | 
					                                </div>
 | 
				
			||||||
                            </div>
 | 
					                            </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">
 | 
					                        <div class="tab-pane fade" id="php-gen" role="tabpanel" aria-labelledby="php-gen-tab">
 | 
				
			||||||
                             <h5 class="mb-3">__MSG_settingsPhpGenTitle__</h5>
 | 
					                             <h5 class="mb-3">__MSG_settingsPhpGenTitle__</h5>
 | 
				
			||||||
                             <div class="row">
 | 
					                             <div class="row">
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user