feat(chat, i18n, m3u): Mejoras en la experiencia de usuario del chat y M3U
Este commit introduce varias mejoras en la experiencia del usuario, incluyendo un chatbot más intuitivo y con mejor feedback visual, notificaciones para la generación de M3U y un soporte de internacionalización ampliado. Cambios principales: - Indicador de "trabajando" en el chat: Se ha añadido un indicador de "escribiendo..." que se muestra mientras se ejecutan las herramientas del chat, mejorando la percepción de que el asistente está procesando la petición. - Traducciones: Se han añadido nuevas claves de i18n para el asistente de IA y las herramientas M3U en español, inglés, francés, italiano, portugués y alemán. - Cierre del chat: Se ha corregido un error que provocaba que el chatbot se cerrara automáticamente después de ejecutar un comando. - Mensajes del chat: Se han eliminado los mensajes de "ejecutando herramienta" y "resultado de la herramienta" para una experiencia de chat más limpia. - Notificaciones M3U: Se han añadido notificaciones visuales para informar al usuario cuando se está generando un archivo M3U.
This commit is contained in:
parent
c5af5483ba
commit
5e7664677e
@ -205,7 +205,7 @@
|
||||
"sendingStreams": { "message": "Sende $count$ Stream(s) an den Server...", "placeholders": { "count": { "content": "$1" } } },
|
||||
"streamAddedSuccess": { "message": "Stream(s) erfolgreich hinzugefügt." },
|
||||
"generatingM3U": { "message": "Generiere M3U für \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"m3uDownloaded": { "message": "M3U für \"$title$\" heruntergeladen.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"m3uDownloaded": { "message": "\"\"$title$\" heruntergeladen.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"errorGeneratingM3U": { "message": "Fehler beim Generieren der M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"settingsSavedSuccess": { "message": "Einstellungen erfolgreich gespeichert." },
|
||||
"errorSavingSettings": { "message": "Fehler beim Speichern der Einstellungen in der Datenbank." },
|
||||
@ -361,5 +361,59 @@
|
||||
"chatApiInvalidResponse": { "message": "Die API hat eine ungültige Antwort zurückgegeben. Bitte versuchen Sie es erneut." },
|
||||
"chatApiError": { "message": "Fehler bei der Kommunikation mit dem KI-Assistenten" },
|
||||
"downloadAll": { "message": "Alles herunterladen" },
|
||||
"download": { "message": "Herunterladen" }
|
||||
"download": { "message": "Herunterladen" },
|
||||
"aiToolSearchLibraryDesc": { "message": "Durchsucht die Plex-Bibliothek nach Filmen oder Serien." },
|
||||
"aiToolSearchLibraryQueryDesc": { "message": "Der zu suchende Titel." },
|
||||
"aiToolSearchLibraryTypeDesc": { "message": "Die Art des zu suchenden Inhalts (Film oder Serie)." },
|
||||
"aiToolNavigateToPageDesc": { "message": "Navigiert zu einer bestimmten Seite in der Anwendung." },
|
||||
"aiToolNavigateToPagePageDesc": { "message": "Die Seite, zu der navigiert werden soll." },
|
||||
"aiToolGetUserStatsDesc": { "message": "Ruft Benutzerstatistiken ab." },
|
||||
"aiToolShowItemDetailsDesc": { "message": "Zeigt die Details eines Films oder einer Serie an." },
|
||||
"aiToolShowItemDetailsTitleDesc": { "message": "Der Titel des Films oder der Serie." },
|
||||
"aiToolShowItemDetailsTypeDesc": { "message": "Die Art des Inhalts (Film oder Serie)." },
|
||||
"aiToolAddToPlaylistDesc": { "message": "Fügt einen Film oder eine Serie zur Wiedergabeliste hinzu." },
|
||||
"aiToolAddToPlaylistTitleDesc": { "message": "Der Titel des Films oder der Serie." },
|
||||
"aiToolAddToPlaylistTypeDesc": { "message": "Die Art des Inhalts (Film oder Serie)." },
|
||||
"aiToolDownloadPlaylistDesc": { "message": "Lädt die Wiedergabeliste für einen Film oder eine Serie herunter." },
|
||||
"aiToolDownloadPlaylistTitleDesc": { "message": "Der Titel des Films oder der Serie." },
|
||||
"aiToolDownloadPlaylistTypeDesc": { "message": "Die Art des Inhalts (Film oder Serie)." },
|
||||
"aiToolToggleFavoriteDesc": { "message": "Fügt einen Film oder eine Serie zu den Favoriten hinzu oder entfernt sie daraus." },
|
||||
"aiToolToggleFavoriteTitleDesc": { "message": "Der Titel des Films oder der Serie." },
|
||||
"aiToolToggleFavoriteTypeDesc": { "message": "Die Art des Inhalts (Film oder Serie)." },
|
||||
"aiToolGetRecommendationsDesc": { "message": "Ruft Empfehlungen für den Benutzer ab." },
|
||||
"aiToolApplyFiltersDesc": { "message": "Wendet Filter auf die aktuelle Ansicht an." },
|
||||
"aiToolApplyFiltersTypeDesc": { "message": "Die Art des zu filternden Inhalts (Film oder Serie)." },
|
||||
"aiToolApplyFiltersGenreDesc": { "message": "Das zu filternde Genre." },
|
||||
"aiToolApplyFiltersYearDesc": { "message": "Das zu filternde Jahr." },
|
||||
"aiToolApplyFiltersSortDesc": { "message": "Die Reihenfolge, in der die Ergebnisse sortiert werden sollen." },
|
||||
"aiToolPlayMusicByArtistDesc": { "message": "Spielt Musik von einem bestimmten Künstler ab." },
|
||||
"aiToolPlayMusicByArtistNameDesc": { "message": "Der Name des Künstlers." },
|
||||
"aiToolClearChatHistoryDesc": { "message": "Löscht den Chat-Verlauf." },
|
||||
"aiToolDeleteDatabaseDesc": { "message": "Löscht die Datenbank der Erweiterung." },
|
||||
"aiToolUpdateAllTokensDesc": { "message": "Aktualisiert alle Plex-Token." },
|
||||
"aiToolAddPlexTokenDesc": { "message": "Fügt einen neuen Plex-Token hinzu." },
|
||||
"aiToolAddPlexTokenTokenDesc": { "message": "Der hinzuzufügende Plex-Token." },
|
||||
"aiToolChangeRegionDesc": { "message": "Ändert die Region für die Inhaltsentdeckung." },
|
||||
"aiToolChangeRegionRegionDesc": { "message": "Der zweibuchstabige Regionscode (z. B. US, ES, FR)." },
|
||||
"aiToolClearAllFavoritesDesc": { "message": "Löscht alle Benutzerfavoriten." },
|
||||
"aiToolClearRecommendationsViewDesc": { "message": "Löscht die Empfehlungsansicht." },
|
||||
"aiToolFavoritesCleared": { "message": "Favoriten gelöscht." },
|
||||
"aiToolFavoritesClearError": { "message": "Fehler beim Löschen der Favoriten: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolRecommendationsCleared": { "message": "Empfehlungen gelöscht." },
|
||||
"aiToolRecommendationsClearError": { "message": "Fehler beim Löschen der Empfehlungen: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolDatabaseDeleted": { "message": "Datenbank gelöscht. Die Seite wird jetzt neu geladen." },
|
||||
"aiToolDatabaseDeleteError": { "message": "Fehler beim Löschen der Datenbank: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolDatabaseDeleteBlocked": { "message": "Das Löschen der Datenbank ist blockiert. Bitte schließen Sie andere Tabs der Anwendung." },
|
||||
"aiToolUpdateAllTokensSuccess": { "message": "Alle Token wurden erfolgreich aktualisiert." },
|
||||
"aiToolUpdateAllTokensError": { "message": "Fehler beim Aktualisieren der Token: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolAddPlexTokenSuccess": { "message": "Plex-Token erfolgreich hinzugefügt." },
|
||||
"aiToolAddPlexTokenError": { "message": "Fehler beim Hinzufügen des Plex-Tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolChangeRegionSuccess": { "message": "Region auf $region$ geändert. Inhalt wird aktualisiert.", "placeholders": { "region": { "content": "$1" } } },
|
||||
"aiToolChangeRegionError": { "message": "Fehler beim Ändern der Region: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiSystemPrompt_v2": { "message": "Sie sind ein erfahrener Film- und Serienassistent namens CinePlex. Ihre Hauptfunktion ist es, Benutzern bei der Interaktion mit ihrer Medienbibliothek und der Entdeckung von Inhalten zu helfen. Befolgen Sie diese Regeln: 1. Wenn ein Benutzer nach einer Liste oder Empfehlung fragt (z. B. 'nennen Sie mir 5 Horrorfilme', 'die besten Science-Fiction-Serien'), MÜSSEN Sie Ihr eigenes Wissen verwenden, um die Liste zu erstellen und sie in einem nummerierten oder Aufzählungsformat darzustellen. VERWENDEN Sie KEINE Werkzeuge für diese anfängliche Entdeckungsaufgabe. 2. Nachdem Sie die Liste präsentiert haben, wenn der Benutzer Sie bittet, sie herunterzuladen, zu überprüfen oder eine M3U zu erstellen, DANN und nur dann, verwenden Sie das Werkzeug 'generate_m3u_from_titles_list' und übergeben Sie ihm die Titel, die Sie gerade erwähnt haben. 3. Für jede andere Aktion wie das Navigieren in der App, das Abrufen von Statistiken oder die Suche nach einem BESTIMMTEN TITEL in der Bibliothek des Benutzers, verwenden Sie die entsprechenden Werkzeuge. Seien Sie prägnant, freundlich und effizient." },
|
||||
"aiToolSearchLibraryDesc_v2": { "message": "Die Liste der zu verarbeitenden Titel ist leer." },
|
||||
"aiToolM3UNoLocalMatches": { "message": "Anscheinend haben Sie keinen der Titel aus dieser Liste auf Ihren lokalen Servern." },
|
||||
"aiToolGenerateM3UTypeDesc": { "message": "Der zu suchende Inhaltstyp: 'movie' oder 'series'." },
|
||||
"aiToolGenerateM3UFilenameDesc": { "message": "Ein benutzerdefinierter Dateiname für die heruntergeladene M3U-Wiedergabeliste." },
|
||||
"aiToolGenerateM3USuccess": { "message": "Verstanden! Ich erstelle eine M3U-Wiedergabeliste mit $1 Elementen. Der Download sollte in Kürze beginnen.", "placeholders": { "1": { "content": "$1" } } }
|
||||
}
|
@ -358,5 +358,59 @@
|
||||
"chatApiInvalidResponse": { "message": "The API returned an invalid response. Please try again." },
|
||||
"chatApiError": { "message": "Error communicating with the AI assistant" },
|
||||
"downloadAll": { "message": "Download All" },
|
||||
"download": { "message": "Download" }
|
||||
"download": { "message": "Download" },
|
||||
"aiToolSearchLibraryDesc": { "message": "Searches the Plex library for movies or series." },
|
||||
"aiToolSearchLibraryQueryDesc": { "message": "The title to search for." },
|
||||
"aiToolSearchLibraryTypeDesc": { "message": "The type of content to search for (movie or series)." },
|
||||
"aiToolNavigateToPageDesc": { "message": "Navigates to a specific page in the application." },
|
||||
"aiToolNavigateToPagePageDesc": { "message": "The page to navigate to." },
|
||||
"aiToolGetUserStatsDesc": { "message": "Gets user statistics." },
|
||||
"aiToolShowItemDetailsDesc": { "message": "Shows the details of a movie or series." },
|
||||
"aiToolShowItemDetailsTitleDesc": { "message": "The title of the movie or series." },
|
||||
"aiToolShowItemDetailsTypeDesc": { "message": "The type of content (movie or series)." },
|
||||
"aiToolAddToPlaylistDesc": { "message": "Adds a movie or series to the playlist." },
|
||||
"aiToolAddToPlaylistTitleDesc": { "message": "The title of the movie or series." },
|
||||
"aiToolAddToPlaylistTypeDesc": { "message": "The type of content (movie or series)." },
|
||||
"aiToolDownloadPlaylistDesc": { "message": "Downloads the playlist for a movie or series." },
|
||||
"aiToolDownloadPlaylistTitleDesc": { "message": "The title of the movie or series." },
|
||||
"aiToolDownloadPlaylistTypeDesc": { "message": "The type of content (movie or series)." },
|
||||
"aiToolToggleFavoriteDesc": { "message": "Adds or removes a movie or series from favorites." },
|
||||
"aiToolToggleFavoriteTitleDesc": { "message": "The title of the movie or series." },
|
||||
"aiToolToggleFavoriteTypeDesc": { "message": "The type of content (movie or series)." },
|
||||
"aiToolGetRecommendationsDesc": { "message": "Gets recommendations for the user." },
|
||||
"aiToolApplyFiltersDesc": { "message": "Applies filters to the current view." },
|
||||
"aiToolApplyFiltersTypeDesc": { "message": "The type of content to filter (movie or series)." },
|
||||
"aiToolApplyFiltersGenreDesc": { "message": "The genre to filter by." },
|
||||
"aiToolApplyFiltersYearDesc": { "message": "The year to filter by." },
|
||||
"aiToolApplyFiltersSortDesc": { "message": "The order in which to sort the results." },
|
||||
"aiToolPlayMusicByArtistDesc": { "message": "Plays music by a specific artist." },
|
||||
"aiToolPlayMusicByArtistNameDesc": { "message": "The name of the artist." },
|
||||
"aiToolClearChatHistoryDesc": { "message": "Clears the chat history." },
|
||||
"aiToolDeleteDatabaseDesc": { "message": "Deletes the extension's database." },
|
||||
"aiToolUpdateAllTokensDesc": { "message": "Updates all Plex tokens." },
|
||||
"aiToolAddPlexTokenDesc": { "message": "Adds a new Plex token." },
|
||||
"aiToolAddPlexTokenTokenDesc": { "message": "The Plex token to add." },
|
||||
"aiToolChangeRegionDesc": { "message": "Changes the region for content discovery." },
|
||||
"aiToolChangeRegionRegionDesc": { "message": "The two-letter region code (e.g., US, ES, FR)." },
|
||||
"aiToolClearAllFavoritesDesc": { "message": "Clears all user favorites." },
|
||||
"aiToolClearRecommendationsViewDesc": { "message": "Clears the recommendations view." },
|
||||
"aiToolFavoritesCleared": { "message": "Favorites cleared." },
|
||||
"aiToolFavoritesClearError": { "message": "Error clearing favorites: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolRecommendationsCleared": { "message": "Recommendations cleared." },
|
||||
"aiToolRecommendationsClearError": { "message": "Error clearing recommendations: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolDatabaseDeleted": { "message": "Database deleted. The page will now reload." },
|
||||
"aiToolDatabaseDeleteError": { "message": "Error deleting database: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolDatabaseDeleteBlocked": { "message": "Database deletion is blocked. Please close other tabs of the application." },
|
||||
"aiToolUpdateAllTokensSuccess": { "message": "All tokens have been updated successfully." },
|
||||
"aiToolUpdateAllTokensError": { "message": "Error updating tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolAddPlexTokenSuccess": { "message": "Plex token added successfully." },
|
||||
"aiToolAddPlexTokenError": { "message": "Error adding Plex token: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolChangeRegionSuccess": { "message": "Region changed to $region$. Content is being updated.", "placeholders": { "region": { "content": "$1" } } },
|
||||
"aiToolChangeRegionError": { "message": "Error changing region: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiSystemPrompt_v2": { "message": "You are an expert film and series assistant named CinePlex. Your main function is to help users interact with their media library and discover content. Follow these rules: 1. When a user asks for a list or recommendation (e.g., 'tell me 5 horror movies', 'the best science fiction series'), you MUST use your own knowledge to generate the list and present it in a numbered or bulleted format. DO NOT use tools for this initial discovery task. 2. After presenting the list, if the user asks you to download it, check it, or create an M3U, THEN and only then, use the 'generate_m3u_from_titles_list' tool, passing it the titles you just mentioned. 3. For any other action such as navigating the app, getting statistics, or searching for a SPECIFIC TITLE in the user's library, use the appropriate tools. Be concise, friendly, and efficient." },
|
||||
"aiToolSearchLibraryDesc_v2": { "message": "The list of titles to process is empty." },
|
||||
"aiToolM3UNoLocalMatches": { "message": "It seems you don't have any of the titles from that list on your local servers." },
|
||||
"aiToolGenerateM3UTypeDesc": { "message": "The type of content to search for: 'movie' or 'series'." },
|
||||
"aiToolGenerateM3UFilenameDesc": { "message": "A custom filename for the downloaded M3U playlist." },
|
||||
"aiToolGenerateM3USuccess": { "message": "Got it! I'm generating an M3U playlist with $1 items. The download should start shortly.", "placeholders": { "1": { "content": "$1" } } }
|
||||
}
|
@ -358,5 +358,59 @@
|
||||
"chatApiInvalidResponse": { "message": "La API ha devuelto una respuesta no válida. Por favor, inténtalo de nuevo." },
|
||||
"chatApiError": { "message": "Error al comunicarse con el asistente de IA" },
|
||||
"downloadAll": { "message": "Descargar todo" },
|
||||
"download": { "message": "Descargar" }
|
||||
"download": { "message": "Descargar" },
|
||||
"aiToolSearchLibraryDesc": { "message": "Busca en la biblioteca de Plex películas o series." },
|
||||
"aiToolSearchLibraryQueryDesc": { "message": "El título a buscar." },
|
||||
"aiToolSearchLibraryTypeDesc": { "message": "El tipo de contenido a buscar (película o serie)." },
|
||||
"aiToolNavigateToPageDesc": { "message": "Navega a una página específica de la aplicación." },
|
||||
"aiToolNavigateToPagePageDesc": { "message": "La página a la que navegar." },
|
||||
"aiToolGetUserStatsDesc": { "message": "Obtiene estadísticas del usuario." },
|
||||
"aiToolShowItemDetailsDesc": { "message": "Muestra los detalles de una película o serie." },
|
||||
"aiToolShowItemDetailsTitleDesc": { "message": "El título de la película o serie." },
|
||||
"aiToolShowItemDetailsTypeDesc": { "message": "El tipo de contenido (película o serie)." },
|
||||
"aiToolAddToPlaylistDesc": { "message": "Añade una película o serie a la lista de reproducción." },
|
||||
"aiToolAddToPlaylistTitleDesc": { "message": "El título de la película o serie." },
|
||||
"aiToolAddToPlaylistTypeDesc": { "message": "El tipo de contenido (película o serie)." },
|
||||
"aiToolDownloadPlaylistDesc": { "message": "Descarga la lista de reproducción de una película o serie." },
|
||||
"aiToolDownloadPlaylistTitleDesc": { "message": "El título de la película o serie." },
|
||||
"aiToolDownloadPlaylistTypeDesc": { "message": "El tipo de contenido (película o serie)." },
|
||||
"aiToolToggleFavoriteDesc": { "message": "Añade o quita una película o serie de favoritos." },
|
||||
"aiToolToggleFavoriteTitleDesc": { "message": "El título de la película o serie." },
|
||||
"aiToolToggleFavoriteTypeDesc": { "message": "El tipo de contenido (película o serie)." },
|
||||
"aiToolGetRecommendationsDesc": { "message": "Obtiene recomendaciones para el usuario." },
|
||||
"aiToolApplyFiltersDesc": { "message": "Aplica filtros a la vista actual." },
|
||||
"aiToolApplyFiltersTypeDesc": { "message": "El tipo de contenido a filtrar (película o serie)." },
|
||||
"aiToolApplyFiltersGenreDesc": { "message": "El género a filtrar." },
|
||||
"aiToolApplyFiltersYearDesc": { "message": "El año a filtrar." },
|
||||
"aiToolApplyFiltersSortDesc": { "message": "El orden en que se mostrarán los resultados." },
|
||||
"aiToolPlayMusicByArtistDesc": { "message": "Reproduce música de un artista específico." },
|
||||
"aiToolPlayMusicByArtistNameDesc": { "message": "El nombre del artista." },
|
||||
"aiToolClearChatHistoryDesc": { "message": "Borra el historial de chat." },
|
||||
"aiToolDeleteDatabaseDesc": { "message": "Borra la base de datos de la extensión." },
|
||||
"aiToolUpdateAllTokensDesc": { "message": "Actualiza todos los tokens de Plex." },
|
||||
"aiToolAddPlexTokenDesc": { "message": "Añade un nuevo token de Plex." },
|
||||
"aiToolAddPlexTokenTokenDesc": { "message": "El token de Plex a añadir." },
|
||||
"aiToolChangeRegionDesc": { "message": "Cambia la región para el descubrimiento de contenido." },
|
||||
"aiToolChangeRegionRegionDesc": { "message": "El código de región de dos letras (por ejemplo, US, ES, FR)." },
|
||||
"aiToolClearAllFavoritesDesc": { "message": "Borra todos los favoritos del usuario." },
|
||||
"aiToolClearRecommendationsViewDesc": { "message": "Limpia la vista de recomendaciones." },
|
||||
"aiToolFavoritesCleared": { "message": "Favoritos eliminados." },
|
||||
"aiToolFavoritesClearError": { "message": "Error al eliminar los favoritos: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolRecommendationsCleared": { "message": "Recomendaciones eliminadas." },
|
||||
"aiToolRecommendationsClearError": { "message": "Error al eliminar las recomendaciones: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolDatabaseDeleted": { "message": "Base de datos eliminada. La página se recargará." },
|
||||
"aiToolDatabaseDeleteError": { "message": "Error al eliminar la base de datos: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolDatabaseDeleteBlocked": { "message": "La eliminación de la base de datos está bloqueada. Cierra otras pestañas de la aplicación." },
|
||||
"aiToolUpdateAllTokensSuccess": { "message": "Todos los tokens se han actualizado correctamente." },
|
||||
"aiToolUpdateAllTokensError": { "message": "Error al actualizar los tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolAddPlexTokenSuccess": { "message": "Token de Plex añadido correctamente." },
|
||||
"aiToolAddPlexTokenError": { "message": "Error al añadir el token de Plex: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolChangeRegionSuccess": { "message": "Región cambiada a $region$. El contenido se está actualizando.", "placeholders": { "region": { "content": "$1" } } },
|
||||
"aiToolChangeRegionError": { "message": "Error al cambiar la región: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiSystemPrompt_v2": { "message": "Eres un asistente experto en cine y series llamado CinePlex. Tu función principal es ayudar a los usuarios a interactuar con su biblioteca de medios y descubrir contenido. Sigue estas reglas: 1. Cuando un usuario pida una lista o recomendación (ej. 'dime 5 películas de terror', 'las mejores series de ciencia ficción'), DEBES usar tu propio conocimiento para generar la lista y presentarla en un formato numerado o con viñetas. NO uses herramientas para esta tarea inicial de descubrimiento. 2. Después de presentar la lista, si el usuario te pide que la descargues, la compruebes o crees un M3U, ENTONCES y solo entonces, utiliza la herramienta 'generate_m3u_from_titles_list' pasándole los títulos que acabas de mencionar. 3. Para cualquier otra acción como navegar por la app, obtener estadísticas o buscar un TÍTULO ESPECÍFICO en la biblioteca del usuario, utiliza las herramientas apropiadas. Sé conciso, amigable y eficiente." },
|
||||
"aiToolSearchLibraryDesc_v2": { "message": "La lista de títulos para procesar está vacía." },
|
||||
"aiToolM3UNoLocalMatches": { "message": "Parece que no tienes ninguno de los títulos de esa lista en tus servidores locales." },
|
||||
"aiToolGenerateM3UTypeDesc": { "message": "El tipo de contenido a buscar: 'movie' o 'series'." },
|
||||
"aiToolGenerateM3UFilenameDesc": { "message": "Un nombre de archivo personalizado para la lista de reproducción M3U descargada." },
|
||||
"aiToolGenerateM3USuccess": { "message": "¡Entendido! Estoy generando una lista de reproducción M3U con $1 elementos. La descarga debería comenzar en breve.", "placeholders": { "1": { "content": "$1" } } }
|
||||
}
|
@ -187,7 +187,7 @@
|
||||
"couldNotLoadContent": { "message": "Impossible de charger le contenu." },
|
||||
"noFavorites": { "message": "Vous n'avez pas encore de favoris." },
|
||||
"errorLoadingFavorites": { "message": "Erreur lors du chargement des favoris." },
|
||||
"historyEmpty": { "message": "Votre historique est vide." },
|
||||
"historyEmpty": { "message": "Votre historique est isEmpty." },
|
||||
"historyEmptySub": { "message": "Parcourez et regardez du contenu pour qu'il apparaisse ici." },
|
||||
"errorGeneratingRecommendations": { "message": "Erreur lors de la génération des recommandations." },
|
||||
"noRecommendations": { "message": "Nous avons besoin de mieux vous connaître pour vous faire des recommandations !" },
|
||||
@ -244,7 +244,7 @@
|
||||
"shuffleOn": { "message": "Mode aléatoire activé." },
|
||||
"shuffleOff": { "message": "Mode aléatoire désactivé." },
|
||||
"downloadingSong": { "message": "Début du téléchargement de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"songDownloaded": { "message": "\"\"$title$\" téléchargé.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"songDownloaded": { "message": "\"\"$title$\"\" téléchargé.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"errorDownloadingSong": { "message": "Erreur lors du téléchargement de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"generatingAlbumM3U": { "message": "Génération du M3U pour \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
|
||||
"albumM3UGenerated": { "message": "M3U de l'album \"$artist$\" généré.", "placeholders": { "artist": { "content": "$1" } } },
|
||||
@ -358,5 +358,59 @@
|
||||
"chatApiInvalidResponse": { "message": "L'API a renvoyé une réponse non valide. Veuillez réessayer." },
|
||||
"chatApiError": { "message": "Erreur de communication avec l'assistant IA" },
|
||||
"downloadAll": { "message": "Tout télécharger" },
|
||||
"download": { "message": "Télécharger" }
|
||||
"download": { "message": "Télécharger" },
|
||||
"aiToolSearchLibraryDesc": { "message": "Recherche dans la bibliothèque Plex des films ou des séries." },
|
||||
"aiToolSearchLibraryQueryDesc": { "message": "Le titre à rechercher." },
|
||||
"aiToolSearchLibraryTypeDesc": { "message": "Le type de contenu à rechercher (film ou série)." },
|
||||
"aiToolNavigateToPageDesc": { "message": "Navigue vers une page spécifique de l'application." },
|
||||
"aiToolNavigateToPagePageDesc": { "message": "La page vers laquelle naviguer." },
|
||||
"aiToolGetUserStatsDesc": { "message": "Obtient les statistiques de l'utilisateur." },
|
||||
"aiToolShowItemDetailsDesc": { "message": "Affiche les détails d'un film ou d'une série." },
|
||||
"aiToolShowItemDetailsTitleDesc": { "message": "Le titre du film ou de la série." },
|
||||
"aiToolShowItemDetailsTypeDesc": { "message": "Le type de contenu (film ou série)." },
|
||||
"aiToolAddToPlaylistDesc": { "message": "Ajoute un film ou une série à la liste de lecture." },
|
||||
"aiToolAddToPlaylistTitleDesc": { "message": "Le titre du film ou de la série." },
|
||||
"aiToolAddToPlaylistTypeDesc": { "message": "Le type de contenu (film ou série)." },
|
||||
"aiToolDownloadPlaylistDesc": { "message": "Télécharge la liste de lecture d'un film ou d'une série." },
|
||||
"aiToolDownloadPlaylistTitleDesc": { "message": "Le titre du film ou de la série." },
|
||||
"aiToolDownloadPlaylistTypeDesc": { "message": "Le type de contenu (film ou série)." },
|
||||
"aiToolToggleFavoriteDesc": { "message": "Ajoute ou supprime un film ou une série des favoris." },
|
||||
"aiToolToggleFavoriteTitleDesc": { "message": "Le titre du film ou de la série." },
|
||||
"aiToolToggleFavoriteTypeDesc": { "message": "Le type de contenu (film ou série)." },
|
||||
"aiToolGetRecommendationsDesc": { "message": "Obtient des recommandations pour l'utilisateur." },
|
||||
"aiToolApplyFiltersDesc": { "message": "Applique des filtres à la vue actuelle." },
|
||||
"aiToolApplyFiltersTypeDesc": { "message": "Le type de contenu à filtrer (film ou série)." },
|
||||
"aiToolApplyFiltersGenreDesc": { "message": "Le genre par lequel filtrer." },
|
||||
"aiToolApplyFiltersYearDesc": { "message": "L'année par laquelle filtrer." },
|
||||
"aiToolApplyFiltersSortDesc": { "message": "L'ordre dans lequel trier les résultats." },
|
||||
"aiToolPlayMusicByArtistDesc": { "message": "Joue de la musique d'un artiste spécifique." },
|
||||
"aiToolPlayMusicByArtistNameDesc": { "message": "Le nom de l'artiste." },
|
||||
"aiToolClearChatHistoryDesc": { "message": "Efface l'historique du chat." },
|
||||
"aiToolDeleteDatabaseDesc": { "message": "Supprime la base de données de l'extension." },
|
||||
"aiToolUpdateAllTokensDesc": { "message": "Met à jour tous les jetons Plex." },
|
||||
"aiToolAddPlexTokenDesc": { "message": "Ajoute un nouveau jeton Plex." },
|
||||
"aiToolAddPlexTokenTokenDesc": { "message": "Le jeton Plex à ajouter." },
|
||||
"aiToolChangeRegionDesc": { "message": "Change la région pour la découverte de contenu." },
|
||||
"aiToolChangeRegionRegionDesc": { "message": "Le code de région à deux lettres (par exemple, US, ES, FR)." },
|
||||
"aiToolClearAllFavoritesDesc": { "message": "Efface tous les favoris de l'utilisateur." },
|
||||
"aiToolClearRecommendationsViewDesc": { "message": "Efface la vue des recommandations." },
|
||||
"aiToolFavoritesCleared": { "message": "Favoris effacés." },
|
||||
"aiToolFavoritesClearError": { "message": "Erreur lors de la suppression des favoris : $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolRecommendationsCleared": { "message": "Recommandations effacées." },
|
||||
"aiToolRecommendationsClearError": { "message": "Erreur lors de la suppression des recommandations : $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolDatabaseDeleted": { "message": "Base de données supprimée. La page va maintenant se recharger." },
|
||||
"aiToolDatabaseDeleteError": { "message": "Erreur lors de la suppression de la base de données : $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolDatabaseDeleteBlocked": { "message": "La suppression de la base de données est bloquée. Veuillez fermer les autres onglets de l'application." },
|
||||
"aiToolUpdateAllTokensSuccess": { "message": "Tous les jetons ont été mis à jour avec succès." },
|
||||
"aiToolUpdateAllTokensError": { "message": "Erreur lors de la mise à jour des jetons : $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolAddPlexTokenSuccess": { "message": "Jeton Plex ajouté avec succès." },
|
||||
"aiToolAddPlexTokenError": { "message": "Erreur lors de l'ajout du jeton Plex : $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolChangeRegionSuccess": { "message": "Région changée en $region$. Le contenu est en cours de mise à jour.", "placeholders": { "region": { "content": "$1" } } },
|
||||
"aiToolChangeRegionError": { "message": "Erreur lors du changement de région : $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiSystemPrompt_v2": { "message": "Vous êtes un assistant expert en films et séries nommé CinePlex. Votre fonction principale est d'aider les utilisateurs à interagir avec leur médiathèque et à découvrir du contenu. Suivez ces règles : 1. Lorsqu'un utilisateur demande une liste ou une recommandation (par exemple, 'donnez-moi 5 films d'horreur', 'les meilleures séries de science-fiction'), vous DEVEZ utiliser vos propres connaissances pour générer la liste et la présenter sous forme de liste numérotée ou à puces. N'UTILISEZ PAS d'outils pour cette tâche de découverte initiale. 2. Après avoir présenté la liste, si l'utilisateur vous demande de la télécharger, de la vérifier ou de créer un M3U, ALORS et seulement alors, utilisez l'outil 'generate_m3u_from_titles_list' en lui passant les titres que vous venez de mentionner. 3. Pour toute autre action telle que la navigation dans l'application, l'obtention de statistiques ou la recherche d'un TITRE SPÉCIFIQUE dans la bibliothèque de l'utilisateur, utilisez les outils appropriés. Soyez concis, amical et efficace." },
|
||||
"aiToolSearchLibraryDesc_v2": { "message": "La liste des titres à traiter est vide." },
|
||||
"aiToolM3UNoLocalMatches": { "message": "Il semble que vous n'ayez aucun des titres de cette liste sur vos serveurs locaux." },
|
||||
"aiToolGenerateM3UTypeDesc": { "message": "Le type de contenu à rechercher : 'movie' ou 'series'." },
|
||||
"aiToolGenerateM3UFilenameDesc": { "message": "Un nom de fichier personnalisé pour la liste de lecture M3U téléchargée." },
|
||||
"aiToolGenerateM3USuccess": { "message": "Compris ! Je génère une liste de lecture M3U avec $1 éléments. Le téléchargement devrait commencer sous peu.", "placeholders": { "1": { "content": "$1" } } }
|
||||
}
|
@ -358,5 +358,59 @@
|
||||
"chatApiInvalidResponse": { "message": "L'API ha restituito una risposta non valida. Per favore, riprova." },
|
||||
"chatApiError": { "message": "Errore di comunicazione con l'assistente AI" },
|
||||
"downloadAll": { "message": "Scarica tutto" },
|
||||
"download": { "message": "Scarica" }
|
||||
"download": { "message": "Scarica" },
|
||||
"aiToolSearchLibraryDesc": { "message": "Cerca nella libreria Plex film o serie." },
|
||||
"aiToolSearchLibraryQueryDesc": { "message": "Il titolo da cercare." },
|
||||
"aiToolSearchLibraryTypeDesc": { "message": "Il tipo di contenuto da cercare (film o serie)." },
|
||||
"aiToolNavigateToPageDesc": { "message": "Naviga a una pagina specifica dell'applicazione." },
|
||||
"aiToolNavigateToPagePageDesc": { "message": "La pagina a cui navigare." },
|
||||
"aiToolGetUserStatsDesc": { "message": "Ottiene le statistiche dell'utente." },
|
||||
"aiToolShowItemDetailsDesc": { "message": "Mostra i dettagli di un film o di una serie." },
|
||||
"aiToolShowItemDetailsTitleDesc": { "message": "Il titolo del film o della serie." },
|
||||
"aiToolShowItemDetailsTypeDesc": { "message": "Il tipo di contenuto (film o serie)." },
|
||||
"aiToolAddToPlaylistDesc": { "message": "Aggiunge un film o una serie alla playlist." },
|
||||
"aiToolAddToPlaylistTitleDesc": { "message": "Il titolo del film o della serie." },
|
||||
"aiToolAddToPlaylistTypeDesc": { "message": "Il tipo di contenuto (film o serie)." },
|
||||
"aiToolDownloadPlaylistDesc": { "message": "Scarica la playlist di un film o di una serie." },
|
||||
"aiToolDownloadPlaylistTitleDesc": { "message": "Il titolo del film o della serie." },
|
||||
"aiToolDownloadPlaylistTypeDesc": { "message": "Il tipo di contenuto (film o serie)." },
|
||||
"aiToolToggleFavoriteDesc": { "message": "Aggiunge o rimuove un film o una serie dai preferiti." },
|
||||
"aiToolToggleFavoriteTitleDesc": { "message": "Il titolo del film o della serie." },
|
||||
"aiToolToggleFavoriteTypeDesc": { "message": "Il tipo di contenuto (film o serie)." },
|
||||
"aiToolGetRecommendationsDesc": { "message": "Ottiene consigli per l'utente." },
|
||||
"aiToolApplyFiltersDesc": { "message": "Applica filtri alla vista corrente." },
|
||||
"aiToolApplyFiltersTypeDesc": { "message": "Il tipo di contenuto da filtrare (film o serie)." },
|
||||
"aiToolApplyFiltersGenreDesc": { "message": "Il genere per cui filtrare." },
|
||||
"aiToolApplyFiltersYearDesc": { "message": "L'anno per cui filtrare." },
|
||||
"aiToolApplyFiltersSortDesc": { "message": "L'ordine in cui ordinare i risultati." },
|
||||
"aiToolPlayMusicByArtistDesc": { "message": "Riproduce la musica di un artista specifico." },
|
||||
"aiToolPlayMusicByArtistNameDesc": { "message": "Il nome dell'artista." },
|
||||
"aiToolClearChatHistoryDesc": { "message": "Cancella la cronologia della chat." },
|
||||
"aiToolDeleteDatabaseDesc": { "message": "Elimina il database dell'estensione." },
|
||||
"aiToolUpdateAllTokensDesc": { "message": "Aggiorna tutti i token Plex." },
|
||||
"aiToolAddPlexTokenDesc": { "message": "Aggiunge un nuovo token Plex." },
|
||||
"aiToolAddPlexTokenTokenDesc": { "message": "Il token Plex da aggiungere." },
|
||||
"aiToolChangeRegionDesc": { "message": "Cambia la regione per la scoperta di contenuti." },
|
||||
"aiToolChangeRegionRegionDesc": { "message": "Il codice regionale a due lettere (ad es. US, ES, FR)." },
|
||||
"aiToolClearAllFavoritesDesc": { "message": "Cancella tutti i preferiti dell'utente." },
|
||||
"aiToolClearRecommendationsViewDesc": { "message": "Cancella la vista dei consigliati." },
|
||||
"aiToolFavoritesCleared": { "message": "Preferiti cancellati." },
|
||||
"aiToolFavoritesClearError": { "message": "Errore durante la cancellazione dei preferiti: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolRecommendationsCleared": { "message": "Consigliati cancellati." },
|
||||
"aiToolRecommendationsClearError": { "message": "Errore durante la cancellazione dei consigliati: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolDatabaseDeleted": { "message": "Database eliminato. La pagina verrà ricaricata." },
|
||||
"aiToolDatabaseDeleteError": { "message": "Errore durante l'eliminazione del database: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolDatabaseDeleteBlocked": { "message": "L'eliminazione del database è bloccata. Chiudere le altre schede dell'applicazione." },
|
||||
"aiToolUpdateAllTokensSuccess": { "message": "Tutti i token sono stati aggiornati con successo." },
|
||||
"aiToolUpdateAllTokensError": { "message": "Errore durante l'aggiornamento dei token: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolAddPlexTokenSuccess": { "message": "Token Plex aggiunto con successo." },
|
||||
"aiToolAddPlexTokenError": { "message": "Errore durante l'aggiunta del token Plex: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolChangeRegionSuccess": { "message": "Regione cambiata in $region$. Il contenuto è in fase di aggiornamento.", "placeholders": { "region": { "content": "$1" } } },
|
||||
"aiToolChangeRegionError": { "message": "Errore durante la modifica della regione: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiSystemPrompt_v2": { "message": "Sei un assistente esperto di film e serie chiamato CinePlex. La tua funzione principale è aiutare gli utenti a interagire con la loro libreria multimediale e scoprire contenuti. Segui queste regole: 1. Quando un utente chiede un elenco o una raccomandazione (ad es. 'dimmi 5 film dell'orrore', 'le migliori serie di fantascienza'), DEVI usare le tue conoscenze per generare l'elenco e presentarlo in un formato numerato o puntato. NON usare strumenti per questa attività di scoperta iniziale. 2. Dopo aver presentato l'elenco, se l'utente ti chiede di scaricarlo, controllarlo o creare un M3U, ALLORA e solo allora, usa lo strumento 'generate_m3u_from_titles_list' passandogli i titoli che hai appena menzionato. 3. Per qualsiasi altra azione come navigare nell'app, ottenere statistiche o cercare un TITOLO SPECIFICO nella libreria dell'utente, usa gli strumenti appropriati. Sii conciso, amichevole ed efficiente." },
|
||||
"aiToolSearchLibraryDesc_v2": { "message": "L'elenco dei titoli da elaborare è vuoto." },
|
||||
"aiToolM3UNoLocalMatches": { "message": "Sembra che tu non abbia nessuno dei titoli di quell'elenco sui tuoi server locali." },
|
||||
"aiToolGenerateM3UTypeDesc": { "message": "Il tipo di contenuto da cercare: 'movie' o 'series'." },
|
||||
"aiToolGenerateM3UFilenameDesc": { "message": "Un nome file personalizzato per la playlist M3U scaricata." },
|
||||
"aiToolGenerateM3USuccess": { "message": "Capito! Sto generando una playlist M3U con $1 elementi. Il download dovrebbe iniziare a breve.", "placeholders": { "1": { "content": "$1" } } }
|
||||
}
|
@ -358,5 +358,59 @@
|
||||
"chatApiInvalidResponse": { "message": "A API retornou uma resposta inválida. Por favor, tente novamente." },
|
||||
"chatApiError": { "message": "Erro ao se comunicar com o assistente de IA" },
|
||||
"downloadAll": { "message": "Baixar tudo" },
|
||||
"download": { "message": "Baixar" }
|
||||
"download": { "message": "Baixar" },
|
||||
"aiToolSearchLibraryDesc": { "message": "Pesquisa na biblioteca Plex por filmes ou séries." },
|
||||
"aiToolSearchLibraryQueryDesc": { "message": "O título a ser pesquisado." },
|
||||
"aiToolSearchLibraryTypeDesc": { "message": "O tipo de conteúdo a ser pesquisado (filme ou série)." },
|
||||
"aiToolNavigateToPageDesc": { "message": "Navega para uma página específica no aplicativo." },
|
||||
"aiToolNavigateToPagePageDesc": { "message": "A página para a qual navegar." },
|
||||
"aiToolGetUserStatsDesc": { "message": "Obtém estatísticas do usuário." },
|
||||
"aiToolShowItemDetailsDesc": { "message": "Mostra os detalhes de um filme ou série." },
|
||||
"aiToolShowItemDetailsTitleDesc": { "message": "O título do filme ou série." },
|
||||
"aiToolShowItemDetailsTypeDesc": { "message": "O tipo de conteúdo (filme ou série)." },
|
||||
"aiToolAddToPlaylistDesc": { "message": "Adiciona um filme ou série à lista de reprodução." },
|
||||
"aiToolAddToPlaylistTitleDesc": { "message": "O título do filme ou série." },
|
||||
"aiToolAddToPlaylistTypeDesc": { "message": "O tipo de conteúdo (filme ou série)." },
|
||||
"aiToolDownloadPlaylistDesc": { "message": "Baixa a lista de reprodução de um filme ou série." },
|
||||
"aiToolDownloadPlaylistTitleDesc": { "message": "O título do filme ou série." },
|
||||
"aiToolDownloadPlaylistTypeDesc": { "message": "O tipo de conteúdo (filme ou série)." },
|
||||
"aiToolToggleFavoriteDesc": { "message": "Adiciona ou remove um filme ou série dos favoritos." },
|
||||
"aiToolToggleFavoriteTitleDesc": { "message": "O título do filme ou série." },
|
||||
"aiToolToggleFavoriteTypeDesc": { "message": "O tipo de conteúdo (filme ou série)." },
|
||||
"aiToolGetRecommendationsDesc": { "message": "Obtém recomendações para o usuário." },
|
||||
"aiToolApplyFiltersDesc": { "message": "Aplica filtros à visualização atual." },
|
||||
"aiToolApplyFiltersTypeDesc": { "message": "O tipo de conteúdo a ser filtrado (filme ou série)." },
|
||||
"aiToolApplyFiltersGenreDesc": { "message": "O gênero pelo qual filtrar." },
|
||||
"aiToolApplyFiltersYearDesc": { "message": "O ano pelo qual filtrar." },
|
||||
"aiToolApplyFiltersSortDesc": { "message": "A ordem na qual os resultados devem ser classificados." },
|
||||
"aiToolPlayMusicByArtistDesc": { "message": "Toca música de um artista específico." },
|
||||
"aiToolPlayMusicByArtistNameDesc": { "message": "O nome do artista." },
|
||||
"aiToolClearChatHistoryDesc": { "message": "Limpa o histórico de bate-papo." },
|
||||
"aiToolDeleteDatabaseDesc": { "message": "Exclui o banco de dados da extensão." },
|
||||
"aiToolUpdateAllTokensDesc": { "message": "Atualiza todos os tokens do Plex." },
|
||||
"aiToolAddPlexTokenDesc": { "message": "Adiciona um novo token do Plex." },
|
||||
"aiToolAddPlexTokenTokenDesc": { "message": "O token do Plex a ser adicionado." },
|
||||
"aiToolChangeRegionDesc": { "message": "Altera a região para descoberta de conteúdo." },
|
||||
"aiToolChangeRegionRegionDesc": { "message": "O código de região de duas letras (por exemplo, US, ES, FR)." },
|
||||
"aiToolClearAllFavoritesDesc": { "message": "Limpa todos os favoritos do usuário." },
|
||||
"aiToolClearRecommendationsViewDesc": { "message": "Limpa a visualização de recomendações." },
|
||||
"aiToolFavoritesCleared": { "message": "Favoritos limpos." },
|
||||
"aiToolFavoritesClearError": { "message": "Erro ao limpar favoritos: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolRecommendationsCleared": { "message": "Recomendações limpas." },
|
||||
"aiToolRecommendationsClearError": { "message": "Erro ao limpar recomendações: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolDatabaseDeleted": { "message": "Banco de dados excluído. A página será recarregada agora." },
|
||||
"aiToolDatabaseDeleteError": { "message": "Erro ao excluir o banco de dados: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolDatabaseDeleteBlocked": { "message": "A exclusão do banco de dados está bloqueada. Feche outras abas do aplicativo." },
|
||||
"aiToolUpdateAllTokensSuccess": { "message": "Todos os tokens foram atualizados com sucesso." },
|
||||
"aiToolUpdateAllTokensError": { "message": "Erro ao atualizar tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolAddPlexTokenSuccess": { "message": "Token do Plex adicionado com sucesso." },
|
||||
"aiToolAddPlexTokenError": { "message": "Erro ao adicionar token do Plex: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiToolChangeRegionSuccess": { "message": "Região alterada para $region$. O conteúdo está sendo atualizado.", "placeholders": { "region": { "content": "$1" } } },
|
||||
"aiToolChangeRegionError": { "message": "Erro ao alterar a região: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"aiSystemPrompt_v2": { "message": "Você é um assistente especialista em filmes e séries chamado CinePlex. Sua função principal é ajudar os usuários a interagir com sua biblioteca de mídia e descobrir conteúdo. Siga estas regras: 1. Quando um usuário pedir uma lista ou recomendação (por exemplo, 'diga-me 5 filmes de terror', 'as melhores séries de ficção científica'), você DEVE usar seu próprio conhecimento para gerar a lista e apresentá-la em formato numerado ou com marcadores. NÃO use ferramentas para esta tarefa inicial de descoberta. 2. Depois de apresentar a lista, se o usuário pedir para você baixá-la, verificá-la ou criar um M3U, ENTÃO e somente então, use a ferramenta 'generate_m3u_from_titles_list', passando a ela os títulos que você acabou de mencionar. 3. Para qualquer outra ação, como navegar no aplicativo, obter estatísticas ou pesquisar um TÍTULO ESPECÍFICO na biblioteca do usuário, use as ferramentas apropriadas. Seja conciso, amigável e eficiente." },
|
||||
"aiToolSearchLibraryDesc_v2": { "message": "A lista de títulos a processar está vazia." },
|
||||
"aiToolM3UNoLocalMatches": { "message": "Parece que você não tem nenhum dos títulos dessa lista em seus servidores locais." },
|
||||
"aiToolGenerateM3UTypeDesc": { "message": "O tipo de conteúdo a ser pesquisado: 'movie' ou 'series'." },
|
||||
"aiToolGenerateM3UFilenameDesc": { "message": "Um nome de arquivo personalizado para a lista de reprodução M3U baixada." },
|
||||
"aiToolGenerateM3USuccess": { "message": "Entendido! Estou gerando uma lista de reprodução M3U com $1 itens. O download deve começar em breve.", "placeholders": { "1": { "content": "$1" } } }
|
||||
}
|
560
js/ai-tools.js
Normal file
560
js/ai-tools.js
Normal file
@ -0,0 +1,560 @@
|
||||
import { state } from './state.js';
|
||||
import { getFromDB, clearStore, addItemsToStore } from './db.js';
|
||||
import { showNotification, _ } from './utils.js';
|
||||
import { switchView, showItemDetails, addStreamToList, downloadM3U, generateStatistics, toggleFavorite, loadRecommendations, applyFilters as applyUIFilters, clearAllFavorites, clearRecommendations, loadInitialContent, loadContent, clearAllHistory } from './ui.js';
|
||||
import { fetchTMDB } from './api.js';
|
||||
import { updateAllTokens, addPlexToken } from './plex.js';
|
||||
import { config } from './config.js';
|
||||
|
||||
export class AITools {
|
||||
constructor(chatInstance) {
|
||||
this.chat = chatInstance;
|
||||
this.genreCache = { movie: null, tv: null };
|
||||
}
|
||||
|
||||
get toolDefinitions() {
|
||||
return [
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'search_library',
|
||||
description: _('aiToolSearchLibraryDesc_v2'),
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: _('aiToolSearchLibraryQueryDesc') },
|
||||
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolSearchLibraryTypeDesc') }
|
||||
},
|
||||
required: ['query']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'navigate_to_page',
|
||||
description: _('aiToolNavigateToPageDesc'),
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'string',
|
||||
enum: ['movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', 'm3u-generator'],
|
||||
description: _('aiToolNavigateToPagePageDesc')
|
||||
}
|
||||
},
|
||||
required: ['page']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_user_stats',
|
||||
description: _('aiToolGetUserStatsDesc'),
|
||||
parameters: { type: 'object', properties: {} }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'show_item_details',
|
||||
description: _('aiToolShowItemDetailsDesc'),
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: _('aiToolShowItemDetailsTitleDesc') },
|
||||
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolShowItemDetailsTypeDesc') }
|
||||
},
|
||||
required: ['title', 'type']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'add_to_playlist',
|
||||
description: _('aiToolAddToPlaylistDesc'),
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: _('aiToolAddToPlaylistTitleDesc') },
|
||||
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolAddToPlaylistTypeDesc') }
|
||||
},
|
||||
required: ['title', 'type']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'check_and_download_titles_list',
|
||||
description: _('aiToolCheckAndDownloadDesc'),
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
titles: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: _('aiToolGenerateM3UFromTitlesTitlesDesc')
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['movie', 'series'],
|
||||
description: _('aiToolGenerateM3UTypeDesc')
|
||||
},
|
||||
filename: { type: 'string', description: _('aiToolGenerateM3UFilenameDesc') }
|
||||
},
|
||||
required: ['titles', 'type']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'toggle_favorite',
|
||||
description: _('aiToolToggleFavoriteDesc'),
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: _('aiToolToggleFavoriteTitleDesc') },
|
||||
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolToggleFavoriteTypeDesc') }
|
||||
},
|
||||
required: ['title', 'type']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_recommendations',
|
||||
description: _('aiToolGetRecommendationsDesc'),
|
||||
parameters: { type: 'object', properties: {} }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'apply_filters',
|
||||
description: _('aiToolApplyFiltersDesc'),
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolApplyFiltersTypeDesc') },
|
||||
genre: { type: 'string', description: _('aiToolApplyFiltersGenreDesc') },
|
||||
year: { type: 'string', description: _('aiToolApplyFiltersYearDesc') },
|
||||
sort: { type: 'string', enum: ['popularity.desc', 'vote_average.desc', 'release_date.desc', 'first_air_date.desc'], description: _('aiToolApplyFiltersSortDesc') }
|
||||
},
|
||||
required: ['type']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'play_music_by_artist',
|
||||
description: _('aiToolPlayMusicByArtistDesc'),
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
artist_name: { type: 'string', description: _('aiToolPlayMusicByArtistNameDesc') }
|
||||
},
|
||||
required: ['artist_name']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'clear_chat_history',
|
||||
description: _('aiToolClearChatHistoryDesc'),
|
||||
parameters: { type: 'object', properties: {} }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'delete_database',
|
||||
description: _('aiToolDeleteDatabaseDesc'),
|
||||
parameters: { type: 'object', properties: {} }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'update_all_tokens',
|
||||
description: _('aiToolUpdateAllTokensDesc'),
|
||||
parameters: { type: 'object', properties: {} }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'add_plex_token',
|
||||
description: _('aiToolAddPlexTokenDesc'),
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
token: { type: 'string', description: _('aiToolAddPlexTokenTokenDesc') }
|
||||
},
|
||||
required: ['token']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'change_region',
|
||||
description: _('aiToolChangeRegionDesc'),
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
region: { type: 'string', description: _('aiToolChangeRegionRegionDesc') }
|
||||
},
|
||||
required: ['region']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'clear_all_favorites',
|
||||
description: _('aiToolClearAllFavoritesDesc'),
|
||||
parameters: { type: 'object', properties: {} }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'clear_viewing_history',
|
||||
description: "Clears the user's content viewing history from the history page.",
|
||||
parameters: { type: 'object', properties: {} }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'clear_recommendations_view',
|
||||
description: _('aiToolClearRecommendationsViewDesc'),
|
||||
parameters: { type: 'object', properties: {} }
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async "search_library"({ query, type }) {
|
||||
const movieEntries = await getFromDB('movies');
|
||||
const seriesEntries = await getFromDB('series');
|
||||
const allContent = [
|
||||
...movieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))),
|
||||
...seriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' })))
|
||||
];
|
||||
const searchTerm = query.toLowerCase().trim();
|
||||
let results = allContent.filter(item => item.title.toLowerCase().includes(searchTerm));
|
||||
if (type) {
|
||||
results = results.filter(item => item.type === type);
|
||||
}
|
||||
if (results.length === 0) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolSearchNotFound', query) });
|
||||
}
|
||||
const formattedResults = results.slice(0, 10).map(item => ({ title: item.title, year: item.year, type: item.type }));
|
||||
return JSON.stringify({ success: true, count: results.length, results: formattedResults });
|
||||
}
|
||||
|
||||
"navigate_to_page"({ page }) {
|
||||
try {
|
||||
switchView(page);
|
||||
return JSON.stringify({ success: true, message: _('aiToolNavigateSuccess', page) });
|
||||
} catch (error) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolNavigateError', page) });
|
||||
}
|
||||
}
|
||||
|
||||
async "get_user_stats"() {
|
||||
try {
|
||||
const movieItems = (await getFromDB('movies')).flatMap(s => s.titulos);
|
||||
const seriesItems = (await getFromDB('series')).flatMap(s => s.titulos);
|
||||
const artistItems = (await getFromDB('artists')).flatMap(s => s.titulos);
|
||||
const stats = {
|
||||
totalMovies: new Set(movieItems.map(item => item.title)).size,
|
||||
totalSeries: new Set(seriesItems.map(item => item.title)).size,
|
||||
totalArtists: new Set(artistItems.map(item => item.title)).size,
|
||||
};
|
||||
switchView('stats');
|
||||
return JSON.stringify({ success: true, stats });
|
||||
} catch (error) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolStatsError') });
|
||||
}
|
||||
}
|
||||
|
||||
async "show_item_details"({ title, type }) {
|
||||
const content = await this.findTmdbContent(title, type);
|
||||
if (!content) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
|
||||
}
|
||||
showItemDetails(Number(content.id), content.type);
|
||||
return JSON.stringify({ success: true, message: _('aiToolShowItemDetailsSuccess', title) });
|
||||
}
|
||||
|
||||
async "add_to_playlist"({ title, type }) {
|
||||
const content = await this.findLocalContent(title, type);
|
||||
if (!content) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
|
||||
}
|
||||
addStreamToList(content.title, content.type);
|
||||
return JSON.stringify({ success: true, message: _('aiToolAddToPlaylistSuccess', title) });
|
||||
}
|
||||
|
||||
async "check_and_download_titles_list"({ titles, type, filename }) {
|
||||
try {
|
||||
if (!titles || titles.length === 0) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolM3UNoTitlesProvided') });
|
||||
}
|
||||
|
||||
showNotification(_('aiToolM3UCheckingTitles'), 'info');
|
||||
|
||||
const plexStore = type === 'movie' ? 'movies' : 'series';
|
||||
const jellyfinStore = type === 'movie' ? 'jellyfin_movies' : 'jellyfin_series';
|
||||
|
||||
const plexContent = await getFromDB(plexStore);
|
||||
const jellyfinContent = await getFromDB(jellyfinStore);
|
||||
|
||||
const allLocalItems = [
|
||||
...plexContent.flatMap(server => server.titulos || []),
|
||||
...jellyfinContent.flatMap(lib => lib.titulos || [])
|
||||
];
|
||||
|
||||
const normalize = (str) => str ? str.toLowerCase().trim().replace(/[^a-z0-9]/g, '') : '';
|
||||
const normalizedTitlesFromAI = new Set(titles.map(normalize));
|
||||
|
||||
const matchedItems = [];
|
||||
const seenTitles = new Set();
|
||||
|
||||
for (const localItem of allLocalItems) {
|
||||
const normalizedLocalTitle = normalize(localItem.title);
|
||||
if (normalizedTitlesFromAI.has(normalizedLocalTitle) && !seenTitles.has(normalizedLocalTitle)) {
|
||||
matchedItems.push({ title: localItem.title, type });
|
||||
seenTitles.add(normalizedLocalTitle);
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedItems.length === 0) {
|
||||
return JSON.stringify({ success: true, message: _('aiToolM3UNoLocalMatchesForDownload') });
|
||||
}
|
||||
|
||||
downloadM3U(matchedItems, null, filename);
|
||||
|
||||
return JSON.stringify({ success: true, message: _('aiToolM3UDownloadStarted', [String(matchedItems.length), String(titles.length)]) });
|
||||
|
||||
} catch (error) {
|
||||
return JSON.stringify({ success: false, message: `Error creating playlist: ${error.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
async "toggle_favorite"({ title, type }) {
|
||||
const item = await this.findTmdbContent(title, type);
|
||||
if (!item) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
|
||||
}
|
||||
toggleFavorite(item.id, type);
|
||||
const isFavorite = state.favorites.some(fav => fav.id === item.id && fav.type === type);
|
||||
const message = isFavorite ? _('aiToolFavoriteAdded', title) : _('aiToolFavoriteRemoved', title);
|
||||
return JSON.stringify({ success: true, message });
|
||||
}
|
||||
|
||||
async "get_recommendations"() {
|
||||
switchView('recommendations');
|
||||
return JSON.stringify({ success: true, message: _('aiToolRecommendationsSuccess') });
|
||||
}
|
||||
|
||||
async "apply_filters"({ type, genre, year, sort }) {
|
||||
switchView(type === 'movie' ? 'movies' : 'series');
|
||||
let genreId = '';
|
||||
if (genre) {
|
||||
genreId = await this.getGenreId(genre, type);
|
||||
if (!genreId) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolApplyFiltersGenreNotFound', genre) });
|
||||
}
|
||||
}
|
||||
state.currentParams.genre = genreId;
|
||||
state.currentParams.year = year || '';
|
||||
state.currentParams.sort = sort || 'popularity.desc';
|
||||
applyUIFilters();
|
||||
return JSON.stringify({ success: true, message: _('aiToolApplyFiltersSuccess') });
|
||||
}
|
||||
|
||||
async "play_music_by_artist"({ artist_name }) {
|
||||
if (!state.musicPlayer || !state.musicPlayer.isReady) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolPlayMusicNotReady') });
|
||||
}
|
||||
const allArtists = state.musicPlayer._generateFullArtistListForToken('all');
|
||||
const searchTerm = artist_name.toLowerCase().trim();
|
||||
const artist = allArtists.find(a => a.title.toLowerCase() === searchTerm);
|
||||
if (!artist) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolPlayMusicArtistNotFound', artist_name) });
|
||||
}
|
||||
state.musicPlayer.showPlayer();
|
||||
const songs = await state.musicPlayer.getArtistSongs(artist);
|
||||
if (!songs || songs.length === 0) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolPlayMusicNoSongs', artist_name) });
|
||||
}
|
||||
state.musicPlayer.cancionesActuales = songs;
|
||||
state.musicPlayer.playSong(0);
|
||||
return JSON.stringify({ success: true, message: _('aiToolPlayMusicSuccess', artist_name) });
|
||||
}
|
||||
|
||||
async "clear_chat_history"() {
|
||||
try {
|
||||
this.chat.clearHistory();
|
||||
return JSON.stringify({ success: true, message: _('aiToolChatHistoryCleared') });
|
||||
} catch (error) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolChatHistoryClearError', error.message) });
|
||||
}
|
||||
}
|
||||
|
||||
async "delete_database"() {
|
||||
try {
|
||||
if (confirm(_('aiToolConfirmDeleteDatabase'))) {
|
||||
if (state.db) {
|
||||
state.db.close();
|
||||
}
|
||||
const deleteRequest = indexedDB.deleteDatabase(config.dbName);
|
||||
return new Promise((resolve) => {
|
||||
deleteRequest.onsuccess = () => {
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
resolve(JSON.stringify({ success: true, message: _('aiToolDatabaseDeleted') }));
|
||||
};
|
||||
deleteRequest.onerror = (event) => {
|
||||
resolve(JSON.stringify({ success: false, message: _('aiToolDatabaseDeleteError', event.target.error) }));
|
||||
};
|
||||
deleteRequest.onblocked = () => {
|
||||
resolve(JSON.stringify({ success: false, message: _('aiToolDatabaseDeleteBlocked') }));
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return JSON.stringify({ success: false, message: _('aiToolDeleteDatabaseCancelled') });
|
||||
}
|
||||
} catch (error) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolDatabaseDeleteError', error.message) });
|
||||
}
|
||||
}
|
||||
|
||||
async "update_all_tokens"() {
|
||||
try {
|
||||
await updateAllTokens();
|
||||
return JSON.stringify({ success: true, message: _('aiToolUpdateAllTokensSuccess') });
|
||||
} catch (error) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolUpdateAllTokensError', error.message) });
|
||||
}
|
||||
}
|
||||
|
||||
async "add_plex_token"({ token }) {
|
||||
try {
|
||||
await addPlexToken(token);
|
||||
return JSON.stringify({ success: true, message: _('aiToolAddPlexTokenSuccess') });
|
||||
} catch (error) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolAddPlexTokenError', error.message) });
|
||||
}
|
||||
}
|
||||
|
||||
async "change_region"({ region }) {
|
||||
try {
|
||||
state.settings.watchRegion = region;
|
||||
await addItemsToStore('settings', [{ id: 'user_settings', ...state.settings }]);
|
||||
await loadInitialContent();
|
||||
if (['movies', 'series', 'search'].includes(state.currentView)) {
|
||||
await loadContent();
|
||||
}
|
||||
return JSON.stringify({ success: true, message: _('aiToolChangeRegionSuccess', region) });
|
||||
} catch (error) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolChangeRegionError', error.message) });
|
||||
}
|
||||
}
|
||||
|
||||
async "clear_all_favorites"() {
|
||||
try {
|
||||
clearAllFavorites();
|
||||
return JSON.stringify({ success: true, message: _('aiToolFavoritesCleared') });
|
||||
} catch (error) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolFavoritesClearError', error.message) });
|
||||
}
|
||||
}
|
||||
|
||||
async "clear_viewing_history"() {
|
||||
try {
|
||||
clearAllHistory();
|
||||
return JSON.stringify({ success: true, message: "Viewing history has been cleared." });
|
||||
} catch (error) {
|
||||
return JSON.stringify({ success: false, message: `Failed to clear viewing history: ${error.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
async "clear_recommendations_view"() {
|
||||
try {
|
||||
clearRecommendations();
|
||||
return JSON.stringify({ success: true, message: _('aiToolRecommendationsCleared') });
|
||||
} catch (error) {
|
||||
return JSON.stringify({ success: false, message: _('aiToolRecommendationsClearError', error.message) });
|
||||
}
|
||||
}
|
||||
|
||||
async findLocalContent(title, type) {
|
||||
const movieEntries = await getFromDB('movies');
|
||||
const seriesEntries = await getFromDB('series');
|
||||
const allContent = [
|
||||
...movieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))),
|
||||
...seriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' })))
|
||||
];
|
||||
const searchTerm = title.toLowerCase().trim();
|
||||
return allContent.find(item => item.title.toLowerCase() === searchTerm && (!type || item.type === type));
|
||||
}
|
||||
|
||||
async findTmdbContent(title, type) {
|
||||
try {
|
||||
const searchResults = await fetchTMDB(`search/${type}?query=${encodeURIComponent(title)}`);
|
||||
if (searchResults && searchResults.results.length > 0) {
|
||||
const item = searchResults.results[0];
|
||||
return { id: item.id, type };
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getGenreId(genreName, type) {
|
||||
if (!this.genreCache[type]) {
|
||||
try {
|
||||
const data = await fetchTMDB(`genre/${type}/list`);
|
||||
this.genreCache[type] = data.genres;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const genre = this.genreCache[type].find(g => g.name.toLowerCase() === genreName.toLowerCase());
|
||||
return genre ? genre.id : null;
|
||||
}
|
||||
|
||||
async executeTool(toolCall) {
|
||||
const functionName = toolCall.function.name;
|
||||
const args = JSON.parse(toolCall.function.arguments);
|
||||
|
||||
if (typeof this[functionName] === 'function') {
|
||||
this.chat.addTypingIndicator();
|
||||
try {
|
||||
const result = await this[functionName](args);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = _('aiToolExecutionError', functionName, error.message);
|
||||
this.chat.addMessage(errorMessage, 'tool-result', true, functionName);
|
||||
return JSON.stringify({ success: false, error: errorMessage });
|
||||
} finally {
|
||||
this.chat.removeTypingIndicator();
|
||||
}
|
||||
} else {
|
||||
const errorMessage = _('aiToolUnknown', functionName);
|
||||
this.chat.addMessage(errorMessage, 'tool-result', true, functionName);
|
||||
return JSON.stringify({ success: false, error: errorMessage });
|
||||
}
|
||||
}
|
||||
}
|
202
js/chat.js
202
js/chat.js
@ -1,7 +1,6 @@
|
||||
import { state } from './state.js';
|
||||
import { showNotification, _ } from './utils.js';
|
||||
import { getFromDB } from './db.js';
|
||||
import { showItemDetails, addStreamToList, downloadM3U } from './ui.js';
|
||||
import { AITools } from './ai-tools.js';
|
||||
|
||||
export class Chat {
|
||||
constructor() {
|
||||
@ -19,6 +18,7 @@ export class Chat {
|
||||
this.isDragging = false;
|
||||
this.offset = { x: 0, y: 0 };
|
||||
this.conversationHistory = [];
|
||||
this.aiTools = new AITools(this);
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
@ -60,8 +60,9 @@ export class Chat {
|
||||
gsap.to(this.dom.fab, { scale: 0, opacity: 0, duration: 0.2, ease: 'power2.in' });
|
||||
|
||||
if (this.conversationHistory.length === 0) {
|
||||
this.addMessage(_('chatWelcome'), 'assistant');
|
||||
this.conversationHistory.push({ role: 'assistant', content: _('chatWelcome') });
|
||||
const welcomeMessage = _('chatWelcome');
|
||||
this.addMessage(welcomeMessage, 'assistant');
|
||||
this.conversationHistory.push({ role: 'assistant', content: welcomeMessage });
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,10 +88,14 @@ export class Chat {
|
||||
this.addTypingIndicator();
|
||||
|
||||
try {
|
||||
const response = await this.getOpenAIResponse();
|
||||
const response = await this.getOpenAIResponseWithTools();
|
||||
this.removeTypingIndicator();
|
||||
this.addMessage(response, 'assistant');
|
||||
this.conversationHistory.push({ role: 'assistant', content: response });
|
||||
|
||||
if (response) {
|
||||
this.addMessage(response, 'assistant');
|
||||
this.conversationHistory.push({ role: 'assistant', content: response });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.removeTypingIndicator();
|
||||
this.addMessage(error.message, 'assistant', true);
|
||||
@ -99,7 +104,7 @@ export class Chat {
|
||||
}
|
||||
}
|
||||
|
||||
addMessage(text, sender, isError = false) {
|
||||
addMessage(text, sender, isError = false, toolName = null) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = `message-wrapper ${sender}-wrapper`;
|
||||
|
||||
@ -107,20 +112,26 @@ export class Chat {
|
||||
messageEl.classList.add('message', `${sender}-message`);
|
||||
if(isError) messageEl.style.color = 'var(--danger)';
|
||||
|
||||
if (sender === 'assistant') {
|
||||
if (sender === 'assistant' || sender === 'tool-call' || sender === 'tool-result') {
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = 'avatar';
|
||||
avatar.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm-2.5-5h5v2h-5v-2z"/></svg>`;
|
||||
let icon = '';
|
||||
if (sender === 'assistant') {
|
||||
icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm-2.5-5h5v2h-5v-2z"/></svg>';
|
||||
} else {
|
||||
icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/></svg>';
|
||||
}
|
||||
avatar.innerHTML = icon;
|
||||
wrapper.appendChild(avatar);
|
||||
}
|
||||
|
||||
const p = document.createElement('p');
|
||||
p.textContent = text;
|
||||
messageEl.appendChild(p);
|
||||
|
||||
if (sender === 'assistant' && !isError) {
|
||||
this.addMediaActionButtons(messageEl, text);
|
||||
if (sender === 'tool-call' || sender === 'tool-result') {
|
||||
p.innerHTML = `<strong>${toolName}:</strong> ${text}`;
|
||||
} else {
|
||||
p.textContent = text;
|
||||
}
|
||||
messageEl.appendChild(p);
|
||||
|
||||
wrapper.appendChild(messageEl);
|
||||
this.dom.messagesContainer.appendChild(wrapper);
|
||||
@ -151,48 +162,18 @@ export class Chat {
|
||||
if (indicator) indicator.remove();
|
||||
}
|
||||
|
||||
async getLocalContent() {
|
||||
const movieEntries = await getFromDB('movies');
|
||||
const seriesEntries = await getFromDB('series');
|
||||
|
||||
const processEntries = (entries, type) => {
|
||||
return entries.flatMap(entry =>
|
||||
(entry.titulos || []).map(titulo => ({ ...titulo, type }))
|
||||
);
|
||||
};
|
||||
|
||||
const movies = processEntries(movieEntries, 'movie');
|
||||
const series = processEntries(seriesEntries, 'show');
|
||||
|
||||
return { movies, series };
|
||||
}
|
||||
|
||||
async findLocalContent(title) {
|
||||
const { movies, series } = await this.getLocalContent();
|
||||
const searchTerm = title.trim().toLowerCase();
|
||||
|
||||
const allContent = [...movies, ...series];
|
||||
|
||||
let foundContent = allContent.find(m => m.title.toLowerCase() === searchTerm);
|
||||
if (foundContent) return foundContent;
|
||||
|
||||
foundContent = allContent.find(m => m.title.toLowerCase().includes(searchTerm));
|
||||
return foundContent;
|
||||
}
|
||||
|
||||
async getOpenAIResponse() {
|
||||
async getOpenAIResponseWithTools() {
|
||||
const apiKey = state.settings.openaiApiKey;
|
||||
if (!apiKey) {
|
||||
return _('chatApiKeyMissing');
|
||||
}
|
||||
|
||||
const systemPrompt = `You are a helpful assistant for a movie and TV show enthusiast named CinePlex Assistant. Your main goal is to provide information about films, series, etc. When you identify a movie or series title in your response, you MUST enclose it in double quotes, for example: "The Matrix". Do not mention the user's local library or your ability to access it.`;
|
||||
|
||||
|
||||
const systemPrompt = _('aiSystemPrompt_v2');
|
||||
const messagesForApi = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...this.conversationHistory
|
||||
];
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.proxyapi.ru/openai/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
@ -203,30 +184,38 @@ export class Chat {
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o-2024-05-13',
|
||||
messages: messagesForApi,
|
||||
max_tokens: 250,
|
||||
tools: this.aiTools.toolDefinitions,
|
||||
tool_choice: 'auto',
|
||||
max_tokens: 400,
|
||||
temperature: 0.7
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
const errorMessage = errorData?.error?.message || errorData?.error || `Error HTTP ${response.status}`;
|
||||
console.error('Proxy API Error Response:', errorData);
|
||||
throw new Error(errorMessage);
|
||||
throw new Error(errorData?.error?.message || `Error HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) {
|
||||
return data.choices[0].message.content;
|
||||
const message = data.choices[0].message;
|
||||
|
||||
if (message.tool_calls) {
|
||||
this.conversationHistory.push(message);
|
||||
for (const toolCall of message.tool_calls) {
|
||||
const toolResult = await this.aiTools.executeTool(toolCall);
|
||||
this.conversationHistory.push({ role: 'tool', tool_call_id: toolCall.id, name: toolCall.function.name, content: toolResult });
|
||||
}
|
||||
// Now call the API again with the tool results
|
||||
return await this.getOpenAIResponseWithTools();
|
||||
} else if (message.content) {
|
||||
return message.content;
|
||||
} else {
|
||||
console.error('Invalid successful response format from proxy:', data);
|
||||
throw new Error(_('chatApiInvalidResponse'));
|
||||
return _('chatApiInvalidResponse');
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('OpenAI Proxy API call failed:', error);
|
||||
showNotification(_('chatApiError'), 'error');
|
||||
console.error('OpenAI API call failed:', error);
|
||||
showNotification(_('chatApiError') + `: ${error.message}`, 'error');
|
||||
return _('chatApiError') + `: ${error.message}`;
|
||||
}
|
||||
}
|
||||
@ -282,82 +271,11 @@ export class Chat {
|
||||
this.dom.header.style.cursor = 'move';
|
||||
}
|
||||
|
||||
async addMediaActionButtons(messageEl, text) {
|
||||
const contentTitles = this.extractContentTitles(text);
|
||||
if (contentTitles.length === 0) return;
|
||||
|
||||
const allContent = [];
|
||||
for (const title of contentTitles) {
|
||||
const content = await this.findLocalContent(title);
|
||||
if (content) {
|
||||
allContent.push(content);
|
||||
}
|
||||
}
|
||||
|
||||
if (allContent.length > 0) {
|
||||
allContent.forEach(content => {
|
||||
const itemContainer = document.createElement('div');
|
||||
itemContainer.className = 'chat-item-actions';
|
||||
|
||||
const titleEl = document.createElement('span');
|
||||
titleEl.className = 'chat-action-title';
|
||||
titleEl.textContent = content.title;
|
||||
itemContainer.appendChild(titleEl);
|
||||
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.className = 'chat-action-buttons';
|
||||
|
||||
const buttons = [
|
||||
{ label: _('moreInfo'), action: 'info', icon: 'ℹ️' },
|
||||
{ label: _('addStream'), action: 'stream', icon: '▶️' },
|
||||
{ label: _('download'), action: 'download', icon: '⬇️' }
|
||||
];
|
||||
|
||||
buttons.forEach(btnInfo => {
|
||||
const button = document.createElement('button');
|
||||
button.innerHTML = `${btnInfo.icon} ${btnInfo.label}`;
|
||||
button.addEventListener('click', () => this.handleMediaAction(btnInfo.action, content, button));
|
||||
buttonContainer.appendChild(button);
|
||||
});
|
||||
itemContainer.appendChild(buttonContainer);
|
||||
messageEl.appendChild(itemContainer);
|
||||
});
|
||||
|
||||
if (allContent.length > 1) {
|
||||
const downloadAllButtonContainer = document.createElement('div');
|
||||
downloadAllButtonContainer.className = 'chat-action-buttons chat-download-all';
|
||||
const downloadAllButton = document.createElement('button');
|
||||
downloadAllButton.innerHTML = `⬇️ ${_('downloadAll')}`;
|
||||
downloadAllButton.addEventListener('click', (e) => {
|
||||
const streams = allContent.map(content => ({ title: content.title, type: content.type }));
|
||||
downloadM3U(streams, e.target);
|
||||
});
|
||||
downloadAllButtonContainer.appendChild(downloadAllButton);
|
||||
messageEl.appendChild(downloadAllButtonContainer);
|
||||
}
|
||||
}
|
||||
clearHistory() {
|
||||
this.conversationHistory = [];
|
||||
this.dom.messagesContainer.innerHTML = '';
|
||||
const welcomeMessage = _('chatWelcome');
|
||||
this.addMessage(welcomeMessage, 'assistant');
|
||||
this.conversationHistory.push({ role: 'assistant', content: welcomeMessage });
|
||||
}
|
||||
|
||||
extractContentTitles(text) {
|
||||
const matches = text.match(/"([^"]+)"/g);
|
||||
if (!matches) return [];
|
||||
return matches.map(match => match.substring(1, match.length - 1));
|
||||
}
|
||||
|
||||
handleMediaAction(action, content, buttonEl) {
|
||||
switch (action) {
|
||||
case 'info':
|
||||
showItemDetails(Number(content.id), content.type);
|
||||
this.close();
|
||||
break;
|
||||
case 'stream':
|
||||
addStreamToList(content.title, content.type, buttonEl);
|
||||
break;
|
||||
case 'download':
|
||||
downloadM3U(content.title, content.type, buttonEl);
|
||||
break;
|
||||
default:
|
||||
showNotification(`Acción desconocida: ${action}`, 'warning');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -520,6 +520,24 @@ export class MusicPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
async getArtistSongs(artist) {
|
||||
if (!this.isReady) return;
|
||||
const { id: artistaId, isJellyfin, serverUrl, userId, token, protocolo, ip, puerto } = artist;
|
||||
|
||||
try {
|
||||
let canciones;
|
||||
if (isJellyfin) {
|
||||
canciones = await getMusicUrlsFromJellyfin(serverUrl, userId, token, artistaId);
|
||||
} else {
|
||||
canciones = await getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId);
|
||||
}
|
||||
return canciones;
|
||||
} catch (error) {
|
||||
showNotification(_('errorFetchingArtistSongs'), "error");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
handleSongsLoaded(canciones, artistId) {
|
||||
if (!this.isReady) return;
|
||||
if (!Array.isArray(canciones)) canciones = [];
|
||||
@ -684,26 +702,35 @@ export class MusicPlayer {
|
||||
}
|
||||
|
||||
this.audioPlayer.load();
|
||||
this.audioPlayer.play().then(() => {
|
||||
this.isPlaying = true;
|
||||
document.getElementById('playPauseBtn').innerHTML = '<i class="fas fa-pause"></i>';
|
||||
this.currentSongId = cancion.id;
|
||||
this.currentSongArtistId = cancion.artistId;
|
||||
this.markCurrentSong();
|
||||
this.ensureArtistVisible(cancion.artistId);
|
||||
if (playIconElement) {
|
||||
playIconElement.className = 'fas fa-play play-icon';
|
||||
}
|
||||
if (!this.miniplayerManuallyClosed) {
|
||||
document.getElementById('fab-music-player').style.display = 'none';
|
||||
}
|
||||
this.preloadNextSong();
|
||||
}).catch((error) => {
|
||||
this.handleAudioError(_('playbackError'));
|
||||
if (playIconElement) {
|
||||
playIconElement.className = 'fas fa-play play-icon';
|
||||
}
|
||||
});
|
||||
const playPromise = this.audioPlayer.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(() => {
|
||||
this.isPlaying = true;
|
||||
document.getElementById('playPauseBtn').innerHTML = '<i class="fas fa-pause"></i>';
|
||||
this.currentSongId = cancion.id;
|
||||
this.currentSongArtistId = cancion.artistId;
|
||||
this.markCurrentSong();
|
||||
this.ensureArtistVisible(cancion.artistId);
|
||||
if (playIconElement) {
|
||||
playIconElement.className = 'fas fa-play play-icon';
|
||||
}
|
||||
if (!this.miniplayerManuallyClosed) {
|
||||
document.getElementById('fab-music-player').style.display = 'none';
|
||||
}
|
||||
this.preloadNextSong();
|
||||
}).catch(error => {
|
||||
if (error.name === 'NotAllowedError') {
|
||||
this.isPlaying = false;
|
||||
document.getElementById('playPauseBtn').innerHTML = '<i class="fas fa-play"></i>';
|
||||
showNotification(_('autoplayBlocked'), 'warning');
|
||||
} else {
|
||||
this.handleAudioError(_('playbackError'));
|
||||
}
|
||||
if (playIconElement) {
|
||||
playIconElement.className = 'fas fa-play play-icon';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getMimeType(extension) {
|
||||
|
33
js/plex.js
33
js/plex.js
@ -265,4 +265,35 @@ export async function startPlexScan(tipos) {
|
||||
state.plexScanAbortController = null;
|
||||
ocultarSpinner();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function stopPlexScan() {
|
||||
if (state.plexScanAbortController) {
|
||||
state.plexScanAbortController.abort();
|
||||
logToConsole(_('stoppingPlexScan'));
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAllTokens() {
|
||||
const tipos = ['movies', 'series', 'artists', 'photos'];
|
||||
await startPlexScan(tipos);
|
||||
}
|
||||
|
||||
export async function addPlexToken(token) {
|
||||
if (!token || typeof token !== 'string') {
|
||||
showNotification(_('invalidTokenProvided'), 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const existingTokens = await getFromDB('tokens');
|
||||
if (existingTokens.some(t => t.token === token)) {
|
||||
showNotification(_('tokenAlreadyExists'), 'warning');
|
||||
return;
|
||||
}
|
||||
await addItemsToStore('tokens', [{ token: token }]);
|
||||
showNotification(_('tokenAddedSuccessfully'), 'success');
|
||||
emitirEventoActualizacion();
|
||||
} catch (error) {
|
||||
showNotification(_('errorAddingToken', error.message), 'error');
|
||||
}
|
||||
}
|
||||
|
68
js/ui.js
68
js/ui.js
@ -1462,7 +1462,56 @@ export async function addStreamToList(title, type, buttonElement = null) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadM3U(items, buttonElement = null) { if (state.isDownloadingM3U) return; state.isDownloadingM3U = true; let originalButtonContent = null; if (buttonElement) { originalButtonContent = buttonElement.innerHTML; buttonElement.disabled = true; buttonElement.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>`; } const itemsArray = Array.isArray(items) ? items : [{ title: items, type: arguments[1] }]; const collectiveTitle = itemsArray.length > 1 ? 'CinePlex_Playlist' : itemsArray[0].title; showNotification(_('generatingM3U', collectiveTitle), "info"); try { let m3uContent = "#EXTM3U\n"; let streamsFound = 0; for (const item of itemsArray) { try { const streamData = await fetchAllAvailableStreams(item.title, item.type); if (streamData.success && streamData.streams.length > 0) { streamsFound += streamData.streams.length; streamData.streams.forEach(stream => { m3uContent += `${stream.extinf}\n${stream.url}\n`; }); } } catch (error) { console.warn(`Could not fetch streams for ${item.title}:`, error); } } if (streamsFound === 0) throw new Error(_('noStreamsFoundForSelection')); const blob = new Blob([m3uContent], { type: "audio/x-mpegurl;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${collectiveTitle.replace(/[^a-z0-9]/gi, '_')}.m3u`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showNotification(_('m3uDownloaded', collectiveTitle), 'success'); } catch (error) { showNotification(_('errorGeneratingM3U', error.message), "error"); } finally { state.isDownloadingM3U = false; if (buttonElement && originalButtonContent) { buttonElement.innerHTML = originalButtonContent; buttonElement.disabled = false; } }}
|
||||
export async function downloadM3U(items, buttonElement = null, customFilename = null) {
|
||||
if (state.isDownloadingM3U) return;
|
||||
state.isDownloadingM3U = true;
|
||||
let originalButtonContent = null;
|
||||
if (buttonElement) {
|
||||
originalButtonContent = buttonElement.innerHTML;
|
||||
buttonElement.disabled = true;
|
||||
buttonElement.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>`;
|
||||
}
|
||||
const itemsArray = Array.isArray(items) ? items : [{ title: items, type: arguments[1] }];
|
||||
const collectiveTitle = customFilename || (itemsArray.length > 1 ? 'CinePlex_Playlist' : itemsArray[0].title);
|
||||
showNotification(_('generatingM3U', collectiveTitle), "info");
|
||||
try {
|
||||
let m3uContent = "#EXTM3U\n";
|
||||
let streamsFound = 0;
|
||||
for (const item of itemsArray) {
|
||||
try {
|
||||
showNotification(_('searchingStreams', item.title), 'info');
|
||||
const streamData = await fetchAllAvailableStreams(item.title, item.type);
|
||||
if (streamData.success && streamData.streams.length > 0) {
|
||||
streamsFound += streamData.streams.length;
|
||||
streamData.streams.forEach(stream => {
|
||||
m3uContent += `${stream.extinf}\n${stream.url}\n`;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Could not fetch streams for ${item.title}:`, error);
|
||||
}
|
||||
}
|
||||
if (streamsFound === 0) throw new Error(_('noStreamsFoundForSelection'));
|
||||
const blob = new Blob([m3uContent], { type: "audio/x-mpegurl;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${collectiveTitle.replace(/[^a-z0-9]/gi, '_')}.m3u`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
showNotification(_('m3uDownloaded', collectiveTitle), 'success');
|
||||
} catch (error) {
|
||||
showNotification(_('errorGeneratingM3U', error.message), "error");
|
||||
} finally {
|
||||
state.isDownloadingM3U = false;
|
||||
if (buttonElement && originalButtonContent) {
|
||||
buttonElement.innerHTML = originalButtonContent;
|
||||
buttonElement.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function showTrailer(key) {
|
||||
const lightbox = document.getElementById('video-lightbox');
|
||||
@ -1928,6 +1977,23 @@ export function showPrevPhoto() {
|
||||
updatePhotoLightbox();
|
||||
}
|
||||
|
||||
export function clearAllFavorites() {
|
||||
state.favorites = [];
|
||||
localStorage.setItem('cineplex_favorites', JSON.stringify(state.favorites));
|
||||
if (state.currentView === 'favorites') {
|
||||
loadFavorites();
|
||||
}
|
||||
showNotification(_('aiToolFavoritesCleared'), 'success');
|
||||
}
|
||||
|
||||
export function clearRecommendations() {
|
||||
const grid = document.getElementById('recommendations-grid');
|
||||
if(grid) {
|
||||
grid.innerHTML = `<div class="empty-state"><i class="fas fa-user-astronaut fa-3x mb-3"></i><p class="lead">${_('noRecommendations')}</p></div>`;
|
||||
}
|
||||
sessionStorage.removeItem('cineplex_recommendations');
|
||||
showNotification(_('aiToolRecommendationsCleared'), 'success');
|
||||
}
|
||||
|
||||
|
||||
export async function loadProviders() {
|
||||
|
@ -786,6 +786,7 @@
|
||||
<script type="module" src="js/main.js"></script>
|
||||
<script type="module" src="js/activityViewer.js"></script>
|
||||
<script type="module" src="js/m3u-generator.js"></script>
|
||||
<script type="module" src="js/ai-tools.js"></script>
|
||||
<script type="module" src="js/chat.js"></script>
|
||||
|
||||
</body>
|
||||
|
Loading…
x
Reference in New Issue
Block a user