This commit is contained in:
Filipinos 2025-08-16 10:53:11 +02:00
parent 4dc6a89cfe
commit e1abe972db
44 changed files with 11512 additions and 7530 deletions

View File

@ -1,449 +1,516 @@
{ {
"appName": { "message": "CinePlex" }, "appName": { "message": "CinePlex" },
"appDescription": { "message": "Sucht Plex-Server nach Inhalten und zeigt sie in der Benutzeroberfläche an" }, "appDescription": { "message": "Scannt Plex-Server, um Inhalte zu finden, und zeigt sie in der Benutzeroberfläche an" },
"appTagline": { "message": "Filme, Serien und Musik" }, "appTagline": { "message": "Filme, Serien und Musik" },
"appLocaleCode": { "message": "de-DE" }, "appLocaleCode": { "message": "de-DE" },
"toggleNavigation": { "message": "Navigation umschalten" }, "toggleNavigation": { "message": "Navigation umschalten" },
"searchPlaceholder": { "message": "Suche nach Filmen oder Serien..." }, "searchPlaceholder": { "message": "Suche nach Filmen oder Serien..." },
"openMusicPlayer": { "message": "Musik-Player öffnen" }, "openMusicPlayer": { "message": "Musikplayer öffnen" },
"settings": { "message": "Einstellungen" }, "settings": { "message": "Einstellungen" },
"navMovies": { "message": "Filme" }, "navMovies": { "message": "Filme" },
"navSeries": { "message": "Serien" }, "navSeries": { "message": "Serien" },
"navProviders": { "message": "Anbieter" }, "navProviders": { "message": "Anbieter" },
"navPhotos": { "message": "Fotos" }, "navPhotos": { "message": "Fotos" },
"navStats": { "message": "Statistiken" }, "navStats": { "message": "Statistiken" },
"navFavorites": { "message": "Favoriten" }, "navFavorites": { "message": "Favoriten" },
"navHistory": { "message": "Verlauf" }, "navHistory": { "message": "Verlauf" },
"navRecommendations": { "message": "Empfehlungen" }, "navRecommendations": { "message": "Empfehlungen" },
"navMusic": { "message": "Musik" }, "navMusic": { "message": "Musik" },
"navM3uGenerator": { "message": "M3U-Generator" }, "musicFeaturedPlaylists": { "message": "Empfohlene Playlists" },
"heroWelcome": { "message": "" }, "musicRecentlyAdded": { "message": "Kürzlich hinzugefügt" },
"heroSubtitle": { "message": "Entdecken Sie Tausende von Filmen und Serien." }, "navM3uGenerator": { "message": "M3U-Generator" },
"addStream": { "message": "Stream hinzufügen" }, "heroWelcome": { "message": "" },
"moreInfo": { "message": "Mehr Info" }, "heroSubtitle": { "message": "Entdecke Tausende von Filmen und Serien." },
"popularMovies": { "message": "Beliebte Filme" }, "addStream": { "message": "Stream hinzufügen" },
"allGenres": { "message": "Alle Genres" }, "moreInfo": { "message": "Mehr Informationen" },
"allYears": { "message": "Alle Jahre" }, "popularMovies": { "message": "Beliebte Filme" },
"sortPopular": { "message": "Am beliebtesten" }, "allGenres": { "message": "Alle Genres" },
"sortTopRated": { "message": "Am besten bewertet" }, "allYears": { "message": "Alle Jahre" },
"sortRecent": { "message": "Neueste" }, "sortPopular": { "message": "Am beliebtesten" },
"loadMore": { "message": "Mehr laden" }, "sortTopRated": { "message": "Am besten bewertet" },
"photosBreadcrumbHome": { "message": "Alben" }, "sortRecent": { "message": "Am neuesten" },
"selectServer": { "message": "Wählen Sie einen Server" }, "loadMore": { "message": "Mehr laden" },
"loading": { "message": "Laden..." }, "photosBreadcrumbHome": { "message": "Alben" },
"loadingLibraries": { "message": "Lade Bibliotheken..." }, "selectServer": { "message": "Wähle einen Server" },
"photosEmptyState": { "message": "Keine Alben oder Fotos gefunden." }, "loading": { "message": "Wird geladen..." },
"photosEmptyStateSub": { "message": "Bitte wählen Sie einen Server aus oder stellen Sie sicher, dass Sie eine Fotobibliothek in Plex haben." }, "loadingLibraries": { "message": "Bibliotheken werden geladen..." },
"statsTitle": { "message": "Bibliotheksstatistiken" }, "photosEmptyState": { "message": "Keine Alben oder Fotos gefunden." },
"statsAllTokens": { "message": "Alle Token" }, "photosEmptyStateSub": { "message": "Bitte wähle einen Server aus oder stelle sicher, dass du eine Fotobibliothek in Plex hast." },
"statsAnalyzing": { "message": "Analysiere deine Bibliothek..." }, "statsTitle": { "message": "Bibliotheksstatistiken" },
"statsActiveTokens": { "message": "Aktive Token" }, "statsAllTokens": { "message": "Alle Tokens" },
"statsServersFound": { "message": "Gefundene Server" }, "statsAnalyzing": { "message": "Analysiere deine Bibliothek..." },
"statsUniqueMovies": { "message": "Einzigartige Filme" }, "statsActiveTokens": { "message": "Aktive Tokens" },
"statsUniqueSeries": { "message": "Einzigartige Serien" }, "statsServersFound": { "message": "Gefundene Server" },
"statsUniqueArtists": { "message": "Einzigartige Künstler" }, "statsUniqueMovies": { "message": "Einzigartige Filme" },
"statsTokenServers": { "message": "Token-Server" }, "statsUniqueSeries": { "message": "Einzigartige Serien" },
"statsChartMoviesByGenre": { "message": "Inhalt nach Genre (Filme)" }, "statsUniqueArtists": { "message": "Einzigartige Künstler" },
"statsChartSeriesByGenre": { "message": "Inhalt nach Genre (Serien)" }, "statsTokenServers": { "message": "Server des Tokens" },
"statsChartByDecade": { "message": "Inhalt nach Jahrzehnt" }, "statsChartMoviesByGenre": { "message": "Inhalt nach Genre (Filme)" },
"recommendationsTitle": { "message": "Empfehlungen für dich" }, "statsChartSeriesByGenre": { "message": "Inhalt nach Genre (Serien)" },
"historyTitle": { "message": "Wiedergabeverlauf" }, "statsChartByDecade": { "message": "Inhalt nach Jahrzehnt" },
"clearHistory": { "message": "Alles löschen" }, "recommendationsTitle": { "message": "Empfehlungen für dich" },
"consoleTitle": { "message": "Plex-Scan-Konsole" }, "historyTitle": { "message": "Wiedergabeverlauf" },
"footerCredit": { "message": "Eine Oberfläche für Ihr Plex-Universum." }, "clearHistory": { "message": "Alles löschen" },
"closeTrailer": { "message": "Trailer schließen" }, "consoleTitle": { "message": "Plex-Scan-Konsole" },
"close": { "message": "Schließen" }, "footerCredit": { "message": "Eine Benutzeroberfläche für dein Plex-Universum." },
"photoViewer": { "message": "Fotobetrachter" }, "closeTrailer": { "message": "Trailer schließen" },
"previous": { "message": "Zurück" }, "close": { "message": "Schließen" },
"next": { "message": "Weiter" }, "photoViewer": { "message": "Fotoanzeige" },
"notificationTemplateText": { "message": "Benachrichtigung" }, "previous": { "message": "Zurück" },
"settingsTitleFull": { "message": "Einstellungen und Konfiguration" }, "next": { "message": "Weiter" },
"settingsTabGeneral": { "message": "Allgemein" }, "notificationTemplateText": { "message": "Benachrichtigung" },
"settingsTabPlex": { "message": "Plex" }, "settingsTitleFull": { "message": "Einstellungen und Konfiguration" },
"settingsTabJellyfin": { "message": "Jellyfin" }, "settingsTabGeneral": { "message": "Allgemein" },
"settingsTabPhpGen": { "message": "PHP-Generator" }, "settingsTabPlex": { "message": "Plex" },
"settingsTabData": { "message": "Daten" }, "settingsTabJellyfin": { "message": "Jellyfin" },
"settingsApiServer": { "message": "API- und Server-Einstellungen" }, "settingsTabPhpGen": { "message": "PHP-Generator" },
"settingsTmdbApiLabel": { "message": "TMDB-API-Schlüssel (optional)" }, "settingsTabData": { "message": "Daten" },
"settingsTmdbApiPlaceholder": { "message": "Der Standardschlüssel wird verwendet, wenn das Feld leer gelassen wird" }, "settingsApiServer": { "message": "API- und Serverkonfiguration" },
"settingsGoogleApiLabel": { "message": "Google Gemini-API-Schlüssel (optional)" }, "settingsTmdbApiLabel": { "message": "TMDB-API-Schlüssel (Optional)" },
"settingsGoogleApiPlaceholder": { "message": "Wird für die Nutzung des KI-Assistenten benötigt" }, "settingsTmdbApiPlaceholder": { "message": "Der Standardschlüssel wird verwendet, wenn das Feld leer gelassen wird" },
"settingsRegionLabel": { "message": "Region für die Inhaltsentdeckung" }, "settingsGoogleApiLabel": { "message": "Google Gemini API-Schlüssel (Optional)" },
"allRegions": { "message": "Alle Regionen" }, "settingsGoogleApiPlaceholder": { "message": "Erforderlich, um den KI-Assistenten zu verwenden" },
"settingsPhpUrlLabel": { "message": "Server-URL zum Hinzufügen von Streams" }, "settingsRegionLabel": { "message": "Region für die Inhaltssuche" },
"settingsPhpUrlPlaceholder": { "message": "https://ihr-server.com/pfad/zum/skript.php" }, "allRegions": { "message": "Alle Regionen" },
"settingsInterface": { "message": "Benutzeroberfläche" }, "settingsPhpUrlLabel": { "message": "Server-URL zum Hinzufügen von Streams" },
"settingsLightTheme": { "message": "Heller Modus" }, "settingsPhpUrlPlaceholder": { "message": "https://dein-server.com/pfad/zum/skript.php" },
"settingsShowHero": { "message": "Willkommensbereich 'Hero' anzeigen" }, "settingsInterface": { "message": "Benutzeroberfläche" },
"settingsScanContent": { "message": "Inhalt scannen" }, "settingsLightTheme": { "message": "Heller Modus" },
"settingsScanDesc": { "message": "Wählen Sie aus, was gescannt werden soll, und drücken Sie die Taste." }, "settingsShowHero": { "message": "Willkommensbereich 'Hero' anzeigen" },
"settingsScanMovies": { "message": "Filme" }, "settingsScanContent": { "message": "Inhaltsscan" },
"settingsScanShows": { "message": "Serien" }, "settingsScanDesc": { "message": "Wähle aus, was gescannt werden soll, und drücke die Taste." },
"settingsScanArtists": { "message": "Musik" }, "settingsScanMovies": { "message": "Filme" },
"settingsScanPhotos": { "message": "Fotos" }, "settingsScanShows": { "message": "Serien" },
"settingsSelectAll": { "message": "Alles auswählen" }, "settingsScanArtists": { "message": "Musik" },
"settingsStartScan": { "message": "Scan starten" }, "settingsScanPhotos": { "message": "Fotos" },
"settingsPlexTokens": { "message": "Plex-Token" }, "settingsSelectAll": { "message": "Alles auswählen" },
"settingsPlexTokensDesc": { "message": "Bearbeiten Sie die Liste der Plex-Token (JSON-Format)." }, "settingsStartScan": { "message": "Scan starten" },
"settingsSaveTokens": { "message": "Token speichern" }, "settingsPlexTokens": { "message": "Plex-Tokens" },
"settingsJellyfinTitle": { "message": "Jellyfin-Einstellungen" }, "settingsPlexTokensDesc": { "message": "Bearbeite die Liste der Plex-Tokens (JSON-Format)." },
"settingsJellyfinDesc": { "message": "Fügen Sie Ihre Jellyfin-Serverdetails hinzu, um deren Inhalte zu scannen." }, "settingsSaveTokens": { "message": "Tokens speichern" },
"jellyfinUrlLabel": { "message": "Jellyfin-Server-URL" }, "settingsJellyfinTitle": { "message": "Jellyfin-Konfiguration" },
"jellyfinUserLabel": { "message": "Benutzername" }, "settingsJellyfinDesc": { "message": "Füge die Daten deines Jellyfin-Servers hinzu, um dessen Inhalt zu scannen." },
"jellyfinPassLabel": { "message": "Passwort" }, "jellyfinUrlLabel": { "message": "Jellyfin-Server-URL" },
"jellyfinConnectAndScan": { "message": "Verbinden und scannen" }, "jellyfinUserLabel": { "message": "Benutzername" },
"settingsPhpGenTitle": { "message": "PHP-Skript-Generator für Server" }, "jellyfinPassLabel": { "message": "Passwort" },
"settingsPhpFileOptions": { "message": "Dateioptionen" }, "jellyfinConnectAndScan": { "message": "Verbinden und Scannen" },
"settingsPhpSavePathLabel": { "message": "Speicherpfad auf dem Server" }, "settingsPhpGenTitle": { "message": "PHP-Skript-Generator für den Server" },
"settingsPhpSavePathPlaceholder": { "message": "Bsp.: /var/www/html/listen (leer für denselben Ordner)" }, "settingsPhpFileOptions": { "message": "Dateioptionen" },
"settingsPhpFilenameLabel": { "message": "Dateiname" }, "settingsPhpSavePathLabel": { "message": "Speicherpfad auf dem Server" },
"settingsPhpFileAction": { "message": "Dateiaktion" }, "settingsPhpSavePathPlaceholder": { "message": "Bsp: /var/www/html/listen (leer für denselben Ordner)" },
"settingsPhpActionAppend": { "message": "Am Ende der Datei anfügen (kumulativ)" }, "settingsPhpFilenameLabel": { "message": "Dateiname" },
"settingsPhpActionOverwrite": { "message": "Datei überschreiben (von vorne beginnen)" }, "settingsPhpFileAction": { "message": "Aktion für die Datei" },
"settingsPhpSecurity": { "message": "Sicherheit (optional)" }, "settingsPhpActionAppend": { "message": "An das Ende der Datei anhängen (kumulativ)" },
"settingsPhpUseSecretKey": { "message": "Geheimschlüssel verwenden (empfohlen)" }, "settingsPhpActionOverwrite": { "message": "Datei überschreiben (von vorne beginnen)" },
"settingsPhpSecretKeyPlaceholder": { "message": "Geben Sie einen sicheren Geheimschlüssel ein" }, "settingsPhpSecurity": { "message": "Sicherheit (Optional)" },
"settingsPhpGeneratedCode": { "message": "Generierter Code" }, "settingsPhpUseSecretKey": { "message": "Geheimschlüssel verwenden (Empfohlen)" },
"settingsPhpGeneratedPlaceholder": { "message": "Der generierte PHP-Code wird hier angezeigt." }, "settingsPhpSecretKeyPlaceholder": { "message": "Gib einen sicheren Geheimschlüssel ein" },
"settingsGenerateScript": { "message": "Skript generieren" }, "settingsPhpGeneratedCode": { "message": "Generierter Code" },
"settingsCopyScript": { "message": "Skript kopieren" }, "settingsPhpGeneratedPlaceholder": { "message": "Der generierte PHP-Code wird hier angezeigt." },
"settingsDataManagement": { "message": "Lokale Datenbankverwaltung" }, "settingsGenerateScript": { "message": "Skript generieren" },
"settingsImportDb": { "message": "DB aus Datei importieren" }, "settingsCopyScript": { "message": "Skript kopieren" },
"settingsExportDb": { "message": "DB in Datei exportieren" }, "settingsDataManagement": { "message": "Verwaltung der lokalen Datenbank" },
"settingsClearContent": { "message": "Lokale Inhaltsdaten löschen" }, "settingsImportDb": { "message": "DB aus Datei importieren" },
"settingsClearContentDesc": { "message": "Diese Aktion löscht Filme, Serien und Musik aus der lokalen Datenbank, hat aber keine Auswirkungen auf Ihre Favoriten oder Ihre Einstellungen." }, "settingsExportDb": { "message": "DB in Datei exportieren" },
"settingsClose": { "message": "Schließen" }, "settingsClearContent": { "message": "Lokale Inhaltsdaten löschen" },
"settingsSave": { "message": "Einstellungen speichern" }, "settingsClearContentDesc": { "message": "Diese Aktion löscht Filme, Serien und Musik aus der lokalen Datenbank, hat aber keine Auswirkungen auf deine Favoriten oder Einstellungen." },
"musicSidenavTitle": { "message": "Plex-Musik" }, "settingsClose": { "message": "Schließen" },
"musicAllServers": { "message": "Alle Server" }, "settingsSave": { "message": "Einstellungen speichern" },
"musicSearchArtistPlaceholder": { "message": "Suche nach einem Künstler..." }, "musicSidenavTitle": { "message": "Plex-Musik" },
"musicSearchDiscographyPlaceholder": { "message": "In Diskografie suchen..." }, "musicAllServers": { "message": "Alle Server" },
"musicNothingPlaying": { "message": "Nichts wird abgespielt" }, "musicSearchArtistPlaceholder": { "message": "Künstler suchen..." },
"musicSelectSong": { "message": "Wählen Sie ein Lied" }, "musicSearchDiscographyPlaceholder": { "message": "In der Diskografie suchen..." },
"musicToStart": { "message": "um die Wiedergabe zu starten" }, "musicNothingPlaying": { "message": "Nichts wird abgespielt" },
"miniplayerDownloadSong": { "message": "Lied herunterladen" }, "musicSelectSong": { "message": "Wähle ein Lied" },
"miniplayerDownloadAlbum": { "message": "M3U-Album herunterladen" }, "musicToStart": { "message": "um die Wiedergabe zu starten" },
"miniplayerVolume": { "message": "Lautstärke" }, "miniplayerDownloadSong": { "message": "Lied herunterladen" },
"miniplayerShuffle": { "message": "Zufallswiedergabe" }, "miniplayerDownloadAlbum": { "message": "M3U herunterladen" },
"miniplayerEqualizer": { "message": "Equalizer" }, "miniplayerVolume": { "message": "Lautstärke" },
"miniplayerOpenList": { "message": "Liste öffnen" }, "miniplayerShuffle": { "message": "Zufallswiedergabe" },
"eqTitle": { "message": "Grafischer Equalizer" }, "miniplayerEqualizer": { "message": "Equalizer" },
"eqPresetsLabel": { "message": "Voreinstellungen" }, "miniplayerOpenList": { "message": "Liste öffnen" },
"eqPresetFlat": { "message": "Flach" }, "eqTitle": { "message": "Grafischer Equalizer" },
"eqPresetRock": { "message": "Rock" }, "eqPresetsLabel": { "message": "Voreinstellungen" },
"eqPresetPop": { "message": "Pop" }, "eqPresetFlat": { "message": "Linear" },
"eqPresetJazz": { "message": "Jazz" }, "eqPresetRock": { "message": "Rock" },
"eqPresetClassical": { "message": "Klassik" }, "eqPresetPop": { "message": "Pop" },
"eqPresetBassBoost": { "message": "Bass-Boost" }, "eqPresetJazz": { "message": "Jazz" },
"eqPreampLabel": { "message": "Vorverstärker" }, "eqPresetClassical": { "message": "Klassik" },
"infoModalTitle": { "message": "Informationen" }, "eqPresetBassBoost": { "message": "Bassverstärkung" },
"infoModalFieldTitle": { "message": "Titel:" }, "eqPreampLabel": { "message": "Vorverstärker" },
"infoModalFieldArtist": { "message": "Künstler:" }, "infoModalTitle": { "message": "Information" },
"infoModalFieldAlbum": { "message": "Album:" }, "infoModalFieldTitle": { "message": "Titel:" },
"infoModalFieldSong": { "message": "Lied:" }, "infoModalFieldArtist": { "message": "Künstler:" },
"infoModalFieldYear": { "message": "Jahr:" }, "infoModalFieldAlbum": { "message": "Album:" },
"infoModalFieldGenre": { "message": "Genre:" }, "infoModalFieldSong": { "message": "Lied:" },
"lang_en": { "message": "Englisch" }, "infoModalFieldYear": { "message": "Jahr:" },
"lang_es": { "message": "Spanisch" }, "infoModalFieldGenre": { "message": "Genre:" },
"lang_fr": { "message": "Französisch" }, "lang_en": { "message": "Englisch" },
"lang_de": { "message": "Deutsch" }, "lang_es": { "message": "Spanisch" },
"lang_it": { "message": "Italienisch" }, "lang_fr": { "message": "Französisch" },
"lang_pt": { "message": "Portugiesisch" }, "lang_de": { "message": "Deutsch" },
"essentialFeaturesNotSupported": { "message": "Ihr Browser unterstützt keine wesentlichen Funktionen." }, "lang_it": { "message": "Italienisch" },
"dbAccessError": { "message": "Fehler beim Zugriff auf die lokale Datenbank." }, "lang_pt": { "message": "Portugiesisch" },
"dbUpdateNeeded": { "message": "Die Datenbank muss aktualisiert werden, bitte laden Sie die Seite neu." }, "essentialFeaturesNotSupported": { "message": "Dein Browser unterstützt wesentliche Funktionen nicht." },
"dbBlocked": { "message": "Bitte schließen Sie andere Tabs dieser Anwendung, um fortzufahren." }, "dbAccessError": { "message": "Fehler beim Zugriff auf die lokale Datenbank." },
"deletingContentData": { "message": "Lokale Inhaltsdaten werden gelöscht..." }, "dbUpdateNeeded": { "message": "Die Datenbank muss aktualisiert werden, bitte lade die Seite neu." },
"noContentDataToDelete": { "message": "Keine Inhaltsdaten zum Löschen vorhanden." }, "dbBlocked": { "message": "Bitte schließe andere Tabs dieser Anwendung, um fortzufahren." },
"contentDataDeleted": { "message": "Inhaltsdaten aus IndexedDB gelöscht." }, "deletingContentData": { "message": "Lokale Inhaltsdaten werden gelöscht..." },
"errorDeletingData": { "message": "Fehler beim Löschen von Daten: $message$", "placeholders": { "message": { "content": "$1" } } }, "noContentDataToDelete": { "message": "Keine Inhaltsdaten zum Löschen vorhanden." },
"aceEditorNotAvailable": { "message": "Texteditor nicht verfügbar." }, "contentDataDeleted": { "message": "Inhaltsdaten aus IndexedDB gelöscht." },
"errorLoadingTokens": { "message": "Fehler beim Laden der Token zur Bearbeitung." }, "errorDeletingData": { "message": "Fehler beim Löschen der Daten: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingTokensMessage": { "message": "Fehler beim Laden der Token: $message$", "placeholders": { "message": { "content": "$1" } } }, "aceEditorNotAvailable": { "message": "Texteditor nicht verfügbar." },
"aceEditorNotAvailableToSave": { "message": "Editor zum Speichern nicht verfügbar." }, "errorLoadingTokens": { "message": "Fehler beim Laden der Tokens zur Bearbeitung." },
"invalidJsonFormat": { "message": "Ungültiges JSON-Format. Es muss { \"tokens\": [...] } sein" }, "errorLoadingTokensMessage": { "message": "Fehler beim Laden der Tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"tokensSaved": { "message": "Token erfolgreich gespeichert." }, "aceEditorNotAvailableToSave": { "message": "Editor zum Speichern nicht verfügbar." },
"errorSavingTokens": { "message": "Fehler beim Speichern der Token: $message$", "placeholders": { "message": { "content": "$1" } } }, "invalidJsonFormat": { "message": "Ungültiges JSON-Format. Es muss { \"tokens\": [...] } sein." },
"dbNotAvailable": { "message": "IndexedDB ist nicht verfügbar." }, "tokensSaved": { "message": "Tokens erfolgreich gespeichert." },
"dbExported": { "message": "Datenbank erfolgreich exportiert." }, "errorSavingTokens": { "message": "Fehler beim Speichern der Tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorExportingDb": { "message": "Fehler beim Exportieren der Datenbank: $message$", "placeholders": { "message": { "content": "$1" } } }, "dbNotAvailable": { "message": "IndexedDB ist nicht verfügbar." },
"invalidJsonFile": { "message": "Die Datei enthält kein gültiges JSON-Objekt." }, "dbExported": { "message": "Datenbank erfolgreich exportiert." },
"noDataToImport": { "message": "Die Datei enthält keine Daten für die aktuellen DB-Abschnitte." }, "errorExportingDb": { "message": "Fehler beim Exportieren der Datenbank: $message$", "placeholders": { "message": { "content": "$1" } } },
"dbImported": { "message": "Datenbank erfolgreich importiert." }, "invalidJsonFile": { "message": "Die Datei enthält kein gültiges JSON-Objekt." },
"errorImportingDb": { "message": "Fehler beim Importieren der Datenbank: $message$", "placeholders": { "message": { "content": "$1" } } }, "noDataToImport": { "message": "Die Datei enthält keine Daten für die aktuellen DB-Abschnitte." },
"updatingView": { "message": "Ansicht mit neuen Daten wird aktualisiert..." }, "dbImported": { "message": "Datenbank erfolgreich importiert." },
"confirmClearContent": { "message": "Sind Sie sicher, dass Sie lokale Inhaltsdaten (Filme, Serien, Musik usw.) löschen möchten? Favoriten und Einstellungen werden NICHT gelöscht." }, "errorImportingDb": { "message": "Fehler beim Importieren der Datenbank: $message$", "placeholders": { "message": { "content": "$1" } } },
"trailerNotFound": { "message": "Kein Trailer für diesen Titel gefunden." }, "updatingView": { "message": "Ansicht wird mit neuen Daten aktualisiert..." },
"confirmClearHistory": { "message": "Sind Sie sicher, dass Sie Ihren gesamten Wiedergabeverlauf löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden." }, "confirmClearContent": { "message": "Bist du sicher, dass du die lokalen Inhaltsdaten (Filme, Serien, Musik usw.) löschen möchtest? Favoriten und Einstellungen werden NICHT gelöscht." },
"historyCleared": { "message": "Wiedergabeverlauf gelöscht." }, "trailerNotFound": { "message": "Für diesen Titel wurde kein Trailer gefunden." },
"historyItemDeleted": { "message": "Element aus dem Verlauf gelöscht." }, "confirmClearHistory": { "message": "Bist du sicher, dass du deinen gesamten Wiedergabeverlauf löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden." },
"errorGeneratingScript": { "message": "Generieren Sie zuerst ein Skript, um es kopieren zu können." }, "historyCleared": { "message": "Wiedergabeverlauf gelöscht." },
"scriptCopied": { "message": "PHP-Skript in die Zwischenablage kopiert." }, "historyItemDeleted": { "message": "Element aus dem Verlauf gelöscht." },
"errorCopyingScript": { "message": "Fehler beim Kopieren des Skripts." }, "errorGeneratingScript": { "message": "Generiere zuerst ein Skript, um es kopieren zu können." },
"scriptGenerated": { "message": "PHP-Skript generiert." }, "scriptCopied": { "message": "PHP-Skript in die Zwischenablage kopiert." },
"errorLoadingAlbum": { "message": "Fehler beim Laden des Albums: $message$", "placeholders": { "message": { "content": "$1" } } }, "errorCopyingScript": { "message": "Fehler beim Kopieren des Skripts." },
"noPhotoServerSelected": { "message": "Fehler: Es wurde kein Fotoserver ausgewählt." }, "scriptGenerated": { "message": "PHP-Skript generiert." },
"loadingGenres": { "message": "Lade Genres..." }, "errorLoadingAlbum": { "message": "Fehler beim Laden des Albums: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingGenres": { "message": "Fehler beim Laden" }, "noPhotoServerSelected": { "message": "Fehler: Es wurde kein Fotoserver ausgewählt." },
"noContentFound": { "message": "Keine Ergebnisse gefunden." }, "loadingGenres": { "message": "Genres werden geladen..." },
"couldNotLoadContent": { "message": "Inhalt konnte nicht geladen werden." }, "errorLoadingGenres": { "message": "Fehler beim Laden" },
"noFavorites": { "message": "Sie haben noch keine Favoriten." }, "noContentFound": { "message": "Keine Ergebnisse gefunden." },
"errorLoadingFavorites": { "message": "Fehler beim Laden der Favoriten." }, "couldNotLoadContent": { "message": "Inhalt konnte nicht geladen werden." },
"historyEmpty": { "message": "Ihr Verlauf ist leer." }, "noFavorites": { "message": "Du hast noch keine Favoriten." },
"historyEmptySub": { "message": "Entdecken und sehen Sie sich Inhalte an, damit sie hier erscheinen." }, "errorLoadingFavorites": { "message": "Fehler beim Laden der Favoriten." },
"errorGeneratingRecommendations": { "message": "Fehler beim Generieren von Empfehlungen." }, "historyEmpty": { "message": "Dein Verlauf ist leer." },
"noRecommendations": { "message": "Wir müssen Sie besser kennenlernen, um Ihnen Empfehlungen geben zu können!" }, "historyEmptySub": { "message": "Entdecke und schaue Inhalte an, damit sie hier erscheinen." },
"errorGeneratingStats": { "message": "Fehler beim Generieren von Statistiken." }, "errorGeneratingRecommendations": { "message": "Fehler beim Generieren von Empfehlungen." },
"noServersForToken": { "message": "Keine zugehörigen Server für dieses Token gefunden." }, "noRecommendations": { "message": "Wir müssen dich besser kennenlernen, um dir Empfehlungen geben zu können!" },
"searchingActorContent": { "message": "Suche nach Inhalten von $actorName$", "placeholders": { "actorName": { "content": "$1" } } }, "errorGeneratingStats": { "message": "Fehler beim Generieren von Statistiken." },
"errorLoadingActorContent": { "message": "Inhalt für $actorName$ konnte nicht geladen werden.", "placeholders": { "actorName": { "content": "$1" } } }, "noServersForToken": { "message": "Keine zugehörigen Server für dieses Token gefunden." },
"errorAddingStream": { "message": "Fehler beim Hinzufügen von Stream(s): $message$", "placeholders": { "message": { "content": "$1" } } }, "searchingActorContent": { "message": "Suche nach Inhalten von $actorName$", "placeholders": { "actorName": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "Die PHP-Server-URL ist nicht konfiguriert. Bitte konfigurieren Sie sie in den Einstellungen." }, "errorLoadingActorContent": { "message": "Inhalt für $actorName$ konnte nicht geladen werden.", "placeholders": { "actorName": { "content": "$1" } } },
"searchingStreams": { "message": "Suche nach Streams für \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "errorAddingStream": { "message": "Fehler beim Hinzufügen von Stream(s): $message$", "placeholders": { "message": { "content": "$1" } } },
"sendingStreams": { "message": "Sende $count$ Stream(s) an den Server...", "placeholders": { "count": { "content": "$1" } } }, "phpUrlNotConfigured": { "message": "Die PHP-Server-URL ist nicht konfiguriert. Bitte richte sie in den Einstellungen ein." },
"streamAddedSuccess": { "message": "Stream(s) erfolgreich hinzugefügt." }, "searchingStreams": { "message": "Suche nach Streams für „$title$“", "placeholders": { "title": { "content": "$1" } } },
"generatingM3U": { "message": "Generiere M3U für \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "sendingStreams": { "message": "Sende $count$ Stream(s) an den Server...", "placeholders": { "count": { "content": "$1" } } },
"m3uDownloaded": { "message": "\"$title$\" heruntergeladen.", "placeholders": { "title": { "content": "$1" } } }, "streamAddedSuccess": { "message": "Stream(s) erfolgreich hinzugefügt." },
"errorGeneratingM3U": { "message": "Fehler beim Generieren von M3U: $message$", "placeholders": { "message": { "content": "$1" } } }, "generatingM3U": { "message": "Generiere M3U für „$title$“", "placeholders": { "title": { "content": "$1" } } },
"settingsSavedSuccess": { "message": "Einstellungen erfolgreich gespeichert." }, "m3uDownloaded": { "message": "„$title$“ heruntergeladen.", "placeholders": { "title": { "content": "$1" } } },
"errorSavingSettings": { "message": "Fehler beim Speichern der Einstellungen in der Datenbank." }, "errorGeneratingM3U": { "message": "Fehler beim Generieren der M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
"languageChangeReload": { "message": "Sprache geändert. Die Anwendung wird jetzt neu geladen." }, "settingsSavedSuccess": { "message": "Einstellungen erfolgreich gespeichert." },
"addedToFavorites": { "message": "Zu den Favoriten hinzugefügt." }, "errorSavingSettings": { "message": "Fehler beim Speichern der Einstellungen in der Datenbank." },
"removedFromFavorites": { "message": "Aus den Favoriten entfernt." }, "languageChangeReload": { "message": "Sprache geändert. Die Anwendung wird jetzt neu geladen." },
"plexScanInProgress": { "message": "Plex-Scan läuft bereits." }, "addedToFavorites": { "message": "Zu Favoriten hinzugefügt." },
"plexScanStarting": { "message": "Plex-Scan wird gestartet..." }, "removedFromFavorites": { "message": "Aus Favoriten entfernt." },
"noPlexTokens": { "message": "Keine Plex-Token konfiguriert." }, "plexScanInProgress": { "message": "Der Plex-Scan läuft bereits." },
"clearingSections": { "message": "Lösche Abschnitte: $sections$", "placeholders": { "sections": { "content": "$1" } } }, "plexScanStarting": { "message": "Plex-Scan wird gestartet..." },
"initialScanPhaseComplete": { "message": "Erste Scanphase abgeschlossen." }, "noPlexTokens": { "message": "Keine Plex-Tokens konfiguriert." },
"retryPhaseFinished": { "message": "Wiederholungsphase abgeschlossen." }, "clearingSections": { "message": "Abschnitte werden geleert: $sections$", "placeholders": { "sections": { "content": "$1" } } },
"plexScanFinished": { "message": "Scan abgeschlossen. Inhalt wird aktualisiert..." }, "initialScanPhaseComplete": { "message": "Initiale Scan-Phase abgeschlossen." },
"scanCancelled": { "message": "Scan vom Benutzer abgebrochen." }, "retryPhaseFinished": { "message": "Wiederholungsphase abgeschlossen." },
"scanCancelledInfo": { "message": "Scan abgebrochen." }, "plexScanFinished": { "message": "Scan abgeschlossen. Inhalt wird aktualisiert..." },
"errorInitializingMusicPlayer": { "message": "Fehler beim Initialisieren des Musik-Players." }, "scanCancelled": { "message": "Scan vom Benutzer abgebrochen." },
"criticalErrorLoadingMusic": { "message": "Kritischer Fehler beim Laden der Musikdaten." }, "scanCancelledInfo": { "message": "Scan abgebrochen." },
"errorLoadingArtists": { "message": "Fehler beim Laden der Künstler." }, "errorInitializingMusicPlayer": { "message": "Fehler beim Initialisieren des Musikplayers." },
"dbUnavailableError": { "message": "Fehler: Datenbank nicht verfügbar." }, "criticalErrorLoadingMusic": { "message": "Kritischer Fehler beim Laden der Musikdaten." },
"updatingMusicData": { "message": "Musikdaten werden aktualisiert..." }, "errorLoadingArtists": { "message": "Fehler beim Laden der Künstler." },
"musicDataUpdated": { "message": "Musikdaten aktualisiert." }, "dbUnavailableError": { "message": "Fehler: Datenbank nicht verfügbar." },
"errorFetchingArtistSongs": { "message": "Fehler beim Abrufen der Lieder des Künstlers." }, "updatingMusicData": { "message": "Musikdaten werden aktualisiert..." },
"errorLoadingSongs": { "message": "Fehler beim Laden der Lieder." }, "musicDataUpdated": { "message": "Musikdaten aktualisiert." },
"noArtistsFound": { "message": "Keine Künstler gefunden." }, "errorFetchingArtistSongs": { "message": "Fehler beim Abrufen der Lieder des Künstlers." },
"shuffleOn": { "message": "Zufallswiedergabe ein." }, "errorLoadingSongs": { "message": "Fehler beim Laden der Lieder." },
"shuffleOff": { "message": "Zufallswiedergabe aus." }, "noArtistsFound": { "message": "Keine Künstler gefunden." },
"playbackError": { "message": "Wiedergabefehler" }, "shuffleOn": { "message": "Zufallswiedergabe aktiviert." },
"errorLabel": { "message": "Fehler" }, "shuffleOff": { "message": "Zufallswiedergabe deaktiviert." },
"reloadingPage": { "message": "Seite wird neu geladen..." }, "playbackError": { "message": "Wiedergabefehler" },
"viewed": { "message": "Gesehen" }, "errorLabel": { "message": "Fehler" },
"local": { "message": "Lokal" }, "reloadingPage": { "message": "Seite wird neu geladen..." },
"topRatedSort": {"message": "Am besten bewertet"}, "viewed": { "message": "Gesehen" },
"recentSort": {"message": "Neueste"}, "local": { "message": "Lokal" },
"popularSort": {"message": "Beliebteste"}, "topRatedSort": {"message": "Am besten bewertet"},
"moviesSectionTitle": {"message": "Filme"}, "recentSort": {"message": "Neueste"},
"seriesSectionTitle": {"message": "Serien"}, "popularSort": {"message": "Beliebte"},
"searchResultsFor": {"message": "Ergebnisse für \"$query$\"", "placeholders": {"query": {"content": "$1"}}}, "moviesSectionTitle": {"message": "Filme"},
"contentFrom": {"message": "Inhalt von $actor$", "placeholders": {"actor": {"content": "$1"}}}, "seriesSectionTitle": {"message": "Serien"},
"explore": {"message": "Entdecken"}, "searchResultsFor": {"message": "Ergebnisse für „$query$“", "placeholders": {"query": {"content": "$1"}}},
"noGenre": {"message": "Ohne Kategorie"}, "contentFrom": {"message": "Inhalt von $actor$", "placeholders": {"actor": {"content": "$1"}}},
"synopsis": {"message": "Zusammenfassung"}, "explore": {"message": "Entdecken"},
"noSynopsis": {"message": "Keine Zusammenfassung verfügbar."}, "noGenre": {"message": "Ohne Kategorie"},
"director": {"message": "Regisseur:"}, "synopsis": {"message": "Inhaltsangabe"},
"writer": {"message": "Autor:"}, "noSynopsis": {"message": "Keine Inhaltsangabe verfügbar."},
"viewOnImdb": {"message": "Auf IMDb ansehen"}, "director": {"message": "Regisseur:"},
"watchTrailer": {"message": "Trailer ansehen"}, "writer": {"message": "Autor:"},
"addToFavorites": {"message": "Zu den Favoriten hinzufügen"}, "viewOnImdb": {"message": "Auf IMDb ansehen"},
"removeFromFavorites": {"message": "Aus den Favoriten entfernen"}, "watchTrailer": {"message": "Trailer ansehen"},
"notAvailable": {"message": "Nicht verfügbar"}, "addToFavorites": {"message": "Zu Favoriten hinzufügen"},
"mainCast": {"message": "Hauptbesetzung"}, "removeFromFavorites": {"message": "Aus Favoriten entfernen"},
"seasonsAndEpisodes": {"message": "Staffeln und Episoden"}, "notAvailable": {"message": "Nicht verfügbar"},
"similarContent": {"message": "Ähnlicher Inhalt"}, "mainCast": {"message": "Hauptbesetzung"},
"filmography": {"message": "Filmografie"}, "seasonsAndEpisodes": {"message": "Staffeln und Episoden"},
"availableOn": {"message": "Verfügbar auf"}, "similarContent": {"message": "Ähnlicher Inhalt"},
"episodesCount": {"message": "$count$ Episoden", "placeholders": {"count": {"content": "$1"}}}, "filmography": {"message": "Filmografie"},
"seasonsCount": {"message": "$count$ Staffeln", "placeholders": {"count": {"content": "$1"}}}, "availableOn": {"message": "Verfügbar auf"},
"runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}}, "episodesCount": {"message": "$count$ Episoden", "placeholders": {"count": {"content": "$1"}}},
"noTrailerFound": {"message": "Kein Trailer für diesen Titel gefunden."}, "seasonsCount": {"message": "$count$ Staffeln", "placeholders": {"count": {"content": "$1"}}},
"fatalInitError": {"message": "Fataler Initialisierungsfehler"}, "runtimeMinutes": {"message": "$count$ Min.", "placeholders": {"count": {"content": "$1"}}},
"fatalInitErrorSub": {"message": "Die Anwendung konnte nicht geladen werden."}, "noTrailerFound": {"message": "Für diesen Titel wurde kein Trailer gefunden."},
"invalidStreamInfo": {"message": "Ungültige Informationen."}, "fatalInitError": {"message": "Fataler Initialisierungsfehler"},
"dbUnavailableForStreams": {"message": "Lokale Datenbank nicht verfügbar."}, "fatalInitErrorSub": {"message": "Die Anwendung konnte nicht geladen werden."},
"noPlexServersForStreams": {"message": "Keine Plex-Server."}, "invalidStreamInfo": {"message": "Ungültige Stream-Informationen."},
"notFoundOnServers": {"message": "\"$query$\" auf Plex-Servern nicht gefunden.", "placeholders": {"query": {"content": "$1"}}}, "dbUnavailableForStreams": {"message": "Lokale Datenbank nicht verfügbar."},
"relativeTime_justNow": { "message": "Gerade eben" }, "noPlexServersForStreams": {"message": "Keine Plex-Server."},
"relativeTime_minutesAgo": { "message": "Vor $count$ Minuten", "placeholders": { "count": { "content": "$1" } } }, "notFoundOnServers": {"message": "„$query$“ wurde nicht auf den Plex-Servern gefunden.", "placeholders": {"query": {"content": "$1"}}},
"relativeTime_hoursAgo": { "message": "Vor $count$ Stunden", "placeholders": { "count": { "content": "$1" } } }, "relativeTime_justNow": { "message": "Gerade eben" },
"relativeTime_yesterday": { "message": "Gestern" }, "relativeTime_minutesAgo": { "message": "Vor $count$ Minuten", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_daysAgo": { "message": "Vor $count$ Tagen", "placeholders": { "count": { "content": "$1" } } }, "relativeTime_hoursAgo": { "message": "Vor $count$ Stunden", "placeholders": { "count": { "content": "$1" } } },
"errorLoadingDetails": { "message": "Fehler beim Laden der Details" }, "relativeTime_yesterday": { "message": "Gestern" },
"errorLoadingLocalContent": { "message": "Fehler beim Laden des lokalen Inhalts." }, "relativeTime_daysAgo": { "message": "Vor $count$ Tagen", "placeholders": { "count": { "content": "$1" } } },
"errorServerResponse": { "message": "Nicht erfolgreiche Serverantwort." }, "errorLoadingDetails": { "message": "Fehler beim Laden der Details" },
"errorPlexApi": { "message": "Plex-API-Fehler $status$.", "placeholders": { "status": { "content": "$1" } } }, "errorLoadingLocalContent": { "message": "Fehler beim Laden des lokalen Inhalts." },
"errorParsingPlexXml": { "message": "Fehler beim Parsen von Plex-XML." }, "errorServerResponse": { "message": "Nicht erfolgreiche Serverantwort." },
"untitled": { "message": "Ohne Titel" }, "errorPlexApi": { "message": "Plex API-Fehler $status$.", "placeholders": { "status": { "content": "$1" } } },
"itemCount": { "message": "$count$ Elemente", "placeholders": { "count": { "content": "$1" } } }, "errorParsingPlexXml": { "message": "Fehler beim Parsen des Plex-XML." },
"noPhotoServers": { "message": "Keine Fotoserver" }, "untitled": { "message": "Ohne Titel" },
"jellyfinScanInProgress": { "message": "Jellyfin-Scan läuft bereits." }, "itemCount": { "message": "$count$ Elemente", "placeholders": { "count": { "content": "$1" } } },
"jellyfinScanning": { "message": "Scanne Jellyfin..." }, "noPhotoServers": { "message": "Keine Fotodienste" },
"jellyfinMissingCredentials": { "message": "Bitte vervollständigen Sie die Jellyfin-URL und den Benutzernamen." }, "jellyfinScanInProgress": { "message": "Der Jellyfin-Scan läuft bereits." },
"jellyfinConnecting": { "message": "Verbinde mit Jellyfin unter: $url$", "placeholders": { "url": { "content": "$1" } } }, "jellyfinScanning": { "message": "Scanne Jellyfin..." },
"jellyfinAuthFailed": { "message": "Jellyfin-Authentifizierung fehlgeschlagen: $message$", "placeholders": { "message": { "content": "$1" } } }, "jellyfinMissingCredentials": { "message": "Bitte fülle die Jellyfin-URL und den Benutzernamen aus." },
"jellyfinAuthSuccess": { "message": "Jellyfin-Authentifizierung erfolgreich." }, "jellyfinConnecting": { "message": "Verbinde mit Jellyfin unter: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinFetchingLibraries": { "message": "Rufe Bibliotheken ab..." }, "jellyfinAuthFailed": { "message": "Jellyfin-Authentifizierung fehlgeschlagen: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinFetchFailed": { "message": "Fehler beim Abrufen der Bibliotheken: $message$", "placeholders": { "message": { "content": "$1" } } }, "jellyfinAuthSuccess": { "message": "Jellyfin-Authentifizierung erfolgreich." },
"jellyfinNoMediaLibraries": { "message": "Keine Film- oder Serienbibliotheken in Jellyfin gefunden." }, "jellyfinFetchingLibraries": { "message": "Bibliotheken werden abgerufen..." },
"jellyfinLibrariesFound": { "message": "$count$ Medienbibliothek(en) gefunden.", "placeholders": { "count": { "content": "$1" } } }, "jellyfinFetchFailed": { "message": "Fehler beim Abrufen der Bibliotheken: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Erfolg] '$libraryName' gescannt, $count$ Titel hinzugefügt.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } }, "jellyfinNoMediaLibraries": { "message": "Keine Film- oder Serienbibliotheken in Jellyfin gefunden." },
"jellyfinLibraryScanFailed": { "message": "Fehler beim Scannen der Bibliothek '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } }, "jellyfinLibrariesFound": { "message": "$count$ Medienbibliothek(en) gefunden.", "placeholders": { "count": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Jellyfin-Scan abgeschlossen. $movies$ Filme und $series$ Serien hinzugefügt.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } }, "jellyfinLibraryScanSuccess": { "message": "[Erfolg] '$libraryName' gescannt, $count$ Titel hinzugefügt.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Jellyfin-Anmeldeinformationen nicht konfiguriert." }, "jellyfinLibraryScanFailed": { "message": "Fehler beim Scannen der Bibliothek '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } },
"notFoundOnJellyfin": { "message": "\"$query$\" auf Jellyfin nicht gefunden.", "placeholders": { "query": { "content": "$1" } } }, "jellyfinScanSuccess": { "message": "Jellyfin-Scan abgeschlossen. $movies$ Filme und $series$ Serien hinzugefügt.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" auf keinem Server gefunden.", "placeholders": { "query": { "content": "$1" } } }, "noJellyfinCredentials": { "message": "Jellyfin-Anmeldeinformationen nicht konfiguriert." },
"localOnPlex": { "message": "Auf Plex" }, "notFoundOnJellyfin": { "message": "„$query$“ wurde nicht auf Jellyfin gefunden.", "placeholders": {"query": {"content": "$1"}}},
"searchOnPlex": { "message": "Auf Plex suchen" }, "notFoundOnAnyServer": { "message": "„$query$“ wurde auf keinem Server gefunden.", "placeholders": {"query": {"content": "$1"}}},
"jellyfinTitle": { "message": "Jellyfin-Inhalt" }, "localOnPlex": { "message": "Auf Plex" },
"noJellyfinContent": { "message": "Kein Jellyfin-Inhalt gefunden." }, "searchOnPlex": { "message": "Auf Plex suchen" },
"noJellyfinContentSub": { "message": "Stellen Sie sicher, dass Sie Ihren Jellyfin-Server in den Einstellungen gescannt haben." }, "jellyfinTitle": { "message": "Jellyfin-Inhalt" },
"activityViewerTitle": { "message": "Server-Aktivitätsanzeige" }, "noJellyfinContent": { "message": "Kein Jellyfin-Inhalt gefunden." },
"activitySelectServer": { "message": "Wählen Sie einen Server" }, "noJellyfinContentSub": { "message": "Stelle sicher, dass du deinen Jellyfin-Server in den Einstellungen gescannt hast." },
"activityCheckBtn": { "message": "Aktualisieren" }, "activityViewerTitle": { "message": "Server-Aktivitätsanzeige" },
"activityNoSessions": { "message": "Keine aktiven Sitzungen auf diesem Server." }, "activitySelectServer": { "message": "Wähle einen Server" },
"activitySessionUser": { "message": "Benutzer" }, "activityCheckBtn": { "message": "Aktualisieren" },
"activitySessionDevice": { "message": "Gerät" }, "activityNoSessions": { "message": "Auf diesem Server gibt es keine aktiven Sitzungen." },
"activitySessionContent": { "message": "Inhalt" }, "activitySessionUser": { "message": "Benutzer" },
"activitySessionState": { "message": "Status" }, "activitySessionDevice": { "message": "Gerät" },
"activitySessionIdentifier": { "message": "Client-Identifikator" }, "activitySessionContent": { "message": "Inhalt" },
"activityCopyID": { "message": "ID kopieren" }, "activitySessionState": { "message": "Status" },
"activityError": { "message": "Serveraktivität konnte nicht abgerufen werden." }, "activitySessionIdentifier": { "message": "Client-Identifikator" },
"activityCopied": { "message": "Identifikator in die Zwischenablage kopiert!" }, "activityCopyID": { "message": "ID kopieren" },
"activityCopyError": { "message": "Fehler beim Kopieren des Identifikators." }, "activityError": { "message": "Serveraktivität konnte nicht abgerufen werden." },
"noProvidersFound": { "message": "Keine Anbieter gefunden." }, "activityCopied": { "message": "Identifikator in die Zwischenablage kopiert!" },
"availableOnPlex": { "message": "Verfügbar auf Plex" }, "activityCopyError": { "message": "Fehler beim Kopieren des Identifikators." },
"m3uGeneratorTitle": { "message": "M3U-Listen-Generator" }, "noProvidersFound": { "message": "Keine Anbieter gefunden." },
"selectAServer": { "message": "Wählen Sie einen Server..." }, "availableOnPlex": { "message": "Verfügbar auf Plex" },
"downloadM3u": { "message": "M3U herunterladen" }, "m3uGeneratorTitle": { "message": "M3U-Listen-Generator" },
"m3uGenerator": { "message": "M3U-Generator" }, "selectAServer": { "message": "Wähle einen Server..." },
"selectLibraries": { "message": "Bibliotheken auswählen" }, "downloadM3u": { "message": "M3U herunterladen" },
"howToUse": { "message": "Anwendung" }, "m3uGenerator": { "message": "M3U-Generator" },
"m3uInstruction1": { "message": "Wählen Sie einen Server aus der Liste." }, "selectLibraries": { "message": "Bibliotheken auswählen" },
"m3uInstruction2": { "message": "Wählen Sie eine oder mehrere Bibliotheken aus, die einbezogen werden sollen." }, "howToUse": { "message": "Anwendung" },
"m3uInstruction3": { "message": "Klicken Sie auf die Download-Schaltfläche." }, "m3uInstruction1": { "message": "Wähle einen Server aus der Liste." },
"m3uInstruction4": { "message": "Importieren Sie die .m3u-Datei in Ihren kompatiblen Player." }, "m3uInstruction2": { "message": "Wähle eine oder mehrere Bibliotheken aus, die einbezogen werden sollen." },
"chatOpen": { "message": "Chat öffnen" }, "m3uInstruction3": { "message": "Klicke auf den Download-Button." },
"chatTitle": { "message": "KI-Assistent" }, "m3uInstruction4": { "message": "Importiere die .m3u-Datei in deinen kompatiblen Player." },
"chatClose": { "message": "X" }, "chatOpen": { "message": "Chat öffnen" },
"chatPlaceholder": { "message": "Geben Sie Ihre Nachricht ein..." }, "chatTitle": { "message": "KI-Assistent" },
"chatSend": { "message": "➤" }, "chatClose": { "message": "X" },
"chatWelcome": { "message": "Willkommen! Ich bin Ihr CinePlex-Assistent. Fragen Sie mich nach Filmen, Serien oder allem, was Sie sonst noch wissen möchten." }, "chatPlaceholder": { "message": "Schreibe deine Nachricht..." },
"chatGoogleApiKeyMissing": { "message": "Der Google Gemini-API-Schlüssel ist nicht konfiguriert. Bitte legen Sie ihn in den Erweiterungseinstellungen fest, um den KI-Assistenten zu verwenden." }, "chatSend": { "message": "➤" },
"chatApiInvalidResponse": { "message": "Die API hat eine ungültige Antwort zurückgegeben. Bitte versuchen Sie es erneut." }, "chatWelcome": { "message": "Willkommen! Ich bin dein CinePlex-Assistent. Frag mich nach Filmen, Serien oder allem, was du sonst wissen möchtest." },
"chatApiError": { "message": "Fehler bei der Kommunikation mit dem KI-Assistenten" }, "chatGoogleApiKeyMissing": { "message": "Der Google Gemini API-Schlüssel ist nicht konfiguriert. Bitte richte ihn in den Erweiterungseinstellungen ein, um den KI-Assistenten zu verwenden." },
"downloadAll": { "message": "Alles herunterladen" }, "chatApiInvalidResponse": { "message": "Die API hat eine ungültige Antwort zurückgegeben. Bitte versuche es erneut." },
"download": { "message": "Herunterladen" }, "chatApiError": { "message": "Fehler bei der Kommunikation mit dem KI-Assistenten" },
"aiToolSearchLibraryDesc": { "message": "Durchsucht die Plex-Bibliothek des Benutzers nach Filmen oder Serien nach Titel." }, "downloadAll": { "message": "Alles herunterladen" },
"aiToolSearchLibraryQueryParamDesc": { "message": "Der Titel des zu suchenden Films oder der zu suchenden Serie." }, "download": { "message": "Herunterladen" },
"aiToolSearchLibraryTypeParamDesc": { "message": "Der Typ des zu suchenden Inhalts. Kann 'movie' für Filme oder 'series' für Serien sein. (Optional)." }, "aiToolSearchLibraryDesc": { "message": "Durchsucht die Plex-Bibliothek des Benutzers nach Filmen oder Serien nach Titel." },
"aiToolSearchLibraryResolutionParamDesc": { "message": "Die zu suchende Videoauflösung (z. B. '4k', '1080p'). (Optional)." }, "aiToolSearchLibraryQueryParamDesc": { "message": "Der Titel des zu suchenden Films oder der zu suchenden Serie." },
"aiToolSearchLibraryContainerParamDesc": { "message": "Das zu suchende Video-Containerformat (z. B. 'mkv', 'mp4'). (Optional)." }, "aiToolSearchLibraryTypeParamDesc": { "message": "Der Typ des zu suchenden Inhalts. Kann 'movie' für Filme oder 'series' für Serien sein. (Optional)." },
"aiToolNavigateToPageDesc": { "message": "Navigiert den Benutzer zu einer bestimmten Seite der Anwendungsoberfläche." }, "aiToolSearchLibraryResolutionParamDesc": { "message": "Die zu suchende Videoauflösung (z.B. '4k', '1080p'). (Optional)." },
"aiToolNavigateToPagePageParamDesc": { "message": "Der Name der Seite, zu der navigiert werden soll, z. B.: 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers' oder 'm3u-generator'." }, "aiToolSearchLibraryContainerParamDesc": { "message": "Das zu suchende Video-Containerformat (z.B. 'mkv', 'mp4'). (Optional)." },
"aiToolGetUserStatsDesc": { "message": "Ruft die Bibliotheksstatistiken des Benutzers ab und zeigt sie an, z. B. die Gesamtzahl der einzigartigen Filme, Serien und Künstler." }, "aiToolNavigateToPageDesc": { "message": "Navigiert den Benutzer zu einer bestimmten Seite der Anwendungsoberfläche." },
"aiToolShowItemDetailsDesc": { "message": "Zeigt die Detailseite eines bestimmten Films oder einer bestimmten Serie nach Titel und Typ an." }, "aiToolNavigateToPagePageParamDesc": { "message": "Der Name der Seite, zu der navigiert werden soll, z.B.: 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', 'm3u-generator' oder 'music'." },
"aiToolShowItemDetailsTitleParamDesc": { "message": "Der genaue Titel des Films oder der Serie." }, "aiToolGetUserStatsDesc": { "message": "Ruft die Bibliotheksstatistiken des Benutzers ab und zeigt sie an, wie z.B. die Gesamtzahl der einzigartigen Filme, Serien und Künstler." },
"aiToolShowItemDetailsTypeParamDesc": { "message": "Der Typ des Inhalts. Muss 'movie' oder 'series' sein." }, "aiToolShowItemDetailsDesc": { "message": "Zeigt die Detailseite für einen bestimmten Film oder eine bestimmte Serie anhand ihres Titels und Typs an." },
"aiToolAddToPlaylistDesc": { "message": "Fügt einen Film oder eine Serie zur aktuellen Wiedergabeliste des Benutzers hinzu, um sie an einen konfigurierten PHP-Server zu streamen." }, "aiToolShowItemDetailsTitleParamDesc": { "message": "Der genaue Titel des Films oder der Serie." },
"aiToolAddToPlaylistTitleParamDesc": { "message": "Der Titel des hinzuzufügenden Films oder der hinzuzufügenden Serie." }, "aiToolShowItemDetailsTypeParamDesc": { "message": "Der Inhaltstyp. Muss 'movie' oder 'series' sein." },
"aiToolAddToPlaylistTypeParamDesc": { "message": "Der Typ des Inhalts. Muss 'movie' oder 'series' sein." }, "aiToolAddToPlaylistDesc": { "message": "Fügt einen Film oder eine Serie zur aktuellen Wiedergabeliste des Benutzers hinzu, um sie an einen konfigurierten PHP-Server zu streamen." },
"aiToolCheckAndDownloadDesc": { "message": "Überprüft die Verfügbarkeit einer Liste von Film- oder Serientiteln auf den lokalen Servern des Benutzers und generiert und lädt, falls gefunden, eine M3U-Wiedergabelistendatei mit den gefundenen Streams herunter." }, "aiToolAddToPlaylistTitleParamDesc": { "message": "Der Titel des hinzuzufügenden Films oder der Serie." },
"aiToolCheckAndDownloadTitlesParamDesc": { "message": "Ein Array von Film- oder Serientiteln zum Suchen und Herunterladen." }, "aiToolAddToPlaylistTypeParamDesc": { "message": "Der Inhaltstyp. Muss 'movie' oder 'series' sein." },
"aiToolCheckAndDownloadTypeParamDesc": { "message": "Der Inhaltstyp der Liste. Muss 'movie' oder 'series' sein." }, "aiToolDownloadSingleMovieM3UDesc": { "message": "Generiert und lädt eine M3U-Wiedergabelistendatei für einen einzelnen lokal verfügbaren Film herunter." },
"aiToolCheckAndDownloadFilenameParamDesc": { "message": "Der Name der herunterzuladenden M3U-Datei (z. B. 'MeineListe.m3u'). Wenn nicht angegeben, wird ein Standardname verwendet." }, "aiToolDownloadSingleMovieM3UTitleParamDesc": { "message": "Der Titel des Films, für den die M3U generiert werden soll." },
"aiToolToggleFavoriteDesc": { "message": "Fügt einen Film oder eine Serie zur Favoritenliste des Benutzers hinzu oder entfernt sie daraus." }, "aiToolDownloadSingleMovieM3UYearParamDesc": { "message": "Das Erscheinungsjahr des Films (optional, für höhere Genauigkeit)." },
"aiToolToggleFavoriteTitleParamDesc": { "message": "Der Titel des Films oder der Serie." }, "aiToolDownloadSeriesSeasonM3UDesc": { "message": "Generiert und lädt eine M3U-Wiedergabelistendatei für eine bestimmte Staffel einer lokal verfügbaren Serie herunter." },
"aiToolToggleFavoriteTypeParamDesc": { "message": "Der Typ des Inhalts. Muss 'movie' oder 'series' sein." }, "aiToolDownloadSeriesSeasonM3UTitleParamDesc": { "message": "Der Titel der Serie." },
"aiToolGetRecommendationsDesc": { "message": "Generiert und zeigt eine Liste von Film- oder Serienempfehlungen basierend auf dem Wiedergabeverlauf und den Favoriten des Benutzers an." }, "aiToolDownloadSeriesSeasonM3USeasonParamDesc": { "message": "Die Nummer der herunterzuladenden Staffel." },
"aiToolApplyFiltersDesc": { "message": "Wendet Filter auf die aktuelle Ansicht von Filmen oder Serien an, um die Ergebnisse nach Typ, Genre, Jahr und Sortierreihenfolge zu verfeinern." }, "aiToolDownloadSeriesSeasonM3UYearParamDesc": { "message": "Das Erscheinungsjahr der Serie (optional)." },
"aiToolApplyFiltersTypeParamDesc": { "message": "Der Typ des Inhalts, auf den die Filter angewendet werden sollen. Muss 'movie' oder 'series' sein." }, "aiToolCheckAndDownloadDesc": { "message": "Überprüft die Verfügbarkeit einer Liste von Film- oder Serientiteln auf den lokalen Servern des Benutzers und generiert und lädt, falls gefunden, eine M3U-Wiedergabelistendatei mit den gefundenen Streams herunter." },
"aiToolApplyFiltersGenreParamDesc": { "message": "Der Name des Genres, nach dem gefiltert werden soll (z. B. 'Action', 'Drama')." }, "aiToolCheckAndDownloadTitlesParamDesc": { "message": "Ein Array von Film- oder Serientiteln, die gesucht und heruntergeladen werden sollen." },
"aiToolApplyFiltersYearParamDesc": { "message": "Das Erscheinungsjahr, nach dem gefiltert werden soll (z. B. '2023')." }, "aiToolCheckAndDownloadTypeParamDesc": { "message": "Der Inhaltstyp der Liste. Muss 'movie' oder 'series' sein." },
"aiToolApplyFiltersSortParamDesc": { "message": "Das Sortierkriterium für die Ergebnisse. Gültige Werte: 'popularity.desc' (beliebt), 'vote_average.desc' (am besten bewertet), 'release_date.desc' (neu für Filme) oder 'first_air_date.desc' (neu für Serien)." }, "aiToolCheckAndDownloadFilenameParamDesc": { "message": "Der Name der herunterzuladenden M3U-Datei (z.B. 'MeineListe.m3u'). Wenn nicht angegeben, wird ein Standardname verwendet." },
"aiToolPlayMusicByArtistDesc": { "message": "Öffnet den Musik-Player und beginnt mit der Wiedergabe von Liedern eines bestimmten Künstlers aus der Bibliothek des Benutzers." }, "aiToolToggleFavoriteDesc": { "message": "Fügt einen Film oder eine Serie zur Favoritenliste des Benutzers hinzu oder entfernt sie daraus." },
"aiToolPlayMusicByArtistNameParamDesc": { "message": "Der genaue Name des Künstlers, dessen Lieder abgespielt werden sollen." }, "aiToolToggleFavoriteTitleParamDesc": { "message": "Der Titel des Films oder der Serie." },
"aiToolClearChatHistoryDesc": { "message": "Löscht den gesamten Nachrichtenverlauf der aktuellen Konversation mit dem KI-Assistenten." }, "aiToolToggleFavoriteTypeParamDesc": { "message": "Der Inhaltstyp. Muss 'movie' oder 'series' sein." },
"aiToolDeleteDatabaseDesc": { "message": "Löscht die gesamte lokale Datenbank der Erweiterung, einschließlich gescannter Inhalte, Einstellungen und Favoriten. Diese Aktion ist irreversibel und lädt die Anwendung neu." }, "aiToolGetRecommendationsDesc": { "message": "Generiert und zeigt eine Liste von Film- oder Serienempfehlungen basierend auf dem Wiedergabeverlauf und den Favoriten des Benutzers an." },
"aiToolUpdateAllTokensDesc": { "message": "Startet einen vollständigen Scan aller Plex-Server und Bibliotheken, die mit den in der Erweiterung konfigurierten Token verknüpft sind. Aktualisiert alle Filme, Serien, Künstler und Fotos." }, "aiToolApplyFiltersDesc": { "message": "Wendet Filter auf die aktuelle Film- oder Serienansicht an, um die Ergebnisse nach Typ, Genre, Jahr und Sortierreihenfolge zu verfeinern." },
"aiToolAddPlexTokenDesc": { "message": "Fügt der Konfiguration der Erweiterung einen neuen X-Plex-Token hinzu, sodass die Anwendung Inhalte von neuen Plex-Servern scannen kann." }, "aiToolApplyFiltersTypeParamDesc": { "message": "Der Inhaltstyp, auf den die Filter angewendet werden sollen. Muss 'movie' oder 'series' sein." },
"aiToolAddPlexTokenTokenParamDesc": { "message": "Die hinzuzufügende X-Plex-Token-Zeichenfolge." }, "aiToolApplyFiltersGenreParamDesc": { "message": "Der Name des Genres, nach dem gefiltert werden soll (z.B. 'Action', 'Drama')." },
"aiToolChangeRegionDesc": { "message": "Ändert die für die Inhaltsentdeckung in der TMDB-API verwendete Region. Dies wirkt sich auf die in den Film- und Serienabschnitten angezeigten Ergebnisse sowie auf die Streaming-Anbieter aus." }, "aiToolApplyFiltersYearParamDesc": { "message": "Das Erscheinungsjahr, nach dem gefiltert werden soll (z.B. '2023')." },
"aiToolChangeRegionRegionParamDesc": { "message": "Der zweibuchstabige ISO 3166-1-Ländercode für die neue Region (z. B. 'US' für die Vereinigten Staaten, 'ES' für Spanien, 'MX' für Mexiko)." }, "aiToolApplyFiltersSortParamDesc": { "message": "Das Sortierkriterium für die Ergebnisse. Gültige Werte: 'popularity.desc' (beliebt), 'vote_average.desc' (am besten bewertet), 'release_date.desc' (neu für Filme) oder 'first_air_date.desc' (neu für Serien)." },
"aiToolClearAllFavoritesDesc": { "message": "Entfernt alle Filme und Serien, die der Benutzer als Favoriten markiert hat." }, "aiToolListAvailableMusicGenresDesc": { "message": "Listet alle einzigartigen Musikgenres auf, die in der lokalen Bibliothek des Benutzers verfügbar sind." },
"aiToolClearViewingHistoryDesc": { "message": "Löscht den Wiedergabeverlauf des Benutzers von der Verlaufsseite." }, "aiToolSearchMusicByGenreDesc": { "message": "Sucht nach Künstlern in der Musikbibliothek des Benutzers, die zu einem bestimmten Genre gehören." },
"aiToolClearRecommendationsViewDesc": { "message": "Löscht die Empfehlungsansicht und entfernt zwischengespeicherte Empfehlungen." }, "aiToolSearchMusicByGenreNameParamDesc": { "message": "Der Name des zu suchenden Musikgenres (z.B. 'Rock', 'Pop', 'Jazz')." },
"aiToolSearchNotFound": { "message": "'$query' in Ihrer Bibliothek nicht gefunden.", "placeholders": { "query": { "content": "$1" } } }, "aiToolPlayMusicByArtistDesc": { "message": "Öffnet den Musikplayer und beginnt mit der Wiedergabe von Liedern eines bestimmten Künstlers aus der Bibliothek des Benutzers." },
"aiToolNavigateSuccess": { "message": "Zur Seite $page$ navigiert.", "placeholders": { "page": { "content": "$1" } } }, "aiToolPlayMusicByArtistNameParamDesc": { "message": "Der genaue Name des Künstlers, dessen Lieder wiedergegeben werden sollen." },
"aiToolNavigateError": { "message": "Fehler beim Navigieren zur Seite $page$.", "placeholders": { "page": { "content": "$1" } } }, "aiToolClearChatHistoryDesc": { "message": "Löscht den gesamten Nachrichtenverlauf des aktuellen Gesprächs mit dem KI-Assistenten." },
"aiToolStatsError": { "message": "Fehler beim Abrufen der Statistiken." }, "aiToolDeleteDatabaseDesc": { "message": "Löscht die gesamte lokale Datenbank der Erweiterung, einschließlich gescannter Inhalte, Einstellungen und Favoriten. Diese Aktion ist unumkehrbar und lädt die Anwendung neu." },
"aiToolItemNotFound": { "message": "Element '$title' nicht gefunden.", "placeholders": { "title": { "content": "$1" } } }, "aiToolUpdateAllTokensDesc": { "message": "Startet einen vollständigen Scan aller Plex-Server und -Bibliotheken, die mit den in der Erweiterung konfigurierten Tokens verknüpft sind. Aktualisiert alle Filme, Serien, Künstler und Fotos." },
"aiToolShowItemDetailsSuccess": { "message": "Zeige Details für '$title'.", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenDesc": { "message": "Fügt ein neues X-Plex-Token zur Konfiguration der Erweiterung hinzu, sodass die Anwendung Inhalte von neuen Plex-Servern scannen kann." },
"aiToolAddToPlaylistSuccess": { "message": "'$title' zur Wiedergabeliste hinzugefügt.", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenTokenParamDesc": { "message": "Der X-Plex-Token-String, der hinzugefügt werden soll." },
"aiToolFavoriteAdded": { "message": "'$title' zu den Favoriten hinzugefügt.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionDesc": { "message": "Ändert die Region, die für die Inhaltssuche in der TMDB-API verwendet wird. Dies wirkt sich auf die in den Film- und Serienbereichen angezeigten Ergebnisse sowie auf die Streaming-Anbieter aus." },
"aiToolFavoriteRemoved": { "message": "'$title' aus den Favoriten entfernt.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionRegionParamDesc": { "message": "Der zweistellige ISO 3166-1-Ländercode für die neue Region (z.B. 'US' für Vereinigte Staaten, 'DE' für Deutschland, 'AT' für Österreich)." },
"aiToolRecommendationsSuccess": { "message": "Zeige Empfehlungen an." }, "aiToolClearAllFavoritesDesc": { "message": "Entfernt alle Filme und Serien, die der Benutzer als Favoriten markiert hat." },
"aiToolApplyFiltersGenreNotFound": { "message": "Genre '$genre' nicht gefunden.", "placeholders": { "genre": { "content": "$1" } } }, "aiToolClearViewingHistoryDesc": { "message": "Löscht den Wiedergabeverlauf des Benutzers von der Verlaufsseite." },
"aiToolApplyFiltersSuccess": { "message": "Filter erfolgreich angewendet." }, "aiToolClearRecommendationsViewDesc": { "message": "Löscht die Empfehlungsansicht und entfernt zwischengespeicherte Empfehlungen." },
"aiToolPlayMusicNotReady": { "message": "Der Musik-Player ist nicht bereit. Stellen Sie sicher, dass Ihre Plex-Musikbibliothek gescannt wurde." }, "aiToolSearchNotFound": { "message": "„$query“ wurde nicht in deiner Bibliothek gefunden.", "placeholders": { "query": { "content": "$1" } } },
"aiToolPlayMusicArtistNotFound": { "message": "Künstler '$artist_name' nicht gefunden.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolNavigateSuccess": { "message": "Zur Seite $page$ navigiert.", "placeholders": { "page": { "content": "$1" } } },
"aiToolPlayMusicNoSongs": { "message": "Keine Lieder für '$artist_name' gefunden.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolNavigateError": { "message": "Fehler beim Navigieren zur Seite $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolPlayMusicSuccess": { "message": "Spiele Musik von '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolStatsError": { "message": "Fehler beim Abrufen der Statistiken." },
"aiToolChatHistoryCleared": { "message": "Chatverlauf gelöscht." }, "aiToolItemNotFound": { "message": "Element '$title' nicht gefunden.", "placeholders": { "title": { "content": "$1" } } },
"aiToolConfirmDeleteDatabase": { "message": "Sind Sie sicher, dass Sie die lokale Datenbank löschen möchten? Diese Aktion ist irreversibel." }, "aiToolShowItemDetailsSuccess": { "message": "Zeige Details für '$title'.", "placeholders": { "title": { "content": "$1" } } },
"aiToolDeleteDatabaseCancelled": { "message": "Löschen der Datenbank abgebrochen." }, "aiToolAddToPlaylistSuccess": { "message": "'$title' zur Wiedergabeliste hinzugefügt.", "placeholders": { "title": { "content": "$1" } } },
"aiToolExecutionError": { "message": "Fehler beim Ausführen des Tools '$toolName': $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolFavoriteAdded": { "message": "'$title' zu den Favoriten hinzugefügt.", "placeholders": { "title": { "content": "$1" } } },
"aiToolUnknown": { "message": "Unbekanntes Tool: '$toolName'.", "placeholders": { "toolName": { "content": "$1" } } }, "aiToolFavoriteRemoved": { "message": "'$title' aus den Favoriten entfernt.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoritesCleared": { "message": "Favoriten gelöscht." }, "aiToolRecommendationsSuccess": { "message": "Zeige Empfehlungen." },
"aiToolFavoritesClearError": { "message": "Fehler beim Löschen der Favoriten: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolApplyFiltersGenreNotFound": { "message": "Genre '$genre' nicht gefunden.", "placeholders": { "genre": { "content": "$1" } } },
"aiToolRecommendationsCleared": { "message": "Empfehlungen gelöscht." }, "aiToolApplyFiltersSuccess": { "message": "Filter erfolgreich angewendet." },
"aiToolRecommendationsClearError": { "message": "Fehler beim Löschen der Empfehlungen: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolSearchMusicByGenreNotFound": { "message": "Ich habe keine Künstler des Genres '$genre_name' in deiner Bibliothek gefunden.", "placeholders": { "genre_name": { "content": "$1" } } },
"aiToolDatabaseDeleted": { "message": "Datenbank gelöscht. Die Seite wird neu geladen." }, "aiToolPlayMusicNotReady": { "message": "Der Musikplayer ist nicht bereit. Stelle sicher, dass deine Plex-Musikbibliothek gescannt wurde." },
"aiToolDatabaseDeleteError": { "message": "Fehler beim Löschen der Datenbank: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolPlayMusicArtistNotFound": { "message": "Künstler '$artist_name' nicht gefunden.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolDatabaseDeleteBlocked": { "message": "Das Löschen der Datenbank ist blockiert. Schließen Sie andere Tabs der Anwendung." }, "aiToolPlayMusicNoSongs": { "message": "Keine Lieder für '$artist_name' gefunden.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolUpdateAllTokensSuccess": { "message": "Alle Token wurden erfolgreich aktualisiert." }, "aiToolPlayMusicSuccess": { "message": "Spiele Musik von '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolUpdateAllTokensError": { "message": "Fehler beim Aktualisieren der Token: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolChatHistoryCleared": { "message": "Chatverlauf gelöscht." },
"aiToolAddPlexTokenSuccess": { "message": "Plex-Token erfolgreich hinzugefügt." }, "aiToolConfirmDeleteDatabase": { "message": "Bist du sicher, dass du die lokale Datenbank löschen möchtest? Diese Aktion ist unumkehrbar." },
"aiToolAddPlexTokenError": { "message": "Fehler beim Hinzufügen des Plex-Tokens: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolDeleteDatabaseCancelled": { "message": "Löschen der Datenbank abgebrochen." },
"aiToolChangeRegionSuccess": { "message": "Region auf $region$ geändert. Der Inhalt wird aktualisiert.", "placeholders": { "region": { "content": "$1" } } }, "aiToolExecutionError": { "message": "Fehler bei der Ausführung des Werkzeugs '$toolName': $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } },
"aiToolChangeRegionError": { "message": "Fehler beim Ändern der Region: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolUnknown": { "message": "Unbekanntes Werkzeug: '$toolName'.", "placeholders": { "toolName": { "content": "$1" } } },
"aiToolViewingHistoryCleared": { "message": "Wiedergabeverlauf gelöscht." }, "aiToolFavoritesCleared": { "message": "Favoriten gelöscht." },
"aiToolViewingHistoryClearError": { "message": "Fehler beim Löschen des Wiedergabeverlaufs: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolFavoritesClearError": { "message": "Fehler beim Löschen der Favoriten: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiSystemPrompt_v3": { "message": "Sie sind ein erfahrener Film- und Serienassistent namens CinePlex. Ihre Hauptfunktion besteht darin, Benutzern bei der Entdeckung von Inhalten und der Interaktion mit ihrer Bibliothek zu helfen. Befolgen Sie diese Regeln strikt: 1. **TUN SIE NIEMALS SO**, als hätten Sie eine Aktion ausgeführt, wenn Sie kein Werkzeug dafür verwendet haben. Sagen Sie zum Beispiel nicht 'Ich habe X heruntergeladen', wenn Sie das Download-Tool nicht verwendet haben. 2. Bei Empfehlungs- oder Listenanfragen (z. B. 'Nennen Sie mir 5 Horrorfilme') verwenden Sie Ihr eigenes Wissen, um die Liste zu erstellen. Präsentieren Sie sie in nummerierter oder Aufzählungsform. Fragen Sie den Benutzer nach der Anzeige der Liste proaktiv, ob er die Verfügbarkeit auf seinen lokalen Servern überprüfen und eine M3U-Datei erstellen soll. 3. **NUR** wenn der Benutzer bestätigt, dass er die Liste überprüfen oder herunterladen möchte, verwenden Sie das Tool `check_and_download_titles_list`. Verwenden Sie es nicht ohne ausdrückliche Bestätigung. 4. Für alle anderen Aktionen wie das Navigieren, das Abrufen von Statistiken, das Suchen nach einem bestimmten Titel oder das Filtern nach Auflösung oder Container verwenden Sie die entsprechenden Tools. Seien Sie immer prägnant, freundlich und effizient." }, "aiToolRecommendationsCleared": { "message": "Empfehlungen gelöscht." },
"aiToolM3UNoTitlesProvided": { "message": "Bitte geben Sie eine Liste von Titeln an, um die Wiedergabeliste zu erstellen." }, "aiToolRecommendationsClearError": { "message": "Fehler beim Löschen der Empfehlungen: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolM3UCheckingTitles": { "message": "Überprüfe die Titel auf Ihren lokalen Servern..." }, "aiToolDatabaseDeleted": { "message": "Datenbank gelöscht. Die Seite wird neu geladen." },
"aiToolM3UNoLocalMatchesForDownload": { "message": "Ich habe keinen der Filme oder Serien aus der Liste auf Ihren lokalen Servern gefunden." }, "aiToolDatabaseDeleteError": { "message": "Fehler beim Löschen der Datenbank: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolM3UDownloadStarted": { "message": "Fertig! Ich habe $1 von $2 Titeln auf Ihren Servern gefunden und den Download der M3U-Wiedergabeliste gestartet.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } }, "aiToolDatabaseDeleteBlocked": { "message": "Das Löschen der Datenbank ist blockiert. Schließe andere Tabs der Anwendung." },
"backToProviders": { "message": "Zurück zu den Anbietern" }, "aiToolUpdateAllTokensSuccess": { "message": "Alle Tokens wurden erfolgreich aktualisiert." },
"artistsCounterSingle": { "message": "$total$ Künstler", "placeholders": { "total": { "content": "$1" } } }, "aiToolUpdateAllTokensError": { "message": "Fehler beim Aktualisieren der Tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Laden..." }, "aiToolAddPlexTokenSuccess": { "message": "Plex-Token erfolgreich hinzugefügt." },
"downloadingSong": { "message": "Starte Download von \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenError": { "message": "Fehler beim Hinzufügen des Plex-Tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" heruntergeladen.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionSuccess": { "message": "Region auf $region$ geändert. Der Inhalt wird aktualisiert.", "placeholders": { "region": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Fehler beim Herunterladen von \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionError": { "message": "Fehler beim Ändern der Region: $message$", "placeholders": { "message": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Generiere M3U für \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } }, "aiToolViewingHistoryCleared": { "message": "Wiedergabeverlauf gelöscht." },
"albumM3UGenerated": { "message": "M3U für Album \"$artist$\" generiert.", "placeholders": { "artist": { "content": "$1" } } }, "aiToolViewingHistoryClearError": { "message": "Fehler beim Löschen des Wiedergabeverlaufs: $message$", "placeholders": { "message": { "content": "$1" } } },
"retyingSection": { "message": "Wiederhole Abschnitt \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolM3UDownloadStartedSingle": { "message": "Starte den M3U-Download für '$movie_title'.", "placeholders": { "movie_title": { "content": "$1" } } },
"retrySuccess": { "message": "[ERFOLG] Wiederholung von \"$title$\" abgeschlossen.", "placeholders": { "title": { "content": "$1" } } }, "aiToolM3UDownloadStartedSeason": { "message": "Starte den M3U-Download für Staffel $1 von '$2'.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } },
"retryError": { "message": "[FEHLER] Wiederholung für \"$title$\" fehlgeschlagen: $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolM3UNoTitlesProvided": { "message": "Bitte gib eine Liste von Titeln an, um die Wiedergabeliste zu erstellen." },
"startingRetryPhase": { "message": "Starte Wiederholungsphase für $count$ Abschnitte...", "placeholders": { "count": { "content": "$1" } } }, "aiToolM3UCheckingTitles": { "message": "Überprüfe die Titel auf deinen lokalen Servern..." },
"tokenFoundServers": { "message": "Token $token$... hat $count$ Server gefunden.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } }, "aiToolM3UNoLocalMatchesForDownload": { "message": "Ich konnte keinen der Filme oder Serien aus der Liste auf deinen lokalen Servern finden." },
"errorProcessingToken": { "message": "Fehler beim Verarbeiten des Tokens $token$...: $message$", "placeholders": { "token": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolM3UDownloadStarted": { "message": "Fertig! Ich habe $1 von $2 Titeln auf deinen Servern gefunden und den Download der M3U-Wiedergabeliste gestartet.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } },
"plexScanFatalError": { "message": "FATALER FEHLER: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolTrailerNotFoundSpecific": { "message": "Entschuldigung, ich konnte keinen verfügbaren Trailer für '$title' finden.", "placeholders": { "title": { "content": "$1" } } },
"errorDuringScan": { "message": "Fehler während des Scans: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiSystemPrompt_v4": {
"stoppingPlexScan": { "message": "Plex-Scan wird gestoppt..." }, "message": "Du bist ein virtueller Assistent, der in eine Chrome-Erweiterung integriert ist und mit Plex- und Jellyfin-Servern interagiert. Deine Hauptfunktion ist es, dem Benutzer beim Suchen, Verwalten, Abspielen und Herunterladen von Multimedia-Inhalten sowie bei der Verwaltung benutzerdefinierter Einstellungen zu helfen.\n\nHÖCHSTE PRIORITÄT: Immer wenn sich die Frage des Benutzers auf Multimedia-Inhalte (Filme, Serien, Musik) bezieht, MUSST du davon ausgehen, dass sie sich auf seine lokale Bibliothek bezieht. Nutze die Werkzeuge, um seine Datenbank zu durchsuchen, BEVOR du im Web suchst.\n\n🎯 Allgemeine Verhaltensregeln:\nAntworte immer klar, prägnant und direkt. Sei proaktiv und liefere alle relevanten Informationen auf einmal, um Nachfragen zu vermeiden. Wenn du beispielsweise die Verfügbarkeit einer Serie bestätigst, gib auch die Details zu den Staffeln an.\n\nVergleiche das aktuelle Datum mit den Google-Suchergebnissen, wenn du nach externen Informationen gefragt wirst, um sicherzustellen, dass sie aktuell sind.\n\nVerwende beim Aufrufen von Werkzeugen die exakten Namen der im System definierten Befehle (function.name).\n\n📦 Schlüsselfunktionen für Multimedia-Inhalte:\nUm eine M3U für einen einzelnen Film zu generieren, verwende download_single_movie_m3u.\nUm eine bestimmte Staffel einer Serie herunterzuladen, verwende download_series_season_m3u.\nFür mehrere Titel (Filme oder ganze Serien) verwende immer check_and_download_titles_list.\nUm lokale Inhalte zu suchen: search_library.\nUm in TMDB zu suchen: search_tmdb_content.\nFür Trendinhalte: get_trending_content.\nUm Details eines Titels anzuzeigen: show_item_details.\nUm zur PHP-Wiedergabeliste hinzuzufügen: add_to_playlist.\nUm die lokale Verfügbarkeit zu prüfen: check_local_availability.\nWenn eine Serie lokal verfügbar ist, gib an, wie viele Staffeln es gibt und auf welchen Servern, indem du get_local_series_seasons verwendest.\nUm Empfehlungen anzusehen: get_recommendations.\nUm Filter anzuwenden: apply_filters.\nUm Verlauf oder Favoriten anzuzeigen: view_history, view_favorites.\nUm als Favorit zu markieren: toggle_favorite.\nUm Trailer abzuspielen: play_trailer.\n\n🎵 Musikfunktionen:\nWenn der Benutzer nach allgemeinen Empfehlungen für Musikgenres fragt (z.B. 'empfiehl mir ein Genre, um mich aufzuheitern'), verwende zuerst list_available_music_genres, um zu sehen, welche Genres er hat, und basiere deine Empfehlung auf dieser Liste.\nUm alle verfügbaren Musikgenres in der Bibliothek aufzulisten: list_available_music_genres.\nUm Künstler nach Genre zu suchen: search_music_by_genre.\nUm Lieder nach Titel und/oder Künstler abzuspielen: play_song.\nUm Musik eines Künstlers abzuspielen: play_music_by_artist.\n\n🧰 Verwaltungs- und Konfigurationsfunktionen:\nUm Benutzerstatistiken zu erhalten: get_user_stats.\nUm zu bestimmten Abschnitten zu navigieren: navigate_to_page.\nUm Tokens zu aktualisieren: update_all_tokens, add_plex_token.\nUm die Inhaltsregion zu ändern: change_region.\nUm die lokale Datenbank zu exportieren oder zu importieren: export_local_database, import_local_database.\nUm die Datenbank zu löschen: delete_database.\nUm Favoriten, Verlauf oder Empfehlungen zu löschen: clear_all_favorites, clear_viewing_history, clear_recommendations_view.\nUm den Hell-/Dunkelmodus umzuschalten: toggle_light_mode.\nUm den Hero-Bereich ein- oder auszublenden: toggle_hero_section.\n\n⚠ Zusätzliche Überlegungen:\nPriorisiere lokal verfügbare Inhalte. Verwende check_local_availability, bevor du Wiedergabe- oder Download-Optionen anzeigst.\nWenn eine Aktion fehlschlägt, informiere klar und unmissverständlich darüber.\nVermeide es, die Anfrage des Benutzers unnötig zu wiederholen, es sei denn, es hilft, die Antwort zu kontextualisieren."
"invalidTokenProvided": { "message": "Ungültiges Token angegeben." }, },
"tokenAlreadyExists": { "message": "Token existiert bereits." }, "backToProviders": { "message": "Zurück zu den Anbietern" },
"tokenAddedSuccessfully": { "message": "Token erfolgreich hinzugefügt." }, "artistsCounterSingle": { "message": "$total$ Künstler", "placeholders": { "total": { "content": "$1" } } },
"noStreamsFoundForSelection": { "message": "Keine Streams für die Auswahl gefunden." }, "artistsCounterLoading": { "message": "Wird geladen..." },
"autoplayBlocked": { "message": "Autoplay blockiert." }, "downloadingSong": { "message": "Starte den Download von „$title$“", "placeholders": { "title": { "content": "$1" } } },
"page": { "message": "Seite" }, "songDownloaded": { "message": "„$title$“ heruntergeladen.", "placeholders": { "title": { "content": "$1" } } },
"all": { "message": "Alle" }, "errorDownloadingSong": { "message": "Fehler beim Herunterladen von „$title$“", "placeholders": { "title": { "content": "$1" } } },
"userScore": { "message": "Benutzerbewertung" }, "generatingAlbumM3U": { "message": "Generiere M3U für „$artist$“", "placeholders": { "artist": { "content": "$1" } } },
"duration": { "message": "Dauer" }, "albumM3UGenerated": { "message": "M3U für das Album „$artist$“ generiert.", "placeholders": { "artist": { "content": "$1" } } },
"min": { "message": "Min" }, "retyingSection": { "message": "Wiederhole Abschnitt „$title$“", "placeholders": { "title": { "content": "$1" } } },
"max": { "message": "Max" } "retrySuccess": { "message": "[ERFOLG] Wiederholung von „$title$“ abgeschlossen.", "placeholders": { "title": { "content": "$1" } } },
"retryError": { "message": "[FINALER FEHLER] Wiederholung für „$title$“ fehlgeschlagen: $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } },
"startingRetryPhase": { "message": "Starte Wiederholungsphase für $count$ Abschnitte...", "placeholders": { "count": { "content": "$1" } } },
"tokenFoundServers": { "message": "Token $token$... fand $count$ Server.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } },
"errorProcessingToken": { "message": "Fehler bei der Verarbeitung von Token $token$...: $message$", "placeholders": { "token": { "content": "$1" }, "message": { "content": "$2" } } },
"plexScanFatalError": { "message": "FATALER FEHLER: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorDuringScan": { "message": "Fehler während des Scans: $message$", "placeholders": { "message": { "content": "$1" } } },
"stoppingPlexScan": { "message": "Plex-Scan wird gestoppt..." },
"invalidTokenProvided": { "message": "Ungültiges Token bereitgestellt." },
"tokenAlreadyExists": { "message": "Das Token existiert bereits." },
"tokenAddedSuccessfully": { "message": "Token erfolgreich hinzugefügt." },
"noStreamsFoundForSelection": { "message": "Keine Streams für die Auswahl gefunden." },
"autoplayBlocked": { "message": "Automatische Wiedergabe blockiert." },
"welcomeToCinePlex": { "message": "" },
"page": { "message": "Seite" },
"all": { "message": "Alle" },
"userScore": { "message": "Bewertung" },
"duration": { "message": "Dauer" },
"min": { "message": "Min" },
"max": { "message": "Max" },
"aiToolFindStreamingProvidersDesc": { "message": "Findet heraus, wo man einen Film oder eine Serie auf Streaming-Diensten ansehen kann." },
"aiToolFindStreamingProvidersTitleParamDesc": { "message": "Der Titel des zu suchenden Films oder der Serie." },
"aiToolFindStreamingProvidersTypeParamDesc": { "message": "Der Inhaltstyp (Film oder Serie)." },
"aiToolFindStreamingProvidersYearParamDesc": { "message": "Das Erscheinungsjahr des Inhalts (optional)." },
"aiToolNoStreamingProviders": { "message": "Keine Streaming-Anbieter für {title} gefunden." },
"aiToolStreamingProvidersFound": { "message": "{title} ist auf folgenden Diensten verfügbar: {providers}." },
"aiToolStreamingProviderError": { "message": "Fehler bei der Suche nach Streaming-Anbietern: {message}." },
"aiToolGetLocalSeriesSeasonsDesc": { "message": "Überprüft, ob eine TV-Serie lokal verfügbar ist und gibt eine detaillierte Aufschlüsselung der verfügbaren Staffeln auf jedem Server zurück." },
"aiToolGetLocalSeriesSeasonsTitleParamDesc": { "message": "Der Titel der zu überprüfenden Serie." },
"aiToolGetLocalSeriesSeasonsYearParamDesc": { "message": "Das Erscheinungsjahr der Serie (optional für höhere Genauigkeit)." },
"aiToolLocalSeriesNoSeasons": { "message": "Die Serie '$series_title' befindet sich in deiner Bibliothek, aber es wurden keine Staffeldetails gefunden.", "placeholders": { "series_title": { "content": "$1" } } },
"artist": { "message": "Künstler" },
"tracks": { "message": "Titel" },
"noSongsFound": { "message": "Keine Lieder für diesen Künstler gefunden." },
"durationMin": { "message": "Dauer (Min)" },
"score": { "message": "Bewertung" },
"searchGenre": { "message": "Genre suchen..." },
"searchArtists": { "message": "Künstler suchen..." },
"preparingMusicLibrary": { "message": "Deine Musikbibliothek wird vorbereitet..." },
"preparingMusicLibraryDesc": { "message": "Dieser einmalige Vorgang kann einige Minuten dauern, wenn du viele Künstler hast." },
"artistsProgress": { "message": "0 / 0 Künstler" },
"starting": { "message": "Wird gestartet..." },
"artistName": { "message": "Künstlername" },
"playPause": { "message": "Wiedergabe/Pause" },
"noLocalFilesFound": { "message": "Keine lokalen Dateien für diesen Titel gefunden." },
"server": { "message": "Server" },
"title": { "message": "Titel" },
"year": { "message": "Jahr" },
"resolution": { "message": "Auflösung" },
"size": { "message": "Größe" },
"container": { "message": "Container" },
"action": { "message": "Aktion" },
"generate": { "message": "Generieren" },
"availableLocalFiles": { "message": "Verfügbare lokale Dateien" },
"downloadSeason": { "message": "Staffel herunterladen" },
"errorLoadingServersM3u": { "message": "Fehler beim Laden der Server für den M3U-Generator:" },
"errorFetchingLibraries": { "message": "Fehler beim Abrufen der Bibliotheken." },
"selectServerAndLibrary": { "message": "Bitte wähle einen Server und mindestens eine Bibliothek aus." },
"generating": { "message": "Wird generiert..." },
"errorProcessingLibrary": { "message": "Fehler bei der Verarbeitung der Bibliothek" },
"errorProcessingLibrarySkipping": { "message": "Fehler bei der Verarbeitung der Bibliothek. Wird übersprungen." },
"allLibrariesFailed": { "message": "Alle ausgewählten Bibliotheken konnten nicht verarbeitet werden." },
"m3uGeneratedWithErrors": { "message": "M3U mit einigen Fehlern generiert. Einige Bibliotheken könnten fehlen." },
"m3uDownloadedSuccess": { "message": "M3U-Wiedergabeliste erfolgreich heruntergeladen." },
"errorGeneratingM3uFile": { "message": "Fehler beim Generieren der M3U-Datei." },
"chatSources": { "message": "Quellen" },
"chatUnnamedSource": { "message": "Unbenannte Quelle" },
"googleApiFailure": { "message": "Aufruf der Google AI API fehlgeschlagen:" }
} }

View File

@ -1,449 +1,516 @@
{ {
"appName": { "message": "CinePlex" }, "appName": { "message": "CinePlex" },
"appDescription": { "message": "Scans Plex servers for content and displays it in the interface" }, "appDescription": { "message": "Scans Plex servers to find content and displays it in the interface" },
"appTagline": { "message": "Movies, Series, and Music" }, "appTagline": { "message": "Movies, Series, and Music" },
"appLocaleCode": { "message": "en-US" }, "appLocaleCode": { "message": "en-US" },
"toggleNavigation": { "message": "Toggle Navigation" }, "toggleNavigation": { "message": "Toggle Navigation" },
"searchPlaceholder": { "message": "Search for movies or series..." }, "searchPlaceholder": { "message": "Search for movies or series..." },
"openMusicPlayer": { "message": "Open Music Player" }, "openMusicPlayer": { "message": "Open Music Player" },
"settings": { "message": "Settings" }, "settings": { "message": "Settings" },
"navMovies": { "message": "Movies" }, "navMovies": { "message": "Movies" },
"navSeries": { "message": "Series" }, "navSeries": { "message": "Series" },
"navProviders": { "message": "Providers" }, "navProviders": { "message": "Providers" },
"navPhotos": { "message": "Photos" }, "navPhotos": { "message": "Photos" },
"navStats": { "message": "Statistics" }, "navStats": { "message": "Statistics" },
"navFavorites": { "message": "Favorites" }, "navFavorites": { "message": "Favorites" },
"navHistory": { "message": "History" }, "navHistory": { "message": "History" },
"navRecommendations": { "message": "Recommendations" }, "navRecommendations": { "message": "Recommendations" },
"navMusic": { "message": "Music" }, "navMusic": { "message": "Music" },
"navM3uGenerator": { "message": "M3U Generator" }, "musicFeaturedPlaylists": { "message": "Featured Playlists" },
"heroWelcome": { "message": "" }, "musicRecentlyAdded": { "message": "Recently Added" },
"heroSubtitle": { "message": "Explore thousands of movies and series." }, "navM3uGenerator": { "message": "M3U Generator" },
"addStream": { "message": "Add Stream" }, "heroWelcome": { "message": "" },
"moreInfo": { "message": "More info" }, "heroSubtitle": { "message": "Explore thousands of movies and series." },
"popularMovies": { "message": "Popular Movies" }, "addStream": { "message": "Add Stream" },
"allGenres": { "message": "All genres" }, "moreInfo": { "message": "More info" },
"allYears": { "message": "All years" }, "popularMovies": { "message": "Popular Movies" },
"sortPopular": { "message": "Most popular" }, "allGenres": { "message": "All genres" },
"sortTopRated": { "message": "Top rated" }, "allYears": { "message": "All years" },
"sortRecent": { "message": "Most recent" }, "sortPopular": { "message": "Most popular" },
"loadMore": { "message": "Load more" }, "sortTopRated": { "message": "Top rated" },
"photosBreadcrumbHome": { "message": "Albums" }, "sortRecent": { "message": "Most recent" },
"selectServer": { "message": "Select a server" }, "loadMore": { "message": "Load more" },
"loading": { "message": "Loading..." }, "photosBreadcrumbHome": { "message": "Albums" },
"loadingLibraries": { "message": "Loading libraries..." }, "selectServer": { "message": "Select a server" },
"photosEmptyState": { "message": "No albums or photos found." }, "loading": { "message": "Loading..." },
"photosEmptyStateSub": { "message": "Please select a server or make sure you have a photo library in Plex." }, "loadingLibraries": { "message": "Loading libraries..." },
"statsTitle": { "message": "Library Statistics" }, "photosEmptyState": { "message": "No albums or photos found." },
"statsAllTokens": { "message": "All Tokens" }, "photosEmptyStateSub": { "message": "Please select a server or ensure you have a photo library in Plex." },
"statsAnalyzing": { "message": "Analyzing your library..." }, "statsTitle": { "message": "Library Statistics" },
"statsActiveTokens": { "message": "Active Tokens" }, "statsAllTokens": { "message": "All Tokens" },
"statsServersFound": { "message": "Servers Found" }, "statsAnalyzing": { "message": "Analyzing your library..." },
"statsUniqueMovies": { "message": "Unique Movies" }, "statsActiveTokens": { "message": "Active Tokens" },
"statsUniqueSeries": { "message": "Unique Series" }, "statsServersFound": { "message": "Servers Found" },
"statsUniqueArtists": { "message": "Unique Artists" }, "statsUniqueMovies": { "message": "Unique Movies" },
"statsTokenServers": { "message": "Token Servers" }, "statsUniqueSeries": { "message": "Unique Series" },
"statsChartMoviesByGenre": { "message": "Content by Genre (Movies)" }, "statsUniqueArtists": { "message": "Unique Artists" },
"statsChartSeriesByGenre": { "message": "Content by Genre (Series)" }, "statsTokenServers": { "message": "Token's Servers" },
"statsChartByDecade": { "message": "Content by Decade" }, "statsChartMoviesByGenre": { "message": "Content by Genre (Movies)" },
"recommendationsTitle": { "message": "Recommendations for you" }, "statsChartSeriesByGenre": { "message": "Content by Genre (Series)" },
"historyTitle": { "message": "Viewing History" }, "statsChartByDecade": { "message": "Content by Decade" },
"clearHistory": { "message": "Clear All" }, "recommendationsTitle": { "message": "Recommendations for you" },
"consoleTitle": { "message": "Plex Scan Console" }, "historyTitle": { "message": "Viewing History" },
"footerCredit": { "message": "An interface for your Plex universe." }, "clearHistory": { "message": "Clear All" },
"closeTrailer": { "message": "Close trailer" }, "consoleTitle": { "message": "Plex Scan Console" },
"close": { "message": "Close" }, "footerCredit": { "message": "An interface for your Plex universe." },
"photoViewer": { "message": "Photo Viewer" }, "closeTrailer": { "message": "Close trailer" },
"previous": { "message": "Previous" }, "close": { "message": "Close" },
"next": { "message": "Next" }, "photoViewer": { "message": "Photo viewer" },
"notificationTemplateText": { "message": "Notification" }, "previous": { "message": "Previous" },
"settingsTitleFull": { "message": "Settings and Configuration" }, "next": { "message": "Next" },
"settingsTabGeneral": { "message": "General" }, "notificationTemplateText": { "message": "Notification" },
"settingsTabPlex": { "message": "Plex" }, "settingsTitleFull": { "message": "Settings and Configuration" },
"settingsTabJellyfin": { "message": "Jellyfin" }, "settingsTabGeneral": { "message": "General" },
"settingsTabPhpGen": { "message": "PHP Generator" }, "settingsTabPlex": { "message": "Plex" },
"settingsTabData": { "message": "Data" }, "settingsTabJellyfin": { "message": "Jellyfin" },
"settingsApiServer": { "message": "API and Server Settings" }, "settingsTabPhpGen": { "message": "PHP Generator" },
"settingsTmdbApiLabel": { "message": "TMDB API Key (Optional)" }, "settingsTabData": { "message": "Data" },
"settingsTmdbApiPlaceholder": { "message": "The default key will be used if left blank" }, "settingsApiServer": { "message": "API and Server Configuration" },
"settingsGoogleApiLabel": { "message": "Google Gemini API Key (Optional)" }, "settingsTmdbApiLabel": { "message": "TMDB API Key (Optional)" },
"settingsGoogleApiPlaceholder": { "message": "Required to use the AI assistant" }, "settingsTmdbApiPlaceholder": { "message": "The default key will be used if left blank" },
"settingsRegionLabel": { "message": "Region for content discovery" }, "settingsGoogleApiLabel": { "message": "Google Gemini API Key (Optional)" },
"allRegions": { "message": "All regions" }, "settingsGoogleApiPlaceholder": { "message": "Required to use the AI assistant" },
"settingsPhpUrlLabel": { "message": "Server URL for Adding Streams" }, "settingsRegionLabel": { "message": "Region for content discovery" },
"settingsPhpUrlPlaceholder": { "message": "https://your-server.com/path/to/script.php" }, "allRegions": { "message": "All regions" },
"settingsInterface": { "message": "Interface" }, "settingsPhpUrlLabel": { "message": "Server URL for Adding Streams" },
"settingsLightTheme": { "message": "Light Mode" }, "settingsPhpUrlPlaceholder": { "message": "https://your-server.com/path/to/script.php" },
"settingsShowHero": { "message": "Show 'Hero' welcome section" }, "settingsInterface": { "message": "Interface" },
"settingsScanContent": { "message": "Content Scan" }, "settingsLightTheme": { "message": "Light Mode" },
"settingsScanDesc": { "message": "Select what to scan and press the button." }, "settingsShowHero": { "message": "Show 'Hero' welcome section" },
"settingsScanMovies": { "message": "Movies" }, "settingsScanContent": { "message": "Content Scan" },
"settingsScanShows": { "message": "Series" }, "settingsScanDesc": { "message": "Select what to scan and press the button." },
"settingsScanArtists": { "message": "Music" }, "settingsScanMovies": { "message": "Movies" },
"settingsScanPhotos": { "message": "Photos" }, "settingsScanShows": { "message": "Series" },
"settingsSelectAll": { "message": "Select All" }, "settingsScanArtists": { "message": "Music" },
"settingsStartScan": { "message": "Start Scan" }, "settingsScanPhotos": { "message": "Photos" },
"settingsPlexTokens": { "message": "Plex Tokens" }, "settingsSelectAll": { "message": "Select All" },
"settingsPlexTokensDesc": { "message": "Edit the list of Plex tokens (JSON format)." }, "settingsStartScan": { "message": "Start Scan" },
"settingsSaveTokens": { "message": "Save Tokens" }, "settingsPlexTokens": { "message": "Plex Tokens" },
"settingsJellyfinTitle": { "message": "Jellyfin Settings" }, "settingsPlexTokensDesc": { "message": "Edit the list of Plex tokens (JSON format)." },
"settingsJellyfinDesc": { "message": "Add your Jellyfin server details to scan its content." }, "settingsSaveTokens": { "message": "Save Tokens" },
"jellyfinUrlLabel": { "message": "Jellyfin Server URL" }, "settingsJellyfinTitle": { "message": "Jellyfin Configuration" },
"jellyfinUserLabel": { "message": "Username" }, "settingsJellyfinDesc": { "message": "Add your Jellyfin server details to scan its content." },
"jellyfinPassLabel": { "message": "Password" }, "jellyfinUrlLabel": { "message": "Jellyfin Server URL" },
"jellyfinConnectAndScan": { "message": "Connect and Scan" }, "jellyfinUserLabel": { "message": "Username" },
"settingsPhpGenTitle": { "message": "PHP Script Generator for Server" }, "jellyfinPassLabel": { "message": "Password" },
"settingsPhpFileOptions": { "message": "File Options" }, "jellyfinConnectAndScan": { "message": "Connect and Scan" },
"settingsPhpSavePathLabel": { "message": "Save Path on Server" }, "settingsPhpGenTitle": { "message": "PHP Script Generator for Server" },
"settingsPhpSavePathPlaceholder": { "message": "Ex: /var/www/html/lists (blank for the same folder)" }, "settingsPhpFileOptions": { "message": "File Options" },
"settingsPhpFilenameLabel": { "message": "File Name" }, "settingsPhpSavePathLabel": { "message": "Save Path on Server" },
"settingsPhpFileAction": { "message": "File Action" }, "settingsPhpSavePathPlaceholder": { "message": "Ex: /var/www/html/lists (blank for the same folder)" },
"settingsPhpActionAppend": { "message": "Append to the end of the file (cumulative)" }, "settingsPhpFilenameLabel": { "message": "Filename" },
"settingsPhpActionOverwrite": { "message": "Overwrite the file (start over)" }, "settingsPhpFileAction": { "message": "File Action" },
"settingsPhpSecurity": { "message": "Security (Optional)" }, "settingsPhpActionAppend": { "message": "Append to the end of the file (cumulative)" },
"settingsPhpUseSecretKey": { "message": "Use secret key (Recommended)" }, "settingsPhpActionOverwrite": { "message": "Overwrite the file (start over)" },
"settingsPhpSecretKeyPlaceholder": { "message": "Enter a secure secret key" }, "settingsPhpSecurity": { "message": "Security (Optional)" },
"settingsPhpGeneratedCode": { "message": "Generated Code" }, "settingsPhpUseSecretKey": { "message": "Use secret key (Recommended)" },
"settingsPhpGeneratedPlaceholder": { "message": "The generated PHP code will appear here." }, "settingsPhpSecretKeyPlaceholder": { "message": "Enter a secure secret key" },
"settingsGenerateScript": { "message": "Generate Script" }, "settingsPhpGeneratedCode": { "message": "Generated Code" },
"settingsCopyScript": { "message": "Copy Script" }, "settingsPhpGeneratedPlaceholder": { "message": "The generated PHP code will appear here." },
"settingsDataManagement": { "message": "Local Database Management" }, "settingsGenerateScript": { "message": "Generate Script" },
"settingsImportDb": { "message": "Import DB from File" }, "settingsCopyScript": { "message": "Copy Script" },
"settingsExportDb": { "message": "Export DB to File" }, "settingsDataManagement": { "message": "Local Database Management" },
"settingsClearContent": { "message": "Clear Local Content Data" }, "settingsImportDb": { "message": "Import DB from File" },
"settingsClearContentDesc": { "message": "This action will delete movies, series, and music from the local database, but will not affect your favorites or your settings." }, "settingsExportDb": { "message": "Export DB to File" },
"settingsClose": { "message": "Close" }, "settingsClearContent": { "message": "Clear Local Content Data" },
"settingsSave": { "message": "Save Settings" }, "settingsClearContentDesc": { "message": "This action will delete movies, series, and music from the local database, but will not affect your favorites or settings." },
"musicSidenavTitle": { "message": "Plex Music" }, "settingsClose": { "message": "Close" },
"musicAllServers": { "message": "All Servers" }, "settingsSave": { "message": "Save Settings" },
"musicSearchArtistPlaceholder": { "message": "Search for an artist..." }, "musicSidenavTitle": { "message": "Plex Music" },
"musicSearchDiscographyPlaceholder": { "message": "Search in discography..." }, "musicAllServers": { "message": "All Servers" },
"musicNothingPlaying": { "message": "Nothing playing" }, "musicSearchArtistPlaceholder": { "message": "Search for artist..." },
"musicSelectSong": { "message": "Select a song" }, "musicSearchDiscographyPlaceholder": { "message": "Search in discography..." },
"musicToStart": { "message": "to start playing" }, "musicNothingPlaying": { "message": "Nothing playing" },
"miniplayerDownloadSong": { "message": "Download song" }, "musicSelectSong": { "message": "Select a song" },
"miniplayerDownloadAlbum": { "message": "Download M3U album" }, "musicToStart": { "message": "to start playing" },
"miniplayerVolume": { "message": "Volume" }, "miniplayerDownloadSong": { "message": "Download song" },
"miniplayerShuffle": { "message": "Shuffle" }, "miniplayerDownloadAlbum": { "message": "Download M3U" },
"miniplayerEqualizer": { "message": "Equalizer" }, "miniplayerVolume": { "message": "Volume" },
"miniplayerOpenList": { "message": "Open list" }, "miniplayerShuffle": { "message": "Shuffle" },
"eqTitle": { "message": "Graphic Equalizer" }, "miniplayerEqualizer": { "message": "Equalizer" },
"eqPresetsLabel": { "message": "Presets" }, "miniplayerOpenList": { "message": "Open list" },
"eqPresetFlat": { "message": "Flat" }, "eqTitle": { "message": "Graphic Equalizer" },
"eqPresetRock": { "message": "Rock" }, "eqPresetsLabel": { "message": "Presets" },
"eqPresetPop": { "message": "Pop" }, "eqPresetFlat": { "message": "Flat" },
"eqPresetJazz": { "message": "Jazz" }, "eqPresetRock": { "message": "Rock" },
"eqPresetClassical": { "message": "Classical" }, "eqPresetPop": { "message": "Pop" },
"eqPresetBassBoost": { "message": "Bass Boost" }, "eqPresetJazz": { "message": "Jazz" },
"eqPreampLabel": { "message": "Preamp" }, "eqPresetClassical": { "message": "Classical" },
"infoModalTitle": { "message": "Information" }, "eqPresetBassBoost": { "message": "Bass Boost" },
"infoModalFieldTitle": { "message": "Title:" }, "eqPreampLabel": { "message": "Preamplifier" },
"infoModalFieldArtist": { "message": "Artist:" }, "infoModalTitle": { "message": "Information" },
"infoModalFieldAlbum": { "message": "Album:" }, "infoModalFieldTitle": { "message": "Title:" },
"infoModalFieldSong": { "message": "Song:" }, "infoModalFieldArtist": { "message": "Artist:" },
"infoModalFieldYear": { "message": "Year:" }, "infoModalFieldAlbum": { "message": "Album:" },
"infoModalFieldGenre": { "message": "Genre:" }, "infoModalFieldSong": { "message": "Song:" },
"lang_en": { "message": "English" }, "infoModalFieldYear": { "message": "Year:" },
"lang_es": { "message": "Spanish" }, "infoModalFieldGenre": { "message": "Genre:" },
"lang_fr": { "message": "French" }, "lang_en": { "message": "English" },
"lang_de": { "message": "German" }, "lang_es": { "message": "Spanish" },
"lang_it": { "message": "Italian" }, "lang_fr": { "message": "French" },
"lang_pt": { "message": "Portuguese" }, "lang_de": { "message": "German" },
"essentialFeaturesNotSupported": { "message": "Your browser does not support essential features." }, "lang_it": { "message": "Italian" },
"dbAccessError": { "message": "Error accessing the local database." }, "lang_pt": { "message": "Portuguese" },
"dbUpdateNeeded": { "message": "The database needs to be updated, please reload the page." }, "essentialFeaturesNotSupported": { "message": "Your browser does not support essential features." },
"dbBlocked": { "message": "Please close other tabs of this application to continue." }, "dbAccessError": { "message": "Error accessing the local database." },
"deletingContentData": { "message": "Deleting local content data..." }, "dbUpdateNeeded": { "message": "The database needs to be updated, please reload the page." },
"noContentDataToDelete": { "message": "No content data to delete." }, "dbBlocked": { "message": "Please close other tabs of this application to continue." },
"contentDataDeleted": { "message": "Content data deleted from IndexedDB." }, "deletingContentData": { "message": "Deleting local content data..." },
"errorDeletingData": { "message": "Error deleting data: $message$", "placeholders": { "message": { "content": "$1" } } }, "noContentDataToDelete": { "message": "No content data to delete." },
"aceEditorNotAvailable": { "message": "Text editor not available." }, "contentDataDeleted": { "message": "Content data deleted from IndexedDB." },
"errorLoadingTokens": { "message": "Error loading tokens for editing." }, "errorDeletingData": { "message": "Error deleting data: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingTokensMessage": { "message": "Error loading tokens: $message$", "placeholders": { "message": { "content": "$1" } } }, "aceEditorNotAvailable": { "message": "Text editor not available." },
"aceEditorNotAvailableToSave": { "message": "Editor not available for saving." }, "errorLoadingTokens": { "message": "Error loading tokens for editing." },
"invalidJsonFormat": { "message": "Invalid JSON format. It must be { \"tokens\": [...] }" }, "errorLoadingTokensMessage": { "message": "Error loading tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"tokensSaved": { "message": "Tokens saved successfully." }, "aceEditorNotAvailableToSave": { "message": "Editor not available for saving." },
"errorSavingTokens": { "message": "Error saving tokens: $message$", "placeholders": { "message": { "content": "$1" } } }, "invalidJsonFormat": { "message": "Invalid JSON format. It must be { \"tokens\": [...] }" },
"dbNotAvailable": { "message": "IndexedDB is not available." }, "tokensSaved": { "message": "Tokens saved successfully." },
"dbExported": { "message": "Database exported successfully." }, "errorSavingTokens": { "message": "Error saving tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorExportingDb": { "message": "Error exporting the database: $message$", "placeholders": { "message": { "content": "$1" } } }, "dbNotAvailable": { "message": "IndexedDB is not available." },
"invalidJsonFile": { "message": "The file does not contain a valid JSON object." }, "dbExported": { "message": "Database exported successfully." },
"noDataToImport": { "message": "The file does not contain data for the current DB sections." }, "errorExportingDb": { "message": "Error exporting database: $message$", "placeholders": { "message": { "content": "$1" } } },
"dbImported": { "message": "Database imported successfully." }, "invalidJsonFile": { "message": "The file does not contain a valid JSON object." },
"errorImportingDb": { "message": "Error importing the database: $message$", "placeholders": { "message": { "content": "$1" } } }, "noDataToImport": { "message": "The file does not contain data for the current DB sections." },
"updatingView": { "message": "Updating the view with new data..." }, "dbImported": { "message": "Database imported successfully." },
"confirmClearContent": { "message": "Are you sure you want to delete local content data (Movies, Series, Music, etc.)? Favorites and Settings will NOT be deleted." }, "errorImportingDb": { "message": "Error importing database: $message$", "placeholders": { "message": { "content": "$1" } } },
"trailerNotFound": { "message": "No trailer found for this title." }, "updatingView": { "message": "Updating the view with new data..." },
"confirmClearHistory": { "message": "Are you sure you want to delete all your viewing history? This action cannot be undone." }, "confirmClearContent": { "message": "Are you sure you want to delete the local content data (Movies, Series, Music, etc.)? Favorites and Settings will NOT be deleted." },
"historyCleared": { "message": "Viewing history cleared." }, "trailerNotFound": { "message": "No trailer found for this title." },
"historyItemDeleted": { "message": "Item deleted from history." }, "confirmClearHistory": { "message": "Are you sure you want to delete all your viewing history? This action cannot be undone." },
"errorGeneratingScript": { "message": "First generate a script to be able to copy it." }, "historyCleared": { "message": "Viewing history cleared." },
"scriptCopied": { "message": "PHP script copied to clipboard." }, "historyItemDeleted": { "message": "Item deleted from history." },
"errorCopyingScript": { "message": "Error copying the script." }, "errorGeneratingScript": { "message": "First generate a script to be able to copy it." },
"scriptGenerated": { "message": "PHP script generated." }, "scriptCopied": { "message": "PHP script copied to clipboard." },
"errorLoadingAlbum": { "message": "Error loading album: $message$", "placeholders": { "message": { "content": "$1" } } }, "errorCopyingScript": { "message": "Error copying script." },
"noPhotoServerSelected": { "message": "Error: No photo server has been selected." }, "scriptGenerated": { "message": "PHP script generated." },
"loadingGenres": { "message": "Loading genres..." }, "errorLoadingAlbum": { "message": "Error loading album: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingGenres": { "message": "Error loading" }, "noPhotoServerSelected": { "message": "Error: No photo server has been selected." },
"noContentFound": { "message": "No results found." }, "loadingGenres": { "message": "Loading genres..." },
"couldNotLoadContent": { "message": "Could not load content." }, "errorLoadingGenres": { "message": "Error loading" },
"noFavorites": { "message": "You don't have any favorites yet." }, "noContentFound": { "message": "No results found." },
"errorLoadingFavorites": { "message": "Error loading favorites." }, "couldNotLoadContent": { "message": "Could not load content." },
"historyEmpty": { "message": "Your history is empty." }, "noFavorites": { "message": "You don't have any favorites yet." },
"historyEmptySub": { "message": "Explore and watch content for it to appear here." }, "errorLoadingFavorites": { "message": "Error loading favorites." },
"errorGeneratingRecommendations": { "message": "Error generating recommendations." }, "historyEmpty": { "message": "Your history is empty." },
"noRecommendations": { "message": "We need to get to know you better to give you recommendations!" }, "historyEmptySub": { "message": "Explore and watch content for it to appear here." },
"errorGeneratingStats": { "message": "Error generating statistics." }, "errorGeneratingRecommendations": { "message": "Error generating recommendations." },
"noServersForToken": { "message": "No associated servers found for this token." }, "noRecommendations": { "message": "We need to know you better to give you recommendations!" },
"searchingActorContent": { "message": "Searching for content by $actorName$", "placeholders": { "actorName": { "content": "$1" } } }, "errorGeneratingStats": { "message": "Error generating statistics." },
"errorLoadingActorContent": { "message": "Could not load content for $actorName$.", "placeholders": { "actorName": { "content": "$1" } } }, "noServersForToken": { "message": "No associated servers found for this token." },
"errorAddingStream": { "message": "Error adding stream(s): $message$", "placeholders": { "message": { "content": "$1" } } }, "searchingActorContent": { "message": "Searching for content by $actorName$", "placeholders": { "actorName": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "The PHP server URL is not configured. Please configure it in Settings." }, "errorLoadingActorContent": { "message": "Could not load content for $actorName$.", "placeholders": { "actorName": { "content": "$1" } } },
"searchingStreams": { "message": "Searching for streams for \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "errorAddingStream": { "message": "Error adding stream(s): $message$", "placeholders": { "message": { "content": "$1" } } },
"sendingStreams": { "message": "Sending $count$ stream(s) to the server...", "placeholders": { "count": { "content": "$1" } } }, "phpUrlNotConfigured": { "message": "The PHP server URL is not configured. Please set it up in Settings." },
"streamAddedSuccess": { "message": "Stream(s) added successfully." }, "searchingStreams": { "message": "Searching for streams for \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingM3U": { "message": "Generating M3U for \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "sendingStreams": { "message": "Sending $count$ stream(s) to the server...", "placeholders": { "count": { "content": "$1" } } },
"m3uDownloaded": { "message": "\"$title$\" downloaded.", "placeholders": { "title": { "content": "$1" } } }, "streamAddedSuccess": { "message": "Stream(s) added successfully." },
"errorGeneratingM3U": { "message": "Error generating M3U: $message$", "placeholders": { "message": { "content": "$1" } } }, "generatingM3U": { "message": "Generating M3U for \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"settingsSavedSuccess": { "message": "Settings saved successfully." }, "m3uDownloaded": { "message": "\"$title$\" downloaded.", "placeholders": { "title": { "content": "$1" } } },
"errorSavingSettings": { "message": "Error saving settings to the database." }, "errorGeneratingM3U": { "message": "Error generating M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
"languageChangeReload": { "message": "Language changed. The application will now reload." }, "settingsSavedSuccess": { "message": "Settings saved successfully." },
"addedToFavorites": { "message": "Added to favorites." }, "errorSavingSettings": { "message": "Error saving settings to the database." },
"removedFromFavorites": { "message": "Removed from favorites." }, "languageChangeReload": { "message": "Language changed. The application will now reload." },
"plexScanInProgress": { "message": "Plex scan is already in progress." }, "addedToFavorites": { "message": "Added to favorites." },
"plexScanStarting": { "message": "Starting Plex scan..." }, "removedFromFavorites": { "message": "Removed from favorites." },
"noPlexTokens": { "message": "No Plex tokens configured." }, "plexScanInProgress": { "message": "Plex scan is already in progress." },
"clearingSections": { "message": "Clearing sections: $sections$", "placeholders": { "sections": { "content": "$1" } } }, "plexScanStarting": { "message": "Starting Plex scan..." },
"initialScanPhaseComplete": { "message": "Initial scan phase finished." }, "noPlexTokens": { "message": "No Plex tokens configured." },
"retryPhaseFinished": { "message": "Retry phase finished." }, "clearingSections": { "message": "Clearing sections: $sections$", "placeholders": { "sections": { "content": "$1" } } },
"plexScanFinished": { "message": "Scan finished. Updating content..." }, "initialScanPhaseComplete": { "message": "Initial scan phase completed." },
"scanCancelled": { "message": "Scan cancelled by the user." }, "retryPhaseFinished": { "message": "Retry phase finished." },
"scanCancelledInfo": { "message": "Scan cancelled." }, "plexScanFinished": { "message": "Scan finished. Updating content..." },
"errorInitializingMusicPlayer": { "message": "Error initializing the music player." }, "scanCancelled": { "message": "Scan canceled by the user." },
"criticalErrorLoadingMusic": { "message": "Critical error loading music data." }, "scanCancelledInfo": { "message": "Scan canceled." },
"errorLoadingArtists": { "message": "Error loading artists." }, "errorInitializingMusicPlayer": { "message": "Error initializing music player." },
"dbUnavailableError": { "message": "Error: Database not available." }, "criticalErrorLoadingMusic": { "message": "Critical error loading music data." },
"updatingMusicData": { "message": "Updating music data..." }, "errorLoadingArtists": { "message": "Error loading artists." },
"musicDataUpdated": { "message": "Music data updated." }, "dbUnavailableError": { "message": "Error: Database not available." },
"errorFetchingArtistSongs": { "message": "Error fetching the artist's songs." }, "updatingMusicData": { "message": "Updating music data..." },
"errorLoadingSongs": { "message": "Error loading songs." }, "musicDataUpdated": { "message": "Music data updated." },
"noArtistsFound": { "message": "No artists found." }, "errorFetchingArtistSongs": { "message": "Error fetching artist's songs." },
"shuffleOn": { "message": "Shuffle mode on." }, "errorLoadingSongs": { "message": "Error loading songs." },
"shuffleOff": { "message": "Shuffle mode off." }, "noArtistsFound": { "message": "No artists found." },
"playbackError": { "message": "Playback error" }, "shuffleOn": { "message": "Shuffle mode on." },
"errorLabel": { "message": "Error" }, "shuffleOff": { "message": "Shuffle mode off." },
"reloadingPage": { "message": "Reloading the page..." }, "playbackError": { "message": "Playback error" },
"viewed": { "message": "Viewed" }, "errorLabel": { "message": "Error" },
"local": { "message": "Local" }, "reloadingPage": { "message": "Reloading page..." },
"topRatedSort": {"message": "Top Rated"}, "viewed": { "message": "Viewed" },
"recentSort": {"message": "Recent"}, "local": { "message": "Local" },
"popularSort": {"message": "Popular"}, "topRatedSort": {"message": "Top Rated"},
"moviesSectionTitle": {"message": "Movies"}, "recentSort": {"message": "Recent"},
"seriesSectionTitle": {"message": "Series"}, "popularSort": {"message": "Popular"},
"searchResultsFor": {"message": "Results for \"$query$\"", "placeholders": {"query": {"content": "$1"}}}, "moviesSectionTitle": {"message": "Movies"},
"contentFrom": {"message": "Content from $actor$", "placeholders": {"actor": {"content": "$1"}}}, "seriesSectionTitle": {"message": "Series"},
"explore": {"message": "Explore"}, "searchResultsFor": {"message": "Results for \"$query$\"", "placeholders": {"query": {"content": "$1"}}},
"noGenre": {"message": "Uncategorized"}, "contentFrom": {"message": "Content from $actor$", "placeholders": {"actor": {"content": "$1"}}},
"synopsis": {"message": "Synopsis"}, "explore": {"message": "Explore"},
"noSynopsis": {"message": "No synopsis available."}, "noGenre": {"message": "Uncategorized"},
"director": {"message": "Director:"}, "synopsis": {"message": "Synopsis"},
"writer": {"message": "Writer:"}, "noSynopsis": {"message": "No synopsis available."},
"viewOnImdb": {"message": "View on IMDb"}, "director": {"message": "Director:"},
"watchTrailer": {"message": "Watch Trailer"}, "writer": {"message": "Writer:"},
"addToFavorites": {"message": "Add to favorites"}, "viewOnImdb": {"message": "View on IMDb"},
"removeFromFavorites": {"message": "Remove from favorites"}, "watchTrailer": {"message": "Watch Trailer"},
"notAvailable": {"message": "Not available"}, "addToFavorites": {"message": "Add to favorites"},
"mainCast": {"message": "Main Cast"}, "removeFromFavorites": {"message": "Remove from favorites"},
"seasonsAndEpisodes": {"message": "Seasons and Episodes"}, "notAvailable": {"message": "Not available"},
"similarContent": {"message": "Similar Content"}, "mainCast": {"message": "Main Cast"},
"filmography": {"message": "Filmography"}, "seasonsAndEpisodes": {"message": "Seasons and Episodes"},
"availableOn": {"message": "Available on"}, "similarContent": {"message": "Similar Content"},
"episodesCount": {"message": "$count$ Episodes", "placeholders": {"count": {"content": "$1"}}}, "filmography": {"message": "Filmography"},
"seasonsCount": {"message": "$count$ Seasons", "placeholders": {"count": {"content": "$1"}}}, "availableOn": {"message": "Available on"},
"runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}}, "episodesCount": {"message": "$count$ Episodes", "placeholders": {"count": {"content": "$1"}}},
"noTrailerFound": {"message": "No trailer found for this title."}, "seasonsCount": {"message": "$count$ Seasons", "placeholders": {"count": {"content": "$1"}}},
"fatalInitError": {"message": "Fatal initialization error"}, "runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}},
"fatalInitErrorSub": {"message": "Could not load the application."}, "noTrailerFound": {"message": "No trailer found for this title."},
"invalidStreamInfo": {"message": "Invalid information."}, "fatalInitError": {"message": "Fatal initialization error"},
"dbUnavailableForStreams": {"message": "Local database not available."}, "fatalInitErrorSub": {"message": "Could not load the application."},
"noPlexServersForStreams": {"message": "No Plex servers."}, "invalidStreamInfo": {"message": "Invalid stream info."},
"notFoundOnServers": {"message": "\"$query$\" not found on Plex servers.", "placeholders": {"query": {"content": "$1"}}}, "dbUnavailableForStreams": {"message": "Local database not available."},
"relativeTime_justNow": { "message": "Just now" }, "noPlexServersForStreams": {"message": "No Plex servers."},
"relativeTime_minutesAgo": { "message": "$count$ minutes ago", "placeholders": { "count": { "content": "$1" } } }, "notFoundOnServers": {"message": "\"$query$\" not found on Plex servers.", "placeholders": {"query": {"content": "$1"}}},
"relativeTime_hoursAgo": { "message": "$count$ hours ago", "placeholders": { "count": { "content": "$1" } } }, "relativeTime_justNow": { "message": "Just now" },
"relativeTime_yesterday": { "message": "Yesterday" }, "relativeTime_minutesAgo": { "message": "$count$ minutes ago", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_daysAgo": { "message": "$count$ days ago", "placeholders": { "count": { "content": "$1" } } }, "relativeTime_hoursAgo": { "message": "$count$ hours ago", "placeholders": { "count": { "content": "$1" } } },
"errorLoadingDetails": { "message": "Error Loading Details" }, "relativeTime_yesterday": { "message": "Yesterday" },
"errorLoadingLocalContent": { "message": "Error loading local content." }, "relativeTime_daysAgo": { "message": "$count$ days ago", "placeholders": { "count": { "content": "$1" } } },
"errorServerResponse": { "message": "Unsuccessful server response." }, "errorLoadingDetails": { "message": "Error Loading Details" },
"errorPlexApi": { "message": "Plex API error $status$.", "placeholders": { "status": { "content": "$1" } } }, "errorLoadingLocalContent": { "message": "Error loading local content." },
"errorParsingPlexXml": { "message": "Error parsing Plex XML." }, "errorServerResponse": { "message": "Unsuccessful server response." },
"untitled": { "message": "Untitled" }, "errorPlexApi": { "message": "Plex API error $status$.", "placeholders": { "status": { "content": "$1" } } },
"itemCount": { "message": "$count$ items", "placeholders": { "count": { "content": "$1" } } }, "errorParsingPlexXml": { "message": "Error parsing Plex XML." },
"noPhotoServers": { "message": "No photo servers" }, "untitled": { "message": "Untitled" },
"jellyfinScanInProgress": { "message": "Jellyfin scan is already in progress." }, "itemCount": { "message": "$count$ items", "placeholders": { "count": { "content": "$1" } } },
"jellyfinScanning": { "message": "Scanning Jellyfin..." }, "noPhotoServers": { "message": "No photo servers" },
"jellyfinMissingCredentials": { "message": "Please complete the Jellyfin URL and username." }, "jellyfinScanInProgress": { "message": "Jellyfin scan is already in progress." },
"jellyfinConnecting": { "message": "Connecting to Jellyfin at: $url$", "placeholders": { "url": { "content": "$1" } } }, "jellyfinScanning": { "message": "Scanning Jellyfin..." },
"jellyfinAuthFailed": { "message": "Jellyfin authentication failed: $message$", "placeholders": { "message": { "content": "$1" } } }, "jellyfinMissingCredentials": { "message": "Please fill in the Jellyfin URL and username." },
"jellyfinAuthSuccess": { "message": "Jellyfin authentication successful." }, "jellyfinConnecting": { "message": "Connecting to Jellyfin at: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinFetchingLibraries": { "message": "Fetching libraries..." }, "jellyfinAuthFailed": { "message": "Jellyfin authentication failed: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinFetchFailed": { "message": "Error fetching libraries: $message$", "placeholders": { "message": { "content": "$1" } } }, "jellyfinAuthSuccess": { "message": "Jellyfin authentication successful." },
"jellyfinNoMediaLibraries": { "message": "No movie or series libraries found in Jellyfin." }, "jellyfinFetchingLibraries": { "message": "Fetching libraries..." },
"jellyfinLibrariesFound": { "message": "$count$ media library(s) found.", "placeholders": { "count": { "content": "$1" } } }, "jellyfinFetchFailed": { "message": "Error fetching libraries: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Success] '$libraryName' scanned, $count$ titles added.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } }, "jellyfinNoMediaLibraries": { "message": "No movie or series libraries found in Jellyfin." },
"jellyfinLibraryScanFailed": { "message": "Error scanning library '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } }, "jellyfinLibrariesFound": { "message": "$count$ media library(s) found.", "placeholders": { "count": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Jellyfin scan completed. Added $movies$ movies and $series$ series.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } }, "jellyfinLibraryScanSuccess": { "message": "[Success] '$libraryName' scanned, $count$ titles added.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Jellyfin credentials not configured." }, "jellyfinLibraryScanFailed": { "message": "Error scanning library '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } },
"notFoundOnJellyfin": { "message": "\"$query$\" not found on Jellyfin.", "placeholders": { "query": { "content": "$1" } } }, "jellyfinScanSuccess": { "message": "Jellyfin scan completed. Added $movies$ movies and $series$ series.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" not found on any server.", "placeholders": { "query": { "content": "$1" } } }, "noJellyfinCredentials": { "message": "Jellyfin credentials not configured." },
"localOnPlex": { "message": "On Plex" }, "notFoundOnJellyfin": { "message": "\"$query$\" not found on Jellyfin.", "placeholders": {"query": {"content": "$1"}}},
"searchOnPlex": { "message": "Search on Plex" }, "notFoundOnAnyServer": { "message": "\"$query$\" not found on any server.", "placeholders": {"query": {"content": "$1"}}},
"jellyfinTitle": { "message": "Jellyfin Content" }, "localOnPlex": { "message": "On Plex" },
"noJellyfinContent": { "message": "No Jellyfin content found." }, "searchOnPlex": { "message": "Search on Plex" },
"noJellyfinContentSub": { "message": "Make sure you have scanned your Jellyfin server in the settings." }, "jellyfinTitle": { "message": "Jellyfin Content" },
"activityViewerTitle": { "message": "Server Activity Viewer" }, "noJellyfinContent": { "message": "No Jellyfin content found." },
"activitySelectServer": { "message": "Select a server" }, "noJellyfinContentSub": { "message": "Make sure you have scanned your Jellyfin server in the settings." },
"activityCheckBtn": { "message": "Refresh" }, "activityViewerTitle": { "message": "Server Activity Viewer" },
"activityNoSessions": { "message": "No active sessions on this server." }, "activitySelectServer": { "message": "Select a server" },
"activitySessionUser": { "message": "User" }, "activityCheckBtn": { "message": "Refresh" },
"activitySessionDevice": { "message": "Device" }, "activityNoSessions": { "message": "There are no active sessions on this server." },
"activitySessionContent": { "message": "Content" }, "activitySessionUser": { "message": "User" },
"activitySessionState": { "message": "State" }, "activitySessionDevice": { "message": "Device" },
"activitySessionIdentifier": { "message": "Client Identifier" }, "activitySessionContent": { "message": "Content" },
"activityCopyID": { "message": "Copy ID" }, "activitySessionState": { "message": "State" },
"activityError": { "message": "Could not get server activity." }, "activitySessionIdentifier": { "message": "Client Identifier" },
"activityCopied": { "message": "Identifier copied to clipboard!" }, "activityCopyID": { "message": "Copy ID" },
"activityCopyError": { "message": "Error copying the identifier." }, "activityError": { "message": "Could not get server activity." },
"noProvidersFound": { "message": "No providers found." }, "activityCopied": { "message": "Identifier copied to clipboard!" },
"availableOnPlex": { "message": "Available on Plex" }, "activityCopyError": { "message": "Error copying identifier." },
"m3uGeneratorTitle": { "message": "M3U List Generator" }, "noProvidersFound": { "message": "No providers found." },
"selectAServer": { "message": "Select a server..." }, "availableOnPlex": { "message": "Available on Plex" },
"downloadM3u": { "message": "Download M3U" }, "m3uGeneratorTitle": { "message": "M3U List Generator" },
"m3uGenerator": { "message": "M3U Generator" }, "selectAServer": { "message": "Select a server..." },
"selectLibraries": { "message": "Select Libraries" }, "downloadM3u": { "message": "Download M3U" },
"howToUse": { "message": "How to Use" }, "m3uGenerator": { "message": "M3U Generator" },
"m3uInstruction1": { "message": "Choose a server from the list." }, "selectLibraries": { "message": "Select Libraries" },
"m3uInstruction2": { "message": "Select one or more libraries to include." }, "howToUse": { "message": "How to Use" },
"m3uInstruction3": { "message": "Click the download button." }, "m3uInstruction1": { "message": "Choose a server from the list." },
"m3uInstruction4": { "message": "Import the .m3u file into your compatible player." }, "m3uInstruction2": { "message": "Select one or more libraries to include." },
"chatOpen": { "message": "Open Chat" }, "m3uInstruction3": { "message": "Click the download button." },
"chatTitle": { "message": "AI Assistant" }, "m3uInstruction4": { "message": "Import the .m3u file into your compatible player." },
"chatClose": { "message": "X" }, "chatOpen": { "message": "Open Chat" },
"chatPlaceholder": { "message": "Type your message..." }, "chatTitle": { "message": "AI Assistant" },
"chatSend": { "message": "➤" }, "chatClose": { "message": "X" },
"chatWelcome": { "message": "Welcome! I'm your CinePlex assistant. Ask me about movies, series, or anything else you want to know." }, "chatPlaceholder": { "message": "Type your message..." },
"chatGoogleApiKeyMissing": { "message": "The Google Gemini API key is not configured. Please set it in the extension settings to use the AI assistant." }, "chatSend": { "message": "➤" },
"chatApiInvalidResponse": { "message": "The API returned an invalid response. Please try again." }, "chatWelcome": { "message": "Welcome! I'm your CinePlex assistant. Ask me about movies, series, or anything else you'd like to know." },
"chatApiError": { "message": "Error communicating with the AI assistant" }, "chatGoogleApiKeyMissing": { "message": "The Google Gemini API key is not configured. Please set it up in the extension settings to use the AI assistant." },
"downloadAll": { "message": "Download all" }, "chatApiInvalidResponse": { "message": "The API returned an invalid response. Please try again." },
"download": { "message": "Download" }, "chatApiError": { "message": "Error communicating with the AI assistant" },
"aiToolSearchLibraryDesc": { "message": "Searches the user's Plex library for movies or series by title." }, "downloadAll": { "message": "Download all" },
"aiToolSearchLibraryQueryParamDesc": { "message": "The title of the movie or series to search for." }, "download": { "message": "Download" },
"aiToolSearchLibraryTypeParamDesc": { "message": "The type of content to search for. It can be 'movie' for movies or 'series' for series. (Optional)." }, "aiToolSearchLibraryDesc": { "message": "Searches the user's Plex library for movies or series by title." },
"aiToolSearchLibraryResolutionParamDesc": { "message": "The video resolution to search for (e.g., '4k', '1080p'). (Optional)." }, "aiToolSearchLibraryQueryParamDesc": { "message": "The title of the movie or series to search for." },
"aiToolSearchLibraryContainerParamDesc": { "message": "The video container format to search for (e.g., 'mkv', 'mp4'). (Optional)." }, "aiToolSearchLibraryTypeParamDesc": { "message": "The type of content to search for. Can be 'movie' or 'series'. (Optional)." },
"aiToolNavigateToPageDesc": { "message": "Navigates the user to a specific page of the application interface." }, "aiToolSearchLibraryResolutionParamDesc": { "message": "The video resolution to search for (e.g., '4k', '1080p'). (Optional)." },
"aiToolNavigateToPagePageParamDesc": { "message": "The name of the page to navigate to, e.g.: 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', or 'm3u-generator'." }, "aiToolSearchLibraryContainerParamDesc": { "message": "The video container format to search for (e.g., 'mkv', 'mp4'). (Optional)." },
"aiToolGetUserStatsDesc": { "message": "Gets and displays the user's library statistics, such as the total number of unique movies, series, and artists." }, "aiToolNavigateToPageDesc": { "message": "Navigates the user to a specific page within the application's interface." },
"aiToolShowItemDetailsDesc": { "message": "Displays the details page of a specific movie or series by its title and type." }, "aiToolNavigateToPagePageParamDesc": { "message": "The name of the page to navigate to, e.g., 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', 'm3u-generator', or 'music'." },
"aiToolShowItemDetailsTitleParamDesc": { "message": "The exact title of the movie or series." }, "aiToolGetUserStatsDesc": { "message": "Gets and displays the user's library statistics, such as the total number of unique movies, series, and artists." },
"aiToolShowItemDetailsTypeParamDesc": { "message": "The type of content. It must be 'movie' or 'series'." }, "aiToolShowItemDetailsDesc": { "message": "Displays the details page for a specific movie or series by its title and type." },
"aiToolAddToPlaylistDesc": { "message": "Adds a movie or series to the user's current playlist to stream it to a configured PHP server." }, "aiToolShowItemDetailsTitleParamDesc": { "message": "The exact title of the movie or series." },
"aiToolAddToPlaylistTitleParamDesc": { "message": "The title of the movie or series to add." }, "aiToolShowItemDetailsTypeParamDesc": { "message": "The type of content. Must be 'movie' or 'series'." },
"aiToolAddToPlaylistTypeParamDesc": { "message": "The type of content. It must be 'movie' or 'series'." }, "aiToolAddToPlaylistDesc": { "message": "Adds a movie or series to the user's current playlist to be streamed to a configured PHP server." },
"aiToolCheckAndDownloadDesc": { "message": "Checks the availability of a list of movie or series titles on the user's local servers and, if found, generates and downloads an M3U playlist file with the found streams." }, "aiToolAddToPlaylistTitleParamDesc": { "message": "The title of the movie or series to add." },
"aiToolCheckAndDownloadTitlesParamDesc": { "message": "An array of movie or series titles to search for and download." }, "aiToolAddToPlaylistTypeParamDesc": { "message": "The type of content. Must be 'movie' or 'series'." },
"aiToolCheckAndDownloadTypeParamDesc": { "message": "The content type of the list. It must be 'movie' or 'series'." }, "aiToolDownloadSingleMovieM3UDesc": { "message": "Generates and downloads an M3U playlist file for a single locally available movie." },
"aiToolCheckAndDownloadFilenameParamDesc": { "message": "The name of the M3U file to download (e.g., 'MyList.m3u'). If not provided, a default name will be used." }, "aiToolDownloadSingleMovieM3UTitleParamDesc": { "message": "The title of the movie for which the M3U will be generated." },
"aiToolToggleFavoriteDesc": { "message": "Adds or removes a movie or series from the user's favorites list." }, "aiToolDownloadSingleMovieM3UYearParamDesc": { "message": "The release year of the movie (optional, for better accuracy)." },
"aiToolToggleFavoriteTitleParamDesc": { "message": "The title of the movie or series." }, "aiToolDownloadSeriesSeasonM3UDesc": { "message": "Generates and downloads an M3U playlist file for a specific season of a locally available series." },
"aiToolToggleFavoriteTypeParamDesc": { "message": "The type of content. It must be 'movie' or 'series'." }, "aiToolDownloadSeriesSeasonM3UTitleParamDesc": { "message": "The title of the series." },
"aiToolGetRecommendationsDesc": { "message": "Generates and displays a list of movie or series recommendations based on the user's viewing history and favorites." }, "aiToolDownloadSeriesSeasonM3USeasonParamDesc": { "message": "The season number to download." },
"aiToolApplyFiltersDesc": { "message": "Applies filters to the current view of movies or series, allowing to refine the results by type, genre, year, and sort order." }, "aiToolDownloadSeriesSeasonM3UYearParamDesc": { "message": "The release year of the series (optional)." },
"aiToolApplyFiltersTypeParamDesc": { "message": "The type of content to apply the filters to. It must be 'movie' or 'series'." }, "aiToolCheckAndDownloadDesc": { "message": "Checks the availability of a list of movie or series titles on the user's local servers and, if found, generates and downloads an M3U playlist file with the found streams." },
"aiToolApplyFiltersGenreParamDesc": { "message": "The name of the genre to filter by (e.g., 'Action', 'Drama')." }, "aiToolCheckAndDownloadTitlesParamDesc": { "message": "An array of movie or series titles to search for and download." },
"aiToolApplyFiltersYearParamDesc": { "message": "The release year to filter by (e.g., '2023')." }, "aiToolCheckAndDownloadTypeParamDesc": { "message": "The content type of the list. Must be 'movie' or 'series'." },
"aiToolApplyFiltersSortParamDesc": { "message": "The sorting criterion for the results. Valid values: 'popularity.desc' (popular), 'vote_average.desc' (top rated), 'release_date.desc' (recent for movies) or 'first_air_date.desc' (recent for series)." }, "aiToolCheckAndDownloadFilenameParamDesc": { "message": "The name of the M3U file to download (e.g., 'MyList.m3u'). If not provided, a default name will be used." },
"aiToolPlayMusicByArtistDesc": { "message": "Opens the music player and starts playing songs by a specific artist from the user's library." }, "aiToolToggleFavoriteDesc": { "message": "Adds or removes a movie or series from the user's favorites list." },
"aiToolPlayMusicByArtistNameParamDesc": { "message": "The exact name of the artist whose songs are to be played." }, "aiToolToggleFavoriteTitleParamDesc": { "message": "The title of the movie or series." },
"aiToolClearChatHistoryDesc": { "message": "Clears all message history from the current conversation with the AI assistant." }, "aiToolToggleFavoriteTypeParamDesc": { "message": "The type of content. Must be 'movie' or 'series'." },
"aiToolDeleteDatabaseDesc": { "message": "Deletes the entire local database of the extension, including scanned content, settings, and favorites. This action is irreversible and will reload the application." }, "aiToolGetRecommendationsDesc": { "message": "Generates and displays a list of movie or series recommendations based on the user's viewing history and favorites." },
"aiToolUpdateAllTokensDesc": { "message": "Initiates a full scan of all Plex servers and libraries associated with the tokens configured in the extension. Updates all movies, series, artists, and photos." }, "aiToolApplyFiltersDesc": { "message": "Applies filters to the current movie or series view, allowing results to be refined by type, genre, year, and sort order." },
"aiToolAddPlexTokenDesc": { "message": "Adds a new X-Plex token to the extension's configuration, allowing the application to scan content from new Plex servers." }, "aiToolApplyFiltersTypeParamDesc": { "message": "The content type to apply filters to. Must be 'movie' or 'series'." },
"aiToolAddPlexTokenTokenParamDesc": { "message": "The X-Plex token string to be added." }, "aiToolApplyFiltersGenreParamDesc": { "message": "The name of the genre to filter by (e.g., 'Action', 'Drama')." },
"aiToolChangeRegionDesc": { "message": "Changes the region used for content discovery in the TMDB API. This will affect the results shown in the movie and series sections, as well as the streaming providers." }, "aiToolApplyFiltersYearParamDesc": { "message": "The release year to filter by (e.g., '2023')." },
"aiToolChangeRegionRegionParamDesc": { "message": "The two-letter ISO 3166-1 country code for the new region (e.g., 'US' for the United States, 'ES' for Spain, 'MX' for Mexico)." }, "aiToolApplyFiltersSortParamDesc": { "message": "The sorting criteria for the results. Valid values: 'popularity.desc' (popular), 'vote_average.desc' (top rated), 'release_date.desc' (recent for movies), or 'first_air_date.desc' (recent for series)." },
"aiToolClearAllFavoritesDesc": { "message": "Removes all movies and series that the user has marked as favorites." }, "aiToolListAvailableMusicGenresDesc": { "message": "Lists all unique music genres available in the user's local library." },
"aiToolClearViewingHistoryDesc": { "message": "Clears the user's viewing history from the history page." }, "aiToolSearchMusicByGenreDesc": { "message": "Searches for artists in the user's music library that belong to a specific genre." },
"aiToolClearRecommendationsViewDesc": { "message": "Clears the recommendations view and removes cached recommendations." }, "aiToolSearchMusicByGenreNameParamDesc": { "message": "The name of the music genre to search for (e.g., 'Rock', 'Pop', 'Jazz')." },
"aiToolSearchNotFound": { "message": "'$query$' not found in your library.", "placeholders": { "query": { "content": "$1" } } }, "aiToolPlayMusicByArtistDesc": { "message": "Opens the music player and starts playing songs by a specific artist from the user's library." },
"aiToolNavigateSuccess": { "message": "Navigated to the $page$ page.", "placeholders": { "page": { "content": "$1" } } }, "aiToolPlayMusicByArtistNameParamDesc": { "message": "The exact name of the artist whose songs are to be played." },
"aiToolNavigateError": { "message": "Error navigating to the $page$ page.", "placeholders": { "page": { "content": "$1" } } }, "aiToolClearChatHistoryDesc": { "message": "Clears the entire message history of the current conversation with the AI assistant." },
"aiToolStatsError": { "message": "Error getting statistics." }, "aiToolDeleteDatabaseDesc": { "message": "Deletes the entire local database of the extension, including scanned content, settings, and favorites. This action is irreversible and will reload the application." },
"aiToolItemNotFound": { "message": "Item '$title$' not found.", "placeholders": { "title": { "content": "$1" } } }, "aiToolUpdateAllTokensDesc": { "message": "Initiates a full scan of all Plex servers and libraries associated with the configured tokens in the extension. Updates all movies, series, artists, and photos." },
"aiToolShowItemDetailsSuccess": { "message": "Showing details for '$title$'.", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenDesc": { "message": "Adds a new X-Plex token to the extension's configuration, allowing the application to scan content from new Plex servers." },
"aiToolAddToPlaylistSuccess": { "message": "Added '$title$' to the playlist.", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenTokenParamDesc": { "message": "The X-Plex token string to be added." },
"aiToolFavoriteAdded": { "message": "Added '$title$' to favorites.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionDesc": { "message": "Changes the region used for content discovery in the TMDB API. This will affect the results displayed in the movies and series sections, as well as the streaming providers." },
"aiToolFavoriteRemoved": { "message": "Removed '$title$' from favorites.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionRegionParamDesc": { "message": "The two-letter ISO 3166-1 country code for the new region (e.g., 'US' for United States, 'ES' for Spain, 'MX' for Mexico)." },
"aiToolRecommendationsSuccess": { "message": "Showing recommendations." }, "aiToolClearAllFavoritesDesc": { "message": "Removes all movies and series that the user has marked as favorites." },
"aiToolApplyFiltersGenreNotFound": { "message": "Genre '$genre$' not found.", "placeholders": { "genre": { "content": "$1" } } }, "aiToolClearViewingHistoryDesc": { "message": "Clears the user's viewing history from the history page." },
"aiToolApplyFiltersSuccess": { "message": "Filters applied successfully." }, "aiToolClearRecommendationsViewDesc": { "message": "Clears the recommendations view and removes cached recommendations." },
"aiToolPlayMusicNotReady": { "message": "The music player is not ready. Make sure your Plex music library has been scanned." }, "aiToolSearchNotFound": { "message": "Could not find '$query' in your library.", "placeholders": { "query": { "content": "$1" } } },
"aiToolPlayMusicArtistNotFound": { "message": "Artist '$artist_name$' not found.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolNavigateSuccess": { "message": "Navigated to the $page$ page.", "placeholders": { "page": { "content": "$1" } } },
"aiToolPlayMusicNoSongs": { "message": "No songs found for '$artist_name$'.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolNavigateError": { "message": "Error navigating to the $page$ page.", "placeholders": { "page": { "content": "$1" } } },
"aiToolPlayMusicSuccess": { "message": "Playing music by '$artist_name$'.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolStatsError": { "message": "Error getting statistics." },
"aiToolChatHistoryCleared": { "message": "Chat history cleared." }, "aiToolItemNotFound": { "message": "Item '$title' not found.", "placeholders": { "title": { "content": "$1" } } },
"aiToolConfirmDeleteDatabase": { "message": "Are you sure you want to delete the local database? This action is irreversible." }, "aiToolShowItemDetailsSuccess": { "message": "Showing details for '$title'.", "placeholders": { "title": { "content": "$1" } } },
"aiToolDeleteDatabaseCancelled": { "message": "Database deletion cancelled." }, "aiToolAddToPlaylistSuccess": { "message": "Added '$title' to the playlist.", "placeholders": { "title": { "content": "$1" } } },
"aiToolExecutionError": { "message": "Error executing tool '$toolName': $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolFavoriteAdded": { "message": "Added '$title' to favorites.", "placeholders": { "title": { "content": "$1" } } },
"aiToolUnknown": { "message": "Unknown tool: '$toolName'.", "placeholders": { "toolName": { "content": "$1" } } }, "aiToolFavoriteRemoved": { "message": "Removed '$title' from favorites.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoritesCleared": { "message": "Favorites cleared." }, "aiToolRecommendationsSuccess": { "message": "Showing recommendations." },
"aiToolFavoritesClearError": { "message": "Error clearing favorites: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolApplyFiltersGenreNotFound": { "message": "Genre '$genre' not found.", "placeholders": { "genre": { "content": "$1" } } },
"aiToolRecommendationsCleared": { "message": "Recommendations cleared." }, "aiToolApplyFiltersSuccess": { "message": "Filters applied successfully." },
"aiToolRecommendationsClearError": { "message": "Error clearing recommendations: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolSearchMusicByGenreNotFound": { "message": "I couldn't find artists of the genre '$genre_name' in your library.", "placeholders": { "genre_name": { "content": "$1" } } },
"aiToolDatabaseDeleted": { "message": "Database deleted. The page will reload." }, "aiToolPlayMusicNotReady": { "message": "The music player is not ready. Make sure your Plex music library has been scanned." },
"aiToolDatabaseDeleteError": { "message": "Error deleting the database: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolPlayMusicArtistNotFound": { "message": "Artist '$artist_name' not found.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolDatabaseDeleteBlocked": { "message": "Database deletion is blocked. Close other tabs of the application." }, "aiToolPlayMusicNoSongs": { "message": "No songs found for '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolUpdateAllTokensSuccess": { "message": "All tokens have been updated successfully." }, "aiToolPlayMusicSuccess": { "message": "Playing music by '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolUpdateAllTokensError": { "message": "Error updating tokens: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolChatHistoryCleared": { "message": "Chat history cleared." },
"aiToolAddPlexTokenSuccess": { "message": "Plex token added successfully." }, "aiToolConfirmDeleteDatabase": { "message": "Are you sure you want to delete the local database? This action is irreversible." },
"aiToolAddPlexTokenError": { "message": "Error adding the Plex token: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolDeleteDatabaseCancelled": { "message": "Database deletion cancelled." },
"aiToolChangeRegionSuccess": { "message": "Region changed to $region$. The content is being updated.", "placeholders": { "region": { "content": "$1" } } }, "aiToolExecutionError": { "message": "Error executing tool '$toolName': $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } },
"aiToolChangeRegionError": { "message": "Error changing the region: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolUnknown": { "message": "Unknown tool: '$toolName'.", "placeholders": { "toolName": { "content": "$1" } } },
"aiToolViewingHistoryCleared": { "message": "Viewing history cleared." }, "aiToolFavoritesCleared": { "message": "Favorites cleared." },
"aiToolViewingHistoryClearError": { "message": "Error clearing the viewing history: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolFavoritesClearError": { "message": "Error clearing favorites: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiSystemPrompt_v3": { "message": "You are an expert film and series assistant called CinePlex. Your main function is to help users discover content and interact with their library. Follow these rules strictly: 1. **NEVER** pretend you have performed an action if you have not used a tool for it. For example, do not say 'I have downloaded X' if you have not used the download tool. 2. For recommendation or list requests (e.g., 'tell me 5 horror movies'), use your own knowledge to generate the list. Present it in numbered or bulleted format. After displaying the list, proactively ask the user if they want you to check for availability on their local servers and create an M3U file. 3. **ONLY** if the user confirms they want to check or download the list, use the `check_and_download_titles_list` tool. Do not use it without explicit confirmation. 4. For any other action such as navigating, getting statistics, or searching for a specific title, or filtering by resolution or container, use the appropriate tools. Always be concise, friendly, and efficient." }, "aiToolRecommendationsCleared": { "message": "Recommendations cleared." },
"aiToolM3UNoTitlesProvided": { "message": "Please provide a list of titles to create the playlist." }, "aiToolRecommendationsClearError": { "message": "Error clearing recommendations: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolM3UCheckingTitles": { "message": "Checking the titles on your local servers..." }, "aiToolDatabaseDeleted": { "message": "Database deleted. The page will now reload." },
"aiToolM3UNoLocalMatchesForDownload": { "message": "I haven't found any of the movies or series from the list on your local servers." }, "aiToolDatabaseDeleteError": { "message": "Error deleting database: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolM3UDownloadStarted": { "message": "Done! I found $1 of the $2 titles on your servers and have started the download of the M3U playlist.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } }, "aiToolDatabaseDeleteBlocked": { "message": "Database deletion is blocked. Please close other application tabs." },
"backToProviders": { "message": "Back to Providers" }, "aiToolUpdateAllTokensSuccess": { "message": "All tokens have been updated successfully." },
"artistsCounterSingle": { "message": "$total$ Artist", "placeholders": { "total": { "content": "$1" } } }, "aiToolUpdateAllTokensError": { "message": "Error updating tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Loading..." }, "aiToolAddPlexTokenSuccess": { "message": "Plex token added successfully." },
"downloadingSong": { "message": "Starting download of \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenError": { "message": "Error adding Plex token: $message$", "placeholders": { "message": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" downloaded.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionSuccess": { "message": "Region changed to $region$. Content is being updated.", "placeholders": { "region": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Error downloading \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionError": { "message": "Error changing region: $message$", "placeholders": { "message": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Generating M3U for \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } }, "aiToolViewingHistoryCleared": { "message": "Viewing history cleared." },
"albumM3UGenerated": { "message": "M3U for album \"$artist$\" generated.", "placeholders": { "artist": { "content": "$1" } } }, "aiToolViewingHistoryClearError": { "message": "Error clearing viewing history: $message$", "placeholders": { "message": { "content": "$1" } } },
"retyingSection": { "message": "Retrying section \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolM3UDownloadStartedSingle": { "message": "Starting M3U download for '$movie_title'.", "placeholders": { "movie_title": { "content": "$1" } } },
"retrySuccess": { "message": "[SUCCESS] Retry of \"$title$\" completed.", "placeholders": { "title": { "content": "$1" } } }, "aiToolM3UDownloadStartedSeason": { "message": "Starting M3U download for season $1 of '$2'.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } },
"retryError": { "message": "[FINAL ERROR] Retry failed for \"$title$\": $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolM3UNoTitlesProvided": { "message": "Please provide a list of titles to create the playlist." },
"startingRetryPhase": { "message": "Starting retry phase for $count$ sections...", "placeholders": { "count": { "content": "$1" } } }, "aiToolM3UCheckingTitles": { "message": "Checking titles on your local servers..." },
"tokenFoundServers": { "message": "Token $token$... found $count$ servers.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } }, "aiToolM3UNoLocalMatchesForDownload": { "message": "I couldn't find any of the movies or series from the list on your local servers." },
"errorProcessingToken": { "message": "Error processing token $token$...: $message$", "placeholders": { "token": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolM3UDownloadStarted": { "message": "Done! I found $1 of the $2 titles on your servers and have started downloading the M3U playlist.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } },
"plexScanFatalError": { "message": "FATAL ERROR: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolTrailerNotFoundSpecific": { "message": "Sorry, I couldn't find an available trailer for '$title'.", "placeholders": { "title": { "content": "$1" } } },
"errorDuringScan": { "message": "Error during scan: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiSystemPrompt_v4": {
"stoppingPlexScan": { "message": "Stopping Plex scan..." }, "message": "You are a virtual assistant integrated into a Chrome extension that interacts with Plex and Jellyfin servers. Your main function is to help the user search, manage, play, and download multimedia content, as well as manage custom settings.\n\nTOP PRIORITY: Whenever the user's question refers to multimedia content (movies, series, music), you MUST assume it refers to their local library. Use the tools to search their database BEFORE searching the web.\n\n🎯 General behavior rules:\nAlways respond clearly, concisely, and directly. Be proactive and provide all relevant information at once to avoid follow-up questions. For example, when confirming the availability of a series, include the season details.\n\nCompare the current date with Google search results when asked for external information to ensure it is up-to-date.\n\nUse the exact names of the commands defined in the system (function.name) when calling tools.\n\n📦 Key functions for multimedia content:\nTo generate an M3U for a single movie, use download_single_movie_m3u.\nTo download a specific season of a series, use download_series_season_m3u.\nFor multiple titles (movies or full series), always use check_and_download_titles_list.\nTo search local content: search_library.\nTo search TMDB: search_tmdb_content.\nFor trending content: get_trending_content.\nTo show details of a title: show_item_details.\nTo add to the PHP playlist: add_to_playlist.\nTo check local availability: check_local_availability.\nIf a series is available locally, report how many seasons there are and on which servers using get_local_series_seasons.\nTo see recommendations: get_recommendations.\nTo apply filters: apply_filters.\nTo view history or favorites: view_history, view_favorites.\nTo mark as favorite: toggle_favorite.\nTo play trailer: play_trailer.\n\n🎵 Music functions:\nIf the user asks for general music genre recommendations (e.g., 'recommend a genre to cheer me up'), first use list_available_music_genres to see what genres they have and base your recommendation on that list.\nTo list all available music genres in the library: list_available_music_genres.\nTo search for artists by genre: search_music_by_genre.\nTo play songs by title and/or artist: play_song.\nTo play music by an artist: play_music_by_artist.\n\n🧰 Management and configuration functions:\nTo get user statistics: get_user_stats.\nTo navigate to specific sections: navigate_to_page.\nTo update tokens: update_all_tokens, add_plex_token.\nTo change content region: change_region.\nTo export or import the local database: export_local_database, import_local_database.\nTo delete the database: delete_database.\nTo clear favorites, history, or recommendations: clear_all_favorites, clear_viewing_history, clear_recommendations_view.\nTo toggle light/dark mode: toggle_light_mode.\nTo show or hide the hero section: toggle_hero_section.\n\n⚠ Additional considerations:\nPrioritize locally available content. Use check_local_availability before showing playback or download options.\nIf an action fails, report it clearly and bluntly.\nAvoid unnecessarily repeating the user's request, unless it helps to contextualize the response."
"invalidTokenProvided": { "message": "Invalid token provided." }, },
"tokenAlreadyExists": { "message": "The token already exists." }, "backToProviders": { "message": "Back to Providers" },
"tokenAddedSuccessfully": { "message": "Token added successfully." }, "artistsCounterSingle": { "message": "$total$ Artist", "placeholders": { "total": { "content": "$1" } } },
"noStreamsFoundForSelection": { "message": "No streams found for the selection." }, "artistsCounterLoading": { "message": "Loading..." },
"autoplayBlocked": { "message": "Autoplay blocked." }, "downloadingSong": { "message": "Starting download of \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"page": { "message": "Page" }, "songDownloaded": { "message": "\"$title$\" downloaded.", "placeholders": { "title": { "content": "$1" } } },
"all": { "message": "All" }, "errorDownloadingSong": { "message": "Error downloading \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"userScore": { "message": "User Score" }, "generatingAlbumM3U": { "message": "Generating M3U for \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
"duration": { "message": "Duration" }, "albumM3UGenerated": { "message": "M3U for album \"$artist$\" generated.", "placeholders": { "artist": { "content": "$1" } } },
"min": { "message": "Min" }, "retyingSection": { "message": "Retrying section \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"max": { "message": "Max" } "retrySuccess": { "message": "[SUCCESS] Retry of \"$title$\" completed.", "placeholders": { "title": { "content": "$1" } } },
"retryError": { "message": "[FINAL ERROR] Retry for \"$title$\" failed: $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } },
"startingRetryPhase": { "message": "Starting retry phase for $count$ sections...", "placeholders": { "count": { "content": "$1" } } },
"tokenFoundServers": { "message": "Token $token$... found $count$ servers.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } },
"errorProcessingToken": { "message": "Error processing token $token$...: $message$", "placeholders": { "token": { "content": "$1" }, "message": { "content": "$2" } } },
"plexScanFatalError": { "message": "FATAL ERROR: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorDuringScan": { "message": "Error during scan: $message$", "placeholders": { "message": { "content": "$1" } } },
"stoppingPlexScan": { "message": "Stopping Plex scan..." },
"invalidTokenProvided": { "message": "Invalid token provided." },
"tokenAlreadyExists": { "message": "Token already exists." },
"tokenAddedSuccessfully": { "message": "Token added successfully." },
"noStreamsFoundForSelection": { "message": "No streams found for the selection." },
"autoplayBlocked": { "message": "Autoplay blocked." },
"welcomeToCinePlex": { "message": "" },
"page": { "message": "Page" },
"all": { "message": "All" },
"userScore": { "message": "Score" },
"duration": { "message": "Duration" },
"min": { "message": "Min" },
"max": { "message": "Max" },
"aiToolFindStreamingProvidersDesc": { "message": "Finds where to watch a movie or series on streaming services." },
"aiToolFindStreamingProvidersTitleParamDesc": { "message": "The title of the movie or series to search for." },
"aiToolFindStreamingProvidersTypeParamDesc": { "message": "The content type (movie or series)." },
"aiToolFindStreamingProvidersYearParamDesc": { "message": "The release year of the content (optional)." },
"aiToolNoStreamingProviders": { "message": "No streaming providers found for {title}." },
"aiToolStreamingProvidersFound": { "message": "{title} is available on the following services: {providers}." },
"aiToolStreamingProviderError": { "message": "Error searching for streaming providers: {message}." },
"aiToolGetLocalSeriesSeasonsDesc": { "message": "Checks if a TV series is available locally and returns a detailed breakdown of the available seasons on each server." },
"aiToolGetLocalSeriesSeasonsTitleParamDesc": { "message": "The title of the series to check." },
"aiToolGetLocalSeriesSeasonsYearParamDesc": { "message": "The release year of the series (optional for greater accuracy)." },
"aiToolLocalSeriesNoSeasons": { "message": "The series '$series_title' is in your library, but no season details were found.", "placeholders": { "series_title": { "content": "$1" } } },
"artist": { "message": "Artist" },
"tracks": { "message": "tracks" },
"noSongsFound": { "message": "No songs found for this artist." },
"durationMin": { "message": "Duration (Min)" },
"score": { "message": "Score" },
"searchGenre": { "message": "Search genre..." },
"searchArtists": { "message": "Search artists..." },
"preparingMusicLibrary": { "message": "Preparing your music library..." },
"preparingMusicLibraryDesc": { "message": "This one-time process may take a few minutes if you have many artists." },
"artistsProgress": { "message": "0 / 0 artists" },
"starting": { "message": "Starting..." },
"artistName": { "message": "Artist Name" },
"playPause": { "message": "Play/Pause" },
"noLocalFilesFound": { "message": "No local files found for this title." },
"server": { "message": "Server" },
"title": { "message": "Title" },
"year": { "message": "Year" },
"resolution": { "message": "Resolution" },
"size": { "message": "Size" },
"container": { "message": "Container" },
"action": { "message": "Action" },
"generate": { "message": "Generate" },
"availableLocalFiles": { "message": "Available Local Files" },
"downloadSeason": { "message": "Download Season" },
"errorLoadingServersM3u": { "message": "Error loading servers for the M3U generator:" },
"errorFetchingLibraries": { "message": "Error fetching libraries." },
"selectServerAndLibrary": { "message": "Please select a server and at least one library." },
"generating": { "message": "Generating..." },
"errorProcessingLibrary": { "message": "Error processing library" },
"errorProcessingLibrarySkipping": { "message": "Error processing library. Skipping." },
"allLibrariesFailed": { "message": "All selected libraries failed to process." },
"m3uGeneratedWithErrors": { "message": "M3U generated with some errors. Some libraries may be missing." },
"m3uDownloadedSuccess": { "message": "M3U playlist downloaded successfully." },
"errorGeneratingM3uFile": { "message": "Error generating the M3U file." },
"chatSources": { "message": "Sources" },
"chatUnnamedSource": { "message": "Unnamed source" },
"googleApiFailure": { "message": "Google AI API call failed:" }
} }

View File

@ -1,450 +1,516 @@
{ {
"appName": { "message": "CinePlex" }, "appName": { "message": "CinePlex" },
"appDescription": { "message": "Escanea servidores de Plex para encontrar contenido y lo muestra en la interfaz" }, "appDescription": { "message": "Escanea servidores de Plex para encontrar contenido y lo muestra en la interfaz" },
"appTagline": { "message": "Películas, Series y Música" }, "appTagline": { "message": "Películas, Series y Música" },
"appLocaleCode": { "message": "es-ES" }, "appLocaleCode": { "message": "es-ES" },
"toggleNavigation": { "message": "Alternar Navegación" }, "toggleNavigation": { "message": "Alternar Navegación" },
"searchPlaceholder": { "message": "Buscar películas o series..." }, "searchPlaceholder": { "message": "Buscar películas o series..." },
"openMusicPlayer": { "message": "Abrir Reproductor de Música" }, "openMusicPlayer": { "message": "Abrir Reproductor de Música" },
"settings": { "message": "Ajustes" }, "settings": { "message": "Ajustes" },
"navMovies": { "message": "Películas" }, "navMovies": { "message": "Películas" },
"navSeries": { "message": "Series" }, "navSeries": { "message": "Series" },
"navProviders": { "message": "Proveedores" }, "navProviders": { "message": "Proveedores" },
"navPhotos": { "message": "Fotos" }, "navPhotos": { "message": "Fotos" },
"navStats": { "message": "Estadísticas" }, "navStats": { "message": "Estadísticas" },
"navFavorites": { "message": "Favoritos" }, "navFavorites": { "message": "Favoritos" },
"navHistory": { "message": "Historial" }, "navHistory": { "message": "Historial" },
"navRecommendations": { "message": "Recomendaciones" }, "navRecommendations": { "message": "Recomendaciones" },
"navMusic": { "message": "Música" }, "navMusic": { "message": "Música" },
"navM3uGenerator": { "message": "Generador M3U" }, "musicFeaturedPlaylists": { "message": "Playlists Destacadas" },
"heroWelcome": { "message": "" }, "musicRecentlyAdded": { "message": "Añadido Recientemente" },
"heroSubtitle": { "message": "Explora miles de películas y series." }, "navM3uGenerator": { "message": "Generador M3U" },
"addStream": { "message": "Añadir Stream" }, "heroWelcome": { "message": "" },
"moreInfo": { "message": "Más información" }, "heroSubtitle": { "message": "Explora miles de películas y series." },
"popularMovies": { "message": "Películas Populares" }, "addStream": { "message": "Añadir Stream" },
"allGenres": { "message": "Todos los géneros" }, "moreInfo": { "message": "Más información" },
"allYears": { "message": "Todos los años" }, "popularMovies": { "message": "Películas Populares" },
"sortPopular": { "message": "Más populares" }, "allGenres": { "message": "Todos los géneros" },
"sortTopRated": { "message": "Mejor valoradas" }, "allYears": { "message": "Todos los años" },
"sortRecent": { "message": "Más recientes" }, "sortPopular": { "message": "Más populares" },
"loadMore": { "message": "Cargar más" }, "sortTopRated": { "message": "Mejor valoradas" },
"photosBreadcrumbHome": { "message": "Álbumes" }, "sortRecent": { "message": "Más recientes" },
"selectServer": { "message": "Selecciona un servidor" }, "loadMore": { "message": "Cargar más" },
"loading": { "message": "Cargando..." }, "photosBreadcrumbHome": { "message": "Álbumes" },
"loadingLibraries": { "message": "Cargando bibliotecas..." }, "selectServer": { "message": "Selecciona un servidor" },
"photosEmptyState": { "message": "No se encontraron álbumes ni fotos." }, "loading": { "message": "Cargando..." },
"photosEmptyStateSub": { "message": "Por favor, selecciona un servidor o asegúrate de tener una biblioteca de fotos en Plex." }, "loadingLibraries": { "message": "Cargando bibliotecas..." },
"statsTitle": { "message": "Estadísticas de la Biblioteca" }, "photosEmptyState": { "message": "No se encontraron álbumes ni fotos." },
"statsAllTokens": { "message": "Todos los Tokens" }, "photosEmptyStateSub": { "message": "Por favor, selecciona un servidor o asegúrate de tener una biblioteca de fotos en Plex." },
"statsAnalyzing": { "message": "Analizando tu biblioteca..." }, "statsTitle": { "message": "Estadísticas de la Biblioteca" },
"statsActiveTokens": { "message": "Tokens Activos" }, "statsAllTokens": { "message": "Todos los Tokens" },
"statsServersFound": { "message": "Servidores Encontrados" }, "statsAnalyzing": { "message": "Analizando tu biblioteca..." },
"statsUniqueMovies": { "message": "Películas Únicas" }, "statsActiveTokens": { "message": "Tokens Activos" },
"statsUniqueSeries": { "message": "Series Únicas" }, "statsServersFound": { "message": "Servidores Encontrados" },
"statsUniqueArtists": { "message": "Artistas Únicos" }, "statsUniqueMovies": { "message": "Películas Únicas" },
"statsTokenServers": { "message": "Servidores del Token" }, "statsUniqueSeries": { "message": "Series Únicas" },
"statsChartMoviesByGenre": { "message": "Contenido por Género (Películas)" }, "statsUniqueArtists": { "message": "Artistas Únicos" },
"statsChartSeriesByGenre": { "message": "Contenido por Género (Series)" }, "statsTokenServers": { "message": "Servidores del Token" },
"statsChartByDecade": { "message": "Contenido por Década" }, "statsChartMoviesByGenre": { "message": "Contenido por Género (Películas)" },
"recommendationsTitle": { "message": "Recomendaciones para ti" }, "statsChartSeriesByGenre": { "message": "Contenido por Género (Series)" },
"historyTitle": { "message": "Historial de Visualización" }, "statsChartByDecade": { "message": "Contenido por Década" },
"clearHistory": { "message": "Borrar Todo" }, "recommendationsTitle": { "message": "Recomendaciones para ti" },
"consoleTitle": { "message": "Consola de Escaneo Plex" }, "historyTitle": { "message": "Historial de Visualización" },
"footerCredit": { "message": "Una interfaz para tu universo Plex." }, "clearHistory": { "message": "Borrar Todo" },
"closeTrailer": { "message": "Cerrar tráiler" }, "consoleTitle": { "message": "Consola de Escaneo Plex" },
"close": { "message": "Cerrar" }, "footerCredit": { "message": "Una interfaz para tu universo Plex." },
"photoViewer": { "message": "Visor de fotos" }, "closeTrailer": { "message": "Cerrar tráiler" },
"previous": { "message": "Anterior" }, "close": { "message": "Cerrar" },
"next": { "message": "Siguiente" }, "photoViewer": { "message": "Visor de fotos" },
"notificationTemplateText": { "message": "Notificación" }, "previous": { "message": "Anterior" },
"settingsTitleFull": { "message": "Ajustes y Configuración" }, "next": { "message": "Siguiente" },
"settingsTabGeneral": { "message": "General" }, "notificationTemplateText": { "message": "Notificación" },
"settingsTabPlex": { "message": "Plex" }, "settingsTitleFull": { "message": "Ajustes y Configuración" },
"settingsTabJellyfin": { "message": "Jellyfin" }, "settingsTabGeneral": { "message": "General" },
"settingsTabPhpGen": { "message": "Generador PHP" }, "settingsTabPlex": { "message": "Plex" },
"settingsTabData": { "message": "Datos" }, "settingsTabJellyfin": { "message": "Jellyfin" },
"settingsApiServer": { "message": "Configuración de API y Servidor" }, "settingsTabPhpGen": { "message": "Generador PHP" },
"settingsTmdbApiLabel": { "message": "Clave de API de TMDB (Opcional)" }, "settingsTabData": { "message": "Datos" },
"settingsTmdbApiPlaceholder": { "message": "Se usará la clave por defecto si se deja en blanco" }, "settingsApiServer": { "message": "Configuración de API y Servidor" },
"settingsGoogleApiLabel": { "message": "Clave de API de Google Gemini (Opcional)" }, "settingsTmdbApiLabel": { "message": "Clave de API de TMDB (Opcional)" },
"settingsGoogleApiPlaceholder": { "message": "Necesaria para usar el asistente de IA" }, "settingsTmdbApiPlaceholder": { "message": "Se usará la clave por defecto si se deja en blanco" },
"settingsRegionLabel": { "message": "Región para descubrimiento de contenido" }, "settingsGoogleApiLabel": { "message": "Clave de API de Google Gemini (Opcional)" },
"allRegions": { "message": "Todas las regiones" }, "settingsGoogleApiPlaceholder": { "message": "Necesaria para usar el asistente de IA" },
"settingsPhpUrlLabel": { "message": "URL del Servidor para Añadir Streams" }, "settingsRegionLabel": { "message": "Región para descubrimiento de contenido" },
"settingsPhpUrlPlaceholder": { "message": "https://tu-servidor.com/ruta/al/script.php" }, "allRegions": { "message": "Todas las regiones" },
"settingsInterface": { "message": "Interfaz" }, "settingsPhpUrlLabel": { "message": "URL del Servidor para Añadir Streams" },
"settingsLightTheme": { "message": "Modo Claro" }, "settingsPhpUrlPlaceholder": { "message": "https://tu-servidor.com/ruta/al/script.php" },
"settingsShowHero": { "message": "Mostrar sección de bienvenida 'Hero'" }, "settingsInterface": { "message": "Interfaz" },
"settingsScanContent": { "message": "Escaneo de Contenido" }, "settingsLightTheme": { "message": "Modo Claro" },
"settingsScanDesc": { "message": "Selecciona qué escanear y pulsa el botón." }, "settingsShowHero": { "message": "Mostrar sección de bienvenida 'Hero'" },
"settingsScanMovies": { "message": "Películas" }, "settingsScanContent": { "message": "Escaneo de Contenido" },
"settingsScanShows": { "message": "Series" }, "settingsScanDesc": { "message": "Selecciona qué escanear y pulsa el botón." },
"settingsScanArtists": { "message": "Música" }, "settingsScanMovies": { "message": "Películas" },
"settingsScanPhotos": { "message": "Fotos" }, "settingsScanShows": { "message": "Series" },
"settingsSelectAll": { "message": "Seleccionar Todo" }, "settingsScanArtists": { "message": "Música" },
"settingsStartScan": { "message": "Iniciar Escaneo" }, "settingsScanPhotos": { "message": "Fotos" },
"settingsPlexTokens": { "message": "Tokens de Plex" }, "settingsSelectAll": { "message": "Seleccionar Todo" },
"settingsPlexTokensDesc": { "message": "Edita la lista de tokens de Plex (formato JSON)." }, "settingsStartScan": { "message": "Iniciar Escaneo" },
"settingsSaveTokens": { "message": "Guardar Tokens" }, "settingsPlexTokens": { "message": "Tokens de Plex" },
"settingsJellyfinTitle": { "message": "Configuración de Jellyfin" }, "settingsPlexTokensDesc": { "message": "Edita la lista de tokens de Plex (formato JSON)." },
"settingsJellyfinDesc": { "message": "Añade los datos de tu servidor Jellyfin para escanear su contenido." }, "settingsSaveTokens": { "message": "Guardar Tokens" },
"jellyfinUrlLabel": { "message": "URL del Servidor Jellyfin" }, "settingsJellyfinTitle": { "message": "Configuración de Jellyfin" },
"jellyfinUserLabel": { "message": "Nombre de Usuario" }, "settingsJellyfinDesc": { "message": "Añade los datos de tu servidor Jellyfin para escanear su contenido." },
"jellyfinPassLabel": { "message": "Contraseña" }, "jellyfinUrlLabel": { "message": "URL del Servidor Jellyfin" },
"jellyfinConnectAndScan": { "message": "Conectar y Escanear" }, "jellyfinUserLabel": { "message": "Nombre de Usuario" },
"settingsPhpGenTitle": { "message": "Generador de Script PHP para el Servidor" }, "jellyfinPassLabel": { "message": "Contraseña" },
"settingsPhpFileOptions": { "message": "Opciones del Archivo" }, "jellyfinConnectAndScan": { "message": "Conectar y Escanear" },
"settingsPhpSavePathLabel": { "message": "Ruta de Guardado en el Servidor" }, "settingsPhpGenTitle": { "message": "Generador de Script PHP para el Servidor" },
"settingsPhpSavePathPlaceholder": { "message": "Ej: /var/www/html/listas (en blanco para la misma carpeta)" }, "settingsPhpFileOptions": { "message": "Opciones del Archivo" },
"settingsPhpFilenameLabel": { "message": "Nombre del Archivo" }, "settingsPhpSavePathLabel": { "message": "Ruta de Guardado en el Servidor" },
"settingsPhpFileAction": { "message": "Acción sobre el Archivo" }, "settingsPhpSavePathPlaceholder": { "message": "Ej: /var/www/html/listas (en blanco para la misma carpeta)" },
"settingsPhpActionAppend": { "message": "Añadir al final del archivo (acumulativo)" }, "settingsPhpFilenameLabel": { "message": "Nombre del Archivo" },
"settingsPhpActionOverwrite": { "message": "Sobrescribir el archivo (empezar de nuevo)" }, "settingsPhpFileAction": { "message": "Acción sobre el Archivo" },
"settingsPhpSecurity": { "message": "Seguridad (Opcional)" }, "settingsPhpActionAppend": { "message": "Añadir al final del archivo (acumulativo)" },
"settingsPhpUseSecretKey": { "message": "Usar clave secreta (Recomendado)" }, "settingsPhpActionOverwrite": { "message": "Sobrescribir el archivo (empezar de nuevo)" },
"settingsPhpSecretKeyPlaceholder": { "message": "Introduce una clave secreta segura" }, "settingsPhpSecurity": { "message": "Seguridad (Opcional)" },
"settingsPhpGeneratedCode": { "message": "Código Generado" }, "settingsPhpUseSecretKey": { "message": "Usar clave secreta (Recomendado)" },
"settingsPhpGeneratedPlaceholder": { "message": "El código PHP generado aparecerá aquí." }, "settingsPhpSecretKeyPlaceholder": { "message": "Introduce una clave secreta segura" },
"settingsGenerateScript": { "message": "Generar Script" }, "settingsPhpGeneratedCode": { "message": "Código Generado" },
"settingsCopyScript": { "message": "Copiar Script" }, "settingsPhpGeneratedPlaceholder": { "message": "El código PHP generado aparecerá aquí." },
"settingsDataManagement": { "message": "Gestión de la Base de Datos Local" }, "settingsGenerateScript": { "message": "Generar Script" },
"settingsImportDb": { "message": "Importar BD desde Archivo" }, "settingsCopyScript": { "message": "Copiar Script" },
"settingsExportDb": { "message": "Exportar BD a Archivo" }, "settingsDataManagement": { "message": "Gestión de la Base de Datos Local" },
"settingsClearContent": { "message": "Borrar Datos de Contenido Local" }, "settingsImportDb": { "message": "Importar BD desde Archivo" },
"settingsClearContentDesc": { "message": "Esta acción borrará películas, series y música de la base de datos local, pero no afectará a tus favoritos ni a tus ajustes." }, "settingsExportDb": { "message": "Exportar BD a Archivo" },
"settingsClose": { "message": "Cerrar" }, "settingsClearContent": { "message": "Borrar Datos de Contenido Local" },
"settingsSave": { "message": "Guardar Ajustes" }, "settingsClearContentDesc": { "message": "Esta acción borrará películas, series y música de la base de datos local, pero no afectará a tus favoritos ni a tus ajustes." },
"musicSidenavTitle": { "message": "Música de Plex" }, "settingsClose": { "message": "Cerrar" },
"musicAllServers": { "message": "Todos los Servidores" }, "settingsSave": { "message": "Guardar Ajustes" },
"musicSearchArtistPlaceholder": { "message": "Buscar artista..." }, "musicSidenavTitle": { "message": "Música de Plex" },
"musicSearchDiscographyPlaceholder": { "message": "Buscar en la discografía..." }, "musicAllServers": { "message": "Todos los Servidores" },
"musicNothingPlaying": { "message": "Nada en reproducción" }, "musicSearchArtistPlaceholder": { "message": "Buscar artista..." },
"musicSelectSong": { "message": "Selecciona una canción" }, "musicSearchDiscographyPlaceholder": { "message": "Buscar en la discografía..." },
"musicToStart": { "message": "para empezar a reproducir" }, "musicNothingPlaying": { "message": "Nada en reproducción" },
"miniplayerDownloadSong": { "message": "Descargar canción" }, "musicSelectSong": { "message": "Selecciona una canción" },
"miniplayerDownloadAlbum": { "message": "Descargar álbum M3U" }, "musicToStart": { "message": "para empezar a reproducir" },
"miniplayerVolume": { "message": "Volumen" }, "miniplayerDownloadSong": { "message": "Descargar canción" },
"miniplayerShuffle": { "message": "Aleatorio" }, "miniplayerDownloadAlbum": { "message": "Descargar M3U" },
"miniplayerEqualizer": { "message": "Ecualizador" }, "miniplayerVolume": { "message": "Volumen" },
"miniplayerOpenList": { "message": "Abrir lista" }, "miniplayerShuffle": { "message": "Aleatorio" },
"eqTitle": { "message": "Ecualizador Gráfico" }, "miniplayerEqualizer": { "message": "Ecualizador" },
"eqPresetsLabel": { "message": "Preajustes" }, "miniplayerOpenList": { "message": "Abrir lista" },
"eqPresetFlat": { "message": "Plano" }, "eqTitle": { "message": "Ecualizador Gráfico" },
"eqPresetRock": { "message": "Rock" }, "eqPresetsLabel": { "message": "Preajustes" },
"eqPresetPop": { "message": "Pop" }, "eqPresetFlat": { "message": "Plano" },
"eqPresetJazz": { "message": "Jazz" }, "eqPresetRock": { "message": "Rock" },
"eqPresetClassical": { "message": "Clásica" }, "eqPresetPop": { "message": "Pop" },
"eqPresetBassBoost": { "message": "Refuerzo de Graves" }, "eqPresetJazz": { "message": "Jazz" },
"eqPreampLabel": { "message": "Preamplificador" }, "eqPresetClassical": { "message": "Clásica" },
"infoModalTitle": { "message": "Información" }, "eqPresetBassBoost": { "message": "Refuerzo de Graves" },
"infoModalFieldTitle": { "message": "Título:" }, "eqPreampLabel": { "message": "Preamplificador" },
"infoModalFieldArtist": { "message": "Artista:" }, "infoModalTitle": { "message": "Información" },
"infoModalFieldAlbum": { "message": "Álbum:" }, "infoModalFieldTitle": { "message": "Título:" },
"infoModalFieldSong": { "message": "Canción:" }, "infoModalFieldArtist": { "message": "Artista:" },
"infoModalFieldYear": { "message": "Año:" }, "infoModalFieldAlbum": { "message": "Álbum:" },
"infoModalFieldGenre": { "message": "Género:" }, "infoModalFieldSong": { "message": "Canción:" },
"lang_en": { "message": "Inglés" }, "infoModalFieldYear": { "message": "Año:" },
"lang_es": { "message": "Español" }, "infoModalFieldGenre": { "message": "Género:" },
"lang_fr": { "message": "Francés" }, "lang_en": { "message": "Inglés" },
"lang_de": { "message": "Alemán" }, "lang_es": { "message": "Español" },
"lang_it": { "message": "Italiano" }, "lang_fr": { "message": "Francés" },
"lang_pt": { "message": "Portugués" }, "lang_de": { "message": "Alemán" },
"essentialFeaturesNotSupported": { "message": "Tu navegador no soporta funciones esenciales." }, "lang_it": { "message": "Italiano" },
"dbAccessError": { "message": "Error al acceder a la base de datos local." }, "lang_pt": { "message": "Portugués" },
"dbUpdateNeeded": { "message": "La base de datos necesita actualizarse, por favor recarga la página." }, "essentialFeaturesNotSupported": { "message": "Tu navegador no soporta funciones esenciales." },
"dbBlocked": { "message": "Por favor, cierra otras pestañas de esta aplicación para continuar." }, "dbAccessError": { "message": "Error al acceder a la base de datos local." },
"deletingContentData": { "message": "Borrando datos de contenido locales..." }, "dbUpdateNeeded": { "message": "La base de datos necesita actualizarse, por favor recarga la página." },
"noContentDataToDelete": { "message": "No hay datos de contenido que borrar." }, "dbBlocked": { "message": "Por favor, cierra otras pestañas de esta aplicación para continuar." },
"contentDataDeleted": { "message": "Datos de contenido borrados de IndexedDB." }, "deletingContentData": { "message": "Borrando datos de contenido locales..." },
"errorDeletingData": { "message": "Error al borrar datos: $message$", "placeholders": { "message": { "content": "$1" } } }, "noContentDataToDelete": { "message": "No hay datos de contenido que borrar." },
"aceEditorNotAvailable": { "message": "Editor de texto no disponible." }, "contentDataDeleted": { "message": "Datos de contenido borrados de IndexedDB." },
"errorLoadingTokens": { "message": "Error al cargar tokens para editar." }, "errorDeletingData": { "message": "Error al borrar datos: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingTokensMessage": { "message": "Error al cargar tokens: $message$", "placeholders": { "message": { "content": "$1" } } }, "aceEditorNotAvailable": { "message": "Editor de texto no disponible." },
"aceEditorNotAvailableToSave": { "message": "Editor no disponible para guardar." }, "errorLoadingTokens": { "message": "Error al cargar tokens para editar." },
"invalidJsonFormat": { "message": "Formato JSON inválido. Debe ser { \"tokens\": [...] }" }, "errorLoadingTokensMessage": { "message": "Error al cargar tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"tokensSaved": { "message": "Tokens guardados correctamente." }, "aceEditorNotAvailableToSave": { "message": "Editor no disponible para guardar." },
"errorSavingTokens": { "message": "Error al guardar tokens: $message$", "placeholders": { "message": { "content": "$1" } } }, "invalidJsonFormat": { "message": "Formato JSON inválido. Debe ser { \"tokens\": [...] }" },
"dbNotAvailable": { "message": "IndexedDB no está disponible." }, "tokensSaved": { "message": "Tokens guardados correctamente." },
"dbExported": { "message": "Base de datos exportada con éxito." }, "errorSavingTokens": { "message": "Error al guardar tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorExportingDb": { "message": "Error al exportar la base de datos: $message$", "placeholders": { "message": { "content": "$1" } } }, "dbNotAvailable": { "message": "IndexedDB no está disponible." },
"invalidJsonFile": { "message": "El archivo no contiene un objeto JSON válido." }, "dbExported": { "message": "Base de datos exportada con éxito." },
"noDataToImport": { "message": "El archivo no contiene datos para las secciones de la BD actual." }, "errorExportingDb": { "message": "Error al exportar la base de datos: $message$", "placeholders": { "message": { "content": "$1" } } },
"dbImported": { "message": "Base de datos importada correctamente." }, "invalidJsonFile": { "message": "El archivo no contiene un objeto JSON válido." },
"errorImportingDb": { "message": "Error al importar la base de datos: $message$", "placeholders": { "message": { "content": "$1" } } }, "noDataToImport": { "message": "El archivo no contiene datos para las secciones de la BD actual." },
"updatingView": { "message": "Actualizando la vista con los nuevos datos..." }, "dbImported": { "message": "Base de datos importada correctamente." },
"confirmClearContent": { "message": "¿Estás seguro de que deseas borrar los datos de contenido locales (Películas, Series, Música, etc.)? Los Favoritos y Ajustes NO se borrarán." }, "errorImportingDb": { "message": "Error al importar la base de datos: $message$", "placeholders": { "message": { "content": "$1" } } },
"trailerNotFound": { "message": "No se encontró tráiler para este título." }, "updatingView": { "message": "Actualizando la vista con los nuevos datos..." },
"confirmClearHistory": { "message": "¿Estás seguro de que deseas borrar todo tu historial de visualización? Esta acción no se puede rehacer." }, "confirmClearContent": { "message": "¿Estás seguro de que deseas borrar los datos de contenido locales (Películas, Series, Música, etc.)? Los Favoritos y Ajustes NO se borrarán." },
"historyCleared": { "message": "Historial de visualización borrado." }, "trailerNotFound": { "message": "No se encontró tráiler para este título." },
"historyItemDeleted": { "message": "Elemento borrado del historial." }, "confirmClearHistory": { "message": "¿Estás seguro de que deseas borrar todo tu historial de visualización? Esta acción no se puede rehacer." },
"errorGeneratingScript": { "message": "Primero genera un script para poder copiarlo." }, "historyCleared": { "message": "Historial de visualización borrado." },
"scriptCopied": { "message": "Script PHP copiado al portapapeles." }, "historyItemDeleted": { "message": "Elemento borrado del historial." },
"errorCopyingScript": { "message": "Error al copiar el script." }, "errorGeneratingScript": { "message": "Primero genera un script para poder copiarlo." },
"scriptGenerated": { "message": "Script PHP generado." }, "scriptCopied": { "message": "Script PHP copiado al portapapeles." },
"errorLoadingAlbum": { "message": "Error al cargar álbum: $message$", "placeholders": { "message": { "content": "$1" } } }, "errorCopyingScript": { "message": "Error al copiar el script." },
"noPhotoServerSelected": { "message": "Error: No se ha seleccionado un servidor de fotos." }, "scriptGenerated": { "message": "Script PHP generado." },
"loadingGenres": { "message": "Cargando géneros..." }, "errorLoadingAlbum": { "message": "Error al cargar álbum: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingGenres": { "message": "Error al cargar" }, "noPhotoServerSelected": { "message": "Error: No se ha seleccionado un servidor de fotos." },
"noContentFound": { "message": "No se encontraron resultados." }, "loadingGenres": { "message": "Cargando géneros..." },
"couldNotLoadContent": { "message": "No se pudo cargar el contenido." }, "errorLoadingGenres": { "message": "Error al cargar" },
"noFavorites": { "message": "Aún no tienes favoritos." }, "noContentFound": { "message": "No se encontraron resultados." },
"errorLoadingFavorites": { "message": "Error al cargar favoritos." }, "couldNotLoadContent": { "message": "No se pudo cargar el contenido." },
"historyEmpty": { "message": "Tu historial está vacío." }, "noFavorites": { "message": "Aún no tienes favoritos." },
"historyEmptySub": { "message": "Explora y mira contenido para que aparezca aquí." }, "errorLoadingFavorites": { "message": "Error al cargar favoritos." },
"errorGeneratingRecommendations": { "message": "Error al generar recomendaciones." }, "historyEmpty": { "message": "Tu historial está vacío." },
"noRecommendations": { "message": "¡Necesitamos conocerte mejor para darte recomendaciones!" }, "historyEmptySub": { "message": "Explora y mira contenido para que aparezca aquí." },
"errorGeneratingStats": { "message": "Error al generar estadísticas." }, "errorGeneratingRecommendations": { "message": "Error al generar recomendaciones." },
"noServersForToken": { "message": "No se encontraron servidores asociados para este token." }, "noRecommendations": { "message": "¡Necesitamos conocerte mejor para darte recomendaciones!" },
"searchingActorContent": { "message": "Buscando contenido de $actorName$", "placeholders": { "actorName": { "content": "$1" } } }, "errorGeneratingStats": { "message": "Error al generar estadísticas." },
"errorLoadingActorContent": { "message": "No se pudo cargar el contenido para $actorName$.", "placeholders": { "actorName": { "content": "$1" } } }, "noServersForToken": { "message": "No se encontraron servidores asociados para este token." },
"errorAddingStream": { "message": "Error al añadir stream(s): $message$", "placeholders": { "message": { "content": "$1" } } }, "searchingActorContent": { "message": "Buscando contenido de $actorName$", "placeholders": { "actorName": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "La URL del servidor PHP no está configurada. Por favor, configúrala en Ajustes." }, "errorLoadingActorContent": { "message": "No se pudo cargar el contenido para $actorName$.", "placeholders": { "actorName": { "content": "$1" } } },
"searchingStreams": { "message": "Buscando streams para \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "errorAddingStream": { "message": "Error al añadir stream(s): $message$", "placeholders": { "message": { "content": "$1" } } },
"sendingStreams": { "message": "Enviando $count$ stream(s) al servidor...", "placeholders": { "count": { "content": "$1" } } }, "phpUrlNotConfigured": { "message": "La URL del servidor PHP no está configurada. Por favor, configúrala en Ajustes." },
"streamAddedSuccess": { "message": "Stream(s) añadido(s) con éxito." }, "searchingStreams": { "message": "Buscando streams para \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingM3U": { "message": "Generando M3U para \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "sendingStreams": { "message": "Enviando $count$ stream(s) al servidor...", "placeholders": { "count": { "content": "$1" } } },
"m3uDownloaded": { "message": "\"$title$\" descargado.", "placeholders": { "title": { "content": "$1" } } }, "streamAddedSuccess": { "message": "Stream(s) añadido(s) con éxito." },
"errorGeneratingM3U": { "message": "Error al generar M3U: $message$", "placeholders": { "message": { "content": "$1" } } }, "generatingM3U": { "message": "Generando M3U para \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"settingsSavedSuccess": { "message": "Ajustes guardados correctamente." }, "m3uDownloaded": { "message": "\"$title$\" descargado.", "placeholders": { "title": { "content": "$1" } } },
"errorSavingSettings": { "message": "Error al guardar los ajustes en la base de datos." }, "errorGeneratingM3U": { "message": "Error al generar M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
"languageChangeReload": { "message": "Idioma cambiado. La aplicación se recargará ahora." }, "settingsSavedSuccess": { "message": "Ajustes guardados correctamente." },
"addedToFavorites": { "message": "Añadido a favoritos." }, "errorSavingSettings": { "message": "Error al guardar los ajustes en la base de datos." },
"removedFromFavorites": { "message": "Eliminado de favoritos." }, "languageChangeReload": { "message": "Idioma cambiado. La aplicación se recargará ahora." },
"plexScanInProgress": { "message": "El escaneo Plex ya está en curso." }, "addedToFavorites": { "message": "Añadido a favoritos." },
"plexScanStarting": { "message": "Iniciando escaneo Plex..." }, "removedFromFavorites": { "message": "Eliminado de favoritos." },
"noPlexTokens": { "message": "No hay tokens de Plex configurados." }, "plexScanInProgress": { "message": "El escaneo Plex ya está en curso." },
"clearingSections": { "message": "Limpiando secciones: $sections$", "placeholders": { "sections": { "content": "$1" } } }, "plexScanStarting": { "message": "Iniciando escaneo Plex..." },
"initialScanPhaseComplete": { "message": "Fase de escaneo inicial finalizada." }, "noPlexTokens": { "message": "No hay tokens de Plex configurados." },
"retryPhaseFinished": { "message": "Fase de reintentos finalizada." }, "clearingSections": { "message": "Limpiando secciones: $sections$", "placeholders": { "sections": { "content": "$1" } } },
"plexScanFinished": { "message": "Escaneo finalizado. Actualizando contenido..." }, "initialScanPhaseComplete": { "message": "Fase de escaneo inicial finalizada." },
"scanCancelled": { "message": "Escaneo cancelado por el usuario." }, "retryPhaseFinished": { "message": "Fase de reintentos finalizada." },
"scanCancelledInfo": { "message": "Escaneo cancelado." }, "plexScanFinished": { "message": "Escaneo finalizado. Actualizando contenido..." },
"errorInitializingMusicPlayer": { "message": "Error inicializando el reproductor de música." }, "scanCancelled": { "message": "Escaneo cancelado por el usuario." },
"criticalErrorLoadingMusic": { "message": "Error crítico al cargar datos de música." }, "scanCancelledInfo": { "message": "Escaneo cancelado." },
"errorLoadingArtists": { "message": "Error al cargar artistas." }, "errorInitializingMusicPlayer": { "message": "Error inicializando el reproductor de música." },
"dbUnavailableError": { "message": "Error: Base de datos no disponible." }, "criticalErrorLoadingMusic": { "message": "Error crítico al cargar datos de música." },
"updatingMusicData": { "message": "Actualizando datos de música..." }, "errorLoadingArtists": { "message": "Error al cargar artistas." },
"musicDataUpdated": { "message": "Datos de música actualizados." }, "dbUnavailableError": { "message": "Error: Base de datos no disponible." },
"errorFetchingArtistSongs": { "message": "Error al obtener las canciones del artista." }, "updatingMusicData": { "message": "Actualizando datos de música..." },
"errorLoadingSongs": { "message": "Error cargando canciones." }, "musicDataUpdated": { "message": "Datos de música actualizados." },
"noArtistsFound": { "message": "No se encontraron artistas." }, "errorFetchingArtistSongs": { "message": "Error al obtener las canciones del artista." },
"shuffleOn": { "message": "Modo aleatorio activado." }, "errorLoadingSongs": { "message": "Error cargando canciones." },
"shuffleOff": { "message": "Modo aleatorio desactivado." }, "noArtistsFound": { "message": "No se encontraron artistas." },
"playbackError": { "message": "Error de reproducción" }, "shuffleOn": { "message": "Modo aleatorio activado." },
"errorLabel": { "message": "Error" }, "shuffleOff": { "message": "Modo aleatorio desactivado." },
"reloadingPage": { "message": "Recargando la página..." }, "playbackError": { "message": "Error de reproducción" },
"viewed": { "message": "Visto" }, "errorLabel": { "message": "Error" },
"local": { "message": "Local" }, "reloadingPage": { "message": "Recargando la página..." },
"topRatedSort": {"message": "Mejor Valoradas"}, "viewed": { "message": "Visto" },
"recentSort": {"message": "Recientes"}, "local": { "message": "Local" },
"popularSort": {"message": "Populares"}, "topRatedSort": {"message": "Mejor Valoradas"},
"moviesSectionTitle": {"message": "Películas"}, "recentSort": {"message": "Recientes"},
"seriesSectionTitle": {"message": "Series"}, "popularSort": {"message": "Populares"},
"searchResultsFor": {"message": "Resultados para \"$query$\"", "placeholders": {"query": {"content": "$1"}}}, "moviesSectionTitle": {"message": "Películas"},
"contentFrom": {"message": "Contenido de $actor$", "placeholders": {"actor": {"content": "$1"}}}, "seriesSectionTitle": {"message": "Series"},
"explore": {"message": "Explorar"}, "searchResultsFor": {"message": "Resultados para \"$query$\"", "placeholders": {"query": {"content": "$1"}}},
"noGenre": {"message": "Sin categoría"}, "contentFrom": {"message": "Contenido de $actor$", "placeholders": {"actor": {"content": "$1"}}},
"synopsis": {"message": "Sinopsis"}, "explore": {"message": "Explorar"},
"noSynopsis": {"message": "No hay sinopsis disponible."}, "noGenre": {"message": "Sin categoría"},
"director": {"message": "Director:"}, "synopsis": {"message": "Sinopsis"},
"writer": {"message": "Escritor:"}, "noSynopsis": {"message": "No hay sinopsis disponible."},
"viewOnImdb": {"message": "Ver en IMDb"}, "director": {"message": "Director:"},
"watchTrailer": {"message": "Ver Tráiler"}, "writer": {"message": "Escritor:"},
"addToFavorites": {"message": "Añadir a favoritos"}, "viewOnImdb": {"message": "Ver en IMDb"},
"removeFromFavorites": {"message": "Quitar de favoritos"}, "watchTrailer": {"message": "Tráiler"},
"notAvailable": {"message": "No disponible"}, "addToFavorites": {"message": "Añadir favoritos"},
"mainCast": {"message": "Reparto Principal"}, "removeFromFavorites": {"message": "Quitar de favoritos"},
"seasonsAndEpisodes": {"message": "Temporadas y Episodios"}, "notAvailable": {"message": "No disponible"},
"similarContent": {"message": "Contenido Similar"}, "mainCast": {"message": "Reparto Principal"},
"filmography": {"message": "Filmografía"}, "seasonsAndEpisodes": {"message": "Temporadas y Episodios"},
"availableOn": {"message": "Disponible en"}, "similarContent": {"message": "Contenido Similar"},
"episodesCount": {"message": "$count$ Episodios", "placeholders": {"count": {"content": "$1"}}}, "filmography": {"message": "Filmografía"},
"seasonsCount": {"message": "$count$ Temporadas", "placeholders": {"count": {"content": "$1"}}}, "availableOn": {"message": "Disponible en"},
"runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}}, "episodesCount": {"message": "$count$ Episodios", "placeholders": {"count": {"content": "$1"}}},
"noTrailerFound": {"message": "No se encontró tráiler para este título."}, "seasonsCount": {"message": "$count$ Temporadas", "placeholders": {"count": {"content": "$1"}}},
"fatalInitError": {"message": "Error fatal de inicialización"}, "runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}},
"fatalInitErrorSub": {"message": "No se pudo cargar la aplicación."}, "noTrailerFound": {"message": "No se encontró tráiler para este título."},
"invalidStreamInfo": {"message": "Información inválida."}, "fatalInitError": {"message": "Error fatal de inicialización"},
"dbUnavailableForStreams": {"message": "Base de datos local no disponible."}, "fatalInitErrorSub": {"message": "No se pudo cargar la aplicación."},
"noPlexServersForStreams": {"message": "No hay servidores Plex."}, "invalidStreamInfo": {"message": "Información inválida."},
"notFoundOnServers": {"message": "No se encontró \"$query$\" en los servidores de Plex.", "placeholders": {"query": {"content": "$1"}}}, "dbUnavailableForStreams": {"message": "Base de datos local no disponible."},
"relativeTime_justNow": { "message": "Ahora mismo" }, "noPlexServersForStreams": {"message": "No hay servidores Plex."},
"relativeTime_minutesAgo": { "message": "Hace $count$ minutos", "placeholders": { "count": { "content": "$1" } } }, "notFoundOnServers": {"message": "No se encontró \"$query$\" en los servidores de Plex.", "placeholders": {"query": {"content": "$1"}}},
"relativeTime_hoursAgo": { "message": "Hace $count$ horas", "placeholders": { "count": { "content": "$1" } } }, "relativeTime_justNow": { "message": "Ahora mismo" },
"relativeTime_yesterday": { "message": "Ayer" }, "relativeTime_minutesAgo": { "message": "Hace $count$ minutos", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_daysAgo": { "message": "Hace $count$ días", "placeholders": { "count": { "content": "$1" } } }, "relativeTime_hoursAgo": { "message": "Hace $count$ horas", "placeholders": { "count": { "content": "$1" } } },
"errorLoadingDetails": { "message": "Error al Cargar los Detalles" }, "relativeTime_yesterday": { "message": "Ayer" },
"errorLoadingLocalContent": { "message": "Error al cargar el contenido local." }, "relativeTime_daysAgo": { "message": "Hace $count$ días", "placeholders": { "count": { "content": "$1" } } },
"errorServerResponse": { "message": "Respuesta no exitosa del servidor." }, "errorLoadingDetails": { "message": "Error al Cargar los Detalles" },
"errorPlexApi": { "message": "Error $status$ de la API de Plex.", "placeholders": { "status": { "content": "$1" } } }, "errorLoadingLocalContent": { "message": "Error al cargar el contenido local." },
"errorParsingPlexXml": { "message": "Error al analizar el XML de Plex." }, "errorServerResponse": { "message": "Respuesta no exitosa del servidor." },
"untitled": { "message": "Sin título" }, "errorPlexApi": { "message": "Error $status$ de la API de Plex.", "placeholders": { "status": { "content": "$1" } } },
"itemCount": { "message": "$count$ elementos", "placeholders": { "count": { "content": "$1" } } }, "errorParsingPlexXml": { "message": "Error al analizar el XML de Plex." },
"noPhotoServers": { "message": "No hay servidores de fotos" }, "untitled": { "message": "Sin título" },
"jellyfinScanInProgress": { "message": "El escaneo Jellyfin ya está en curso." }, "itemCount": { "message": "$count$ elementos", "placeholders": { "count": { "content": "$1" } } },
"jellyfinScanning": { "message": "Escaneando Jellyfin..." }, "noPhotoServers": { "message": "No hay servidores de fotos" },
"jellyfinMissingCredentials": { "message": "Por favor, completa la URL y el usuario de Jellyfin." }, "jellyfinScanInProgress": { "message": "El escaneo Jellyfin ya está en curso." },
"jellyfinConnecting": { "message": "Conectando a Jellyfin en: $url$", "placeholders": { "url": { "content": "$1" } } }, "jellyfinScanning": { "message": "Escaneando Jellyfin..." },
"jellyfinAuthFailed": { "message": "Autenticación Jellyfin fallida: $message$", "placeholders": { "message": { "content": "$1" } } }, "jellyfinMissingCredentials": { "message": "Por favor, completa la URL y el usuario de Jellyfin." },
"jellyfinAuthSuccess": { "message": "Autenticación Jellyfin exitosa." }, "jellyfinConnecting": { "message": "Conectando a Jellyfin en: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinFetchingLibraries": { "message": "Obteniendo bibliotecas..." }, "jellyfinAuthFailed": { "message": "Autenticación Jellyfin fallida: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinFetchFailed": { "message": "Error al obtener bibliotecas: $message$", "placeholders": { "message": { "content": "$1" } } }, "jellyfinAuthSuccess": { "message": "Autenticación Jellyfin exitosa." },
"jellyfinNoMediaLibraries": { "message": "No se encontraron bibliotecas de películas o series en Jellyfin." }, "jellyfinFetchingLibraries": { "message": "Obteniendo bibliotecas..." },
"jellyfinLibrariesFound": { "message": "$count$ biblioteca(s) de medios encontrada(s).", "placeholders": { "count": { "content": "$1" } } }, "jellyfinFetchFailed": { "message": "Error al obtener bibliotecas: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Éxito] '$libraryName escaneada, $count$ títulos añadidos.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } }, "jellyfinNoMediaLibraries": { "message": "No se encontraron bibliotecas de películas o series en Jellyfin." },
"jellyfinLibraryScanFailed": { "message": "Error al escanear la biblioteca '$libraryName.", "placeholders": { "libraryName": { "content": "$1" } } }, "jellyfinLibrariesFound": { "message": "$count$ biblioteca(s) de medios encontrada(s).", "placeholders": { "count": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Escaneo Jellyfin completado. Añadidas $movies$ películas y $series$ series.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } }, "jellyfinLibraryScanSuccess": { "message": "[Éxito] '$libraryName escaneada, $count$ títulos añadidos.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Credenciales de Jellyfin no configuradas." }, "jellyfinLibraryScanFailed": { "message": "Error al escanear la biblioteca '$libraryName.", "placeholders": { "libraryName": { "content": "$1" } } },
"notFoundOnJellyfin": { "message": "No se encontró \"$query$\" en Jellyfin.", "placeholders": { "query": { "content": "$1" } } }, "jellyfinScanSuccess": { "message": "Escaneo Jellyfin completado. Añadidas $movies$ películas y $series$ series.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"notFoundOnAnyServer": { "message": "No se encontró \"$query$\" en ningún servidor.", "placeholders": { "query": { "content": "$1" } } }, "noJellyfinCredentials": { "message": "Credenciales de Jellyfin no configuradas." },
"localOnPlex": { "message": "En Plex" }, "notFoundOnJellyfin": { "message": "No se encontró \"$query$\" en Jellyfin.", "placeholders": {"query": {"content": "$1"}}},
"searchOnPlex": { "message": "Buscar en Plex" }, "notFoundOnAnyServer": { "message": "No se encontró \"$query$\" en ningún servidor.", "placeholders": {"query": {"content": "$1"}}},
"jellyfinTitle": { "message": "Contenido de Jellyfin" }, "localOnPlex": { "message": "En Plex" },
"noJellyfinContent": { "message": "No se encontró contenido de Jellyfin." }, "searchOnPlex": { "message": "Buscar en Plex" },
"noJellyfinContentSub": { "message": "Asegúrate de haber escaneado tu servidor Jellyfin en los ajustes." }, "jellyfinTitle": { "message": "Contenido de Jellyfin" },
"activityViewerTitle": { "message": "Visor de Actividad del Servidor" }, "noJellyfinContent": { "message": "No se encontró contenido de Jellyfin." },
"activitySelectServer": { "message": "Selecciona un servidor" }, "noJellyfinContentSub": { "message": "Asegúrate de haber escaneado tu servidor Jellyfin en los ajustes." },
"activityCheckBtn": { "message": "Actualizar" }, "activityViewerTitle": { "message": "Visor de Actividad del Servidor" },
"activityNoSessions": { "message": "No hay sesiones activas en este servidor." }, "activitySelectServer": { "message": "Selecciona un servidor" },
"activitySessionUser": { "message": "Usuario" }, "activityCheckBtn": { "message": "Actualizar" },
"activitySessionDevice": { "message": "Dispositivo" }, "activityNoSessions": { "message": "No hay sesiones activas en este servidor." },
"activitySessionContent": { "message": "Contenido" }, "activitySessionUser": { "message": "Usuario" },
"activitySessionState": { "message": "Estado" }, "activitySessionDevice": { "message": "Dispositivo" },
"activitySessionIdentifier": { "message": "Identificador del Cliente" }, "activitySessionContent": { "message": "Contenido" },
"activityCopyID": { "message": "Copiar ID" }, "activitySessionState": { "message": "Estado" },
"activityError": { "message": "No se pudo obtener la actividad del servidor." }, "activitySessionIdentifier": { "message": "Identificador del Cliente" },
"activityCopied": { "message": "¡Identificador copiado al portapapeles!" }, "activityCopyID": { "message": "Copiar ID" },
"activityCopyError": { "message": "Error al copiar el identificador." }, "activityError": { "message": "No se pudo obtener la actividad del servidor." },
"noProvidersFound": { "message": "No se encontraron proveedores." }, "activityCopied": { "message": "¡Identificador copiado al portapapeles!" },
"availableOnPlex": { "message": "Disponible en Plex" }, "activityCopyError": { "message": "Error al copiar el identificador." },
"m3uGeneratorTitle": { "message": "Generador de Listas M3U" }, "noProvidersFound": { "message": "No se encontraron proveedores." },
"selectAServer": { "message": "Selecciona un servidor..." }, "availableOnPlex": { "message": "Disponible en Plex" },
"downloadM3u": { "message": "Descargar M3U" }, "m3uGeneratorTitle": { "message": "Generador de Listas M3U" },
"m3uGenerator": { "message": "Generador M3U" }, "selectAServer": { "message": "Selecciona un servidor..." },
"selectLibraries": { "message": "Seleccionar Bibliotecas" }, "downloadM3u": { "message": "Descargar M3U" },
"howToUse": { "message": "Cómo Usar" }, "m3uGenerator": { "message": "Generador M3U" },
"m3uInstruction1": { "message": "Elige un servidor de la lista." }, "selectLibraries": { "message": "Seleccionar Bibliotecas" },
"m3uInstruction2": { "message": "Selecciona una o más bibliotecas para incluir." }, "howToUse": { "message": "Cómo Usar" },
"m3uInstruction3": { "message": "Haz clic en el botón de descarga." }, "m3uInstruction1": { "message": "Elige un servidor de la lista." },
"m3uInstruction4": { "message": "Importa el archivo .m3u en tu reproductor compatible." }, "m3uInstruction2": { "message": "Selecciona una o más bibliotecas para incluir." },
"chatOpen": { "message": "Abrir Chat" }, "m3uInstruction3": { "message": "Haz clic en el botón de descarga." },
"chatTitle": { "message": "Asistente IA" }, "m3uInstruction4": { "message": "Importa el archivo .m3u en tu reproductor compatible." },
"chatClose": { "message": "X" }, "chatOpen": { "message": "Abrir Chat" },
"chatPlaceholder": { "message": "Escribe tu mensaje..." }, "chatTitle": { "message": "Asistente IA" },
"chatSend": { "message": "➤" }, "chatClose": { "message": "X" },
"chatWelcome": { "message": "¡Bienvenido! Soy tu asistente de CinePlex. Pregúntame sobre películas, series o cualquier otra cosa que quieras saber." }, "chatPlaceholder": { "message": "Escribe tu mensaje..." },
"chatGoogleApiKeyMissing": { "message": "La clave de la API de Google Gemini no está configurada. Por favor, configúrala en los ajustes de la extensión para usar el asistente de IA." }, "chatSend": { "message": "➤" },
"chatApiInvalidResponse": { "message": "La API ha devuelto una respuesta no válida. Por favor, inténtalo de nuevo." }, "chatWelcome": { "message": "¡Bienvenido! Soy tu asistente de CinePlex. Pregúntame sobre películas, series o cualquier otra cosa que quieras saber." },
"chatApiError": { "message": "Error al comunicarse con el asistente de IA" }, "chatGoogleApiKeyMissing": { "message": "La clave de la API de Google Gemini no está configurada. Por favor, configúrala en los ajustes de la extensión para usar el asistente de IA." },
"downloadAll": { "message": "Descargar todo" }, "chatApiInvalidResponse": { "message": "La API ha devuelto una respuesta no válida. Por favor, inténtalo de nuevo." },
"download": { "message": "Descargar" }, "chatApiError": { "message": "Error al comunicarse con el asistente de IA" },
"aiToolSearchLibraryDesc": { "message": "Busca en la biblioteca de Plex del usuario películas o series por título." }, "downloadAll": { "message": "Descargar todo" },
"aiToolSearchLibraryQueryParamDesc": { "message": "El título de la película o serie a buscar." }, "download": { "message": "Descargar" },
"aiToolSearchLibraryTypeParamDesc": { "message": "El tipo de contenido a buscar. Puede ser 'movie' para películas o 'series' para series. (Opcional)." }, "aiToolSearchLibraryDesc": { "message": "Busca en la biblioteca de Plex del usuario películas o series por título." },
"aiToolSearchLibraryResolutionParamDesc": { "message": "La resolución del video a buscar (por ejemplo, '4k', '1080p'). (Opcional)." }, "aiToolSearchLibraryQueryParamDesc": { "message": "El título de la película o serie a buscar." },
"aiToolSearchLibraryContainerParamDesc": { "message": "El formato contenedor del video a buscar (por ejemplo, 'mkv', 'mp4'). (Opcional)." }, "aiToolSearchLibraryTypeParamDesc": { "message": "El tipo de contenido a buscar. Puede ser 'movie' para películas o 'series' para series. (Opcional)." },
"aiToolNavigateToPageDesc": { "message": "Navega al usuario a una página específica de la interfaz de la aplicación." }, "aiToolSearchLibraryResolutionParamDesc": { "message": "La resolución del video a buscar (por ejemplo, '4k', '1080p'). (Opcional)." },
"aiToolNavigateToPagePageParamDesc": { "message": "El nombre de la página a la que navegar, por ejemplo: 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', o 'm3u-generator'." }, "aiToolSearchLibraryContainerParamDesc": { "message": "El formato contenedor del video a buscar (por ejemplo, 'mkv', 'mp4'). (Opcional)." },
"aiToolGetUserStatsDesc": { "message": "Obtiene y muestra las estadísticas de la biblioteca del usuario, como el número total de películas, series y artistas únicos." }, "aiToolNavigateToPageDesc": { "message": "Navega al usuario a una página específica de la interfaz de la aplicación." },
"aiToolShowItemDetailsDesc": { "message": "Muestra la página de detalles de una película o serie específica por su título y tipo." }, "aiToolNavigateToPagePageParamDesc": { "message": "El nombre de la página a la que navegar, por ejemplo: 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', 'm3u-generator' o 'music'." },
"aiToolShowItemDetailsTitleParamDesc": { "message": "El título exacto de la película o serie." }, "aiToolGetUserStatsDesc": { "message": "Obtiene y muestra las estadísticas de la biblioteca del usuario, como el número total de películas, series y artistas únicos." },
"aiToolShowItemDetailsTypeParamDesc": { "message": "El tipo de contenido. Debe ser 'movie' o 'series'." }, "aiToolShowItemDetailsDesc": { "message": "Muestra la página de detalles de una película o serie específica por su título y tipo." },
"aiToolAddToPlaylistDesc": { "message": "Añade una película o serie a la lista de reproducción actual del usuario para transmitirla a un servidor PHP configurado." }, "aiToolShowItemDetailsTitleParamDesc": { "message": "El título exacto de la película o serie." },
"aiToolAddToPlaylistTitleParamDesc": { "message": "El título de la película o serie a añadir." }, "aiToolShowItemDetailsTypeParamDesc": { "message": "El tipo de contenido. Debe ser 'movie' o 'series'." },
"aiToolAddToPlaylistTypeParamDesc": { "message": "El tipo de contenido. Debe ser 'movie' o 'series'." }, "aiToolAddToPlaylistDesc": { "message": "Añade una película o serie a la lista de reproducción actual del usuario para transmitirla a un servidor PHP configurado." },
"aiToolCheckAndDownloadDesc": { "message": "Comprueba la disponibilidad de una lista de títulos de películas o series en los servidores locales del usuario y, si se encuentran, genera y descarga un archivo de lista de reproducción M3U con los streams encontrados." }, "aiToolAddToPlaylistTitleParamDesc": { "message": "El título de la película o serie a añadir." },
"aiToolCheckAndDownloadTitlesParamDesc": { "message": "Una matriz de títulos de películas o series para buscar y descargar." }, "aiToolAddToPlaylistTypeParamDesc": { "message": "El tipo de contenido. Debe ser 'movie' o 'series'." },
"aiToolCheckAndDownloadTypeParamDesc": { "message": "El tipo de contenido de la lista. Debe ser 'movie' o 'series'." }, "aiToolDownloadSingleMovieM3UDesc": { "message": "Genera y descarga un archivo de lista de reproducción M3U para una única película disponible localmente." },
"aiToolCheckAndDownloadFilenameParamDesc": { "message": "El nombre del archivo M3U a descargar (por ejemplo, 'MiLista.m3u'). Si no se proporciona, se usará un nombre por defecto." }, "aiToolDownloadSingleMovieM3UTitleParamDesc": { "message": "El título de la película para la que se generará el M3U." },
"aiToolToggleFavoriteDesc": { "message": "Añade o quita una película o serie de la lista de favoritos del usuario." }, "aiToolDownloadSingleMovieM3UYearParamDesc": { "message": "El año de lanzamiento de la película (opcional, para mayor precisión)." },
"aiToolToggleFavoriteTitleParamDesc": { "message": "El título de la película o serie." }, "aiToolDownloadSeriesSeasonM3UDesc": { "message": "Genera y descarga un archivo de lista de reproducción M3U para una temporada específica de una serie disponible localmente." },
"aiToolToggleFavoriteTypeParamDesc": { "message": "El tipo de contenido. Debe ser 'movie' o 'series'." }, "aiToolDownloadSeriesSeasonM3UTitleParamDesc": { "message": "El título de la serie." },
"aiToolGetRecommendationsDesc": { "message": "Genera y muestra una lista de recomendaciones de películas o series basadas en el historial de visualización y los favoritos del usuario." }, "aiToolDownloadSeriesSeasonM3USeasonParamDesc": { "message": "El número de la temporada a descargar." },
"aiToolApplyFiltersDesc": { "message": "Aplica filtros a la vista actual de películas o series, permitiendo refinar los resultados por tipo, género, año y orden de clasificación." }, "aiToolDownloadSeriesSeasonM3UYearParamDesc": { "message": "El año de lanzamiento de la serie (opcional)." },
"aiToolApplyFiltersTypeParamDesc": { "message": "El tipo de contenido al que aplicar los filtros. Debe ser 'movie' o 'series'." }, "aiToolCheckAndDownloadDesc": { "message": "Comprueba la disponibilidad de una lista de títulos de películas o series en los servidores locales del usuario y, si se encuentran, genera y descarga un archivo de lista de reproducción M3U con los streams encontrados." },
"aiToolApplyFiltersGenreParamDesc": { "message": "El nombre del género por el que filtrar (por ejemplo, 'Acción', 'Drama')." }, "aiToolCheckAndDownloadTitlesParamDesc": { "message": "Una matriz de títulos de películas o series para buscar y descargar." },
"aiToolApplyFiltersYearParamDesc": { "message": "El año de lanzamiento por el que filtrar (por ejemplo, '2023')." }, "aiToolCheckAndDownloadTypeParamDesc": { "message": "El tipo de contenido de la lista. Debe ser 'movie' o 'series'." },
"aiToolApplyFiltersSortParamDesc": { "message": "El criterio de ordenación para los resultados. Valores válidos: 'popularity.desc' (populares), 'vote_average.desc' (mejor valoradas), 'release_date.desc' (recientes para películas) o 'first_air_date.desc' (recientes para series)." }, "aiToolCheckAndDownloadFilenameParamDesc": { "message": "El nombre del archivo M3U a descargar (por ejemplo, 'MiLista.m3u'). Si no se proporciona, se usará un nombre por defecto." },
"aiToolPlayMusicByArtistDesc": { "message": "Abre el reproductor de música y comienza a reproducir canciones de un artista específico de la biblioteca del usuario." }, "aiToolToggleFavoriteDesc": { "message": "Añade o quita una película o serie de la lista de favoritos del usuario." },
"aiToolPlayMusicByArtistNameParamDesc": { "message": "El nombre exacto del artista cuyas canciones se desean reproducir." }, "aiToolToggleFavoriteTitleParamDesc": { "message": "El título de la película o serie." },
"aiToolClearChatHistoryDesc": { "message": "Borra todo el historial de mensajes de la conversación actual con el asistente de IA." }, "aiToolToggleFavoriteTypeParamDesc": { "message": "El tipo de contenido. Debe ser 'movie' o 'series'." },
"aiToolDeleteDatabaseDesc": { "message": "Elimina toda la base de datos local de la extensión, incluyendo el contenido escaneado, los ajustes y los favoritos. Esta acción es irreversible y recargará la aplicación." }, "aiToolGetRecommendationsDesc": { "message": "Genera y muestra una lista de recomendaciones de películas o series basadas en el historial de visualización y los favoritos del usuario." },
"aiToolUpdateAllTokensDesc": { "message": "Inicia un escaneo completo de todos los servidores y bibliotecas de Plex asociados con los tokens configurados en la extensión. Actualiza todas las películas, series, artistas y fotos." }, "aiToolApplyFiltersDesc": { "message": "Aplica filtros a la vista actual de películas o series, permitiendo refinar los resultados por tipo, género, año y orden de clasificación." },
"aiToolAddPlexTokenDesc": { "message": "Añade un nuevo token X-Plex a la configuración de la extensión, permitiendo que la aplicación escanee contenido de nuevos servidores Plex." }, "aiToolApplyFiltersTypeParamDesc": { "message": "El tipo de contenido al que aplicar los filtros. Debe ser 'movie' o 'series'." },
"aiToolAddPlexTokenTokenParamDesc": { "message": "La cadena del token X-Plex que se desea añadir." }, "aiToolApplyFiltersGenreParamDesc": { "message": "El nombre del género por el que filtrar (por ejemplo, 'Acción', 'Drama')." },
"aiToolChangeRegionDesc": { "message": "Cambia la región utilizada para el descubrimiento de contenido en la API de TMDB. Esto afectará a los resultados mostrados en las secciones de películas y series, así como a los proveedores de streaming." }, "aiToolApplyFiltersYearParamDesc": { "message": "El año de lanzamiento por el que filtrar (por ejemplo, '2023')." },
"aiToolChangeRegionRegionParamDesc": { "message": "El código de país ISO 3166-1 de dos letras para la nueva región (por ejemplo, 'US' para Estados Unidos, 'ES' para España, 'MX' para México)." }, "aiToolApplyFiltersSortParamDesc": { "message": "El criterio de ordenación para los resultados. Valores válidos: 'popularity.desc' (populares), 'vote_average.desc' (mejor valoradas), 'release_date.desc' (recientes para películas) o 'first_air_date.desc' (recientes para series)." },
"aiToolClearAllFavoritesDesc": { "message": "Elimina todas las películas y series que el usuario ha marcado como favoritas." }, "aiToolListAvailableMusicGenresDesc": { "message": "Lista todos los géneros musicales únicos disponibles en la biblioteca local del usuario." },
"aiToolClearViewingHistoryDesc": { "message": "Borra el historial de visualización del usuario de la página de historial." }, "aiToolSearchMusicByGenreDesc": { "message": "Busca artistas en la biblioteca musical del usuario que pertenezcan a un género específico." },
"aiToolClearRecommendationsViewDesc": { "message": "Limpia la vista de recomendaciones y elimina las recomendaciones almacenadas en caché." }, "aiToolSearchMusicByGenreNameParamDesc": { "message": "El nombre del género musical a buscar (ej. 'Rock', 'Pop', 'Jazz')." },
"aiToolSearchNotFound": { "message": "No se encontró '$query en tu biblioteca.", "placeholders": { "query": { "content": "$1" } } }, "aiToolPlayMusicByArtistDesc": { "message": "Abre el reproductor de música y comienza a reproducir canciones de un artista específico de la biblioteca del usuario." },
"aiToolNavigateSuccess": { "message": "Navegado a la página de $page$.", "placeholders": { "page": { "content": "$1" } } }, "aiToolPlayMusicByArtistNameParamDesc": { "message": "El nombre exacto del artista cuyas canciones se desean reproducir." },
"aiToolNavigateError": { "message": "Error al navegar a la página de $page$.", "placeholders": { "page": { "content": "$1" } } }, "aiToolClearChatHistoryDesc": { "message": "Borra todo el historial de mensajes de la conversación actual con el asistente de IA." },
"aiToolStatsError": { "message": "Error al obtener estadísticas." }, "aiToolDeleteDatabaseDesc": { "message": "Elimina toda la base de datos local de la extensión, incluyendo el contenido escaneado, los ajustes y los favoritos. Esta acción es irreversible y recargará la aplicación." },
"aiToolItemNotFound": { "message": "No se encontró el elemento '$title.", "placeholders": { "title": { "content": "$1" } } }, "aiToolUpdateAllTokensDesc": { "message": "Inicia un escaneo completo de todos los servidores y bibliotecas de Plex asociados con los tokens configurados en la extensión. Actualiza todas las películas, series, artistas y fotos." },
"aiToolShowItemDetailsSuccess": { "message": "Mostrando detalles de '$title.", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenDesc": { "message": "Añade un nuevo token X-Plex a la configuración de la extensión, permitiendo que la aplicación escanee contenido de nuevos servidores Plex." },
"aiToolAddToPlaylistSuccess": { "message": "Añadido '$title a la lista de reproducción.", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenTokenParamDesc": { "message": "La cadena del token X-Plex que se desea añadir." },
"aiToolFavoriteAdded": { "message": "Añadido '$title a favoritos.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionDesc": { "message": "Cambia la región utilizada para el descubrimiento de contenido en la API de TMDB. Esto afectará a los resultados mostrados en las secciones de películas y series, así como a los proveedores de streaming." },
"aiToolFavoriteRemoved": { "message": "Eliminado '$title de favoritos.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionRegionParamDesc": { "message": "El código de país ISO 3166-1 de dos letras para la nueva región (por ejemplo, 'US' para Estados Unidos, 'ES' para España, 'MX' para México)." },
"aiToolRecommendationsSuccess": { "message": "Mostrando recomendaciones." }, "aiToolClearAllFavoritesDesc": { "message": "Elimina todas las películas y series que el usuario ha marcado como favoritas." },
"aiToolApplyFiltersGenreNotFound": { "message": "Género '$genre no encontrado.", "placeholders": { "genre": { "content": "$1" } } }, "aiToolClearViewingHistoryDesc": { "message": "Borra el historial de visualización del usuario de la página de historial." },
"aiToolApplyFiltersSuccess": { "message": "Filtros aplicados correctamente." }, "aiToolClearRecommendationsViewDesc": { "message": "Limpia la vista de recomendaciones y elimina las recomendaciones almacenadas en caché." },
"aiToolPlayMusicNotReady": { "message": "El reproductor de música no está listo. Asegúrate de que tu biblioteca de música de Plex haya sido escaneada." }, "aiToolSearchNotFound": { "message": "No se encontró '$query en tu biblioteca.", "placeholders": { "query": { "content": "$1" } } },
"aiToolPlayMusicArtistNotFound": { "message": "Artista '$artist_name no encontrado.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolNavigateSuccess": { "message": "Navegado a la página de $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolPlayMusicNoSongs": { "message": "No se encontraron canciones para '$artist_name.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolNavigateError": { "message": "Error al navegar a la página de $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolPlayMusicSuccess": { "message": "Reproduciendo música de '$artist_name.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolStatsError": { "message": "Error al obtener estadísticas." },
"aiToolChatHistoryCleared": { "message": "Historial de chat borrado." }, "aiToolItemNotFound": { "message": "No se encontró el elemento '$title'.", "placeholders": { "title": { "content": "$1" } } },
"aiToolConfirmDeleteDatabase": { "message": "¿Estás seguro de que quieres eliminar la base de datos local? Esta acción es irreversible." }, "aiToolShowItemDetailsSuccess": { "message": "Mostrando detalles de '$title'.", "placeholders": { "title": { "content": "$1" } } },
"aiToolDeleteDatabaseCancelled": { "message": "Eliminación de la base de datos cancelada." }, "aiToolAddToPlaylistSuccess": { "message": "Añadido '$title' a la lista de reproducción.", "placeholders": { "title": { "content": "$1" } } },
"aiToolExecutionError": { "message": "Error al ejecutar la herramienta '$toolName: $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolFavoriteAdded": { "message": "Añadido '$title' a favoritos.", "placeholders": { "title": { "content": "$1" } } },
"aiToolUnknown": { "message": "Herramienta desconocida: '$toolName.", "placeholders": { "toolName": { "content": "$1" } } }, "aiToolFavoriteRemoved": { "message": "Eliminado '$title' de favoritos.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoritesCleared": { "message": "Favoritos eliminados." }, "aiToolRecommendationsSuccess": { "message": "Mostrando recomendaciones." },
"aiToolFavoritesClearError": { "message": "Error al eliminar los favoritos: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolApplyFiltersGenreNotFound": { "message": "Género '$genre' no encontrado.", "placeholders": { "genre": { "content": "$1" } } },
"aiToolRecommendationsCleared": { "message": "Recomendaciones eliminadas." }, "aiToolApplyFiltersSuccess": { "message": "Filtros aplicados correctamente." },
"aiToolRecommendationsClearError": { "message": "Error al eliminar las recomendaciones: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolSearchMusicByGenreNotFound": { "message": "No encontré artistas del género '$genre_name' en tu biblioteca.", "placeholders": { "genre_name": { "content": "$1" } } },
"aiToolDatabaseDeleted": { "message": "Base de datos eliminada. La página se recargará." }, "aiToolPlayMusicNotReady": { "message": "El reproductor de música no está listo. Asegúrate de que tu biblioteca de música de Plex haya sido escaneada." },
"aiToolDatabaseDeleteError": { "message": "Error al eliminar la base de datos: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolPlayMusicArtistNotFound": { "message": "Artista '$artist_name' no encontrado.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolDatabaseDeleteBlocked": { "message": "La eliminación de la base de datos está bloqueada. Cierra otras pestañas de la aplicación." }, "aiToolPlayMusicNoSongs": { "message": "No se encontraron canciones para '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolUpdateAllTokensSuccess": { "message": "Todos los tokens se han actualizado correctamente." }, "aiToolPlayMusicSuccess": { "message": "Reproduciendo música de '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolUpdateAllTokensError": { "message": "Error al actualizar los tokens: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolChatHistoryCleared": { "message": "Historial de chat borrado." },
"aiToolAddPlexTokenSuccess": { "message": "Token de Plex añadido correctamente." }, "aiToolConfirmDeleteDatabase": { "message": "¿Estás seguro de que quieres eliminar la base de datos local? Esta acción es irreversible." },
"aiToolAddPlexTokenError": { "message": "Error al añadir el token de Plex: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolDeleteDatabaseCancelled": { "message": "Eliminación de la base de datos cancelada." },
"aiToolChangeRegionSuccess": { "message": "Región cambiada a $region$. El contenido se está actualizando.", "placeholders": { "region": { "content": "$1" } } }, "aiToolExecutionError": { "message": "Error al ejecutar la herramienta '$toolName': $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } },
"aiToolChangeRegionError": { "message": "Error al cambiar la región: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolUnknown": { "message": "Herramienta desconocida: '$toolName'.", "placeholders": { "toolName": { "content": "$1" } } },
"aiToolViewingHistoryCleared": { "message": "Historial de visualización borrado." }, "aiToolFavoritesCleared": { "message": "Favoritos eliminados." },
"aiToolViewingHistoryClearError": { "message": "Error al borrar el historial de visualización: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolFavoritesClearError": { "message": "Error al eliminar los favoritos: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiSystemPrompt_v3": { "message": "Eres un asistente experto en cine y series llamado CinePlex. Tu función principal es ayudar a los usuarios a descubrir contenido y a interactuar con su biblioteca. Sigue estas reglas rigurosamente: 1. **NUNCA** inventes que has realizado una acción si no has usado una herramienta para ello. Por ejemplo, no digas 'he descargado X' si no has usado la herramienta de descarga. 2. Para peticiones de recomendaciones o listas (ej. 'dime 5 películas de terror'), usa tu propio conocimiento para generar la lista. Preséntala en formato numerado o con viñetas. Después de mostrar la lista, pregunta proactivamente al usuario si quiere que compruebes la disponibilidad en sus servidores locales y crees un archivo M3U. 3. **SOLO** si el usuario confirma que quiere comprobar o descargar la lista, utiliza la herramienta `check_and_download_titles_list`. No la uses sin confirmación explícita. 4. Para cualquier otra acción como navegar, obtener estadísticas o buscar un título específico, o filtrar por resolución o contenedor, usa las herramientas apropiadas. Sé siempre conciso, amigable y eficiente." }, "aiToolRecommendationsCleared": { "message": "Recomendaciones eliminadas." },
"aiToolM3UNoTitlesProvided": { "message": "Por favor, proporciona una lista de títulos para crear la lista de reproducción." }, "aiToolRecommendationsClearError": { "message": "Error al eliminar las recomendaciones: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolM3UCheckingTitles": { "message": "Comprobando los títulos en tus servidores locales..." }, "aiToolDatabaseDeleted": { "message": "Base de datos eliminada. La página se recargará." },
"aiToolM3UNoLocalMatchesForDownload": { "message": "No he encontrado ninguna de las películas o series de la lista en tus servidores locales." }, "aiToolDatabaseDeleteError": { "message": "Error al eliminar la base de datos: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolM3UDownloadStarted": { "message": "¡Hecho! He encontrado $1 de los $2 títulos en tus servidores y he iniciado la descarga de la lista de reproducción M3U.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } }, "aiToolDatabaseDeleteBlocked": { "message": "La eliminación de la base de datos está bloqueada. Cierra otras pestañas de la aplicación." },
"backToProviders": { "message": "Volver a Proveedores" }, "aiToolUpdateAllTokensSuccess": { "message": "Todos los tokens se han actualizado correctamente." },
"artistsCounterSingle": { "message": "$total$ Artista", "placeholders": { "total": { "content": "$1" } } }, "aiToolUpdateAllTokensError": { "message": "Error al actualizar los tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Cargando..." }, "aiToolAddPlexTokenSuccess": { "message": "Token de Plex añadido correctamente." },
"downloadingSong": { "message": "Iniciando descarga de \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenError": { "message": "Error al añadir el token de Plex: $message$", "placeholders": { "message": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" descargado.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionSuccess": { "message": "Región cambiada a $region$. El contenido se está actualizando.", "placeholders": { "region": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Error al descargar \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionError": { "message": "Error al cambiar la región: $message$", "placeholders": { "message": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Generando M3U para \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } }, "aiToolViewingHistoryCleared": { "message": "Historial de visualización borrado." },
"albumM3UGenerated": { "message": "M3U para el álbum \"$artist$\" generado.", "placeholders": { "artist": { "content": "$1" } } }, "aiToolViewingHistoryClearError": { "message": "Error al borrar el historial de visualización: $message$", "placeholders": { "message": { "content": "$1" } } },
"retyingSection": { "message": "Reintentando sección \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolM3UDownloadStartedSingle": { "message": "Iniciando la descarga del M3U para '$movie_title'.", "placeholders": { "movie_title": { "content": "$1" } } },
"retrySuccess": { "message": "[ÉXITO] Reintento de \"$title$\" completado.", "placeholders": { "title": { "content": "$1" } } }, "aiToolM3UDownloadStartedSeason": { "message": "Iniciando la descarga del M3U para la temporada $1 de '$2'.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } },
"retryError": { "message": "[ERROR FINAL] Falló el reintento para \"$title$\": $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolM3UNoTitlesProvided": { "message": "Por favor, proporciona una lista de títulos para crear la lista de reproducción." },
"startingRetryPhase": { "message": "Iniciando fase de reintentos para $count$ secciones...", "placeholders": { "count": { "content": "$1" } } }, "aiToolM3UCheckingTitles": { "message": "Comprobando los títulos en tus servidores locales..." },
"tokenFoundServers": { "message": "Token $token$... encontró $count$ servidores.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } }, "aiToolM3UNoLocalMatchesForDownload": { "message": "No he encontrado ninguna de las películas o series de la lista en tus servidores locales." },
"errorProcessingToken": { "message": "Error procesando token $token$...: $message$", "placeholders": { "token": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolM3UDownloadStarted": { "message": "¡Hecho! He encontrado $1 de los $2 títulos en tus servidores y he iniciado la descarga de la lista de reproducción M3U.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } },
"plexScanFatalError": { "message": "ERROR FATAL: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolTrailerNotFoundSpecific": { "message": "Lo siento, no pude encontrar un tráiler disponible para '$title'.", "placeholders": { "title": { "content": "$1" } } },
"errorDuringScan": { "message": "Error durante el escaneo: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiSystemPrompt_v4": {
"stoppingPlexScan": { "message": "Deteniendo escaneo Plex..." }, "message": "Eres un asistente virtual integrado en una extensión de Chrome que interactúa con servidores Plex y Jellyfin. Tu función principal es ayudar al usuario a buscar, gestionar, reproducir y descargar contenido multimedia, así como administrar ajustes personalizados.\n\nPRIORIDAD MÁXIMA: Siempre que la pregunta del usuario se refiera a contenido multimedia (películas, series, música), DEBES asumir que se refiere a su biblioteca local. Utiliza las herramientas para buscar en su base de datos ANTES de buscar en la web.\n\n🎯 Reglas generales de comportamiento:\nResponde siempre de forma clara, concisa y directa. Sé proactivo y proporciona toda la información relevante de una vez para evitar preguntas de seguimiento. Por ejemplo, al confirmar la disponibilidad de una serie, incluye los detalles de las temporadas.\n\nCompara la fecha actual con los resultados de búsqueda de Google cuando se te pida información externa para garantizar que esté actualizada.\n\nUsa los nombres exactos de los comandos definidos en el sistema (function.name) al llamar herramientas.\n\n📦 Funciones clave para contenido multimedia:\nPara generar un M3U para una única película, usa download_single_movie_m3u.\nPara descargar una temporada específica de una serie, usa download_series_season_m3u.\nPara múltiples títulos (películas o series completas), usa siempre check_and_download_titles_list.\nPara buscar contenido local: search_library.\nPara buscar en TMDB: search_tmdb_content.\nPara contenido en tendencia: get_trending_content.\nPara mostrar detalles de un título: show_item_details.\nPara añadir a la lista de reproducción PHP: add_to_playlist.\nPara comprobar disponibilidad local: check_local_availability.\nSi una serie está disponible localmente, informa de cuántas temporadas hay y en qué servidores usando get_local_series_seasons.\nPara ver recomendaciones: get_recommendations.\nPara aplicar filtros: apply_filters.\nPara ver historial o favoritos: view_history, view_favorites.\nPara marcar como favorito: toggle_favorite.\nPara reproducir tráiler: play_trailer.\n\n🎵 Funciones musicales:\nSi el usuario pide recomendaciones de géneros musicales de forma general (ej. 'recomiéndame un género para animarme'), primero usa list_available_music_genres para ver qué géneros tiene y basa tu recomendación en esa lista.\nPara listar todos los géneros musicales disponibles en la biblioteca: list_available_music_genres.\nPara buscar artistas por género: search_music_by_genre.\nPara reproducir canciones por título y/o artista: play_song.\nPara reproducir música de un artista: play_music_by_artist.\n\n🧰 Funciones de gestión y configuración:\nPara obtener estadísticas del usuario: get_user_stats.\nPara navegar a secciones específicas: navigate_to_page.\nPara actualizar tokens: update_all_tokens, add_plex_token.\nPara cambiar la región de contenido: change_region.\nPara exportar o importar la base de datos local: export_local_database, import_local_database.\nPara borrar la base de datos: delete_database.\nPara limpiar favoritos, historial o recomendaciones: clear_all_favorites, clear_viewing_history, clear_recommendations_view.\nPara cambiar el modo claro/oscuro: toggle_light_mode.\nPara mostrar u ocultar la sección de héroe: toggle_hero_section.\n\n⚠ Consideraciones adicionales:\nPrioriza contenido disponible localmente. Usa check_local_availability antes de mostrar opciones de reproducción o descarga.\nSi una acción falla, informa de manera clara y sin rodeos.\nEvita repetir innecesariamente la solicitud del usuario, salvo que ayude a contextualizar la respuesta."
"invalidTokenProvided": { "message": "Token inválido proporcionado." }, },
"tokenAlreadyExists": { "message": "El token ya existe." }, "backToProviders": { "message": "Volver a Proveedores" },
"tokenAddedSuccessfully": { "message": "Token añadido correctamente." }, "artistsCounterSingle": { "message": "$total$ Artista", "placeholders": { "total": { "content": "$1" } } },
"noStreamsFoundForSelection": { "message": "No se encontraron streams para la selección." }, "artistsCounterLoading": { "message": "Cargando..." },
"autoplayBlocked": { "message": "Reproducción automática bloqueada." }, "downloadingSong": { "message": "Iniciando descarga de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"welcomeToCinePlex": { "message": "" }, "songDownloaded": { "message": "\"$title$\" descargado.", "placeholders": { "title": { "content": "$1" } } },
"page": { "message": "Página" }, "errorDownloadingSong": { "message": "Error al descargar \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"all": { "message": "Todo" }, "generatingAlbumM3U": { "message": "Generando M3U para \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
"userScore": { "message": "Puntuación" }, "albumM3UGenerated": { "message": "M3U para el álbum \"$artist$\" generado.", "placeholders": { "artist": { "content": "$1" } } },
"duration": { "message": "Duración" }, "retyingSection": { "message": "Reintentando sección \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"min": { "message": "Mín" }, "retrySuccess": { "message": "[ÉXITO] Reintento de \"$title$\" completado.", "placeholders": { "title": { "content": "$1" } } },
"max": { "message": "Máx" } "retryError": { "message": "[ERROR FINAL] Falló el reintento para \"$title$\": $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } },
"startingRetryPhase": { "message": "Iniciando fase de reintentos para $count$ secciones...", "placeholders": { "count": { "content": "$1" } } },
"tokenFoundServers": { "message": "Token $token$... encontró $count$ servidores.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } },
"errorProcessingToken": { "message": "Error procesando token $token$...: $message$", "placeholders": { "token": { "content": "$1" }, "message": { "content": "$2" } } },
"plexScanFatalError": { "message": "ERROR FATAL: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorDuringScan": { "message": "Error durante el escaneo: $message$", "placeholders": { "message": { "content": "$1" } } },
"stoppingPlexScan": { "message": "Deteniendo escaneo Plex..." },
"invalidTokenProvided": { "message": "Token inválido proporcionado." },
"tokenAlreadyExists": { "message": "El token ya existe." },
"tokenAddedSuccessfully": { "message": "Token añadido correctamente." },
"noStreamsFoundForSelection": { "message": "No se encontraron streams para la selección." },
"autoplayBlocked": { "message": "Reproducción automática bloqueada." },
"welcomeToCinePlex": { "message": "" },
"page": { "message": "Página" },
"all": { "message": "Todo" },
"userScore": { "message": "Puntuación" },
"duration": { "message": "Duración" },
"min": { "message": "Mín" },
"max": { "message": "Máx" },
"aiToolFindStreamingProvidersDesc": { "message": "Busca dónde ver una película o serie en servicios de streaming." },
"aiToolFindStreamingProvidersTitleParamDesc": { "message": "El título de la película o serie a buscar." },
"aiToolFindStreamingProvidersTypeParamDesc": { "message": "El tipo de contenido (película o serie)." },
"aiToolFindStreamingProvidersYearParamDesc": { "message": "El año de lanzamiento del contenido (opcional)." },
"aiToolNoStreamingProviders": { "message": "No se encontraron proveedores de streaming para {title}." },
"aiToolStreamingProvidersFound": { "message": "{title} está disponible en los siguientes servicios: {providers}." },
"aiToolStreamingProviderError": { "message": "Error al buscar proveedores de streaming: {message}." },
"aiToolGetLocalSeriesSeasonsDesc": { "message": "Comprueba si una serie de TV está disponible localmente y devuelve un desglose detallado de las temporadas disponibles en cada servidor." },
"aiToolGetLocalSeriesSeasonsTitleParamDesc": { "message": "El título de la serie a comprobar." },
"aiToolGetLocalSeriesSeasonsYearParamDesc": { "message": "El año de lanzamiento de la serie (opcional para mayor precisión)." },
"aiToolLocalSeriesNoSeasons": { "message": "La serie '$series_title' está en tu biblioteca, pero no se encontraron detalles de las temporadas.", "placeholders": { "series_title": { "content": "$1" } } },
"artist": { "message": "Artista" },
"tracks": { "message": "pistas" },
"noSongsFound": { "message": "No se encontraron canciones para este artista." },
"durationMin": { "message": "Duración (Min)" },
"score": { "message": "Puntuación" },
"searchGenre": { "message": "Buscar género..." },
"searchArtists": { "message": "Buscar artistas..." },
"preparingMusicLibrary": { "message": "Preparando tu biblioteca musical..." },
"preparingMusicLibraryDesc": { "message": "Este proceso único puede tardar unos minutos si tienes muchos artistas." },
"artistsProgress": { "message": "0 / 0 artistas" },
"starting": { "message": "Iniciando..." },
"artistName": { "message": "Nombre del Artista" },
"playPause": { "message": "Reproducir/Pausar" },
"noLocalFilesFound": { "message": "No se encontraron archivos locales para este título." },
"server": { "message": "Servidor" },
"title": { "message": "Título" },
"year": { "message": "Año" },
"resolution": { "message": "Resolución" },
"size": { "message": "Tamaño" },
"container": { "message": "Contenedor" },
"action": { "message": "Acción" },
"generate": { "message": "Generar" },
"availableLocalFiles": { "message": "Archivos Locales Disponibles" },
"downloadSeason": { "message": "Descargar Temporada" },
"errorLoadingServersM3u": { "message": "Error al cargar los servidores para el generador M3U:" },
"errorFetchingLibraries": { "message": "Error al obtener las bibliotecas." },
"selectServerAndLibrary": { "message": "Por favor, selecciona un servidor y al menos una biblioteca." },
"generating": { "message": "Generando..." },
"errorProcessingLibrary": { "message": "Error al procesar la biblioteca" },
"errorProcessingLibrarySkipping": { "message": "Error al procesar la biblioteca. Omitiendo." },
"allLibrariesFailed": { "message": "Todas las bibliotecas seleccionadas fallaron al procesar." },
"m3uGeneratedWithErrors": { "message": "M3U generado con algunos errores. Algunas bibliotecas pueden faltar." },
"m3uDownloadedSuccess": { "message": "Lista de reproducción M3U descargada con éxito." },
"errorGeneratingM3uFile": { "message": "Error al generar el archivo M3U." },
"chatSources": { "message": "Fuentes" },
"chatUnnamedSource": { "message": "Fuente sin nombre" },
"googleApiFailure": { "message": "Fallo en la llamada a la API de Google AI:" }
} }

View File

@ -1,449 +1,516 @@
{ {
"appName": { "message": "CinePlex" }, "appName": { "message": "CinePlex" },
"appDescription": { "message": "Scanne les serveurs Plex à la recherche de contenu et l'affiche dans l'interface" }, "appDescription": { "message": "Analyse les serveurs Plex pour trouver du contenu et l'affiche dans l'interface" },
"appTagline": { "message": "Films, Séries et Musique" }, "appTagline": { "message": "Films, Séries et Musique" },
"appLocaleCode": { "message": "fr-FR" }, "appLocaleCode": { "message": "fr-FR" },
"toggleNavigation": { "message": "Basculer la navigation" }, "toggleNavigation": { "message": "Basculer la Navigation" },
"searchPlaceholder": { "message": "Rechercher des films ou des séries..." }, "searchPlaceholder": { "message": "Rechercher des films ou des séries..." },
"openMusicPlayer": { "message": "Ouvrir le lecteur de musique" }, "openMusicPlayer": { "message": "Ouvrir le Lecteur de Musique" },
"settings": { "message": "Paramètres" }, "settings": { "message": "Paramètres" },
"navMovies": { "message": "Films" }, "navMovies": { "message": "Films" },
"navSeries": { "message": "Séries" }, "navSeries": { "message": "Séries" },
"navProviders": { "message": "Fournisseurs" }, "navProviders": { "message": "Fournisseurs" },
"navPhotos": { "message": "Photos" }, "navPhotos": { "message": "Photos" },
"navStats": { "message": "Statistiques" }, "navStats": { "message": "Statistiques" },
"navFavorites": { "message": "Favoris" }, "navFavorites": { "message": "Favoris" },
"navHistory": { "message": "Historique" }, "navHistory": { "message": "Historique" },
"navRecommendations": { "message": "Recommandations" }, "navRecommendations": { "message": "Recommandations" },
"navMusic": { "message": "Musique" }, "navMusic": { "message": "Musique" },
"navM3uGenerator": { "message": "Générateur M3U" }, "musicFeaturedPlaylists": { "message": "Playlists à la une" },
"heroWelcome": { "message": "" }, "musicRecentlyAdded": { "message": "Ajouté Récemment" },
"heroSubtitle": { "message": "Explorez des milliers de films et de séries." }, "navM3uGenerator": { "message": "Générateur M3U" },
"addStream": { "message": "Ajouter un flux" }, "heroWelcome": { "message": "" },
"moreInfo": { "message": "Plus d'infos" }, "heroSubtitle": { "message": "Explorez des milliers de films et de séries." },
"popularMovies": { "message": "Films populaires" }, "addStream": { "message": "Ajouter un Stream" },
"allGenres": { "message": "Tous les genres" }, "moreInfo": { "message": "Plus d'informations" },
"allYears": { "message": "Toutes les années" }, "popularMovies": { "message": "Films Populaires" },
"sortPopular": { "message": "Les plus populaires" }, "allGenres": { "message": "Tous les genres" },
"sortTopRated": { "message": "Les mieux notés" }, "allYears": { "message": "Toutes les années" },
"sortRecent": { "message": "Les plus récents" }, "sortPopular": { "message": "Les plus populaires" },
"loadMore": { "message": "Charger plus" }, "sortTopRated": { "message": "Les mieux notés" },
"photosBreadcrumbHome": { "message": "Albums" }, "sortRecent": { "message": "Les plus récents" },
"selectServer": { "message": "Sélectionnez un serveur" }, "loadMore": { "message": "Charger plus" },
"loading": { "message": "Chargement..." }, "photosBreadcrumbHome": { "message": "Albums" },
"loadingLibraries": { "message": "Chargement des bibliothèques..." }, "selectServer": { "message": "Sélectionnez un serveur" },
"photosEmptyState": { "message": "Aucun album ou photo trouvé." }, "loading": { "message": "Chargement..." },
"photosEmptyStateSub": { "message": "Veuillez sélectionner un serveur ou vous assurer que vous disposez d'une photothèque dans Plex." }, "loadingLibraries": { "message": "Chargement des bibliothèques..." },
"statsTitle": { "message": "Statistiques de la bibliothèque" }, "photosEmptyState": { "message": "Aucun album ni photo trouvé." },
"statsAllTokens": { "message": "Tous les jetons" }, "photosEmptyStateSub": { "message": "Veuillez sélectionner un serveur ou vous assurer d'avoir une bibliothèque de photos dans Plex." },
"statsAnalyzing": { "message": "Analyse de votre bibliothèque..." }, "statsTitle": { "message": "Statistiques de la Bibliothèque" },
"statsActiveTokens": { "message": "Jetons actifs" }, "statsAllTokens": { "message": "Tous les Tokens" },
"statsServersFound": { "message": "Serveurs trouvés" }, "statsAnalyzing": { "message": "Analyse de votre bibliothèque..." },
"statsUniqueMovies": { "message": "Films uniques" }, "statsActiveTokens": { "message": "Tokens Actifs" },
"statsUniqueSeries": { "message": "Séries uniques" }, "statsServersFound": { "message": "Serveurs Trouvés" },
"statsUniqueArtists": { "message": "Artistes uniques" }, "statsUniqueMovies": { "message": "Films Uniques" },
"statsTokenServers": { "message": "Serveurs de jetons" }, "statsUniqueSeries": { "message": "Séries Uniques" },
"statsChartMoviesByGenre": { "message": "Contenu par genre (Films)" }, "statsUniqueArtists": { "message": "Artistes Uniques" },
"statsChartSeriesByGenre": { "message": "Contenu par genre (Séries)" }, "statsTokenServers": { "message": "Serveurs du Token" },
"statsChartByDecade": { "message": "Contenu par décennie" }, "statsChartMoviesByGenre": { "message": "Contenu par Genre (Films)" },
"recommendationsTitle": { "message": "Recommandations pour vous" }, "statsChartSeriesByGenre": { "message": "Contenu par Genre (Séries)" },
"historyTitle": { "message": "Historique de visionnage" }, "statsChartByDecade": { "message": "Contenu par Décennie" },
"clearHistory": { "message": "Tout effacer" }, "recommendationsTitle": { "message": "Recommandations pour vous" },
"consoleTitle": { "message": "Console d'analyse Plex" }, "historyTitle": { "message": "Historique de Visionnage" },
"footerCredit": { "message": "Une interface pour votre univers Plex." }, "clearHistory": { "message": "Tout effacer" },
"closeTrailer": { "message": "Fermer la bande-annonce" }, "consoleTitle": { "message": "Console d'Analyse Plex" },
"close": { "message": "Fermer" }, "footerCredit": { "message": "Une interface pour votre univers Plex." },
"photoViewer": { "message": "Visionneuse de photos" }, "closeTrailer": { "message": "Fermer la bande-annonce" },
"previous": { "message": "Précédent" }, "close": { "message": "Fermer" },
"next": { "message": "Suivant" }, "photoViewer": { "message": "Visionneuse de photos" },
"notificationTemplateText": { "message": "Notification" }, "previous": { "message": "Précédent" },
"settingsTitleFull": { "message": "Paramètres et configuration" }, "next": { "message": "Suivant" },
"settingsTabGeneral": { "message": "Général" }, "notificationTemplateText": { "message": "Notification" },
"settingsTabPlex": { "message": "Plex" }, "settingsTitleFull": { "message": "Paramètres et Configuration" },
"settingsTabJellyfin": { "message": "Jellyfin" }, "settingsTabGeneral": { "message": "Général" },
"settingsTabPhpGen": { "message": "Générateur PHP" }, "settingsTabPlex": { "message": "Plex" },
"settingsTabData": { "message": "Données" }, "settingsTabJellyfin": { "message": "Jellyfin" },
"settingsApiServer": { "message": "Paramètres API et serveur" }, "settingsTabPhpGen": { "message": "Générateur PHP" },
"settingsTmdbApiLabel": { "message": "Clé API TMDB (facultatif)" }, "settingsTabData": { "message": "Données" },
"settingsTmdbApiPlaceholder": { "message": "La clé par défaut sera utilisée si ce champ est laissé vide" }, "settingsApiServer": { "message": "Configuration API et Serveur" },
"settingsGoogleApiLabel": { "message": "Clé API Google Gemini (facultatif)" }, "settingsTmdbApiLabel": { "message": "Clé API TMDB (Optionnel)" },
"settingsGoogleApiPlaceholder": { "message": "Requis pour utiliser l'assistant IA" }, "settingsTmdbApiPlaceholder": { "message": "La clé par défaut sera utilisée si laissée vide" },
"settingsRegionLabel": { "message": "Région pour la découverte de contenu" }, "settingsGoogleApiLabel": { "message": "Clé API Google Gemini (Optionnel)" },
"allRegions": { "message": "Toutes les régions" }, "settingsGoogleApiPlaceholder": { "message": "Nécessaire pour utiliser l'assistant IA" },
"settingsPhpUrlLabel": { "message": "URL du serveur pour l'ajout de flux" }, "settingsRegionLabel": { "message": "Région pour la découverte de contenu" },
"settingsPhpUrlPlaceholder": { "message": "https://votre-serveur.com/chemin/vers/script.php" }, "allRegions": { "message": "Toutes les régions" },
"settingsInterface": { "message": "Interface" }, "settingsPhpUrlLabel": { "message": "URL du Serveur pour l'Ajout de Streams" },
"settingsLightTheme": { "message": "Mode clair" }, "settingsPhpUrlPlaceholder": { "message": "https://votre-serveur.com/chemin/vers/script.php" },
"settingsShowHero": { "message": "Afficher la section d'accueil 'Hero'" }, "settingsInterface": { "message": "Interface" },
"settingsScanContent": { "message": "Analyse du contenu" }, "settingsLightTheme": { "message": "Mode Clair" },
"settingsScanDesc": { "message": "Sélectionnez les éléments à analyser et appuyez sur le bouton." }, "settingsShowHero": { "message": "Afficher la section de bienvenue 'Hero'" },
"settingsScanMovies": { "message": "Films" }, "settingsScanContent": { "message": "Analyse de Contenu" },
"settingsScanShows": { "message": "Séries" }, "settingsScanDesc": { "message": "Sélectionnez ce que vous voulez analyser et appuyez sur le bouton." },
"settingsScanArtists": { "message": "Musique" }, "settingsScanMovies": { "message": "Films" },
"settingsScanPhotos": { "message": "Photos" }, "settingsScanShows": { "message": "Séries" },
"settingsSelectAll": { "message": "Tout sélectionner" }, "settingsScanArtists": { "message": "Musique" },
"settingsStartScan": { "message": "Démarrer l'analyse" }, "settingsScanPhotos": { "message": "Photos" },
"settingsPlexTokens": { "message": "Jetons Plex" }, "settingsSelectAll": { "message": "Tout Sélectionner" },
"settingsPlexTokensDesc": { "message": "Modifiez la liste des jetons Plex (format JSON)." }, "settingsStartScan": { "message": "Démarrer l'Analyse" },
"settingsSaveTokens": { "message": "Enregistrer les jetons" }, "settingsPlexTokens": { "message": "Tokens Plex" },
"settingsJellyfinTitle": { "message": "Paramètres Jellyfin" }, "settingsPlexTokensDesc": { "message": "Modifiez la liste des tokens Plex (format JSON)." },
"settingsJellyfinDesc": { "message": "Ajoutez les détails de votre serveur Jellyfin pour analyser son contenu." }, "settingsSaveTokens": { "message": "Enregistrer les Tokens" },
"jellyfinUrlLabel": { "message": "URL du serveur Jellyfin" }, "settingsJellyfinTitle": { "message": "Configuration de Jellyfin" },
"jellyfinUserLabel": { "message": "Nom d'utilisateur" }, "settingsJellyfinDesc": { "message": "Ajoutez les informations de votre serveur Jellyfin pour analyser son contenu." },
"jellyfinPassLabel": { "message": "Mot de passe" }, "jellyfinUrlLabel": { "message": "URL du Serveur Jellyfin" },
"jellyfinConnectAndScan": { "message": "Connecter et analyser" }, "jellyfinUserLabel": { "message": "Nom d'utilisateur" },
"settingsPhpGenTitle": { "message": "Générateur de script PHP pour serveur" }, "jellyfinPassLabel": { "message": "Mot de passe" },
"settingsPhpFileOptions": { "message": "Options de fichier" }, "jellyfinConnectAndScan": { "message": "Connecter et Analyser" },
"settingsPhpSavePathLabel": { "message": "Chemin d'enregistrement sur le serveur" }, "settingsPhpGenTitle": { "message": "Générateur de Script PHP pour le Serveur" },
"settingsPhpSavePathPlaceholder": { "message": "Ex: /var/www/html/listes (vide pour le même dossier)" }, "settingsPhpFileOptions": { "message": "Options du Fichier" },
"settingsPhpFilenameLabel": { "message": "Nom de fichier" }, "settingsPhpSavePathLabel": { "message": "Chemin de Sauvegarde sur le Serveur" },
"settingsPhpFileAction": { "message": "Action sur le fichier" }, "settingsPhpSavePathPlaceholder": { "message": "Ex: /var/www/html/listes (vide pour le même dossier)" },
"settingsPhpActionAppend": { "message": "Ajouter à la fin du fichier (cumulatif)" }, "settingsPhpFilenameLabel": { "message": "Nom du Fichier" },
"settingsPhpActionOverwrite": { "message": "Écraser le fichier (repartir de zéro)" }, "settingsPhpFileAction": { "message": "Action sur le Fichier" },
"settingsPhpSecurity": { "message": "Sécurité (facultatif)" }, "settingsPhpActionAppend": { "message": "Ajouter à la fin du fichier (cumulatif)" },
"settingsPhpUseSecretKey": { "message": "Utiliser une clé secrète (recommandé)" }, "settingsPhpActionOverwrite": { "message": "Écraser le fichier (recommencer)" },
"settingsPhpSecretKeyPlaceholder": { "message": "Saisissez une clé secrète sécurisée" }, "settingsPhpSecurity": { "message": "Sécurité (Optionnel)" },
"settingsPhpGeneratedCode": { "message": "Code généré" }, "settingsPhpUseSecretKey": { "message": "Utiliser une clé secrète (Recommandé)" },
"settingsPhpGeneratedPlaceholder": { "message": "Le code PHP généré apparaîtra ici." }, "settingsPhpSecretKeyPlaceholder": { "message": "Entrez une clé secrète sécurisée" },
"settingsGenerateScript": { "message": "Générer le script" }, "settingsPhpGeneratedCode": { "message": "Code Généré" },
"settingsCopyScript": { "message": "Copier le script" }, "settingsPhpGeneratedPlaceholder": { "message": "Le code PHP généré apparaîtra ici." },
"settingsDataManagement": { "message": "Gestion de la base de données locale" }, "settingsGenerateScript": { "message": "Générer le Script" },
"settingsImportDb": { "message": "Importer la base de données depuis un fichier" }, "settingsCopyScript": { "message": "Copier le Script" },
"settingsExportDb": { "message": "Exporter la base de données vers un fichier" }, "settingsDataManagement": { "message": "Gestion de la Base de Données Locale" },
"settingsClearContent": { "message": "Effacer les données de contenu local" }, "settingsImportDb": { "message": "Importer la BD depuis un Fichier" },
"settingsClearContentDesc": { "message": "Cette action supprimera les films, les séries et la musique de la base de données locale, mais n'affectera pas vos favoris ni vos paramètres." }, "settingsExportDb": { "message": "Exporter la BD vers un Fichier" },
"settingsClose": { "message": "Fermer" }, "settingsClearContent": { "message": "Effacer les Données de Contenu Locales" },
"settingsSave": { "message": "Enregistrer les paramètres" }, "settingsClearContentDesc": { "message": "Cette action effacera les films, séries et musiques de la base de données locale, mais n'affectera pas vos favoris ni vos paramètres." },
"musicSidenavTitle": { "message": "Musique Plex" }, "settingsClose": { "message": "Fermer" },
"musicAllServers": { "message": "Tous les serveurs" }, "settingsSave": { "message": "Enregistrer les Paramètres" },
"musicSearchArtistPlaceholder": { "message": "Rechercher un artiste..." }, "musicSidenavTitle": { "message": "Musique de Plex" },
"musicSearchDiscographyPlaceholder": { "message": "Rechercher dans la discographie..." }, "musicAllServers": { "message": "Tous les Serveurs" },
"musicNothingPlaying": { "message": "Aucune lecture en cours" }, "musicSearchArtistPlaceholder": { "message": "Rechercher un artiste..." },
"musicSelectSong": { "message": "Sélectionnez une chanson" }, "musicSearchDiscographyPlaceholder": { "message": "Rechercher dans la discographie..." },
"musicToStart": { "message": "pour démarrer la lecture" }, "musicNothingPlaying": { "message": "Rien en cours de lecture" },
"miniplayerDownloadSong": { "message": "Télécharger la chanson" }, "musicSelectSong": { "message": "Sélectionnez une chanson" },
"miniplayerDownloadAlbum": { "message": "Télécharger l'album M3U" }, "musicToStart": { "message": "pour commencer la lecture" },
"miniplayerVolume": { "message": "Volume" }, "miniplayerDownloadSong": { "message": "Télécharger la chanson" },
"miniplayerShuffle": { "message": "Aléatoire" }, "miniplayerDownloadAlbum": { "message": "Télécharger M3U" },
"miniplayerEqualizer": { "message": "Égaliseur" }, "miniplayerVolume": { "message": "Volume" },
"miniplayerOpenList": { "message": "Ouvrir la liste" }, "miniplayerShuffle": { "message": "Aléatoire" },
"eqTitle": { "message": "Égaliseur graphique" }, "miniplayerEqualizer": { "message": "Égaliseur" },
"eqPresetsLabel": { "message": "Préréglages" }, "miniplayerOpenList": { "message": "Ouvrir la liste" },
"eqPresetFlat": { "message": "Plat" }, "eqTitle": { "message": "Égaliseur Graphique" },
"eqPresetRock": { "message": "Rock" }, "eqPresetsLabel": { "message": "Préréglages" },
"eqPresetPop": { "message": "Pop" }, "eqPresetFlat": { "message": "Plat" },
"eqPresetJazz": { "message": "Jazz" }, "eqPresetRock": { "message": "Rock" },
"eqPresetClassical": { "message": "Classique" }, "eqPresetPop": { "message": "Pop" },
"eqPresetBassBoost": { "message": "Amplification des basses" }, "eqPresetJazz": { "message": "Jazz" },
"eqPreampLabel": { "message": "Préampli" }, "eqPresetClassical": { "message": "Classique" },
"infoModalTitle": { "message": "Informations" }, "eqPresetBassBoost": { "message": "Renforcement des Basses" },
"infoModalFieldTitle": { "message": "Titre :" }, "eqPreampLabel": { "message": "Préamplificateur" },
"infoModalFieldArtist": { "message": "Artiste :" }, "infoModalTitle": { "message": "Information" },
"infoModalFieldAlbum": { "message": "Album :" }, "infoModalFieldTitle": { "message": "Titre :" },
"infoModalFieldSong": { "message": "Chanson :" }, "infoModalFieldArtist": { "message": "Artiste :" },
"infoModalFieldYear": { "message": "Année :" }, "infoModalFieldAlbum": { "message": "Album :" },
"infoModalFieldGenre": { "message": "Genre :" }, "infoModalFieldSong": { "message": "Chanson :" },
"lang_en": { "message": "Anglais" }, "infoModalFieldYear": { "message": "Année :" },
"lang_es": { "message": "Espagnol" }, "infoModalFieldGenre": { "message": "Genre :" },
"lang_fr": { "message": "Français" }, "lang_en": { "message": "Anglais" },
"lang_de": { "message": "Allemand" }, "lang_es": { "message": "Espagnol" },
"lang_it": { "message": "Italien" }, "lang_fr": { "message": "Français" },
"lang_pt": { "message": "Portugais" }, "lang_de": { "message": "Allemand" },
"essentialFeaturesNotSupported": { "message": "Votre navigateur ne prend pas en charge les fonctionnalités essentielles." }, "lang_it": { "message": "Italien" },
"dbAccessError": { "message": "Erreur d'accès à la base de données locale." }, "lang_pt": { "message": "Portugais" },
"dbUpdateNeeded": { "message": "La base de données doit être mise à jour, veuillez recharger la page." }, "essentialFeaturesNotSupported": { "message": "Votre navigateur ne prend pas en charge les fonctionnalités essentielles." },
"dbBlocked": { "message": "Veuillez fermer les autres onglets de cette application pour continuer." }, "dbAccessError": { "message": "Erreur d'accès à la base de données locale." },
"deletingContentData": { "message": "Suppression des données de contenu local..." }, "dbUpdateNeeded": { "message": "La base de données doit être mise à jour, veuillez recharger la page." },
"noContentDataToDelete": { "message": "Aucune donnée de contenu à supprimer." }, "dbBlocked": { "message": "Veuillez fermer les autres onglets de cette application pour continuer." },
"contentDataDeleted": { "message": "Données de contenu supprimées d'IndexedDB." }, "deletingContentData": { "message": "Suppression des données de contenu locales..." },
"errorDeletingData": { "message": "Erreur lors de la suppression des données : $message$", "placeholders": { "message": { "content": "$1" } } }, "noContentDataToDelete": { "message": "Aucune donnée de contenu à supprimer." },
"aceEditorNotAvailable": { "message": "Éditeur de texte non disponible." }, "contentDataDeleted": { "message": "Données de contenu supprimées d'IndexedDB." },
"errorLoadingTokens": { "message": "Erreur lors du chargement des jetons pour modification." }, "errorDeletingData": { "message": "Erreur lors de la suppression des données : $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingTokensMessage": { "message": "Erreur lors du chargement des jetons : $message$", "placeholders": { "message": { "content": "$1" } } }, "aceEditorNotAvailable": { "message": "Éditeur de texte non disponible." },
"aceEditorNotAvailableToSave": { "message": "Éditeur non disponible pour l'enregistrement." }, "errorLoadingTokens": { "message": "Erreur lors du chargement des tokens pour l'édition." },
"invalidJsonFormat": { "message": "Format JSON non valide. Il doit être { \"tokens\": [...] }" }, "errorLoadingTokensMessage": { "message": "Erreur lors du chargement des tokens : $message$", "placeholders": { "message": { "content": "$1" } } },
"tokensSaved": { "message": "Jetons enregistrés avec succès." }, "aceEditorNotAvailableToSave": { "message": "Éditeur non disponible pour l'enregistrement." },
"errorSavingTokens": { "message": "Erreur lors de l'enregistrement des jetons : $message$", "placeholders": { "message": { "content": "$1" } } }, "invalidJsonFormat": { "message": "Format JSON invalide. Il doit être { \"tokens\": [...] }" },
"dbNotAvailable": { "message": "IndexedDB n'est pas disponible." }, "tokensSaved": { "message": "Tokens enregistrés avec succès." },
"dbExported": { "message": "Base de données exportée avec succès." }, "errorSavingTokens": { "message": "Erreur lors de l'enregistrement des tokens : $message$", "placeholders": { "message": { "content": "$1" } } },
"errorExportingDb": { "message": "Erreur lors de l'exportation de la base de données : $message$", "placeholders": { "message": { "content": "$1" } } }, "dbNotAvailable": { "message": "IndexedDB n'est pas disponible." },
"invalidJsonFile": { "message": "Le fichier ne contient pas d'objet JSON valide." }, "dbExported": { "message": "Base de données exportée avec succès." },
"noDataToImport": { "message": "Le fichier ne contient aucune donnée pour les sections actuelles de la base de données." }, "errorExportingDb": { "message": "Erreur lors de l'exportation de la base de données : $message$", "placeholders": { "message": { "content": "$1" } } },
"dbImported": { "message": "Base de données importée avec succès." }, "invalidJsonFile": { "message": "Le fichier ne contient pas d'objet JSON valide." },
"errorImportingDb": { "message": "Erreur lors de l'importation de la base de données : $message$", "placeholders": { "message": { "content": "$1" } } }, "noDataToImport": { "message": "Le fichier ne contient pas de données pour les sections actuelles de la BD." },
"updatingView": { "message": "Mise à jour de la vue avec les nouvelles données..." }, "dbImported": { "message": "Base de données importée avec succès." },
"confirmClearContent": { "message": "Êtes-vous sûr de vouloir supprimer les données de contenu local (films, séries, musique, etc.) ? Les favoris et les paramètres ne seront PAS supprimés." }, "errorImportingDb": { "message": "Erreur lors de l'importation de la base de données : $message$", "placeholders": { "message": { "content": "$1" } } },
"trailerNotFound": { "message": "Aucune bande-annonce trouvée pour ce titre." }, "updatingView": { "message": "Mise à jour de la vue avec les nouvelles données..." },
"confirmClearHistory": { "message": "Êtes-vous sûr de vouloir effacer tout votre historique de visionnage ? Cette action est irréversible." }, "confirmClearContent": { "message": "Êtes-vous sûr de vouloir supprimer les données de contenu locales (Films, Séries, Musique, etc.) ? Les Favoris et les Paramètres ne seront PAS supprimés." },
"historyCleared": { "message": "Historique de visionnage effacé." }, "trailerNotFound": { "message": "Aucune bande-annonce trouvée pour ce titre." },
"historyItemDeleted": { "message": "Élément supprimé de l'historique." }, "confirmClearHistory": { "message": "Êtes-vous sûr de vouloir supprimer tout votre historique de visionnage ? Cette action est irréversible." },
"errorGeneratingScript": { "message": "Générez d'abord un script pour pouvoir le copier." }, "historyCleared": { "message": "Historique de visionnage effacé." },
"scriptCopied": { "message": "Script PHP copié dans le presse-papiers." }, "historyItemDeleted": { "message": "Élément supprimé de l'historique." },
"errorCopyingScript": { "message": "Erreur lors de la copie du script." }, "errorGeneratingScript": { "message": "Générez d'abord un script pour pouvoir le copier." },
"scriptGenerated": { "message": "Script PHP généré." }, "scriptCopied": { "message": "Script PHP copié dans le presse-papiers." },
"errorLoadingAlbum": { "message": "Erreur lors du chargement de l'album : $message$", "placeholders": { "message": { "content": "$1" } } }, "errorCopyingScript": { "message": "Erreur lors de la copie du script." },
"noPhotoServerSelected": { "message": "Erreur : Aucun serveur photo n'a été sélectionné." }, "scriptGenerated": { "message": "Script PHP généré." },
"loadingGenres": { "message": "Chargement des genres..." }, "errorLoadingAlbum": { "message": "Erreur lors du chargement de l'album : $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingGenres": { "message": "Erreur de chargement" }, "noPhotoServerSelected": { "message": "Erreur : Aucun serveur de photos n'a été sélectionné." },
"noContentFound": { "message": "Aucun résultat trouvé." }, "loadingGenres": { "message": "Chargement des genres..." },
"couldNotLoadContent": { "message": "Impossible de charger le contenu." }, "errorLoadingGenres": { "message": "Erreur de chargement" },
"noFavorites": { "message": "Vous n'avez pas encore de favoris." }, "noContentFound": { "message": "Aucun résultat trouvé." },
"errorLoadingFavorites": { "message": "Erreur lors du chargement des favoris." }, "couldNotLoadContent": { "message": "Impossible de charger le contenu." },
"historyEmpty": { "message": "Votre historique est vide." }, "noFavorites": { "message": "Vous n'avez pas encore de favoris." },
"historyEmptySub": { "message": "Explorez et regardez du contenu pour qu'il apparaisse ici." }, "errorLoadingFavorites": { "message": "Erreur lors du chargement des favoris." },
"errorGeneratingRecommendations": { "message": "Erreur lors de la génération des recommandations." }, "historyEmpty": { "message": "Votre historique est vide." },
"noRecommendations": { "message": "Nous avons besoin de mieux vous connaître pour vous faire des recommandations !" }, "historyEmptySub": { "message": "Explorez et regardez du contenu pour qu'il apparaisse ici." },
"errorGeneratingStats": { "message": "Erreur lors de la génération des statistiques." }, "errorGeneratingRecommendations": { "message": "Erreur lors de la génération des recommandations." },
"noServersForToken": { "message": "Aucun serveur associé trouvé pour ce jeton." }, "noRecommendations": { "message": "Nous avons besoin de mieux vous connaître pour vous faire des recommandations !" },
"searchingActorContent": { "message": "Recherche de contenu de $actorName$", "placeholders": { "actorName": { "content": "$1" } } }, "errorGeneratingStats": { "message": "Erreur lors de la génération des statistiques." },
"errorLoadingActorContent": { "message": "Impossible de charger le contenu pour $actorName$.", "placeholders": { "actorName": { "content": "$1" } } }, "noServersForToken": { "message": "Aucun serveur associé trouvé pour ce token." },
"errorAddingStream": { "message": "Erreur lors de l'ajout de flux : $message$", "placeholders": { "message": { "content": "$1" } } }, "searchingActorContent": { "message": "Recherche de contenu de $actorName$", "placeholders": { "actorName": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "L'URL du serveur PHP n'est pas configurée. Veuillez la configurer dans les paramètres." }, "errorLoadingActorContent": { "message": "Impossible de charger le contenu pour $actorName$.", "placeholders": { "actorName": { "content": "$1" } } },
"searchingStreams": { "message": "Recherche de flux pour \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "errorAddingStream": { "message": "Erreur lors de l'ajout du ou des streams : $message$", "placeholders": { "message": { "content": "$1" } } },
"sendingStreams": { "message": "Envoi de $count$ flux au serveur...", "placeholders": { "count": { "content": "$1" } } }, "phpUrlNotConfigured": { "message": "L'URL du serveur PHP n'est pas configurée. Veuillez la configurer dans les Paramètres." },
"streamAddedSuccess": { "message": "Flux ajouté(s) avec succès." }, "searchingStreams": { "message": "Recherche de streams pour \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingM3U": { "message": "Génération de M3U pour \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "sendingStreams": { "message": "Envoi de $count$ stream(s) au serveur...", "placeholders": { "count": { "content": "$1" } } },
"m3uDownloaded": { "message": "\"$title$\" téléchargé.", "placeholders": { "title": { "content": "$1" } } }, "streamAddedSuccess": { "message": "Stream(s) ajouté(s) avec succès." },
"errorGeneratingM3U": { "message": "Erreur lors de la génération de M3U : $message$", "placeholders": { "message": { "content": "$1" } } }, "generatingM3U": { "message": "Génération du M3U pour \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"settingsSavedSuccess": { "message": "Paramètres enregistrés avec succès." }, "m3uDownloaded": { "message": "\"$title$\" téléchargé.", "placeholders": { "title": { "content": "$1" } } },
"errorSavingSettings": { "message": "Erreur lors de l'enregistrement des paramètres dans la base de données." }, "errorGeneratingM3U": { "message": "Erreur lors de la génération du M3U : $message$", "placeholders": { "message": { "content": "$1" } } },
"languageChangeReload": { "message": "Langue modifiée. L'application va maintenant se recharger." }, "settingsSavedSuccess": { "message": "Paramètres enregistrés avec succès." },
"addedToFavorites": { "message": "Ajouté aux favoris." }, "errorSavingSettings": { "message": "Erreur lors de l'enregistrement des paramètres dans la base de données." },
"removedFromFavorites": { "message": "Supprimé des favoris." }, "languageChangeReload": { "message": "Langue modifiée. L'application va maintenant se recharger." },
"plexScanInProgress": { "message": "L'analyse Plex est déjà en cours." }, "addedToFavorites": { "message": "Ajouté aux favoris." },
"plexScanStarting": { "message": "Démarrage de l'analyse Plex..." }, "removedFromFavorites": { "message": "Retiré des favoris." },
"noPlexTokens": { "message": "Aucun jeton Plex configuré." }, "plexScanInProgress": { "message": "L'analyse Plex est déjà en cours." },
"clearingSections": { "message": "Effacement des sections : $sections$", "placeholders": { "sections": { "content": "$1" } } }, "plexScanStarting": { "message": "Démarrage de l'analyse Plex..." },
"initialScanPhaseComplete": { "message": "Phase d'analyse initiale terminée." }, "noPlexTokens": { "message": "Aucun token Plex configuré." },
"retryPhaseFinished": { "message": "Phase de nouvelle tentative terminée." }, "clearingSections": { "message": "Nettoyage des sections : $sections$", "placeholders": { "sections": { "content": "$1" } } },
"plexScanFinished": { "message": "Analyse terminée. Mise à jour du contenu..." }, "initialScanPhaseComplete": { "message": "Phase d'analyse initiale terminée." },
"scanCancelled": { "message": "Analyse annulée par l'utilisateur." }, "retryPhaseFinished": { "message": "Phase de nouvelle tentative terminée." },
"scanCancelledInfo": { "message": "Analyse annulée." }, "plexScanFinished": { "message": "Analyse terminée. Mise à jour du contenu..." },
"errorInitializingMusicPlayer": { "message": "Erreur lors de l'initialisation du lecteur de musique." }, "scanCancelled": { "message": "Analyse annulée par l'utilisateur." },
"criticalErrorLoadingMusic": { "message": "Erreur critique lors du chargement des données musicales." }, "scanCancelledInfo": { "message": "Analyse annulée." },
"errorLoadingArtists": { "message": "Erreur lors du chargement des artistes." }, "errorInitializingMusicPlayer": { "message": "Erreur lors de l'initialisation du lecteur de musique." },
"dbUnavailableError": { "message": "Erreur : Base de données non disponible." }, "criticalErrorLoadingMusic": { "message": "Erreur critique lors du chargement des données musicales." },
"updatingMusicData": { "message": "Mise à jour des données musicales..." }, "errorLoadingArtists": { "message": "Erreur lors du chargement des artistes." },
"musicDataUpdated": { "message": "Données musicales mises à jour." }, "dbUnavailableError": { "message": "Erreur : Base de données non disponible." },
"errorFetchingArtistSongs": { "message": "Erreur lors de la récupération des chansons de l'artiste." }, "updatingMusicData": { "message": "Mise à jour des données musicales..." },
"errorLoadingSongs": { "message": "Erreur lors du chargement des chansons." }, "musicDataUpdated": { "message": "Données musicales mises à jour." },
"noArtistsFound": { "message": "Aucun artiste trouvé." }, "errorFetchingArtistSongs": { "message": "Erreur lors de la récupération des chansons de l'artiste." },
"shuffleOn": { "message": "Mode aléatoire activé." }, "errorLoadingSongs": { "message": "Erreur lors du chargement des chansons." },
"shuffleOff": { "message": "Mode aléatoire désactivé." }, "noArtistsFound": { "message": "Aucun artiste trouvé." },
"playbackError": { "message": "Erreur de lecture" }, "shuffleOn": { "message": "Mode aléatoire activé." },
"errorLabel": { "message": "Erreur" }, "shuffleOff": { "message": "Mode aléatoire désactivé." },
"reloadingPage": { "message": "Rechargement de la page..." }, "playbackError": { "message": "Erreur de lecture" },
"viewed": { "message": "Vu" }, "errorLabel": { "message": "Erreur" },
"local": { "message": "Local" }, "reloadingPage": { "message": "Rechargement de la page..." },
"topRatedSort": {"message": "Les mieux notés"}, "viewed": { "message": "Vu" },
"recentSort": {"message": "Récents"}, "local": { "message": "Local" },
"popularSort": {"message": "Populaires"}, "topRatedSort": {"message": "Mieux notés"},
"moviesSectionTitle": {"message": "Films"}, "recentSort": {"message": "Récents"},
"seriesSectionTitle": {"message": "Séries"}, "popularSort": {"message": "Populaires"},
"searchResultsFor": {"message": "Résultats pour \"$query$\"", "placeholders": {"query": {"content": "$1"}}}, "moviesSectionTitle": {"message": "Films"},
"contentFrom": {"message": "Contenu de $actor$", "placeholders": {"actor": {"content": "$1"}}}, "seriesSectionTitle": {"message": "Séries"},
"explore": {"message": "Explorer"}, "searchResultsFor": {"message": "Résultats pour \"$query$\"", "placeholders": {"query": {"content": "$1"}}},
"noGenre": {"message": "Non classé"}, "contentFrom": {"message": "Contenu de $actor$", "placeholders": {"actor": {"content": "$1"}}},
"synopsis": {"message": "Synopsis"}, "explore": {"message": "Explorer"},
"noSynopsis": {"message": "Aucun synopsis disponible."}, "noGenre": {"message": "Sans catégorie"},
"director": {"message": "Réalisateur :"}, "synopsis": {"message": "Synopsis"},
"writer": {"message": "Scénariste :"}, "noSynopsis": {"message": "Aucun synopsis disponible."},
"viewOnImdb": {"message": "Voir sur IMDb"}, "director": {"message": "Réalisateur :"},
"watchTrailer": {"message": "Regarder la bande-annonce"}, "writer": {"message": "Scénariste :"},
"addToFavorites": {"message": "Ajouter aux favoris"}, "viewOnImdb": {"message": "Voir sur IMDb"},
"removeFromFavorites": {"message": "Retirer des favoris"}, "watchTrailer": {"message": "Bande-annonce"},
"notAvailable": {"message": "Non disponible"}, "addToFavorites": {"message": "Ajouter aux favoris"},
"mainCast": {"message": "Distribution principale"}, "removeFromFavorites": {"message": "Retirer des favoris"},
"seasonsAndEpisodes": {"message": "Saisons et épisodes"}, "notAvailable": {"message": "Non disponible"},
"similarContent": {"message": "Contenu similaire"}, "mainCast": {"message": "Distribution Principale"},
"filmography": {"message": "Filmographie"}, "seasonsAndEpisodes": {"message": "Saisons et Épisodes"},
"availableOn": {"message": "Disponible sur"}, "similarContent": {"message": "Contenu Similaire"},
"episodesCount": {"message": "$count$ épisodes", "placeholders": {"count": {"content": "$1"}}}, "filmography": {"message": "Filmographie"},
"seasonsCount": {"message": "$count$ saisons", "placeholders": {"count": {"content": "$1"}}}, "availableOn": {"message": "Disponible sur"},
"runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}}, "episodesCount": {"message": "$count$ Épisodes", "placeholders": {"count": {"content": "$1"}}},
"noTrailerFound": {"message": "Aucune bande-annonce trouvée pour ce titre."}, "seasonsCount": {"message": "$count$ Saisons", "placeholders": {"count": {"content": "$1"}}},
"fatalInitError": {"message": "Erreur d'initialisation fatale"}, "runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}},
"fatalInitErrorSub": {"message": "Impossible de charger l'application."}, "noTrailerFound": {"message": "Aucune bande-annonce trouvée pour ce titre."},
"invalidStreamInfo": {"message": "Informations non valides."}, "fatalInitError": {"message": "Erreur fatale d'initialisation"},
"dbUnavailableForStreams": {"message": "Base de données locale non disponible."}, "fatalInitErrorSub": {"message": "Impossible de charger l'application."},
"noPlexServersForStreams": {"message": "Aucun serveur Plex."}, "invalidStreamInfo": {"message": "Informations de stream invalides."},
"notFoundOnServers": {"message": "\"$query$\" introuvable sur les serveurs Plex.", "placeholders": {"query": {"content": "$1"}}}, "dbUnavailableForStreams": {"message": "Base de données locale non disponible."},
"relativeTime_justNow": { "message": "À l'instant" }, "noPlexServersForStreams": {"message": "Aucun serveur Plex."},
"relativeTime_minutesAgo": { "message": "Il y a $count$ minutes", "placeholders": { "count": { "content": "$1" } } }, "notFoundOnServers": {"message": "\"$query$\" non trouvé sur les serveurs Plex.", "placeholders": {"query": {"content": "$1"}}},
"relativeTime_hoursAgo": { "message": "Il y a $count$ heures", "placeholders": { "count": { "content": "$1" } } }, "relativeTime_justNow": { "message": "À l'instant" },
"relativeTime_yesterday": { "message": "Hier" }, "relativeTime_minutesAgo": { "message": "Il y a $count$ minutes", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_daysAgo": { "message": "Il y a $count$ jours", "placeholders": { "count": { "content": "$1" } } }, "relativeTime_hoursAgo": { "message": "Il y a $count$ heures", "placeholders": { "count": { "content": "$1" } } },
"errorLoadingDetails": { "message": "Erreur lors du chargement des détails" }, "relativeTime_yesterday": { "message": "Hier" },
"errorLoadingLocalContent": { "message": "Erreur lors du chargement du contenu local." }, "relativeTime_daysAgo": { "message": "Il y a $count$ jours", "placeholders": { "count": { "content": "$1" } } },
"errorServerResponse": { "message": "Réponse du serveur infructueuse." }, "errorLoadingDetails": { "message": "Erreur de Chargement des Détails" },
"errorPlexApi": { "message": "Erreur $status$ de l'API Plex.", "placeholders": { "status": { "content": "$1" } } }, "errorLoadingLocalContent": { "message": "Erreur lors du chargement du contenu local." },
"errorParsingPlexXml": { "message": "Erreur lors de l'analyse du XML Plex." }, "errorServerResponse": { "message": "Réponse du serveur non réussie." },
"untitled": { "message": "Sans titre" }, "errorPlexApi": { "message": "Erreur $status$ de l'API Plex.", "placeholders": { "status": { "content": "$1" } } },
"itemCount": { "message": "$count$ éléments", "placeholders": { "count": { "content": "$1" } } }, "errorParsingPlexXml": { "message": "Erreur lors de l'analyse du XML de Plex." },
"noPhotoServers": { "message": "Aucun serveur photo" }, "untitled": { "message": "Sans titre" },
"jellyfinScanInProgress": { "message": "L'analyse Jellyfin est déjà en cours." }, "itemCount": { "message": "$count$ éléments", "placeholders": { "count": { "content": "$1" } } },
"jellyfinScanning": { "message": "Analyse de Jellyfin..." }, "noPhotoServers": { "message": "Aucun serveur de photos" },
"jellyfinMissingCredentials": { "message": "Veuillez compléter l'URL et le nom d'utilisateur de Jellyfin." }, "jellyfinScanInProgress": { "message": "L'analyse Jellyfin est déjà en cours." },
"jellyfinConnecting": { "message": "Connexion à Jellyfin à : $url$", "placeholders": { "url": { "content": "$1" } } }, "jellyfinScanning": { "message": "Analyse de Jellyfin..." },
"jellyfinAuthFailed": { "message": "Échec de l'authentification Jellyfin : $message$", "placeholders": { "message": { "content": "$1" } } }, "jellyfinMissingCredentials": { "message": "Veuillez compléter l'URL et le nom d'utilisateur de Jellyfin." },
"jellyfinAuthSuccess": { "message": "Authentification Jellyfin réussie." }, "jellyfinConnecting": { "message": "Connexion à Jellyfin sur : $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinFetchingLibraries": { "message": "Récupération des bibliothèques..." }, "jellyfinAuthFailed": { "message": "Authentification Jellyfin échouée : $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinFetchFailed": { "message": "Erreur lors de la récupération des bibliothèques : $message$", "placeholders": { "message": { "content": "$1" } } }, "jellyfinAuthSuccess": { "message": "Authentification Jellyfin réussie." },
"jellyfinNoMediaLibraries": { "message": "Aucune bibliothèque de films ou de séries trouvée dans Jellyfin." }, "jellyfinFetchingLibraries": { "message": "Récupération des bibliothèques..." },
"jellyfinLibrariesFound": { "message": "$count$ bibliothèque(s) multimédia(s) trouvée(s).", "placeholders": { "count": { "content": "$1" } } }, "jellyfinFetchFailed": { "message": "Erreur lors de la récupération des bibliothèques : $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Succès] '$libraryName' analysée, $count$ titres ajoutés.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } }, "jellyfinNoMediaLibraries": { "message": "Aucune bibliothèque de films ou de séries trouvée dans Jellyfin." },
"jellyfinLibraryScanFailed": { "message": "Erreur lors de l'analyse de la bibliothèque '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } }, "jellyfinLibrariesFound": { "message": "$count$ bibliothèque(s) de médias trouvée(s).", "placeholders": { "count": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Analyse Jellyfin terminée. $movies$ films et $series$ séries ajoutés.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } }, "jellyfinLibraryScanSuccess": { "message": "[Succès] '$libraryName' analysée, $count$ titres ajoutés.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Informations d'identification Jellyfin non configurées." }, "jellyfinLibraryScanFailed": { "message": "Erreur lors de l'analyse de la bibliothèque '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } },
"notFoundOnJellyfin": { "message": "\"$query$\" introuvable sur Jellyfin.", "placeholders": { "query": { "content": "$1" } } }, "jellyfinScanSuccess": { "message": "Analyse Jellyfin terminée. $movies$ films et $series$ séries ajoutés.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" introuvable sur aucun serveur.", "placeholders": { "query": { "content": "$1" } } }, "noJellyfinCredentials": { "message": "Identifiants Jellyfin non configurés." },
"localOnPlex": { "message": "Sur Plex" }, "notFoundOnJellyfin": { "message": "\"$query$\" non trouvé sur Jellyfin.", "placeholders": {"query": {"content": "$1"}}},
"searchOnPlex": { "message": "Rechercher sur Plex" }, "notFoundOnAnyServer": { "message": "\"$query$\" non trouvé sur aucun serveur.", "placeholders": {"query": {"content": "$1"}}},
"jellyfinTitle": { "message": "Contenu Jellyfin" }, "localOnPlex": { "message": "Sur Plex" },
"noJellyfinContent": { "message": "Aucun contenu Jellyfin trouvé." }, "searchOnPlex": { "message": "Chercher sur Plex" },
"noJellyfinContentSub": { "message": "Assurez-vous d'avoir analysé votre serveur Jellyfin dans les paramètres." }, "jellyfinTitle": { "message": "Contenu de Jellyfin" },
"activityViewerTitle": { "message": "Visionneuse d'activité du serveur" }, "noJellyfinContent": { "message": "Aucun contenu Jellyfin trouvé." },
"activitySelectServer": { "message": "Sélectionnez un serveur" }, "noJellyfinContentSub": { "message": "Assurez-vous d'avoir analysé votre serveur Jellyfin dans les paramètres." },
"activityCheckBtn": { "message": "Actualiser" }, "activityViewerTitle": { "message": "Visualiseur d'Activité du Serveur" },
"activityNoSessions": { "message": "Aucune session active sur ce serveur." }, "activitySelectServer": { "message": "Sélectionnez un serveur" },
"activitySessionUser": { "message": "Utilisateur" }, "activityCheckBtn": { "message": "Actualiser" },
"activitySessionDevice": { "message": "Appareil" }, "activityNoSessions": { "message": "Aucune session active sur ce serveur." },
"activitySessionContent": { "message": "Contenu" }, "activitySessionUser": { "message": "Utilisateur" },
"activitySessionState": { "message": "État" }, "activitySessionDevice": { "message": "Appareil" },
"activitySessionIdentifier": { "message": "Identifiant du client" }, "activitySessionContent": { "message": "Contenu" },
"activityCopyID": { "message": "Copier l'ID" }, "activitySessionState": { "message": "État" },
"activityError": { "message": "Impossible d'obtenir l'activité du serveur." }, "activitySessionIdentifier": { "message": "Identifiant du Client" },
"activityCopied": { "message": "Identifiant copié dans le presse-papiers !" }, "activityCopyID": { "message": "Copier l'ID" },
"activityCopyError": { "message": "Erreur lors de la copie de l'identifiant." }, "activityError": { "message": "Impossible d'obtenir l'activité du serveur." },
"noProvidersFound": { "message": "Aucun fournisseur trouvé." }, "activityCopied": { "message": "Identifiant copié dans le presse-papiers !" },
"availableOnPlex": { "message": "Disponible sur Plex" }, "activityCopyError": { "message": "Erreur lors de la copie de l'identifiant." },
"m3uGeneratorTitle": { "message": "Générateur de listes M3U" }, "noProvidersFound": { "message": "Aucun fournisseur trouvé." },
"selectAServer": { "message": "Sélectionnez un serveur..." }, "availableOnPlex": { "message": "Disponible sur Plex" },
"downloadM3u": { "message": "Télécharger M3U" }, "m3uGeneratorTitle": { "message": "Générateur de Listes M3U" },
"m3uGenerator": { "message": "Générateur M3U" }, "selectAServer": { "message": "Sélectionnez un serveur..." },
"selectLibraries": { "message": "Sélectionner les bibliothèques" }, "downloadM3u": { "message": "Télécharger M3U" },
"howToUse": { "message": "Comment utiliser" }, "m3uGenerator": { "message": "Générateur M3U" },
"m3uInstruction1": { "message": "Choisissez un serveur dans la liste." }, "selectLibraries": { "message": "Sélectionner les Bibliothèques" },
"m3uInstruction2": { "message": "Sélectionnez une ou plusieurs bibliothèques à inclure." }, "howToUse": { "message": "Comment Utiliser" },
"m3uInstruction3": { "message": "Cliquez sur le bouton de téléchargement." }, "m3uInstruction1": { "message": "Choisissez un serveur dans la liste." },
"m3uInstruction4": { "message": "Importez le fichier .m3u dans votre lecteur compatible." }, "m3uInstruction2": { "message": "Sélectionnez une ou plusieurs bibliothèques à inclure." },
"chatOpen": { "message": "Ouvrir le chat" }, "m3uInstruction3": { "message": "Cliquez sur le bouton de téléchargement." },
"chatTitle": { "message": "Assistant IA" }, "m3uInstruction4": { "message": "Importez le fichier .m3u dans votre lecteur compatible." },
"chatClose": { "message": "X" }, "chatOpen": { "message": "Ouvrir le Chat" },
"chatPlaceholder": { "message": "Saisissez votre message..." }, "chatTitle": { "message": "Assistant IA" },
"chatSend": { "message": "➤" }, "chatClose": { "message": "X" },
"chatWelcome": { "message": "Bienvenue ! Je suis votre assistant CinePlex. Posez-moi des questions sur les films, les séries ou tout ce que vous voulez savoir." }, "chatPlaceholder": { "message": "Écrivez votre message..." },
"chatGoogleApiKeyMissing": { "message": "La clé API Google Gemini n'est pas configurée. Veuillez la définir dans les paramètres de l'extension pour utiliser l'assistant IA." }, "chatSend": { "message": "➤" },
"chatApiInvalidResponse": { "message": "L'API a renvoyé une réponse non valide. Veuillez réessayer." }, "chatWelcome": { "message": "Bienvenue ! Je suis votre assistant CinePlex. Posez-moi des questions sur les films, les séries ou tout ce que vous voulez savoir." },
"chatApiError": { "message": "Erreur de communication avec l'assistant IA" }, "chatGoogleApiKeyMissing": { "message": "La clé de l'API Google Gemini n'est pas configurée. Veuillez la configurer dans les paramètres de l'extension pour utiliser l'assistant IA." },
"downloadAll": { "message": "Tout télécharger" }, "chatApiInvalidResponse": { "message": "L'API a renvoyé une réponse invalide. Veuillez réessayer." },
"download": { "message": "Télécharger" }, "chatApiError": { "message": "Erreur de communication avec l'assistant IA" },
"aiToolSearchLibraryDesc": { "message": "Recherche dans la bibliothèque Plex de l'utilisateur des films ou des séries par titre." }, "downloadAll": { "message": "Tout télécharger" },
"aiToolSearchLibraryQueryParamDesc": { "message": "Le titre du film ou de la série à rechercher." }, "download": { "message": "Télécharger" },
"aiToolSearchLibraryTypeParamDesc": { "message": "Le type de contenu à rechercher. Peut être 'movie' pour les films ou 'series' pour les séries. (Facultatif)." }, "aiToolSearchLibraryDesc": { "message": "Recherche dans la bibliothèque Plex de l'utilisateur des films ou des séries par titre." },
"aiToolSearchLibraryResolutionParamDesc": { "message": "La résolution vidéo à rechercher (par exemple, '4k', '1080p'). (Facultatif)." }, "aiToolSearchLibraryQueryParamDesc": { "message": "Le titre du film ou de la série à rechercher." },
"aiToolSearchLibraryContainerParamDesc": { "message": "Le format de conteneur vidéo à rechercher (par exemple, 'mkv', 'mp4'). (Facultatif)." }, "aiToolSearchLibraryTypeParamDesc": { "message": "Le type de contenu à rechercher. Peut être 'movie' pour les films ou 'series' pour les séries. (Optionnel)." },
"aiToolNavigateToPageDesc": { "message": "Dirige l'utilisateur vers une page spécifique de l'interface de l'application." }, "aiToolSearchLibraryResolutionParamDesc": { "message": "La résolution vidéo à rechercher (par ex., '4k', '1080p'). (Optionnel)." },
"aiToolNavigateToPagePageParamDesc": { "message": "Le nom de la page vers laquelle naviguer, par exemple : 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers' ou 'm3u-generator'." }, "aiToolSearchLibraryContainerParamDesc": { "message": "Le format de conteneur vidéo à rechercher (par ex., 'mkv', 'mp4'). (Optionnel)." },
"aiToolGetUserStatsDesc": { "message": "Récupère et affiche les statistiques de la bibliothèque de l'utilisateur, telles que le nombre total de films, de séries et d'artistes uniques." }, "aiToolNavigateToPageDesc": { "message": "Navigue l'utilisateur vers une page spécifique de l'interface de l'application." },
"aiToolShowItemDetailsDesc": { "message": "Affiche la page de détails d'un film ou d'une série spécifique par son titre et son type." }, "aiToolNavigateToPagePageParamDesc": { "message": "Le nom de la page vers laquelle naviguer, par ex. : 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', 'm3u-generator' ou 'music'." },
"aiToolShowItemDetailsTitleParamDesc": { "message": "Le titre exact du film ou de la série." }, "aiToolGetUserStatsDesc": { "message": "Obtient et affiche les statistiques de la bibliothèque de l'utilisateur, comme le nombre total de films, séries et artistes uniques." },
"aiToolShowItemDetailsTypeParamDesc": { "message": "Le type de contenu. Doit être 'movie' ou 'series'." }, "aiToolShowItemDetailsDesc": { "message": "Affiche la page de détails d'un film ou d'une série spécifique par son titre et son type." },
"aiToolAddToPlaylistDesc": { "message": "Ajoute un film ou une série à la liste de lecture actuelle de l'utilisateur pour le diffuser sur un serveur PHP configuré." }, "aiToolShowItemDetailsTitleParamDesc": { "message": "Le titre exact du film ou de la série." },
"aiToolAddToPlaylistTitleParamDesc": { "message": "Le titre du film ou de la série à ajouter." }, "aiToolShowItemDetailsTypeParamDesc": { "message": "Le type de contenu. Doit être 'movie' ou 'series'." },
"aiToolAddToPlaylistTypeParamDesc": { "message": "Le type de contenu. Doit être 'movie' ou 'series'." }, "aiToolAddToPlaylistDesc": { "message": "Ajoute un film ou une série à la liste de lecture actuelle de l'utilisateur pour la diffuser sur un serveur PHP configuré." },
"aiToolCheckAndDownloadDesc": { "message": "Vérifie la disponibilité d'une liste de titres de films ou de séries sur les serveurs locaux de l'utilisateur et, si trouvés, génère et télécharge un fichier de liste de lecture M3U avec les flux trouvés." }, "aiToolAddToPlaylistTitleParamDesc": { "message": "Le titre du film ou de la série à ajouter." },
"aiToolCheckAndDownloadTitlesParamDesc": { "message": "Un tableau de titres de films ou de séries à rechercher et à télécharger." }, "aiToolAddToPlaylistTypeParamDesc": { "message": "Le type de contenu. Doit être 'movie' ou 'series'." },
"aiToolCheckAndDownloadTypeParamDesc": { "message": "Le type de contenu de la liste. Doit être 'movie' ou 'series'." }, "aiToolDownloadSingleMovieM3UDesc": { "message": "Génère et télécharge un fichier de liste de lecture M3U pour un seul film disponible localement." },
"aiToolCheckAndDownloadFilenameParamDesc": { "message": "Le nom du fichier M3U à télécharger (par exemple, 'MaListe.m3u'). Si aucun nom n'est fourni, un nom par défaut sera utilisé." }, "aiToolDownloadSingleMovieM3UTitleParamDesc": { "message": "Le titre du film pour lequel le M3U sera généré." },
"aiToolToggleFavoriteDesc": { "message": "Ajoute ou supprime un film ou une série de la liste des favoris de l'utilisateur." }, "aiToolDownloadSingleMovieM3UYearParamDesc": { "message": "L'année de sortie du film (optionnel, pour plus de précision)." },
"aiToolToggleFavoriteTitleParamDesc": { "message": "Le titre du film ou de la série." }, "aiToolDownloadSeriesSeasonM3UDesc": { "message": "Génère et télécharge un fichier de liste de lecture M3U pour une saison spécifique d'une série disponible localement." },
"aiToolToggleFavoriteTypeParamDesc": { "message": "Le type de contenu. Doit être 'movie' ou 'series'." }, "aiToolDownloadSeriesSeasonM3UTitleParamDesc": { "message": "Le titre de la série." },
"aiToolGetRecommendationsDesc": { "message": "Génère et affiche une liste de recommandations de films ou de séries basées sur l'historique de visionnage et les favoris de l'utilisateur." }, "aiToolDownloadSeriesSeasonM3USeasonParamDesc": { "message": "Le numéro de la saison à télécharger." },
"aiToolApplyFiltersDesc": { "message": "Applique des filtres à la vue actuelle des films ou des séries, permettant d'affiner les résultats par type, genre, année et ordre de tri." }, "aiToolDownloadSeriesSeasonM3UYearParamDesc": { "message": "L'année de sortie de la série (optionnel)." },
"aiToolApplyFiltersTypeParamDesc": { "message": "Le type de contenu auquel appliquer les filtres. Doit être 'movie' ou 'series'." }, "aiToolCheckAndDownloadDesc": { "message": "Vérifie la disponibilité d'une liste de titres de films ou de séries sur les serveurs locaux de l'utilisateur et, s'ils sont trouvés, génère et télécharge un fichier de liste de lecture M3U avec les flux trouvés." },
"aiToolApplyFiltersGenreParamDesc": { "message": "Le nom du genre par lequel filtrer (par exemple, 'Action', 'Drame')." }, "aiToolCheckAndDownloadTitlesParamDesc": { "message": "Un tableau de titres de films ou de séries à rechercher et à télécharger." },
"aiToolApplyFiltersYearParamDesc": { "message": "L'année de sortie par laquelle filtrer (par exemple, '2023')." }, "aiToolCheckAndDownloadTypeParamDesc": { "message": "Le type de contenu de la liste. Doit être 'movie' ou 'series'." },
"aiToolApplyFiltersSortParamDesc": { "message": "Le critère de tri pour les résultats. Valeurs valides : 'popularity.desc' (populaires), 'vote_average.desc' (mieux notés), 'release_date.desc' (récents pour les films) ou 'first_air_date.desc' (récents pour les séries)." }, "aiToolCheckAndDownloadFilenameParamDesc": { "message": "Le nom du fichier M3U à télécharger (par ex., 'MaListe.m3u'). S'il n'est pas fourni, un nom par défaut sera utilisé." },
"aiToolPlayMusicByArtistDesc": { "message": "Ouvre le lecteur de musique et commence à jouer les chansons d'un artiste spécifique de la bibliothèque de l'utilisateur." }, "aiToolToggleFavoriteDesc": { "message": "Ajoute ou supprime un film ou une série de la liste des favoris de l'utilisateur." },
"aiToolPlayMusicByArtistNameParamDesc": { "message": "Le nom exact de l'artiste dont les chansons doivent être jouées." }, "aiToolToggleFavoriteTitleParamDesc": { "message": "Le titre du film ou de la série." },
"aiToolClearChatHistoryDesc": { "message": "Efface tout l'historique des messages de la conversation en cours avec l'assistant IA." }, "aiToolToggleFavoriteTypeParamDesc": { "message": "Le type de contenu. Doit être 'movie' ou 'series'." },
"aiToolDeleteDatabaseDesc": { "message": "Supprime l'intégralité de la base de données locale de l'extension, y compris le contenu analysé, les paramètres et les favoris. Cette action est irréversible et rechargera l'application." }, "aiToolGetRecommendationsDesc": { "message": "Génère et affiche une liste de recommandations de films ou de séries basées sur l'historique de visionnage et les favoris de l'utilisateur." },
"aiToolUpdateAllTokensDesc": { "message": "Lance une analyse complète de tous les serveurs et bibliothèques Plex associés aux jetons configurés dans l'extension. Met à jour tous les films, séries, artistes et photos." }, "aiToolApplyFiltersDesc": { "message": "Applique des filtres à la vue actuelle des films ou des séries, permettant d'affiner les résultats par type, genre, année et ordre de tri." },
"aiToolAddPlexTokenDesc": { "message": "Ajoute un nouveau jeton X-Plex à la configuration de l'extension, permettant à l'application d'analyser le contenu de nouveaux serveurs Plex." }, "aiToolApplyFiltersTypeParamDesc": { "message": "Le type de contenu auquel appliquer les filtres. Doit être 'movie' ou 'series'." },
"aiToolAddPlexTokenTokenParamDesc": { "message": "La chaîne de jeton X-Plex à ajouter." }, "aiToolApplyFiltersGenreParamDesc": { "message": "Le nom du genre par lequel filtrer (par ex., 'Action', 'Drame')." },
"aiToolChangeRegionDesc": { "message": "Modifie la région utilisée pour la découverte de contenu dans l'API TMDB. Cela affectera les résultats affichés dans les sections des films et des séries, ainsi que les fournisseurs de streaming." }, "aiToolApplyFiltersYearParamDesc": { "message": "L'année de sortie par laquelle filtrer (par ex., '2023')." },
"aiToolChangeRegionRegionParamDesc": { "message": "Le code de pays ISO 3166-1 à deux lettres pour la nouvelle région (par exemple, 'US' pour les États-Unis, 'ES' pour l'Espagne, 'MX' pour le Mexique)." }, "aiToolApplyFiltersSortParamDesc": { "message": "Le critère de tri pour les résultats. Valeurs valides : 'popularity.desc' (populaires), 'vote_average.desc' (mieux notés), 'release_date.desc' (récents pour les films) ou 'first_air_date.desc' (récents pour les séries)." },
"aiToolClearAllFavoritesDesc": { "message": "Supprime tous les films et séries que l'utilisateur a marqués comme favoris." }, "aiToolListAvailableMusicGenresDesc": { "message": "Liste tous les genres musicaux uniques disponibles dans la bibliothèque locale de l'utilisateur." },
"aiToolClearViewingHistoryDesc": { "message": "Efface l'historique de visionnage de l'utilisateur de la page d'historique." }, "aiToolSearchMusicByGenreDesc": { "message": "Recherche des artistes dans la bibliothèque musicale de l'utilisateur appartenant à un genre spécifique." },
"aiToolClearRecommendationsViewDesc": { "message": "Efface la vue des recommandations et supprime les recommandations mises en cache." }, "aiToolSearchMusicByGenreNameParamDesc": { "message": "Le nom du genre musical à rechercher (ex. 'Rock', 'Pop', 'Jazz')." },
"aiToolSearchNotFound": { "message": "'$query' introuvable dans votre bibliothèque.", "placeholders": { "query": { "content": "$1" } } }, "aiToolPlayMusicByArtistDesc": { "message": "Ouvre le lecteur de musique et commence à jouer les chansons d'un artiste spécifique de la bibliothèque de l'utilisateur." },
"aiToolNavigateSuccess": { "message": "Navigation vers la page $page$.", "placeholders": { "page": { "content": "$1" } } }, "aiToolPlayMusicByArtistNameParamDesc": { "message": "Le nom exact de l'artiste dont les chansons doivent être jouées." },
"aiToolNavigateError": { "message": "Erreur lors de la navigation vers la page $page$.", "placeholders": { "page": { "content": "$1" } } }, "aiToolClearChatHistoryDesc": { "message": "Efface tout l'historique des messages de la conversation actuelle avec l'assistant IA." },
"aiToolStatsError": { "message": "Erreur lors de l'obtention des statistiques." }, "aiToolDeleteDatabaseDesc": { "message": "Supprime toute la base de données locale de l'extension, y compris le contenu analysé, les paramètres et les favoris. Cette action est irréversible et rechargera l'application." },
"aiToolItemNotFound": { "message": "Élément '$title' introuvable.", "placeholders": { "title": { "content": "$1" } } }, "aiToolUpdateAllTokensDesc": { "message": "Lance une analyse complète de tous les serveurs et bibliothèques Plex associés aux tokens configurés dans l'extension. Met à jour tous les films, séries, artistes et photos." },
"aiToolShowItemDetailsSuccess": { "message": "Affichage des détails de '$title'.", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenDesc": { "message": "Ajoute un nouveau token X-Plex à la configuration de l'extension, permettant à l'application d'analyser le contenu de nouveaux serveurs Plex." },
"aiToolAddToPlaylistSuccess": { "message": "'$title' ajouté à la liste de lecture.", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenTokenParamDesc": { "message": "La chaîne du token X-Plex à ajouter." },
"aiToolFavoriteAdded": { "message": "'$title' ajouté aux favoris.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionDesc": { "message": "Modifie la région utilisée pour la découverte de contenu dans l'API de TMDB. Cela affectera les résultats affichés dans les sections des films et des séries, ainsi que les fournisseurs de streaming." },
"aiToolFavoriteRemoved": { "message": "'$title' supprimé des favoris.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionRegionParamDesc": { "message": "Le code de pays ISO 3166-1 à deux lettres pour la nouvelle région (par ex., 'US' pour les États-Unis, 'FR' pour la France, 'CA' pour le Canada)." },
"aiToolRecommendationsSuccess": { "message": "Affichage des recommandations." }, "aiToolClearAllFavoritesDesc": { "message": "Supprime tous les films et séries que l'utilisateur a marqués comme favoris." },
"aiToolApplyFiltersGenreNotFound": { "message": "Genre '$genre' introuvable.", "placeholders": { "genre": { "content": "$1" } } }, "aiToolClearViewingHistoryDesc": { "message": "Efface l'historique de visionnage de l'utilisateur de la page d'historique." },
"aiToolApplyFiltersSuccess": { "message": "Filtres appliqués avec succès." }, "aiToolClearRecommendationsViewDesc": { "message": "Nettoie la vue des recommandations et supprime les recommandations mises en cache." },
"aiToolPlayMusicNotReady": { "message": "Le lecteur de musique n'est pas prêt. Assurez-vous que votre bibliothèque musicale Plex a été analysée." }, "aiToolSearchNotFound": { "message": "Impossible de trouver '$query' dans votre bibliothèque.", "placeholders": { "query": { "content": "$1" } } },
"aiToolPlayMusicArtistNotFound": { "message": "Artiste '$artist_name' introuvable.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolNavigateSuccess": { "message": "Navigué vers la page $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolPlayMusicNoSongs": { "message": "Aucune chanson trouvée pour '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolNavigateError": { "message": "Erreur de navigation vers la page $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolPlayMusicSuccess": { "message": "Lecture de la musique de '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolStatsError": { "message": "Erreur lors de l'obtention des statistiques." },
"aiToolChatHistoryCleared": { "message": "Historique du chat effacé." }, "aiToolItemNotFound": { "message": "Élément '$title' non trouvé.", "placeholders": { "title": { "content": "$1" } } },
"aiToolConfirmDeleteDatabase": { "message": "Êtes-vous sûr de vouloir supprimer la base de données locale ? Cette action est irréversible." }, "aiToolShowItemDetailsSuccess": { "message": "Affichage des détails de '$title'.", "placeholders": { "title": { "content": "$1" } } },
"aiToolDeleteDatabaseCancelled": { "message": "Suppression de la base de données annulée." }, "aiToolAddToPlaylistSuccess": { "message": "Ajouté '$title' à la liste de lecture.", "placeholders": { "title": { "content": "$1" } } },
"aiToolExecutionError": { "message": "Erreur lors de l'exécution de l'outil '$toolName' : $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolFavoriteAdded": { "message": "Ajouté '$title' aux favoris.", "placeholders": { "title": { "content": "$1" } } },
"aiToolUnknown": { "message": "Outil inconnu : '$toolName'.", "placeholders": { "toolName": { "content": "$1" } } }, "aiToolFavoriteRemoved": { "message": "Supprimé '$title' des favoris.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoritesCleared": { "message": "Favoris effacés." }, "aiToolRecommendationsSuccess": { "message": "Affichage des recommandations." },
"aiToolFavoritesClearError": { "message": "Erreur lors de l'effacement des favoris : $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolApplyFiltersGenreNotFound": { "message": "Genre '$genre' non trouvé.", "placeholders": { "genre": { "content": "$1" } } },
"aiToolRecommendationsCleared": { "message": "Recommandations effacées." }, "aiToolApplyFiltersSuccess": { "message": "Filtres appliqués avec succès." },
"aiToolRecommendationsClearError": { "message": "Erreur lors de l'effacement des recommandations : $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolSearchMusicByGenreNotFound": { "message": "Je n'ai pas trouvé d'artistes du genre '$genre_name' dans votre bibliothèque.", "placeholders": { "genre_name": { "content": "$1" } } },
"aiToolDatabaseDeleted": { "message": "Base de données supprimée. La page va être rechargée." }, "aiToolPlayMusicNotReady": { "message": "Le lecteur de musique n'est pas prêt. Assurez-vous que votre bibliothèque musicale Plex a été analysée." },
"aiToolDatabaseDeleteError": { "message": "Erreur lors de la suppression de la base de données : $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolPlayMusicArtistNotFound": { "message": "Artiste '$artist_name' non trouvé.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolDatabaseDeleteBlocked": { "message": "La suppression de la base de données est bloquée. Fermez les autres onglets de l'application." }, "aiToolPlayMusicNoSongs": { "message": "Aucune chanson trouvée pour '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolUpdateAllTokensSuccess": { "message": "Tous les jetons ont été mis à jour avec succès." }, "aiToolPlayMusicSuccess": { "message": "Lecture de la musique de '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolUpdateAllTokensError": { "message": "Erreur lors de la mise à jour des jetons : $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolChatHistoryCleared": { "message": "Historique du chat effacé." },
"aiToolAddPlexTokenSuccess": { "message": "Jeton Plex ajouté avec succès." }, "aiToolConfirmDeleteDatabase": { "message": "Êtes-vous sûr de vouloir supprimer la base de données locale ? Cette action est irréversible." },
"aiToolAddPlexTokenError": { "message": "Erreur lors de l'ajout du jeton Plex : $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolDeleteDatabaseCancelled": { "message": "Suppression de la base de données annulée." },
"aiToolChangeRegionSuccess": { "message": "Région modifiée en $region$. Le contenu est en cours de mise à jour.", "placeholders": { "region": { "content": "$1" } } }, "aiToolExecutionError": { "message": "Erreur lors de l'exécution de l'outil '$toolName' : $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } },
"aiToolChangeRegionError": { "message": "Erreur lors du changement de région : $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolUnknown": { "message": "Outil inconnu : '$toolName'.", "placeholders": { "toolName": { "content": "$1" } } },
"aiToolViewingHistoryCleared": { "message": "Historique de visionnage effacé." }, "aiToolFavoritesCleared": { "message": "Favoris supprimés." },
"aiToolViewingHistoryClearError": { "message": "Erreur lors de l'effacement de l'historique de visionnage : $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolFavoritesClearError": { "message": "Erreur lors de la suppression des favoris : $message$", "placeholders": { "message": { "content": "$1" } } },
"aiSystemPrompt_v3": { "message": "Vous êtes un assistant expert en films et séries appelé CinePlex. Votre fonction principale est d'aider les utilisateurs à découvrir du contenu et à interagir avec leur bibliothèque. Suivez rigoureusement ces règles : 1. **N'INVENTEZ JAMAIS** avoir effectué une action si vous n'avez pas utilisé d'outil pour cela. Par exemple, ne dites pas 'J'ai téléchargé X' si vous n'avez pas utilisé l'outil de téléchargement. 2. Pour les demandes de recommandations ou de listes (par exemple, 'donnez-moi 5 films d'horreur'), utilisez vos propres connaissances pour générer la liste. Présentez-la sous forme de liste numérotée ou à puces. Après avoir affiché la liste, demandez de manière proactive à l'utilisateur s'il souhaite que vous vérifiiez la disponibilité sur ses serveurs locaux et que vous créiez un fichier M3U. 3. **UNIQUEMENT** si l'utilisateur confirme qu'il souhaite vérifier ou télécharger la liste, utilisez l'outil `check_and_download_titles_list`. Ne l'utilisez pas sans confirmation explicite. 4. Pour toute autre action telle que la navigation, l'obtention de statistiques, la recherche d'un titre spécifique ou le filtrage par résolution ou conteneur, utilisez les outils appropriés. Soyez toujours concis, amical et efficace." }, "aiToolRecommendationsCleared": { "message": "Recommandations supprimées." },
"aiToolM3UNoTitlesProvided": { "message": "Veuillez fournir une liste de titres pour créer la liste de lecture." }, "aiToolRecommendationsClearError": { "message": "Erreur lors de la suppression des recommandations : $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolM3UCheckingTitles": { "message": "Vérification des titres sur vos serveurs locaux..." }, "aiToolDatabaseDeleted": { "message": "Base de données supprimée. La page va se recharger." },
"aiToolM3UNoLocalMatchesForDownload": { "message": "Je n'ai trouvé aucun des films ou séries de la liste sur vos serveveurs locaux." }, "aiToolDatabaseDeleteError": { "message": "Erreur lors de la suppression de la base de données : $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolM3UDownloadStarted": { "message": "Terminé ! J'ai trouvé $1 des $2 titres sur vos serveurs et j'ai lancé le téléchargement de la liste de lecture M3U.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } }, "aiToolDatabaseDeleteBlocked": { "message": "La suppression de la base de données est bloquée. Veuillez fermer les autres onglets de l'application." },
"backToProviders": { "message": "Retour aux fournisseurs" }, "aiToolUpdateAllTokensSuccess": { "message": "Tous les tokens ont été mis à jour avec succès." },
"artistsCounterSingle": { "message": "$total$ artiste", "placeholders": { "total": { "content": "$1" } } }, "aiToolUpdateAllTokensError": { "message": "Erreur lors de la mise à jour des tokens : $message$", "placeholders": { "message": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Chargement..." }, "aiToolAddPlexTokenSuccess": { "message": "Token Plex ajouté avec succès." },
"downloadingSong": { "message": "Début du téléchargement de \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenError": { "message": "Erreur lors de l'ajout du token Plex : $message$", "placeholders": { "message": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" téléchargé.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionSuccess": { "message": "Région changée en $region$. Le contenu est en cours de mise à jour.", "placeholders": { "region": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Erreur lors du téléchargement de \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionError": { "message": "Erreur lors du changement de région : $message$", "placeholders": { "message": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Génération de M3U pour \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } }, "aiToolViewingHistoryCleared": { "message": "Historique de visionnage effacé." },
"albumM3UGenerated": { "message": "M3U pour l'album \"$artist$\" généré.", "placeholders": { "artist": { "content": "$1" } } }, "aiToolViewingHistoryClearError": { "message": "Erreur lors de l'effacement de l'historique de visionnage : $message$", "placeholders": { "message": { "content": "$1" } } },
"retyingSection": { "message": "Nouvelle tentative de la section \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolM3UDownloadStartedSingle": { "message": "Démarrage du téléchargement du M3U pour '$movie_title'.", "placeholders": { "movie_title": { "content": "$1" } } },
"retrySuccess": { "message": "[SUCCÈS] Nouvelle tentative pour \"$title$\" terminée.", "placeholders": { "title": { "content": "$1" } } }, "aiToolM3UDownloadStartedSeason": { "message": "Démarrage du téléchargement du M3U pour la saison $1 de '$2'.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } },
"retryError": { "message": "[ERREUR FINALE] Échec de la nouvelle tentative pour \"$title$\" : $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolM3UNoTitlesProvided": { "message": "Veuillez fournir une liste de titres pour créer la liste de lecture." },
"startingRetryPhase": { "message": "Démarrage de la phase de nouvelle tentative pour $count$ sections...", "placeholders": { "count": { "content": "$1" } } }, "aiToolM3UCheckingTitles": { "message": "Vérification des titres sur vos serveurs locaux..." },
"tokenFoundServers": { "message": "Jeton $token$... a trouvé $count$ serveurs.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } }, "aiToolM3UNoLocalMatchesForDownload": { "message": "Je n'ai trouvé aucun des films ou séries de la liste sur vos serveurs locaux." },
"errorProcessingToken": { "message": "Erreur lors du traitement du jeton $token$... : $message$", "placeholders": { "token": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolM3UDownloadStarted": { "message": "Terminé ! J'ai trouvé $1 des $2 titres sur vos serveurs et j'ai commencé le téléchargement de la liste de lecture M3U.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } },
"plexScanFatalError": { "message": "ERREUR FATALE : $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolTrailerNotFoundSpecific": { "message": "Désolé, je n'ai pas pu trouver de bande-annonce disponible pour '$title'.", "placeholders": { "title": { "content": "$1" } } },
"errorDuringScan": { "message": "Erreur pendant l'analyse : $message$", "placeholders": { "message": { "content": "$1" } } }, "aiSystemPrompt_v4": {
"stoppingPlexScan": { "message": "Arrêt de l'analyse Plex..." }, "message": "Vous êtes un assistant virtuel intégré à une extension Chrome qui interagit avec les serveurs Plex et Jellyfin. Votre fonction principale est d'aider l'utilisateur à rechercher, gérer, lire et télécharger du contenu multimédia, ainsi qu'à gérer des paramètres personnalisés.\n\nPRIORITÉ MAXIMALE : Chaque fois que la question de l'utilisateur concerne du contenu multimédia (films, séries, musique), vous DEVEZ supposer qu'elle se réfère à sa bibliothèque locale. Utilisez les outils pour rechercher dans sa base de données AVANT de chercher sur le web.\n\n🎯 Règles générales de comportement :\nRépondez toujours de manière claire, concise et directe. Soyez proactif et fournissez toutes les informations pertinentes en une seule fois pour éviter les questions de suivi. Par exemple, en confirmant la disponibilité d'une série, incluez les détails des saisons.\n\nComparez la date actuelle avec les résultats de recherche Google lorsque l'on vous demande des informations externes pour garantir qu'elles sont à jour.\n\nUtilisez les noms exacts des commandes définies dans le système (function.name) lors de l'appel d'outils.\n\n📦 Fonctions clés pour le contenu multimédia :\nPour générer un M3U pour un seul film, utilisez download_single_movie_m3u.\nPour télécharger une saison spécifique d'une série, utilisez download_series_season_m3u.\nPour plusieurs titres (films ou séries complètes), utilisez toujours check_and_download_titles_list.\nPour rechercher du contenu local : search_library.\nPour rechercher sur TMDB : search_tmdb_content.\nPour le contenu tendance : get_trending_content.\nPour afficher les détails d'un titre : show_item_details.\nPour ajouter à la liste de lecture PHP : add_to_playlist.\nPour vérifier la disponibilité locale : check_local_availability.\nSi une série est disponible localement, indiquez combien de saisons il y a et sur quels serveurs en utilisant get_local_series_seasons.\nPour voir les recommandations : get_recommendations.\nPour appliquer des filtres : apply_filters.\nPour voir l'historique ou les favoris : view_history, view_favorites.\nPour marquer comme favori : toggle_favorite.\nPour lire la bande-annonce : play_trailer.\n\n🎵 Fonctions musicales :\nSi l'utilisateur demande des recommandations de genres musicaux de manière générale (ex. 'recommande-moi un genre pour me remonter le moral'), utilisez d'abord list_available_music_genres pour voir quels genres il possède et basez votre recommandation sur cette liste.\nPour lister tous les genres musicaux disponibles dans la bibliothèque : list_available_music_genres.\nPour rechercher des artistes par genre : search_music_by_genre.\nPour lire des chansons par titre et/ou artiste : play_song.\nPour lire la musique d'un artiste : play_music_by_artist.\n\n🧰 Fonctions de gestion et de configuration :\nPour obtenir les statistiques de l'utilisateur : get_user_stats.\nPour naviguer vers des sections spécifiques : navigate_to_page.\nPour mettre à jour les tokens : update_all_tokens, add_plex_token.\nPour changer la région du contenu : change_region.\nPour exporter ou importer la base de données locale : export_local_database, import_local_database.\nPour supprimer la base de données : delete_database.\nPour effacer les favoris, l'historique ou les recommandations : clear_all_favorites, clear_viewing_history, clear_recommendations_view.\nPour basculer le mode clair/sombre : toggle_light_mode.\nPour afficher ou masquer la section héros : toggle_hero_section.\n\n⚠ Considérations supplémentaires :\nDonnez la priorité au contenu disponible localement. Utilisez check_local_availability avant d'afficher les options de lecture ou de téléchargement.\nSi une action échoue, signalez-le de manière claire et directe.\nÉvitez de répéter inutilement la demande de l'utilisateur, sauf si cela aide à contextualiser la réponse."
"invalidTokenProvided": { "message": "Jeton non valide fourni." }, },
"tokenAlreadyExists": { "message": "Le jeton existe déjà." }, "backToProviders": { "message": "Retour aux Fournisseurs" },
"tokenAddedSuccessfully": { "message": "Jeton ajouté avec succès." }, "artistsCounterSingle": { "message": "$total$ Artiste", "placeholders": { "total": { "content": "$1" } } },
"noStreamsFoundForSelection": { "message": "Aucun flux trouvé pour la sélection." }, "artistsCounterLoading": { "message": "Chargement..." },
"autoplayBlocked": { "message": "Lecture automatique bloquée." }, "downloadingSong": { "message": "Début du téléchargement de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"page": { "message": "Page" }, "songDownloaded": { "message": "\"$title$\" téléchargé.", "placeholders": { "title": { "content": "$1" } } },
"all": { "message": "Tous" }, "errorDownloadingSong": { "message": "Erreur lors du téléchargement de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"userScore": { "message": "Score des utilisateurs" }, "generatingAlbumM3U": { "message": "Génération du M3U pour \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
"duration": { "message": "Durée" }, "albumM3UGenerated": { "message": "M3U pour l'album \"$artist$\" généré.", "placeholders": { "artist": { "content": "$1" } } },
"min": { "message": "Min" }, "retyingSection": { "message": "Nouvelle tentative pour la section \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"max": { "message": "Max" } "retrySuccess": { "message": "[SUCCÈS] Nouvelle tentative pour \"$title$\" terminée.", "placeholders": { "title": { "content": "$1" } } },
"retryError": { "message": "[ERREUR FINALE] Échec de la nouvelle tentative pour \"$title$\" : $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } },
"startingRetryPhase": { "message": "Démarrage de la phase de nouvelle tentative pour $count$ sections...", "placeholders": { "count": { "content": "$1" } } },
"tokenFoundServers": { "message": "Token $token$... a trouvé $count$ serveurs.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } },
"errorProcessingToken": { "message": "Erreur de traitement du token $token$... : $message$", "placeholders": { "token": { "content": "$1" }, "message": { "content": "$2" } } },
"plexScanFatalError": { "message": "ERREUR FATALE : $message$", "placeholders": { "message": { "content": "$1" } } },
"errorDuringScan": { "message": "Erreur pendant l'analyse : $message$", "placeholders": { "message": { "content": "$1" } } },
"stoppingPlexScan": { "message": "Arrêt de l'analyse Plex..." },
"invalidTokenProvided": { "message": "Token fourni invalide." },
"tokenAlreadyExists": { "message": "Le token existe déjà." },
"tokenAddedSuccessfully": { "message": "Token ajouté avec succès." },
"noStreamsFoundForSelection": { "message": "Aucun stream trouvé pour la sélection." },
"autoplayBlocked": { "message": "Lecture automatique bloquée." },
"welcomeToCinePlex": { "message": "" },
"page": { "message": "Page" },
"all": { "message": "Tout" },
"userScore": { "message": "Score" },
"duration": { "message": "Durée" },
"min": { "message": "Min" },
"max": { "message": "Max" },
"aiToolFindStreamingProvidersDesc": { "message": "Trouve où regarder un film ou une série sur les services de streaming." },
"aiToolFindStreamingProvidersTitleParamDesc": { "message": "Le titre du film ou de la série à rechercher." },
"aiToolFindStreamingProvidersTypeParamDesc": { "message": "Le type de contenu (film ou série)." },
"aiToolFindStreamingProvidersYearParamDesc": { "message": "L'année de sortie du contenu (optionnel)." },
"aiToolNoStreamingProviders": { "message": "Aucun fournisseur de streaming trouvé pour {title}." },
"aiToolStreamingProvidersFound": { "message": "{title} est disponible sur les services suivants : {providers}." },
"aiToolStreamingProviderError": { "message": "Erreur lors de la recherche des fournisseurs de streaming : {message}." },
"aiToolGetLocalSeriesSeasonsDesc": { "message": "Vérifie si une série télévisée est disponible localement et renvoie une ventilation détaillée des saisons disponibles sur chaque serveur." },
"aiToolGetLocalSeriesSeasonsTitleParamDesc": { "message": "Le titre de la série à vérifier." },
"aiToolGetLocalSeriesSeasonsYearParamDesc": { "message": "L'année de sortie de la série (optionnel pour une plus grande précision)." },
"aiToolLocalSeriesNoSeasons": { "message": "La série '$series_title' est dans votre bibliothèque, mais aucun détail de saison n'a été trouvé.", "placeholders": { "series_title": { "content": "$1" } } },
"artist": { "message": "Artiste" },
"tracks": { "message": "pistes" },
"noSongsFound": { "message": "Aucune chanson trouvée pour cet artiste." },
"durationMin": { "message": "Durée (Min)" },
"score": { "message": "Score" },
"searchGenre": { "message": "Rechercher un genre..." },
"searchArtists": { "message": "Rechercher des artistes..." },
"preparingMusicLibrary": { "message": "Préparation de votre bibliothèque musicale..." },
"preparingMusicLibraryDesc": { "message": "Ce processus unique peut prendre quelques minutes si vous avez de nombreux artistes." },
"artistsProgress": { "message": "0 / 0 artistes" },
"starting": { "message": "Démarrage..." },
"artistName": { "message": "Nom de l'Artiste" },
"playPause": { "message": "Lecture/Pause" },
"noLocalFilesFound": { "message": "Aucun fichier local trouvé pour ce titre." },
"server": { "message": "Serveur" },
"title": { "message": "Titre" },
"year": { "message": "Année" },
"resolution": { "message": "Résolution" },
"size": { "message": "Taille" },
"container": { "message": "Conteneur" },
"action": { "message": "Action" },
"generate": { "message": "Générer" },
"availableLocalFiles": { "message": "Fichiers Locaux Disponibles" },
"downloadSeason": { "message": "Télécharger la Saison" },
"errorLoadingServersM3u": { "message": "Erreur lors du chargement des serveurs pour le générateur M3U :" },
"errorFetchingLibraries": { "message": "Erreur lors de la récupération des bibliothèques." },
"selectServerAndLibrary": { "message": "Veuillez sélectionner un serveur et au moins une bibliothèque." },
"generating": { "message": "Génération..." },
"errorProcessingLibrary": { "message": "Erreur lors du traitement de la bibliothèque" },
"errorProcessingLibrarySkipping": { "message": "Erreur lors du traitement de la bibliothèque. Ignorer." },
"allLibrariesFailed": { "message": "Toutes les bibliothèques sélectionnées n'ont pas pu être traitées." },
"m3uGeneratedWithErrors": { "message": "M3U généré avec quelques erreurs. Certaines bibliothèques peuvent manquer." },
"m3uDownloadedSuccess": { "message": "Liste de lecture M3U téléchargée avec succès." },
"errorGeneratingM3uFile": { "message": "Erreur lors de la génération du fichier M3U." },
"chatSources": { "message": "Sources" },
"chatUnnamedSource": { "message": "Source sans nom" },
"googleApiFailure": { "message": "Échec de l'appel à l'API Google AI :" }
} }

View File

@ -1,449 +1,516 @@
{ {
"appName": { "message": "CinePlex" }, "appName": { "message": "CinePlex" },
"appDescription": { "message": "Scansiona i server Plex alla ricerca di contenuti e li visualizza nell'interfaccia" }, "appDescription": { "message": "Scansiona i server Plex per trovare contenuti e li visualizza nell'interfaccia" },
"appTagline": { "message": "Film, Serie e Musica" }, "appTagline": { "message": "Film, Serie e Musica" },
"appLocaleCode": { "message": "it-IT" }, "appLocaleCode": { "message": "it-IT" },
"toggleNavigation": { "message": "Attiva/disattiva la navigazione" }, "toggleNavigation": { "message": "Attiva/Disattiva Navigazione" },
"searchPlaceholder": { "message": "Cerca film o serie..." }, "searchPlaceholder": { "message": "Cerca film o serie..." },
"openMusicPlayer": { "message": "Apri il lettore musicale" }, "openMusicPlayer": { "message": "Apri Lettore Musicale" },
"settings": { "message": "Impostazioni" }, "settings": { "message": "Impostazioni" },
"navMovies": { "message": "Film" }, "navMovies": { "message": "Film" },
"navSeries": { "message": "Serie" }, "navSeries": { "message": "Serie" },
"navProviders": { "message": "Provider" }, "navProviders": { "message": "Fornitori" },
"navPhotos": { "message": "Foto" }, "navPhotos": { "message": "Foto" },
"navStats": { "message": "Statistiche" }, "navStats": { "message": "Statistiche" },
"navFavorites": { "message": "Preferiti" }, "navFavorites": { "message": "Preferiti" },
"navHistory": { "message": "Cronologia" }, "navHistory": { "message": "Cronologia" },
"navRecommendations": { "message": "Raccomandazioni" }, "navRecommendations": { "message": "Raccomandazioni" },
"navMusic": { "message": "Musica" }, "navMusic": { "message": "Musica" },
"navM3uGenerator": { "message": "Generatore M3U" }, "musicFeaturedPlaylists": { "message": "Playlist in Evidenza" },
"heroWelcome": { "message": "" }, "musicRecentlyAdded": { "message": "Aggiunti di Recente" },
"heroSubtitle": { "message": "Esplora migliaia di film e serie." }, "navM3uGenerator": { "message": "Generatore M3U" },
"addStream": { "message": "Aggiungi streaming" }, "heroWelcome": { "message": "" },
"moreInfo": { "message": "Più informazioni" }, "heroSubtitle": { "message": "Esplora migliaia di film e serie." },
"popularMovies": { "message": "Film popolari" }, "addStream": { "message": "Aggiungi Stream" },
"allGenres": { "message": "Tutti i generi" }, "moreInfo": { "message": "Maggiori informazioni" },
"allYears": { "message": "Tutti gli anni" }, "popularMovies": { "message": "Film Popolari" },
"sortPopular": { "message": "I più popolari" }, "allGenres": { "message": "Tutti i generi" },
"sortTopRated": { "message": "I più votati" }, "allYears": { "message": "Tutti gli anni" },
"sortRecent": { "message": "I più recenti" }, "sortPopular": { "message": "Più popolari" },
"loadMore": { "message": "Carica altro" }, "sortTopRated": { "message": "I più votati" },
"photosBreadcrumbHome": { "message": "Album" }, "sortRecent": { "message": "I più recenti" },
"selectServer": { "message": "Seleziona un server" }, "loadMore": { "message": "Carica altro" },
"loading": { "message": "Caricamento..." }, "photosBreadcrumbHome": { "message": "Album" },
"loadingLibraries": { "message": "Caricamento delle librerie..." }, "selectServer": { "message": "Seleziona un server" },
"photosEmptyState": { "message": "Nessun album o foto trovati." }, "loading": { "message": "Caricamento in corso..." },
"photosEmptyStateSub": { "message": "Seleziona un server o assicurati di avere una libreria di foto in Plex." }, "loadingLibraries": { "message": "Caricamento librerie..." },
"statsTitle": { "message": "Statistiche della libreria" }, "photosEmptyState": { "message": "Nessun album o foto trovati." },
"statsAllTokens": { "message": "Tutti i token" }, "photosEmptyStateSub": { "message": "Seleziona un server o assicurati di avere una libreria di foto in Plex." },
"statsAnalyzing": { "message": "Analisi della tua libreria..." }, "statsTitle": { "message": "Statistiche della Libreria" },
"statsActiveTokens": { "message": "Token attivi" }, "statsAllTokens": { "message": "Tutti i Token" },
"statsServersFound": { "message": "Server trovati" }, "statsAnalyzing": { "message": "Analisi della tua libreria in corso..." },
"statsUniqueMovies": { "message": "Film unici" }, "statsActiveTokens": { "message": "Token Attivi" },
"statsUniqueSeries": { "message": "Serie uniche" }, "statsServersFound": { "message": "Server Trovati" },
"statsUniqueArtists": { "message": "Artisti unici" }, "statsUniqueMovies": { "message": "Film Unici" },
"statsTokenServers": { "message": "Server token" }, "statsUniqueSeries": { "message": "Serie Uniche" },
"statsChartMoviesByGenre": { "message": "Contenuti per genere (Film)" }, "statsUniqueArtists": { "message": "Artisti Unici" },
"statsChartSeriesByGenre": { "message": "Contenuti per genere (Serie)" }, "statsTokenServers": { "message": "Server del Token" },
"statsChartByDecade": { "message": "Contenuti per decennio" }, "statsChartMoviesByGenre": { "message": "Contenuti per Genere (Film)" },
"recommendationsTitle": { "message": "Raccomandazioni per te" }, "statsChartSeriesByGenre": { "message": "Contenuti per Genere (Serie)" },
"historyTitle": { "message": "Cronologia visualizzazioni" }, "statsChartByDecade": { "message": "Contenuti per Decennio" },
"clearHistory": { "message": "Cancella tutto" }, "recommendationsTitle": { "message": "Raccomandazioni per te" },
"consoleTitle": { "message": "Console di scansione Plex" }, "historyTitle": { "message": "Cronologia di Visione" },
"footerCredit": { "message": "Un'interfaccia per il tuo universo Plex." }, "clearHistory": { "message": "Cancella Tutto" },
"closeTrailer": { "message": "Chiudi trailer" }, "consoleTitle": { "message": "Console di Scansione Plex" },
"close": { "message": "Chiudi" }, "footerCredit": { "message": "Un'interfaccia per il tuo universo Plex." },
"photoViewer": { "message": "Visualizzatore di foto" }, "closeTrailer": { "message": "Chiudi trailer" },
"previous": { "message": "Precedente" }, "close": { "message": "Chiudi" },
"next": { "message": "Successivo" }, "photoViewer": { "message": "Visualizzatore di foto" },
"notificationTemplateText": { "message": "Notifica" }, "previous": { "message": "Precedente" },
"settingsTitleFull": { "message": "Impostazioni e configurazione" }, "next": { "message": "Successivo" },
"settingsTabGeneral": { "message": "Generale" }, "notificationTemplateText": { "message": "Notifica" },
"settingsTabPlex": { "message": "Plex" }, "settingsTitleFull": { "message": "Impostazioni e Configurazione" },
"settingsTabJellyfin": { "message": "Jellyfin" }, "settingsTabGeneral": { "message": "Generale" },
"settingsTabPhpGen": { "message": "Generatore PHP" }, "settingsTabPlex": { "message": "Plex" },
"settingsTabData": { "message": "Dati" }, "settingsTabJellyfin": { "message": "Jellyfin" },
"settingsApiServer": { "message": "Impostazioni API e server" }, "settingsTabPhpGen": { "message": "Generatore PHP" },
"settingsTmdbApiLabel": { "message": "Chiave API TMDB (facoltativa)" }, "settingsTabData": { "message": "Dati" },
"settingsTmdbApiPlaceholder": { "message": "La chiave predefinita verrà utilizzata se lasciata vuota" }, "settingsApiServer": { "message": "Configurazione API e Server" },
"settingsGoogleApiLabel": { "message": "Chiave API Google Gemini (facoltativa)" }, "settingsTmdbApiLabel": { "message": "Chiave API di TMDB (Opzionale)" },
"settingsGoogleApiPlaceholder": { "message": "Necessaria per utilizzare l'assistente AI" }, "settingsTmdbApiPlaceholder": { "message": "Verrà utilizzata la chiave predefinita se lasciato vuoto" },
"settingsRegionLabel": { "message": "Regione per la scoperta di contenuti" }, "settingsGoogleApiLabel": { "message": "Chiave API di Google Gemini (Opzionale)" },
"allRegions": { "message": "Tutte le regioni" }, "settingsGoogleApiPlaceholder": { "message": "Necessaria per utilizzare l'assistente AI" },
"settingsPhpUrlLabel": { "message": "URL del server per l'aggiunta di streaming" }, "settingsRegionLabel": { "message": "Regione per la scoperta di contenuti" },
"settingsPhpUrlPlaceholder": { "message": "https://tuo-server.com/percorso/dello/script.php" }, "allRegions": { "message": "Tutte le regioni" },
"settingsInterface": { "message": "Interfaccia" }, "settingsPhpUrlLabel": { "message": "URL del Server per Aggiungere Stream" },
"settingsLightTheme": { "message": "Modalità chiara" }, "settingsPhpUrlPlaceholder": { "message": "https://tuo-server.com/percorso/dello/script.php" },
"settingsShowHero": { "message": "Mostra la sezione di benvenuto 'Hero'" }, "settingsInterface": { "message": "Interfaccia" },
"settingsScanContent": { "message": "Scansione dei contenuti" }, "settingsLightTheme": { "message": "Modalità Chiara" },
"settingsScanDesc": { "message": "Seleziona cosa scansionare e premi il pulsante." }, "settingsShowHero": { "message": "Mostra sezione di benvenuto 'Hero'" },
"settingsScanMovies": { "message": "Film" }, "settingsScanContent": { "message": "Scansione Contenuti" },
"settingsScanShows": { "message": "Serie" }, "settingsScanDesc": { "message": "Seleziona cosa scansionare e premi il pulsante." },
"settingsScanArtists": { "message": "Musica" }, "settingsScanMovies": { "message": "Film" },
"settingsScanPhotos": { "message": "Foto" }, "settingsScanShows": { "message": "Serie" },
"settingsSelectAll": { "message": "Seleziona tutto" }, "settingsScanArtists": { "message": "Musica" },
"settingsStartScan": { "message": "Avvia scansione" }, "settingsScanPhotos": { "message": "Foto" },
"settingsPlexTokens": { "message": "Token Plex" }, "settingsSelectAll": { "message": "Seleziona Tutto" },
"settingsPlexTokensDesc": { "message": "Modifica l'elenco dei token Plex (formato JSON)." }, "settingsStartScan": { "message": "Avvia Scansione" },
"settingsSaveTokens": { "message": "Salva token" }, "settingsPlexTokens": { "message": "Token di Plex" },
"settingsJellyfinTitle": { "message": "Impostazioni Jellyfin" }, "settingsPlexTokensDesc": { "message": "Modifica l'elenco dei token di Plex (formato JSON)." },
"settingsJellyfinDesc": { "message": "Aggiungi i dettagli del tuo server Jellyfin per scansionarne il contenuto." }, "settingsSaveTokens": { "message": "Salva Token" },
"jellyfinUrlLabel": { "message": "URL del server Jellyfin" }, "settingsJellyfinTitle": { "message": "Configurazione di Jellyfin" },
"jellyfinUserLabel": { "message": "Nome utente" }, "settingsJellyfinDesc": { "message": "Aggiungi i dati del tuo server Jellyfin per scansionarne il contenuto." },
"jellyfinPassLabel": { "message": "Password" }, "jellyfinUrlLabel": { "message": "URL del Server Jellyfin" },
"jellyfinConnectAndScan": { "message": "Connetti e scansiona" }, "jellyfinUserLabel": { "message": "Nome Utente" },
"settingsPhpGenTitle": { "message": "Generatore di script PHP per server" }, "jellyfinPassLabel": { "message": "Password" },
"settingsPhpFileOptions": { "message": "Opzioni file" }, "jellyfinConnectAndScan": { "message": "Connetti e Scansiona" },
"settingsPhpSavePathLabel": { "message": "Percorso di salvataggio sul server" }, "settingsPhpGenTitle": { "message": "Generatore di Script PHP per il Server" },
"settingsPhpSavePathPlaceholder": { "message": "Es: /var/www/html/liste (vuoto per la stessa cartella)" }, "settingsPhpFileOptions": { "message": "Opzioni del File" },
"settingsPhpFilenameLabel": { "message": "Nome file" }, "settingsPhpSavePathLabel": { "message": "Percorso di Salvataggio sul Server" },
"settingsPhpFileAction": { "message": "Azione file" }, "settingsPhpSavePathPlaceholder": { "message": "Es: /var/www/html/liste (vuoto per la stessa cartella)" },
"settingsPhpActionAppend": { "message": "Aggiungi alla fine del file (cumulativo)" }, "settingsPhpFilenameLabel": { "message": "Nome del File" },
"settingsPhpActionOverwrite": { "message": "Sovrascrivi il file (ricomincia da capo)" }, "settingsPhpFileAction": { "message": "Azione sul File" },
"settingsPhpSecurity": { "message": "Sicurezza (facoltativa)" }, "settingsPhpActionAppend": { "message": "Aggiungi in fondo al file (cumulativo)" },
"settingsPhpUseSecretKey": { "message": "Usa chiave segreta (consigliato)" }, "settingsPhpActionOverwrite": { "message": "Sovrascrivi il file (ricomincia da capo)" },
"settingsPhpSecretKeyPlaceholder": { "message": "Inserisci una chiave segreta sicura" }, "settingsPhpSecurity": { "message": "Sicurezza (Opzionale)" },
"settingsPhpGeneratedCode": { "message": "Codice generato" }, "settingsPhpUseSecretKey": { "message": "Usa chiave segreta (Consigliato)" },
"settingsPhpGeneratedPlaceholder": { "message": "Il codice PHP generato apparirà qui." }, "settingsPhpSecretKeyPlaceholder": { "message": "Inserisci una chiave segreta sicura" },
"settingsGenerateScript": { "message": "Genera script" }, "settingsPhpGeneratedCode": { "message": "Codice Generato" },
"settingsCopyScript": { "message": "Copia script" }, "settingsPhpGeneratedPlaceholder": { "message": "Il codice PHP generato apparirà qui." },
"settingsDataManagement": { "message": "Gestione del database locale" }, "settingsGenerateScript": { "message": "Genera Script" },
"settingsImportDb": { "message": "Importa DB da file" }, "settingsCopyScript": { "message": "Copia Script" },
"settingsExportDb": { "message": "Esporta DB su file" }, "settingsDataManagement": { "message": "Gestione del Database Locale" },
"settingsClearContent": { "message": "Cancella i dati dei contenuti locali" }, "settingsImportDb": { "message": "Importa DB da File" },
"settingsClearContentDesc": { "message": "Questa azione eliminerà film, serie e musica dal database locale, ma non influirà sui preferiti o sulle impostazioni." }, "settingsExportDb": { "message": "Esporta DB su File" },
"settingsClose": { "message": "Chiudi" }, "settingsClearContent": { "message": "Cancella Dati Contenuti Locali" },
"settingsSave": { "message": "Salva impostazioni" }, "settingsClearContentDesc": { "message": "Questa azione cancellerà film, serie e musica dal database locale, ma non influenzerà i tuoi preferiti né le tue impostazioni." },
"musicSidenavTitle": { "message": "Musica Plex" }, "settingsClose": { "message": "Chiudi" },
"musicAllServers": { "message": "Tutti i server" }, "settingsSave": { "message": "Salva Impostazioni" },
"musicSearchArtistPlaceholder": { "message": "Cerca un artista..." }, "musicSidenavTitle": { "message": "Musica di Plex" },
"musicSearchDiscographyPlaceholder": { "message": "Cerca nella discografia..." }, "musicAllServers": { "message": "Tutti i Server" },
"musicNothingPlaying": { "message": "Nessuna riproduzione in corso" }, "musicSearchArtistPlaceholder": { "message": "Cerca artista..." },
"musicSelectSong": { "message": "Seleziona un brano" }, "musicSearchDiscographyPlaceholder": { "message": "Cerca nella discografia..." },
"musicToStart": { "message": "per avviare la riproduzione" }, "musicNothingPlaying": { "message": "Nessuna riproduzione in corso" },
"miniplayerDownloadSong": { "message": "Scarica brano" }, "musicSelectSong": { "message": "Seleziona una canzone" },
"miniplayerDownloadAlbum": { "message": "Scarica album M3U" }, "musicToStart": { "message": "per iniziare la riproduzione" },
"miniplayerVolume": { "message": "Volume" }, "miniplayerDownloadSong": { "message": "Scarica canzone" },
"miniplayerShuffle": { "message": "Casuale" }, "miniplayerDownloadAlbum": { "message": "Scarica M3U" },
"miniplayerEqualizer": { "message": "Equalizzatore" }, "miniplayerVolume": { "message": "Volume" },
"miniplayerOpenList": { "message": "Apri elenco" }, "miniplayerShuffle": { "message": "Casuale" },
"eqTitle": { "message": "Equalizzatore grafico" }, "miniplayerEqualizer": { "message": "Equalizzatore" },
"eqPresetsLabel": { "message": "Preimpostazioni" }, "miniplayerOpenList": { "message": "Apri lista" },
"eqPresetFlat": { "message": "Piatto" }, "eqTitle": { "message": "Equalizzatore Grafico" },
"eqPresetRock": { "message": "Rock" }, "eqPresetsLabel": { "message": "Preimpostazioni" },
"eqPresetPop": { "message": "Pop" }, "eqPresetFlat": { "message": "Piatto" },
"eqPresetJazz": { "message": "Jazz" }, "eqPresetRock": { "message": "Rock" },
"eqPresetClassical": { "message": "Classica" }, "eqPresetPop": { "message": "Pop" },
"eqPresetBassBoost": { "message": "Aumento dei bassi" }, "eqPresetJazz": { "message": "Jazz" },
"eqPreampLabel": { "message": "Preamplificatore" }, "eqPresetClassical": { "message": "Classica" },
"infoModalTitle": { "message": "Informazioni" }, "eqPresetBassBoost": { "message": "Potenziamento Bassi" },
"infoModalFieldTitle": { "message": "Titolo:" }, "eqPreampLabel": { "message": "Preamplificatore" },
"infoModalFieldArtist": { "message": "Artista:" }, "infoModalTitle": { "message": "Informazioni" },
"infoModalFieldAlbum": { "message": "Album:" }, "infoModalFieldTitle": { "message": "Titolo:" },
"infoModalFieldSong": { "message": "Brano:" }, "infoModalFieldArtist": { "message": "Artista:" },
"infoModalFieldYear": { "message": "Anno:" }, "infoModalFieldAlbum": { "message": "Album:" },
"infoModalFieldGenre": { "message": "Genere:" }, "infoModalFieldSong": { "message": "Canzone:" },
"lang_en": { "message": "Inglese" }, "infoModalFieldYear": { "message": "Anno:" },
"lang_es": { "message": "Spagnolo" }, "infoModalFieldGenre": { "message": "Genere:" },
"lang_fr": { "message": "Francese" }, "lang_en": { "message": "Inglese" },
"lang_de": { "message": "Tedesco" }, "lang_es": { "message": "Spagnolo" },
"lang_it": { "message": "Italiano" }, "lang_fr": { "message": "Francese" },
"lang_pt": { "message": "Portoghese" }, "lang_de": { "message": "Tedesco" },
"essentialFeaturesNotSupported": { "message": "Il tuo browser non supporta le funzionalità essenziali." }, "lang_it": { "message": "Italiano" },
"dbAccessError": { "message": "Errore di accesso al database locale." }, "lang_pt": { "message": "Portoghese" },
"dbUpdateNeeded": { "message": "Il database deve essere aggiornato, ricarica la pagina." }, "essentialFeaturesNotSupported": { "message": "Il tuo browser non supporta funzionalità essenziali." },
"dbBlocked": { "message": "Chiudi le altre schede di questa applicazione per continuare." }, "dbAccessError": { "message": "Errore di accesso al database locale." },
"deletingContentData": { "message": "Eliminazione dei dati dei contenuti locali..." }, "dbUpdateNeeded": { "message": "Il database deve essere aggiornato, per favore ricarica la pagina." },
"noContentDataToDelete": { "message": "Nessun dato di contenuto da eliminare." }, "dbBlocked": { "message": "Per favore, chiudi le altre schede di questa applicazione per continuare." },
"contentDataDeleted": { "message": "Dati dei contenuti eliminati da IndexedDB." }, "deletingContentData": { "message": "Cancellazione dei dati dei contenuti locali in corso..." },
"errorDeletingData": { "message": "Errore durante l'eliminazione dei dati: $message$", "placeholders": { "message": { "content": "$1" } } }, "noContentDataToDelete": { "message": "Nessun dato di contenuto da cancellare." },
"aceEditorNotAvailable": { "message": "Editor di testo non disponibile." }, "contentDataDeleted": { "message": "Dati dei contenuti cancellati da IndexedDB." },
"errorLoadingTokens": { "message": "Errore durante il caricamento dei token per la modifica." }, "errorDeletingData": { "message": "Errore durante la cancellazione dei dati: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingTokensMessage": { "message": "Errore durante il caricamento dei token: $message$", "placeholders": { "message": { "content": "$1" } } }, "aceEditorNotAvailable": { "message": "Editor di testo non disponibile." },
"aceEditorNotAvailableToSave": { "message": "Editor non disponibile per il salvataggio." }, "errorLoadingTokens": { "message": "Errore durante il caricamento dei token per la modifica." },
"invalidJsonFormat": { "message": "Formato JSON non valido. Deve essere { \"tokens\": [...] }" }, "errorLoadingTokensMessage": { "message": "Errore durante il caricamento dei token: $message$", "placeholders": { "message": { "content": "$1" } } },
"tokensSaved": { "message": "Token salvati correttamente." }, "aceEditorNotAvailableToSave": { "message": "Editor non disponibile per il salvataggio." },
"errorSavingTokens": { "message": "Errore durante il salvataggio dei token: $message$", "placeholders": { "message": { "content": "$1" } } }, "invalidJsonFormat": { "message": "Formato JSON non valido. Deve essere { \"tokens\": [...] }" },
"dbNotAvailable": { "message": "IndexedDB non è disponibile." }, "tokensSaved": { "message": "Token salvati correttamente." },
"dbExported": { "message": "Database esportato correttamente." }, "errorSavingTokens": { "message": "Errore durante il salvataggio dei token: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorExportingDb": { "message": "Errore durante l'esportazione del database: $message$", "placeholders": { "message": { "content": "$1" } } }, "dbNotAvailable": { "message": "IndexedDB non è disponibile." },
"invalidJsonFile": { "message": "Il file non contiene un oggetto JSON valido." }, "dbExported": { "message": "Database esportato con successo." },
"noDataToImport": { "message": "Il file non contiene dati per le sezioni correnti del database." }, "errorExportingDb": { "message": "Errore durante l'esportazione del database: $message$", "placeholders": { "message": { "content": "$1" } } },
"dbImported": { "message": "Database importato correttamente." }, "invalidJsonFile": { "message": "Il file non contiene un oggetto JSON valido." },
"errorImportingDb": { "message": "Errore durante l'importazione del database: $message$", "placeholders": { "message": { "content": "$1" } } }, "noDataToImport": { "message": "Il file non contiene dati per le sezioni del DB corrente." },
"updatingView": { "message": "Aggiornamento della vista con i nuovi dati..." }, "dbImported": { "message": "Database importato correttamente." },
"confirmClearContent": { "message": "Sei sicuro di voler eliminare i dati dei contenuti locali (film, serie, musica, ecc.)? I preferiti e le impostazioni NON verranno eliminati." }, "errorImportingDb": { "message": "Errore durante l'importazione del database: $message$", "placeholders": { "message": { "content": "$1" } } },
"trailerNotFound": { "message": "Nessun trailer trovato per questo titolo." }, "updatingView": { "message": "Aggiornamento della vista con i nuovi dati in corso..." },
"confirmClearHistory": { "message": "Sei sicuro di voler cancellare tutta la cronologia delle visualizzazioni? Questa azione non può essere annullata." }, "confirmClearContent": { "message": "Sei sicuro di voler cancellare i dati dei contenuti locali (Film, Serie, Musica, ecc.)? I Preferiti e le Impostazioni NON verranno cancellati." },
"historyCleared": { "message": "Cronologia visualizzazioni cancellata." }, "trailerNotFound": { "message": "Nessun trailer trovato per questo titolo." },
"historyItemDeleted": { "message": "Elemento eliminato dalla cronologia." }, "confirmClearHistory": { "message": "Sei sicuro di voler cancellare tutta la tua cronologia di visione? Questa azione non può essere annullata." },
"errorGeneratingScript": { "message": "Genera prima uno script per poterlo copiare." }, "historyCleared": { "message": "Cronologia di visione cancellata." },
"scriptCopied": { "message": "Script PHP copiato negli appunti." }, "historyItemDeleted": { "message": "Elemento cancellato dalla cronologia." },
"errorCopyingScript": { "message": "Errore durante la copia dello script." }, "errorGeneratingScript": { "message": "Prima genera uno script per poterlo copiare." },
"scriptGenerated": { "message": "Script PHP generato." }, "scriptCopied": { "message": "Script PHP copiato negli appunti." },
"errorLoadingAlbum": { "message": "Errore durante il caricamento dell'album: $message$", "placeholders": { "message": { "content": "$1" } } }, "errorCopyingScript": { "message": "Errore durante la copia dello script." },
"noPhotoServerSelected": { "message": "Errore: non è stato selezionato alcun server di foto." }, "scriptGenerated": { "message": "Script PHP generato." },
"loadingGenres": { "message": "Caricamento dei generi..." }, "errorLoadingAlbum": { "message": "Errore durante il caricamento dell'album: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingGenres": { "message": "Errore durante il caricamento" }, "noPhotoServerSelected": { "message": "Errore: Nessun server di foto selezionato." },
"noContentFound": { "message": "Nessun risultato trovato." }, "loadingGenres": { "message": "Caricamento generi in corso..." },
"couldNotLoadContent": { "message": "Impossibile caricare il contenuto." }, "errorLoadingGenres": { "message": "Errore di caricamento" },
"noFavorites": { "message": "Non hai ancora preferiti." }, "noContentFound": { "message": "Nessun risultato trovato." },
"errorLoadingFavorites": { "message": "Errore durante il caricamento dei preferiti." }, "couldNotLoadContent": { "message": "Impossibile caricare il contenuto." },
"historyEmpty": { "message": "La tua cronologia è vuota." }, "noFavorites": { "message": "Non hai ancora preferiti." },
"historyEmptySub": { "message": "Esplora e guarda i contenuti perché appaiano qui." }, "errorLoadingFavorites": { "message": "Errore durante il caricamento dei preferiti." },
"errorGeneratingRecommendations": { "message": "Errore durante la generazione delle raccomandazioni." }, "historyEmpty": { "message": "La tua cronologia è vuota." },
"noRecommendations": { "message": "Dobbiamo conoscerti meglio per darti consigli!" }, "historyEmptySub": { "message": "Esplora e guarda contenuti affinché appaiano qui." },
"errorGeneratingStats": { "message": "Errore durante la generazione delle statistiche." }, "errorGeneratingRecommendations": { "message": "Errore durante la generazione delle raccomandazioni." },
"noServersForToken": { "message": "Nessun server associato trovato per questo token." }, "noRecommendations": { "message": "Dobbiamo conoscerti meglio per darti delle raccomandazioni!" },
"searchingActorContent": { "message": "Ricerca di contenuti di $actorName$", "placeholders": { "actorName": { "content": "$1" } } }, "errorGeneratingStats": { "message": "Errore durante la generazione delle statistiche." },
"errorLoadingActorContent": { "message": "Impossibile caricare i contenuti per $actorName$.", "placeholders": { "actorName": { "content": "$1" } } }, "noServersForToken": { "message": "Nessun server associato trovato per questo token." },
"errorAddingStream": { "message": "Errore durante l'aggiunta di streaming: $message$", "placeholders": { "message": { "content": "$1" } } }, "searchingActorContent": { "message": "Ricerca di contenuti di $actorName$", "placeholders": { "actorName": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "L'URL del server PHP non è configurato. Configuralo nelle Impostazioni." }, "errorLoadingActorContent": { "message": "Impossibile caricare i contenuti per $actorName$.", "placeholders": { "actorName": { "content": "$1" } } },
"searchingStreams": { "message": "Ricerca di streaming per \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "errorAddingStream": { "message": "Errore durante l'aggiunta di stream: $message$", "placeholders": { "message": { "content": "$1" } } },
"sendingStreams": { "message": "Invio di $count$ streaming al server...", "placeholders": { "count": { "content": "$1" } } }, "phpUrlNotConfigured": { "message": "L'URL del server PHP non è configurato. Per favore, configuralo nelle Impostazioni." },
"streamAddedSuccess": { "message": "Streaming aggiunto/i con successo." }, "searchingStreams": { "message": "Ricerca di stream per \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingM3U": { "message": "Generazione di M3U per \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "sendingStreams": { "message": "Invio di $count$ stream al server in corso...", "placeholders": { "count": { "content": "$1" } } },
"m3uDownloaded": { "message": "\"$title$\" scaricato.", "placeholders": { "title": { "content": "$1" } } }, "streamAddedSuccess": { "message": "Stream aggiunti con successo." },
"errorGeneratingM3U": { "message": "Errore durante la generazione di M3U: $message$", "placeholders": { "message": { "content": "$1" } } }, "generatingM3U": { "message": "Generazione M3U per \"$title$\" in corso", "placeholders": { "title": { "content": "$1" } } },
"settingsSavedSuccess": { "message": "Impostazioni salvate correttamente." }, "m3uDownloaded": { "message": "\"$title$\" scaricato.", "placeholders": { "title": { "content": "$1" } } },
"errorSavingSettings": { "message": "Errore durante il salvataggio delle impostazioni nel database." }, "errorGeneratingM3U": { "message": "Errore durante la generazione di M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
"languageChangeReload": { "message": "Lingua modificata. L'applicazione verrà ora ricaricata." }, "settingsSavedSuccess": { "message": "Impostazioni salvate correttamente." },
"addedToFavorites": { "message": "Aggiunto ai preferiti." }, "errorSavingSettings": { "message": "Errore durante il salvataggio delle impostazioni nel database." },
"removedFromFavorites": { "message": "Rimosso dai preferiti." }, "languageChangeReload": { "message": "Lingua cambiata. L'applicazione verrà ricaricata ora." },
"plexScanInProgress": { "message": "La scansione di Plex è già in corso." }, "addedToFavorites": { "message": "Aggiunto ai preferiti." },
"plexScanStarting": { "message": "Avvio della scansione di Plex..." }, "removedFromFavorites": { "message": "Rimosso dai preferiti." },
"noPlexTokens": { "message": "Nessun token Plex configurato." }, "plexScanInProgress": { "message": "La scansione Plex è già in corso." },
"clearingSections": { "message": "Cancellazione delle sezioni: $sections$", "placeholders": { "sections": { "content": "$1" } } }, "plexScanStarting": { "message": "Avvio scansione Plex in corso..." },
"initialScanPhaseComplete": { "message": "Fase di scansione iniziale completata." }, "noPlexTokens": { "message": "Nessun token di Plex configurato." },
"retryPhaseFinished": { "message": "Fase di tentativi ripetuti terminata." }, "clearingSections": { "message": "Pulizia sezioni: $sections$", "placeholders": { "sections": { "content": "$1" } } },
"plexScanFinished": { "message": "Scansione terminata. Aggiornamento dei contenuti..." }, "initialScanPhaseComplete": { "message": "Fase di scansione iniziale completata." },
"scanCancelled": { "message": "Scansione annullata dall'utente." }, "retryPhaseFinished": { "message": "Fase di ritentativi completata." },
"scanCancelledInfo": { "message": "Scansione annullata." }, "plexScanFinished": { "message": "Scansione completata. Aggiornamento dei contenuti in corso..." },
"errorInitializingMusicPlayer": { "message": "Errore durante l'inizializzazione del lettore musicale." }, "scanCancelled": { "message": "Scansione annullata dall'utente." },
"criticalErrorLoadingMusic": { "message": "Errore critico durante il caricamento dei dati musicali." }, "scanCancelledInfo": { "message": "Scansione annullata." },
"errorLoadingArtists": { "message": "Errore durante il caricamento degli artisti." }, "errorInitializingMusicPlayer": { "message": "Errore durante l'inizializzazione del lettore musicale." },
"dbUnavailableError": { "message": "Errore: database non disponibile." }, "criticalErrorLoadingMusic": { "message": "Errore critico durante il caricamento dei dati musicali." },
"updatingMusicData": { "message": "Aggiornamento dei dati musicali..." }, "errorLoadingArtists": { "message": "Errore durante il caricamento degli artisti." },
"musicDataUpdated": { "message": "Dati musicali aggiornati." }, "dbUnavailableError": { "message": "Errore: Database non disponibile." },
"errorFetchingArtistSongs": { "message": "Errore durante il recupero dei brani dell'artista." }, "updatingMusicData": { "message": "Aggiornamento dati musicali in corso..." },
"errorLoadingSongs": { "message": "Errore durante il caricamento dei brani." }, "musicDataUpdated": { "message": "Dati musicali aggiornati." },
"noArtistsFound": { "message": "Nessun artista trovato." }, "errorFetchingArtistSongs": { "message": "Errore durante il recupero delle canzoni dell'artista." },
"shuffleOn": { "message": "Modalità casuale attivata." }, "errorLoadingSongs": { "message": "Errore durante il caricamento delle canzoni." },
"shuffleOff": { "message": "Modalità casuale disattivata." }, "noArtistsFound": { "message": "Nessun artista trovato." },
"playbackError": { "message": "Errore di riproduzione" }, "shuffleOn": { "message": "Modalità casuale attivata." },
"errorLabel": { "message": "Errore" }, "shuffleOff": { "message": "Modalità casuale disattivata." },
"reloadingPage": { "message": "Ricaricamento della pagina..." }, "playbackError": { "message": "Errore di riproduzione" },
"viewed": { "message": "Visto" }, "errorLabel": { "message": "Errore" },
"local": { "message": "Locale" }, "reloadingPage": { "message": "Ricaricamento pagina in corso..." },
"topRatedSort": {"message": "I più votati"}, "viewed": { "message": "Visto" },
"recentSort": {"message": "Recenti"}, "local": { "message": "Locale" },
"popularSort": {"message": "Popolari"}, "topRatedSort": {"message": "I più votati"},
"moviesSectionTitle": {"message": "Film"}, "recentSort": {"message": "Recenti"},
"seriesSectionTitle": {"message": "Serie"}, "popularSort": {"message": "Popolari"},
"searchResultsFor": {"message": "Risultati per \"$query$\"", "placeholders": {"query": {"content": "$1"}}}, "moviesSectionTitle": {"message": "Film"},
"contentFrom": {"message": "Contenuti di $actor$", "placeholders": {"actor": {"content": "$1"}}}, "seriesSectionTitle": {"message": "Serie"},
"explore": {"message": "Esplora"}, "searchResultsFor": {"message": "Risultati per \"$query$\"", "placeholders": {"query": {"content": "$1"}}},
"noGenre": {"message": "Senza categoria"}, "contentFrom": {"message": "Contenuti di $actor$", "placeholders": {"actor": {"content": "$1"}}},
"synopsis": {"message": "Sinossi"}, "explore": {"message": "Esplora"},
"noSynopsis": {"message": "Nessuna sinossi disponibile."}, "noGenre": {"message": "Senza categoria"},
"director": {"message": "Regista:"}, "synopsis": {"message": "Sinossi"},
"writer": {"message": "Sceneggiatore:"}, "noSynopsis": {"message": "Nessuna sinossi disponibile."},
"viewOnImdb": {"message": "Vedi su IMDb"}, "director": {"message": "Regista:"},
"watchTrailer": {"message": "Guarda il trailer"}, "writer": {"message": "Sceneggiatore:"},
"addToFavorites": {"message": "Aggiungi ai preferiti"}, "viewOnImdb": {"message": "Vedi su IMDb"},
"removeFromFavorites": {"message": "Rimuovi dai preferiti"}, "watchTrailer": {"message": "Guarda il Trailer"},
"notAvailable": {"message": "Non disponibile"}, "addToFavorites": {"message": "Aggiungi ai preferiti"},
"mainCast": {"message": "Cast principale"}, "removeFromFavorites": {"message": "Rimuovi dai preferiti"},
"seasonsAndEpisodes": {"message": "Stagioni ed episodi"}, "notAvailable": {"message": "Non disponibile"},
"similarContent": {"message": "Contenuti simili"}, "mainCast": {"message": "Cast Principale"},
"filmography": {"message": "Filmografia"}, "seasonsAndEpisodes": {"message": "Stagioni ed Episodi"},
"availableOn": {"message": "Disponibile su"}, "similarContent": {"message": "Contenuti Simili"},
"episodesCount": {"message": "$count$ episodi", "placeholders": {"count": {"content": "$1"}}}, "filmography": {"message": "Filmografia"},
"seasonsCount": {"message": "$count$ stagioni", "placeholders": {"count": {"content": "$1"}}}, "availableOn": {"message": "Disponibile su"},
"runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}}, "episodesCount": {"message": "$count$ Episodi", "placeholders": {"count": {"content": "$1"}}},
"noTrailerFound": {"message": "Nessun trailer trovato per questo titolo."}, "seasonsCount": {"message": "$count$ Stagioni", "placeholders": {"count": {"content": "$1"}}},
"fatalInitError": {"message": "Errore fatale di inizializzazione"}, "runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}},
"fatalInitErrorSub": {"message": "Impossibile caricare l'applicazione."}, "noTrailerFound": {"message": "Nessun trailer trovato per questo titolo."},
"invalidStreamInfo": {"message": "Informazioni non valide."}, "fatalInitError": {"message": "Errore fatale di inizializzazione"},
"dbUnavailableForStreams": {"message": "Database locale non disponibile."}, "fatalInitErrorSub": {"message": "Impossibile caricare l'applicazione."},
"noPlexServersForStreams": {"message": "Nessun server Plex."}, "invalidStreamInfo": {"message": "Informazioni stream non valide."},
"notFoundOnServers": {"message": "\"$query$\" non trovato sui server Plex.", "placeholders": {"query": {"content": "$1"}}}, "dbUnavailableForStreams": {"message": "Database locale non disponibile."},
"relativeTime_justNow": { "message": "Poco fa" }, "noPlexServersForStreams": {"message": "Nessun server Plex."},
"relativeTime_minutesAgo": { "message": "$count$ minuti fa", "placeholders": { "count": { "content": "$1" } } }, "notFoundOnServers": {"message": "\"$query$\" non trovato sui server Plex.", "placeholders": {"query": {"content": "$1"}}},
"relativeTime_hoursAgo": { "message": "$count$ ore fa", "placeholders": { "count": { "content": "$1" } } }, "relativeTime_justNow": { "message": "Poco fa" },
"relativeTime_yesterday": { "message": "Ieri" }, "relativeTime_minutesAgo": { "message": "$count$ minuti fa", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_daysAgo": { "message": "$count$ giorni fa", "placeholders": { "count": { "content": "$1" } } }, "relativeTime_hoursAgo": { "message": "$count$ ore fa", "placeholders": { "count": { "content": "$1" } } },
"errorLoadingDetails": { "message": "Errore durante il caricamento dei dettagli" }, "relativeTime_yesterday": { "message": "Ieri" },
"errorLoadingLocalContent": { "message": "Errore durante il caricamento del contenuto locale." }, "relativeTime_daysAgo": { "message": "$count$ giorni fa", "placeholders": { "count": { "content": "$1" } } },
"errorServerResponse": { "message": "Risposta del server non riuscita." }, "errorLoadingDetails": { "message": "Errore nel Caricamento dei Dettagli" },
"errorPlexApi": { "message": "Errore $status$ dell'API Plex.", "placeholders": { "status": { "content": "$1" } } }, "errorLoadingLocalContent": { "message": "Errore durante il caricamento del contenuto locale." },
"errorParsingPlexXml": { "message": "Errore durante l'analisi dell'XML di Plex." }, "errorServerResponse": { "message": "Risposta del server non riuscita." },
"untitled": { "message": "Senza titolo" }, "errorPlexApi": { "message": "Errore API di Plex $status$.", "placeholders": { "status": { "content": "$1" } } },
"itemCount": { "message": "$count$ elementi", "placeholders": { "count": { "content": "$1" } } }, "errorParsingPlexXml": { "message": "Errore durante l'analisi del XML di Plex." },
"noPhotoServers": { "message": "Nessun server di foto" }, "untitled": { "message": "Senza titolo" },
"jellyfinScanInProgress": { "message": "La scansione di Jellyfin è già in corso." }, "itemCount": { "message": "$count$ elementi", "placeholders": { "count": { "content": "$1" } } },
"jellyfinScanning": { "message": "Scansione di Jellyfin..." }, "noPhotoServers": { "message": "Nessun server di foto" },
"jellyfinMissingCredentials": { "message": "Completa l'URL e il nome utente di Jellyfin." }, "jellyfinScanInProgress": { "message": "La scansione di Jellyfin è già in corso." },
"jellyfinConnecting": { "message": "Connessione a Jellyfin in corso: $url$", "placeholders": { "url": { "content": "$1" } } }, "jellyfinScanning": { "message": "Scansione di Jellyfin in corso..." },
"jellyfinAuthFailed": { "message": "Autenticazione Jellyfin non riuscita: $message$", "placeholders": { "message": { "content": "$1" } } }, "jellyfinMissingCredentials": { "message": "Per favore, completa l'URL e il nome utente di Jellyfin." },
"jellyfinAuthSuccess": { "message": "Autenticazione Jellyfin riuscita." }, "jellyfinConnecting": { "message": "Connessione a Jellyfin su: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinFetchingLibraries": { "message": "Recupero delle librerie..." }, "jellyfinAuthFailed": { "message": "Autenticazione Jellyfin fallita: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinFetchFailed": { "message": "Errore durante il recupero delle librerie: $message$", "placeholders": { "message": { "content": "$1" } } }, "jellyfinAuthSuccess": { "message": "Autenticazione Jellyfin riuscita." },
"jellyfinNoMediaLibraries": { "message": "Nessuna libreria di film o serie trovata in Jellyfin." }, "jellyfinFetchingLibraries": { "message": "Recupero librerie in corso..." },
"jellyfinLibrariesFound": { "message": "$count$ libreria/e multimediale/i trovata/e.", "placeholders": { "count": { "content": "$1" } } }, "jellyfinFetchFailed": { "message": "Errore durante il recupero delle librerie: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Successo] '$libraryName' scansionata, $count$ titoli aggiunti.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } }, "jellyfinNoMediaLibraries": { "message": "Nessuna libreria di film o serie trovata in Jellyfin." },
"jellyfinLibraryScanFailed": { "message": "Errore durante la scansione della libreria '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } }, "jellyfinLibrariesFound": { "message": "$count$ libreria/e multimediale/i trovata/e.", "placeholders": { "count": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Scansione Jellyfin completata. Aggiunti $movies$ film e $series$ serie.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } }, "jellyfinLibraryScanSuccess": { "message": "[Successo] '$libraryName' scansionata, $count$ titoli aggiunti.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Credenziali Jellyfin non configurate." }, "jellyfinLibraryScanFailed": { "message": "Errore durante la scansione della libreria '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } },
"notFoundOnJellyfin": { "message": "\"$query$\" non trovato su Jellyfin.", "placeholders": { "query": { "content": "$1" } } }, "jellyfinScanSuccess": { "message": "Scansione Jellyfin completata. Aggiunti $movies$ film e $series$ serie.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" non trovato su nessun server.", "placeholders": { "query": { "content": "$1" } } }, "noJellyfinCredentials": { "message": "Credenziali di Jellyfin non configurate." },
"localOnPlex": { "message": "Su Plex" }, "notFoundOnJellyfin": { "message": "\"$query$\" non trovato su Jellyfin.", "placeholders": {"query": {"content": "$1"}}},
"searchOnPlex": { "message": "Cerca su Plex" }, "notFoundOnAnyServer": { "message": "\"$query$\" non trovato su nessun server.", "placeholders": {"query": {"content": "$1"}}},
"jellyfinTitle": { "message": "Contenuti Jellyfin" }, "localOnPlex": { "message": "Su Plex" },
"noJellyfinContent": { "message": "Nessun contenuto Jellyfin trovato." }, "searchOnPlex": { "message": "Cerca su Plex" },
"noJellyfinContentSub": { "message": "Assicurati di aver scansionato il tuo server Jellyfin nelle impostazioni." }, "jellyfinTitle": { "message": "Contenuti di Jellyfin" },
"activityViewerTitle": { "message": "Visualizzatore attività del server" }, "noJellyfinContent": { "message": "Nessun contenuto di Jellyfin trovato." },
"activitySelectServer": { "message": "Seleziona un server" }, "noJellyfinContentSub": { "message": "Assicurati di aver scansionato il tuo server Jellyfin nelle impostazioni." },
"activityCheckBtn": { "message": "Aggiorna" }, "activityViewerTitle": { "message": "Visualizzatore Attività del Server" },
"activityNoSessions": { "message": "Nessuna sessione attiva su questo server." }, "activitySelectServer": { "message": "Seleziona un server" },
"activitySessionUser": { "message": "Utente" }, "activityCheckBtn": { "message": "Aggiorna" },
"activitySessionDevice": { "message": "Dispositivo" }, "activityNoSessions": { "message": "Non ci sono sessioni attive su questo server." },
"activitySessionContent": { "message": "Contenuto" }, "activitySessionUser": { "message": "Utente" },
"activitySessionState": { "message": "Stato" }, "activitySessionDevice": { "message": "Dispositivo" },
"activitySessionIdentifier": { "message": "Identificatore del client" }, "activitySessionContent": { "message": "Contenuto" },
"activityCopyID": { "message": "Copia ID" }, "activitySessionState": { "message": "Stato" },
"activityError": { "message": "Impossibile ottenere l'attività del server." }, "activitySessionIdentifier": { "message": "Identificatore del Client" },
"activityCopied": { "message": "Identificatore copiato negli appunti!" }, "activityCopyID": { "message": "Copia ID" },
"activityCopyError": { "message": "Errore durante la copia dell'identificatore." }, "activityError": { "message": "Impossibile ottenere l'attività del server." },
"noProvidersFound": { "message": "Nessun provider trovato." }, "activityCopied": { "message": "Identificatore copiato negli appunti!" },
"availableOnPlex": { "message": "Disponibile su Plex" }, "activityCopyError": { "message": "Errore durante la copia dell'identificatore." },
"m3uGeneratorTitle": { "message": "Generatore di elenchi M3U" }, "noProvidersFound": { "message": "Nessun fornitore trovato." },
"selectAServer": { "message": "Seleziona un server..." }, "availableOnPlex": { "message": "Disponibile su Plex" },
"downloadM3u": { "message": "Scarica M3U" }, "m3uGeneratorTitle": { "message": "Generatore di Liste M3U" },
"m3uGenerator": { "message": "Generatore M3U" }, "selectAServer": { "message": "Seleziona un server..." },
"selectLibraries": { "message": "Seleziona librerie" }, "downloadM3u": { "message": "Scarica M3U" },
"howToUse": { "message": "Come usare" }, "m3uGenerator": { "message": "Generatore M3U" },
"m3uInstruction1": { "message": "Scegli un server dall'elenco." }, "selectLibraries": { "message": "Seleziona Librerie" },
"m3uInstruction2": { "message": "Seleziona una o più librerie da includere." }, "howToUse": { "message": "Come si Usa" },
"m3uInstruction3": { "message": "Fai clic sul pulsante di download." }, "m3uInstruction1": { "message": "Scegli un server dalla lista." },
"m3uInstruction4": { "message": "Importa il file .m3u nel tuo lettore compatibile." }, "m3uInstruction2": { "message": "Seleziona una o più librerie da includere." },
"chatOpen": { "message": "Apri chat" }, "m3uInstruction3": { "message": "Clicca sul pulsante di download." },
"chatTitle": { "message": "Assistente AI" }, "m3uInstruction4": { "message": "Importa il file .m3u nel tuo lettore compatibile." },
"chatClose": { "message": "X" }, "chatOpen": { "message": "Apri Chat" },
"chatPlaceholder": { "message": "Scrivi il tuo messaggio..." }, "chatTitle": { "message": "Assistente AI" },
"chatSend": { "message": "➤" }, "chatClose": { "message": "X" },
"chatWelcome": { "message": "Benvenuto! Sono il tuo assistente CinePlex. Chiedimi di film, serie o qualsiasi altra cosa tu voglia sapere." }, "chatPlaceholder": { "message": "Scrivi il tuo messaggio..." },
"chatGoogleApiKeyMissing": { "message": "La chiave API di Google Gemini non è configurata. Impostala nelle impostazioni dell'estensione per utilizzare l'assistente AI." }, "chatSend": { "message": "➤" },
"chatApiInvalidResponse": { "message": "L'API ha restituito una risposta non valida. Riprova." }, "chatWelcome": { "message": "Benvenuto! Sono il tuo assistente CinePlex. Chiedimi di film, serie o qualsiasi altra cosa tu voglia sapere." },
"chatApiError": { "message": "Errore di comunicazione con l'assistente AI" }, "chatGoogleApiKeyMissing": { "message": "La chiave API di Google Gemini non è configurata. Per favore, configurala nelle impostazioni dell'estensione per utilizzare l'assistente AI." },
"downloadAll": { "message": "Scarica tutto" }, "chatApiInvalidResponse": { "message": "L'API ha restituito una risposta non valida. Per favore, riprova." },
"download": { "message": "Scarica" }, "chatApiError": { "message": "Errore di comunicazione con l'assistente AI" },
"aiToolSearchLibraryDesc": { "message": "Cerca nella libreria Plex dell'utente film o serie per titolo." }, "downloadAll": { "message": "Scarica tutto" },
"aiToolSearchLibraryQueryParamDesc": { "message": "Il titolo del film o della serie da cercare." }, "download": { "message": "Scarica" },
"aiToolSearchLibraryTypeParamDesc": { "message": "Il tipo di contenuto da cercare. Può essere 'movie' per i film o 'series' per le serie. (Facoltativo)." }, "aiToolSearchLibraryDesc": { "message": "Cerca nella libreria Plex dell'utente film o serie per titolo." },
"aiToolSearchLibraryResolutionParamDesc": { "message": "La risoluzione video da cercare (ad es. '4k', '1080p'). (Facoltativo)." }, "aiToolSearchLibraryQueryParamDesc": { "message": "Il titolo del film o della serie da cercare." },
"aiToolSearchLibraryContainerParamDesc": { "message": "Il formato del contenitore video da cercare (ad es. 'mkv', 'mp4'). (Facoltativo)." }, "aiToolSearchLibraryTypeParamDesc": { "message": "Il tipo di contenuto da cercare. Può essere 'movie' per i film o 'series' per le serie. (Opzionale)." },
"aiToolNavigateToPageDesc": { "message": "Indirizza l'utente a una pagina specifica dell'interfaccia dell'applicazione." }, "aiToolSearchLibraryResolutionParamDesc": { "message": "La risoluzione video da cercare (es. '4k', '1080p'). (Opzionale)." },
"aiToolNavigateToPagePageParamDesc": { "message": "Il nome della pagina a cui navigare, ad es.: 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers' o 'm3u-generator'." }, "aiToolSearchLibraryContainerParamDesc": { "message": "Il formato contenitore del video da cercare (es. 'mkv', 'mp4'). (Opzionale)." },
"aiToolGetUserStatsDesc": { "message": "Recupera e visualizza le statistiche della libreria dell'utente, come il numero totale di film, serie e artisti unici." }, "aiToolNavigateToPageDesc": { "message": "Indirizza l'utente a una pagina specifica dell'interfaccia dell'applicazione." },
"aiToolShowItemDetailsDesc": { "message": "Visualizza la pagina dei dettagli di un film o di una serie specifica in base al titolo e al tipo." }, "aiToolNavigateToPagePageParamDesc": { "message": "Il nome della pagina a cui navigare, ad esempio: 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', 'm3u-generator' o 'music'." },
"aiToolShowItemDetailsTitleParamDesc": { "message": "Il titolo esatto del film o della serie." }, "aiToolGetUserStatsDesc": { "message": "Ottiene e visualizza le statistiche della libreria dell'utente, come il numero totale di film, serie e artisti unici." },
"aiToolShowItemDetailsTypeParamDesc": { "message": "Il tipo di contenuto. Deve essere 'movie' o 'series'." }, "aiToolShowItemDetailsDesc": { "message": "Mostra la pagina dei dettagli di un film o di una serie specifica tramite il suo titolo e tipo." },
"aiToolAddToPlaylistDesc": { "message": "Aggiunge un film o una serie alla playlist corrente dell'utente per lo streaming su un server PHP configurato." }, "aiToolShowItemDetailsTitleParamDesc": { "message": "Il titolo esatto del film o della serie." },
"aiToolAddToPlaylistTitleParamDesc": { "message": "Il titolo del film o della serie da aggiungere." }, "aiToolShowItemDetailsTypeParamDesc": { "message": "Il tipo di contenuto. Deve essere 'movie' o 'series'." },
"aiToolAddToPlaylistTypeParamDesc": { "message": "Il tipo di contenuto. Deve essere 'movie' o 'series'." }, "aiToolAddToPlaylistDesc": { "message": "Aggiunge un film o una serie alla playlist corrente dell'utente per lo streaming su un server PHP configurato." },
"aiToolCheckAndDownloadDesc": { "message": "Controlla la disponibilità di un elenco di titoli di film o serie sui server locali dell'utente e, se trovati, genera e scarica un file di playlist M3U con gli streaming trovati." }, "aiToolAddToPlaylistTitleParamDesc": { "message": "Il titolo del film o della serie da aggiungere." },
"aiToolCheckAndDownloadTitlesParamDesc": { "message": "Un array di titoli di film o serie da cercare e scaricare." }, "aiToolAddToPlaylistTypeParamDesc": { "message": "Il tipo di contenuto. Deve essere 'movie' o 'series'." },
"aiToolCheckAndDownloadTypeParamDesc": { "message": "Il tipo di contenuto dell'elenco. Deve essere 'movie' o 'series'." }, "aiToolDownloadSingleMovieM3UDesc": { "message": "Genera e scarica un file di playlist M3U per un singolo film disponibile localmente." },
"aiToolCheckAndDownloadFilenameParamDesc": { "message": "Il nome del file M3U da scaricare (ad es. 'MiaLista.m3u'). Se non fornito, verrà utilizzato un nome predefinito." }, "aiToolDownloadSingleMovieM3UTitleParamDesc": { "message": "Il titolo del film per cui verrà generato il file M3U." },
"aiToolToggleFavoriteDesc": { "message": "Aggiunge o rimuove un film o una serie dall'elenco dei preferiti dell'utente." }, "aiToolDownloadSingleMovieM3UYearParamDesc": { "message": "L'anno di uscita del film (opzionale, per maggiore precisione)." },
"aiToolToggleFavoriteTitleParamDesc": { "message": "Il titolo del film o della serie." }, "aiToolDownloadSeriesSeasonM3UDesc": { "message": "Genera e scarica un file di playlist M3U per una stagione specifica di una serie disponibile localmente." },
"aiToolToggleFavoriteTypeParamDesc": { "message": "Il tipo di contenuto. Deve essere 'movie' o 'series'." }, "aiToolDownloadSeriesSeasonM3UTitleParamDesc": { "message": "Il titolo della serie." },
"aiToolGetRecommendationsDesc": { "message": "Genera e visualizza un elenco di consigli di film o serie basati sulla cronologia di visualizzazione e sui preferiti dell'utente." }, "aiToolDownloadSeriesSeasonM3USeasonParamDesc": { "message": "Il numero della stagione da scaricare." },
"aiToolApplyFiltersDesc": { "message": "Applica filtri alla visualizzazione corrente di film o serie, consentendo di affinare i risultati per tipo, genere, anno e ordine di ordinamento." }, "aiToolDownloadSeriesSeasonM3UYearParamDesc": { "message": "L'anno di uscita della serie (opzionale)." },
"aiToolApplyFiltersTypeParamDesc": { "message": "Il tipo di contenuto a cui applicare i filtri. Deve essere 'movie' o 'series'." }, "aiToolCheckAndDownloadDesc": { "message": "Verifica la disponibilità di un elenco di titoli di film o serie sui server locali dell'utente e, se trovati, genera e scarica un file di playlist M3U con gli stream trovati." },
"aiToolApplyFiltersGenreParamDesc": { "message": "Il nome del genere per cui filtrare (ad es. 'Azione', 'Drammatico')." }, "aiToolCheckAndDownloadTitlesParamDesc": { "message": "Un array di titoli di film o serie da cercare e scaricare." },
"aiToolApplyFiltersYearParamDesc": { "message": "L'anno di uscita per cui filtrare (ad es. '2023')." }, "aiToolCheckAndDownloadTypeParamDesc": { "message": "Il tipo di contenuto dell'elenco. Deve essere 'movie' o 'series'." },
"aiToolApplyFiltersSortParamDesc": { "message": "Il criterio di ordinamento per i risultati. Valori validi: 'popularity.desc' (popolari), 'vote_average.desc' (più votati), 'release_date.desc' (recenti per i film) o 'first_air_date.desc' (recenti per le serie)." }, "aiToolCheckAndDownloadFilenameParamDesc": { "message": "Il nome del file M3U da scaricare (es. 'LaMiaLista.m3u'). Se non fornito, verrà utilizzato un nome predefinito." },
"aiToolPlayMusicByArtistDesc": { "message": "Apre il lettore musicale e avvia la riproduzione dei brani di un artista specifico dalla libreria dell'utente." }, "aiToolToggleFavoriteDesc": { "message": "Aggiunge o rimuove un film o una serie dalla lista dei preferiti dell'utente." },
"aiToolPlayMusicByArtistNameParamDesc": { "message": "Il nome esatto dell'artista di cui si desidera riprodurre i brani." }, "aiToolToggleFavoriteTitleParamDesc": { "message": "Il titolo del film o della serie." },
"aiToolClearChatHistoryDesc": { "message": "Cancella tutta la cronologia dei messaggi della conversazione corrente con l'assistente AI." }, "aiToolToggleFavoriteTypeParamDesc": { "message": "Il tipo di contenuto. Deve essere 'movie' o 'series'." },
"aiToolDeleteDatabaseDesc": { "message": "Elimina l'intero database locale dell'estensione, inclusi i contenuti scansionati, le impostazioni e i preferiti. Questa azione è irreversibile e ricaricherà l'applicazione." }, "aiToolGetRecommendationsDesc": { "message": "Genera e visualizza un elenco di raccomandazioni di film o serie basate sulla cronologia di visione e sui preferiti dell'utente." },
"aiToolUpdateAllTokensDesc": { "message": "Avvia una scansione completa di tutti i server e le librerie Plex associati ai token configurati nell'estensione. Aggiorna tutti i film, le serie, gli artisti e le foto." }, "aiToolApplyFiltersDesc": { "message": "Applica filtri alla vista corrente di film o serie, consentendo di affinare i risultati per tipo, genere, anno e ordine di classificazione." },
"aiToolAddPlexTokenDesc": { "message": "Aggiunge un nuovo token X-Plex alla configurazione dell'estensione, consentendo all'applicazione di scansionare i contenuti di nuovi server Plex." }, "aiToolApplyFiltersTypeParamDesc": { "message": "Il tipo di contenuto a cui applicare i filtri. Deve essere 'movie' o 'series'." },
"aiToolAddPlexTokenTokenParamDesc": { "message": "La stringa del token X-Plex da aggiungere." }, "aiToolApplyFiltersGenreParamDesc": { "message": "Il nome del genere per cui filtrare (es. 'Azione', 'Drammatico')." },
"aiToolChangeRegionDesc": { "message": "Modifica la regione utilizzata per la scoperta di contenuti nell'API TMDB. Ciò influirà sui risultati visualizzati nelle sezioni di film e serie, nonché sui provider di streaming." }, "aiToolApplyFiltersYearParamDesc": { "message": "L'anno di uscita per cui filtrare (es. '2023')." },
"aiToolChangeRegionRegionParamDesc": { "message": "Il codice paese ISO 3166-1 a due lettere per la nuova regione (ad es. 'US' per gli Stati Uniti, 'ES' per la Spagna, 'MX' per il Messico)." }, "aiToolApplyFiltersSortParamDesc": { "message": "Il criterio di ordinamento per i risultati. Valori validi: 'popularity.desc' (popolari), 'vote_average.desc' (più votati), 'release_date.desc' (recenti per i film) o 'first_air_date.desc' (recenti per le serie)." },
"aiToolClearAllFavoritesDesc": { "message": "Rimuove tutti i film e le serie che l'utente ha contrassegnato come preferiti." }, "aiToolListAvailableMusicGenresDesc": { "message": "Elenca tutti i generi musicali unici disponibili nella libreria locale dell'utente." },
"aiToolClearViewingHistoryDesc": { "message": "Cancella la cronologia di visualizzazione dell'utente dalla pagina della cronologia." }, "aiToolSearchMusicByGenreDesc": { "message": "Cerca artisti nella libreria musicale dell'utente che appartengono a un genere specifico." },
"aiToolClearRecommendationsViewDesc": { "message": "Svuota la vista dei consigli e rimuove i consigli memorizzati nella cache." }, "aiToolSearchMusicByGenreNameParamDesc": { "message": "Il nome del genere musicale da cercare (es. 'Rock', 'Pop', 'Jazz')." },
"aiToolSearchNotFound": { "message": "'$query' non trovato nella tua libreria.", "placeholders": { "query": { "content": "$1" } } }, "aiToolPlayMusicByArtistDesc": { "message": "Apre il lettore musicale e inizia a riprodurre le canzoni di un artista specifico dalla libreria dell'utente." },
"aiToolNavigateSuccess": { "message": "Navigato alla pagina $page$.", "placeholders": { "page": { "content": "$1" } } }, "aiToolPlayMusicByArtistNameParamDesc": { "message": "Il nome esatto dell'artista le cui canzoni si desidera riprodurre." },
"aiToolNavigateError": { "message": "Errore durante la navigazione alla pagina $page$.", "placeholders": { "page": { "content": "$1" } } }, "aiToolClearChatHistoryDesc": { "message": "Cancella l'intera cronologia dei messaggi della conversazione corrente con l'assistente AI." },
"aiToolStatsError": { "message": "Errore nel recupero delle statistiche." }, "aiToolDeleteDatabaseDesc": { "message": "Elimina l'intero database locale dell'estensione, inclusi i contenuti scansionati, le impostazioni e i preferiti. Questa azione è irreversibile e ricaricherà l'applicazione." },
"aiToolItemNotFound": { "message": "Elemento '$title' non trovato.", "placeholders": { "title": { "content": "$1" } } }, "aiToolUpdateAllTokensDesc": { "message": "Avvia una scansione completa di tutti i server e le librerie Plex associati ai token configurati nell'estensione. Aggiorna tutti i film, le serie, gli artisti e le foto." },
"aiToolShowItemDetailsSuccess": { "message": "Visualizzazione dei dettagli di '$title'.", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenDesc": { "message": "Aggiunge un nuovo token X-Plex alla configurazione dell'estensione, consentendo all'applicazione di scansionare contenuti da nuovi server Plex." },
"aiToolAddToPlaylistSuccess": { "message": "Aggiunto '$title' alla playlist.", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenTokenParamDesc": { "message": "La stringa del token X-Plex da aggiungere." },
"aiToolFavoriteAdded": { "message": "Aggiunto '$title' ai preferiti.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionDesc": { "message": "Cambia la regione utilizzata per la scoperta di contenuti nell'API di TMDB. Ciò influenzerà i risultati visualizzati nelle sezioni di film e serie, nonché i fornitori di streaming." },
"aiToolFavoriteRemoved": { "message": "Rimosso '$title' dai preferiti.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionRegionParamDesc": { "message": "Il codice paese ISO 3166-1 a due lettere per la nuova regione (es. 'US' per Stati Uniti, 'IT' per Italia, 'ES' per Spagna)." },
"aiToolRecommendationsSuccess": { "message": "Visualizzazione dei consigli." }, "aiToolClearAllFavoritesDesc": { "message": "Rimuove tutti i film e le serie che l'utente ha contrassegnato come preferiti." },
"aiToolApplyFiltersGenreNotFound": { "message": "Genere '$genre' non trovato.", "placeholders": { "genre": { "content": "$1" } } }, "aiToolClearViewingHistoryDesc": { "message": "Cancella la cronologia di visione dell'utente dalla pagina della cronologia." },
"aiToolApplyFiltersSuccess": { "message": "Filtri applicati correttamente." }, "aiToolClearRecommendationsViewDesc": { "message": "Pulisce la vista delle raccomandazioni e rimuove le raccomandazioni memorizzate nella cache." },
"aiToolPlayMusicNotReady": { "message": "Il lettore musicale non è pronto. Assicurati che la tua libreria musicale di Plex sia stata scansionata." }, "aiToolSearchNotFound": { "message": "Impossibile trovare '$query' nella tua libreria.", "placeholders": { "query": { "content": "$1" } } },
"aiToolPlayMusicArtistNotFound": { "message": "Artista '$artist_name' non trovato.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolNavigateSuccess": { "message": "Navigato alla pagina $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolPlayMusicNoSongs": { "message": "Nessun brano trovato per '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolNavigateError": { "message": "Errore durante la navigazione alla pagina $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolPlayMusicSuccess": { "message": "Riproduzione di musica di '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolStatsError": { "message": "Errore nel recupero delle statistiche." },
"aiToolChatHistoryCleared": { "message": "Cronologia della chat cancellata." }, "aiToolItemNotFound": { "message": "Elemento '$title' non trovato.", "placeholders": { "title": { "content": "$1" } } },
"aiToolConfirmDeleteDatabase": { "message": "Sei sicuro di voler eliminare il database locale? Questa azione è irreversibile." }, "aiToolShowItemDetailsSuccess": { "message": "Mostrando i dettagli di '$title'.", "placeholders": { "title": { "content": "$1" } } },
"aiToolDeleteDatabaseCancelled": { "message": "Eliminazione del database annullata." }, "aiToolAddToPlaylistSuccess": { "message": "Aggiunto '$title' alla playlist.", "placeholders": { "title": { "content": "$1" } } },
"aiToolExecutionError": { "message": "Errore durante l'esecuzione dello strumento '$toolName$': $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolFavoriteAdded": { "message": "Aggiunto '$title' ai preferiti.", "placeholders": { "title": { "content": "$1" } } },
"aiToolUnknown": { "message": "Strumento sconosciuto: '$toolName$'.", "placeholders": { "toolName": { "content": "$1" } } }, "aiToolFavoriteRemoved": { "message": "Rimosso '$title' dai preferiti.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoritesCleared": { "message": "Preferiti cancellati." }, "aiToolRecommendationsSuccess": { "message": "Mostrando le raccomandazioni." },
"aiToolFavoritesClearError": { "message": "Errore durante la cancellazione dei preferiti: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolApplyFiltersGenreNotFound": { "message": "Genere '$genre' non trovato.", "placeholders": { "genre": { "content": "$1" } } },
"aiToolRecommendationsCleared": { "message": "Consigli cancellati." }, "aiToolApplyFiltersSuccess": { "message": "Filtri applicati correttamente." },
"aiToolRecommendationsClearError": { "message": "Errore durante la cancellazione dei consigli: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolSearchMusicByGenreNotFound": { "message": "Non ho trovato artisti del genere '$genre_name' nella tua libreria.", "placeholders": { "genre_name": { "content": "$1" } } },
"aiToolDatabaseDeleted": { "message": "Database eliminato. La pagina verrà ricaricata." }, "aiToolPlayMusicNotReady": { "message": "Il lettore musicale non è pronto. Assicurati che la tua libreria musicale di Plex sia stata scansionata." },
"aiToolDatabaseDeleteError": { "message": "Errore durante l'eliminazione del database: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolPlayMusicArtistNotFound": { "message": "Artista '$artist_name' non trovato.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolDatabaseDeleteBlocked": { "message": "L'eliminazione del database è bloccata. Chiudi le altre schede dell'applicazione." }, "aiToolPlayMusicNoSongs": { "message": "Nessuna canzone trovata per '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolUpdateAllTokensSuccess": { "message": "Tutti i token sono stati aggiornati correttamente." }, "aiToolPlayMusicSuccess": { "message": "Riproduzione di musica di '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolUpdateAllTokensError": { "message": "Errore durante l'aggiornamento dei token: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolChatHistoryCleared": { "message": "Cronologia chat cancellata." },
"aiToolAddPlexTokenSuccess": { "message": "Token Plex aggiunto correttamente." }, "aiToolConfirmDeleteDatabase": { "message": "Sei sicuro di voler eliminare il database locale? Questa azione è irreversibile." },
"aiToolAddPlexTokenError": { "message": "Errore durante l'aggiunta del token Plex: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolDeleteDatabaseCancelled": { "message": "Eliminazione del database annullata." },
"aiToolChangeRegionSuccess": { "message": "Regione modificata in $region$. Il contenuto è in fase di aggiornamento.", "placeholders": { "region": { "content": "$1" } } }, "aiToolExecutionError": { "message": "Errore durante l'esecuzione dello strumento '$toolName': $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } },
"aiToolChangeRegionError": { "message": "Errore durante la modifica della regione: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolUnknown": { "message": "Strumento sconosciuto: '$toolName'.", "placeholders": { "toolName": { "content": "$1" } } },
"aiToolViewingHistoryCleared": { "message": "Cronologia visualizzazioni cancellata." }, "aiToolFavoritesCleared": { "message": "Preferiti eliminati." },
"aiToolViewingHistoryClearError": { "message": "Errore durante la cancellazione della cronologia di visualizzazione: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolFavoritesClearError": { "message": "Errore durante l'eliminazione dei preferiti: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiSystemPrompt_v3": { "message": "Sei un assistente esperto di film e serie chiamato CinePlex. La tua funzione principale è aiutare gli utenti a scoprire contenuti e a interagire con la loro libreria. Segui rigorosamente queste regole: 1. **NON FINGERE MAI** di aver eseguito un'azione se non hai utilizzato uno strumento per farlo. Ad esempio, non dire 'Ho scaricato X' se non hai utilizzato lo strumento di download. 2. Per le richieste di consigli o elenchi (ad es. 'dimmi 5 film dell'orrore'), usa le tue conoscenze per generare l'elenco. Presentalo in formato numerato o puntato. Dopo aver visualizzato l'elenco, chiedi proattivamente all'utente se desidera che tu verifichi la disponibilità sui suoi server locali e crei un file M3U. 3. **SOLO** se l'utente conferma di voler controllare o scaricare l'elenco, utilizza lo strumento `check_and_download_titles_list`. Non utilizzarlo senza una conferma esplicita. 4. Per qualsiasi altra azione come la navigazione, l'ottenimento di statistiche, la ricerca di un titolo specifico o il filtraggio per risoluzione o contenitore, utilizza gli strumenti appropriati. Sii sempre conciso, amichevole ed efficiente." }, "aiToolRecommendationsCleared": { "message": "Raccomandazioni eliminate." },
"aiToolM3UNoTitlesProvided": { "message": "Fornisci un elenco di titoli per creare la playlist." }, "aiToolRecommendationsClearError": { "message": "Errore durante l'eliminazione delle raccomandazioni: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolM3UCheckingTitles": { "message": "Controllo dei titoli sui tuoi server locali..." }, "aiToolDatabaseDeleted": { "message": "Database eliminato. La pagina verrà ricaricata." },
"aiToolM3UNoLocalMatchesForDownload": { "message": "Non ho trovato nessuno dei film o delle serie dell'elenco sui tuoi server locali." }, "aiToolDatabaseDeleteError": { "message": "Errore durante l'eliminazione del database: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolM3UDownloadStarted": { "message": "Fatto! Ho trovato $1 dei $2 titoli sui tuoi server e ho avviato il download della playlist M3U.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } }, "aiToolDatabaseDeleteBlocked": { "message": "L'eliminazione del database è bloccata. Chiudi altre schede dell'applicazione." },
"backToProviders": { "message": "Torna ai provider" }, "aiToolUpdateAllTokensSuccess": { "message": "Tutti i token sono stati aggiornati correttamente." },
"artistsCounterSingle": { "message": "$total$ artista", "placeholders": { "total": { "content": "$1" } } }, "aiToolUpdateAllTokensError": { "message": "Errore durante l'aggiornamento dei token: $message$", "placeholders": { "message": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Caricamento..." }, "aiToolAddPlexTokenSuccess": { "message": "Token di Plex aggiunto correttamente." },
"downloadingSong": { "message": "Avvio del download di \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenError": { "message": "Errore durante l'aggiunta del token di Plex: $message$", "placeholders": { "message": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" scaricato.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionSuccess": { "message": "Regione cambiata in $region$. Il contenuto è in fase di aggiornamento.", "placeholders": { "region": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Errore durante il download di \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionError": { "message": "Errore durante il cambio di regione: $message$", "placeholders": { "message": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Generazione di M3U per \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } }, "aiToolViewingHistoryCleared": { "message": "Cronologia di visione cancellata." },
"albumM3UGenerated": { "message": "M3U per l'album \"$artist$\" generato.", "placeholders": { "artist": { "content": "$1" } } }, "aiToolViewingHistoryClearError": { "message": "Errore durante la cancellazione della cronologia di visione: $message$", "placeholders": { "message": { "content": "$1" } } },
"retyingSection": { "message": "Nuovo tentativo per la sezione \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolM3UDownloadStartedSingle": { "message": "Avvio del download del file M3U per '$movie_title'.", "placeholders": { "movie_title": { "content": "$1" } } },
"retrySuccess": { "message": "[SUCCESSO] Nuovo tentativo per \"$title$\" completato.", "placeholders": { "title": { "content": "$1" } } }, "aiToolM3UDownloadStartedSeason": { "message": "Avvio del download del file M3U per la stagione $1 di '$2'.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } },
"retryError": { "message": "[ERRORE FINALE] Tentativo fallito per \"$title$\": $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolM3UNoTitlesProvided": { "message": "Per favore, fornisci un elenco di titoli per creare la playlist." },
"startingRetryPhase": { "message": "Avvio della fase di tentativi ripetuti per $count$ sezioni...", "placeholders": { "count": { "content": "$1" } } }, "aiToolM3UCheckingTitles": { "message": "Controllo dei titoli sui tuoi server locali..." },
"tokenFoundServers": { "message": "Token $token$... ha trovato $count$ server.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } }, "aiToolM3UNoLocalMatchesForDownload": { "message": "Non ho trovato nessuno dei film o delle serie dell'elenco sui tuoi server locali." },
"errorProcessingToken": { "message": "Errore durante l'elaborazione del token $token$...: $message$", "placeholders": { "token": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolM3UDownloadStarted": { "message": "Fatto! Ho trovato $1 dei $2 titoli sui tuoi server e ho avviato il download della playlist M3U.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } },
"plexScanFatalError": { "message": "ERRORE FATALE: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolTrailerNotFoundSpecific": { "message": "Mi dispiace, non sono riuscito a trovare un trailer disponibile per '$title'.", "placeholders": { "title": { "content": "$1" } } },
"errorDuringScan": { "message": "Errore durante la scansione: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiSystemPrompt_v4": {
"stoppingPlexScan": { "message": "Arresto della scansione di Plex..." }, "message": "Sei un assistente virtuale integrato in un'estensione di Chrome che interagisce con i server Plex e Jellyfin. La tua funzione principale è aiutare l'utente a cercare, gestire, riprodurre e scaricare contenuti multimediali, oltre a gestire impostazioni personalizzate.\n\nMASSIMA PRIORITÀ: Ogni volta che la domanda dell'utente si riferisce a contenuti multimediali (film, serie, musica), DEVI presumere che si riferisca alla sua libreria locale. Utilizza gli strumenti per cercare nel suo database PRIMA di cercare sul web.\n\n🎯 Regole generali di comportamento:\nRispondi sempre in modo chiaro, conciso e diretto. Sii proattivo e fornisci tutte le informazioni pertinenti in una sola volta per evitare domande di follow-up. Ad esempio, quando confermi la disponibilità di una serie, includi i dettagli delle stagioni.\n\nConfronta la data attuale con i risultati di ricerca di Google quando ti vengono richieste informazioni esterne per garantire che siano aggiornate.\n\nUsa i nomi esatti dei comandi definiti nel sistema (function.name) quando chiami gli strumenti.\n\n📦 Funzioni chiave per i contenuti multimediali:\nPer generare un M3U per un singolo film, usa download_single_movie_m3u.\nPer scaricare una stagione specifica di una serie, usa download_series_season_m3u.\nPer più titoli (film o serie complete), usa sempre check_and_download_titles_list.\nPer cercare contenuti locali: search_library.\nPer cercare su TMDB: search_tmdb_content.\nPer contenuti di tendenza: get_trending_content.\nPer mostrare i dettagli di un titolo: show_item_details.\nPer aggiungere alla playlist PHP: add_to_playlist.\nPer verificare la disponibilità locale: check_local_availability.\nSe una serie è disponibile localmente, informa di quante stagioni ci sono e su quali server usando get_local_series_seasons.\nPer vedere le raccomandazioni: get_recommendations.\nPer applicare filtri: apply_filters.\nPer visualizzare la cronologia o i preferiti: view_history, view_favorites.\nPer contrassegnare come preferito: toggle_favorite.\nPer riprodurre il trailer: play_trailer.\n\n🎵 Funzioni musicali:\nSe l'utente chiede raccomandazioni generali su generi musicali (es. 'raccomandami un genere per tirarmi su di morale'), usa prima list_available_music_genres per vedere quali generi ha e basa la tua raccomandazione su quella lista.\nPer elencare tutti i generi musicali disponibili nella libreria: list_available_music_genres.\nPer cercare artisti per genere: search_music_by_genre.\nPer riprodurre canzoni per titolo e/o artista: play_song.\nPer riprodurre musica di un artista: play_music_by_artist.\n\n🧰 Funzioni di gestione e configurazione:\nPer ottenere le statistiche dell'utente: get_user_stats.\nPer navigare a sezioni specifiche: navigate_to_page.\nPer aggiornare i token: update_all_tokens, add_plex_token.\nPer cambiare la regione dei contenuti: change_region.\nPer esportare o importare il database locale: export_local_database, import_local_database.\nPer eliminare il database: delete_database.\nPer cancellare preferiti, cronologia o raccomandazioni: clear_all_favorites, clear_viewing_history, clear_recommendations_view.\nPer attivare/disattivare la modalità chiara/scura: toggle_light_mode.\nPer mostrare o nascondere la sezione hero: toggle_hero_section.\n\n⚠ Considerazioni aggiuntive:\nDai priorità ai contenuti disponibili localmente. Usa check_local_availability prima di mostrare le opzioni di riproduzione o download.\nSe un'azione fallisce, segnalalo in modo chiaro e diretto.\nEvita di ripetere inutilmente la richiesta dell'utente, a meno che non aiuti a contestualizzare la risposta."
"invalidTokenProvided": { "message": "Token non valido fornito." }, },
"tokenAlreadyExists": { "message": "Il token esiste già." }, "backToProviders": { "message": "Torna ai Fornitori" },
"tokenAddedSuccessfully": { "message": "Token aggiunto correttamente." }, "artistsCounterSingle": { "message": "$total$ Artista", "placeholders": { "total": { "content": "$1" } } },
"noStreamsFoundForSelection": { "message": "Nessuno streaming trovato per la selezione." }, "artistsCounterLoading": { "message": "Caricamento in corso..." },
"autoplayBlocked": { "message": "Riproduzione automatica bloccata." }, "downloadingSong": { "message": "Avvio del download di \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"page": { "message": "Pagina" }, "songDownloaded": { "message": "\"$title$\" scaricato.", "placeholders": { "title": { "content": "$1" } } },
"all": { "message": "Tutti" }, "errorDownloadingSong": { "message": "Errore durante il download di \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"userScore": { "message": "Punteggio degli utenti" }, "generatingAlbumM3U": { "message": "Generazione M3U per \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
"duration": { "message": "Durata" }, "albumM3UGenerated": { "message": "M3U per l'album \"$artist$\" generato.", "placeholders": { "artist": { "content": "$1" } } },
"min": { "message": "Min" }, "retyingSection": { "message": "Riprovo la sezione \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"max": { "message": "Max" } "retrySuccess": { "message": "[SUCCESSO] Riprova di \"$title$\" completata.", "placeholders": { "title": { "content": "$1" } } },
"retryError": { "message": "[ERRORE FINALE] Riprova per \"$title$\" fallita: $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } },
"startingRetryPhase": { "message": "Avvio della fase di ritentativi per $count$ sezioni...", "placeholders": { "count": { "content": "$1" } } },
"tokenFoundServers": { "message": "Token $token$... ha trovato $count$ server.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } },
"errorProcessingToken": { "message": "Errore nell'elaborazione del token $token$...: $message$", "placeholders": { "token": { "content": "$1" }, "message": { "content": "$2" } } },
"plexScanFatalError": { "message": "ERRORE FATALE: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorDuringScan": { "message": "Errore durante la scansione: $message$", "placeholders": { "message": { "content": "$1" } } },
"stoppingPlexScan": { "message": "Interruzione scansione Plex..." },
"invalidTokenProvided": { "message": "Token fornito non valido." },
"tokenAlreadyExists": { "message": "Il token esiste già." },
"tokenAddedSuccessfully": { "message": "Token aggiunto correttamente." },
"noStreamsFoundForSelection": { "message": "Nessun stream trovato per la selezione." },
"autoplayBlocked": { "message": "Riproduzione automatica bloccata." },
"welcomeToCinePlex": { "message": "" },
"page": { "message": "Pagina" },
"all": { "message": "Tutto" },
"userScore": { "message": "Punteggio" },
"duration": { "message": "Durata" },
"min": { "message": "Min" },
"max": { "message": "Max" },
"aiToolFindStreamingProvidersDesc": { "message": "Trova dove guardare un film o una serie sui servizi di streaming." },
"aiToolFindStreamingProvidersTitleParamDesc": { "message": "Il titolo del film o della serie da cercare." },
"aiToolFindStreamingProvidersTypeParamDesc": { "message": "Il tipo di contenuto (film o serie)." },
"aiToolFindStreamingProvidersYearParamDesc": { "message": "L'anno di uscita del contenuto (opzionale)." },
"aiToolNoStreamingProviders": { "message": "Nessun fornitore di streaming trovato per {title}." },
"aiToolStreamingProvidersFound": { "message": "{title} è disponibile sui seguenti servizi: {providers}." },
"aiToolStreamingProviderError": { "message": "Errore durante la ricerca dei fornitori di streaming: {message}." },
"aiToolGetLocalSeriesSeasonsDesc": { "message": "Verifica se una serie TV è disponibile localmente e restituisce un resoconto dettagliato delle stagioni disponibili su ciascun server." },
"aiToolGetLocalSeriesSeasonsTitleParamDesc": { "message": "Il titolo della serie da verificare." },
"aiToolGetLocalSeriesSeasonsYearParamDesc": { "message": "L'anno di uscita della serie (opzionale per maggiore precisione)." },
"aiToolLocalSeriesNoSeasons": { "message": "La serie '$series_title' è nella tua libreria, ma non sono stati trovati dettagli sulle stagioni.", "placeholders": { "series_title": { "content": "$1" } } },
"artist": { "message": "Artista" },
"tracks": { "message": "tracce" },
"noSongsFound": { "message": "Nessuna canzone trovata per questo artista." },
"durationMin": { "message": "Durata (Min)" },
"score": { "message": "Punteggio" },
"searchGenre": { "message": "Cerca genere..." },
"searchArtists": { "message": "Cerca artisti..." },
"preparingMusicLibrary": { "message": "Preparazione della tua libreria musicale in corso..." },
"preparingMusicLibraryDesc": { "message": "Questo processo una tantum potrebbe richiedere alcuni minuti se hai molti artisti." },
"artistsProgress": { "message": "0 / 0 artisti" },
"starting": { "message": "Avvio in corso..." },
"artistName": { "message": "Nome Artista" },
"playPause": { "message": "Riproduci/Pausa" },
"noLocalFilesFound": { "message": "Nessun file locale trovato per questo titolo." },
"server": { "message": "Server" },
"title": { "message": "Titolo" },
"year": { "message": "Anno" },
"resolution": { "message": "Risoluzione" },
"size": { "message": "Dimensione" },
"container": { "message": "Contenitore" },
"action": { "message": "Azione" },
"generate": { "message": "Genera" },
"availableLocalFiles": { "message": "File Locali Disponibili" },
"downloadSeason": { "message": "Scarica Stagione" },
"errorLoadingServersM3u": { "message": "Errore durante il caricamento dei server per il generatore M3U:" },
"errorFetchingLibraries": { "message": "Errore durante il recupero delle librerie." },
"selectServerAndLibrary": { "message": "Seleziona un server e almeno una libreria." },
"generating": { "message": "Generazione in corso..." },
"errorProcessingLibrary": { "message": "Errore durante l'elaborazione della libreria" },
"errorProcessingLibrarySkipping": { "message": "Errore durante l'elaborazione della libreria. Salto." },
"allLibrariesFailed": { "message": "Tutte le librerie selezionate non sono state elaborate." },
"m3uGeneratedWithErrors": { "message": "M3U generato con alcuni errori. Alcune librerie potrebbero mancare." },
"m3uDownloadedSuccess": { "message": "Playlist M3U scaricata con successo." },
"errorGeneratingM3uFile": { "message": "Errore durante la generazione del file M3U." },
"chatSources": { "message": "Fonti" },
"chatUnnamedSource": { "message": "Fonte senza nome" },
"googleApiFailure": { "message": "Chiamata API di Google AI fallita:" }
} }

View File

@ -1,449 +1,516 @@
{ {
"appName": { "message": "CinePlex" }, "appName": { "message": "CinePlex" },
"appDescription": { "message": "Examina servidores Plex em busca de conteúdo e o exibe na interface" }, "appDescription": { "message": "Escaneia servidores Plex para encontrar conteúdo e o exibe na interface" },
"appTagline": { "message": "Filmes, Séries e Música" }, "appTagline": { "message": "Filmes, Séries e Música" },
"appLocaleCode": { "message": "pt-BR" }, "appLocaleCode": { "message": "pt-BR" },
"toggleNavigation": { "message": "Alternar navegação" }, "toggleNavigation": { "message": "Alternar Navegação" },
"searchPlaceholder": { "message": "Pesquisar filmes ou séries..." }, "searchPlaceholder": { "message": "Buscar filmes ou séries..." },
"openMusicPlayer": { "message": "Abrir reprodutor de música" }, "openMusicPlayer": { "message": "Abrir Player de Música" },
"settings": { "message": "Configurações" }, "settings": { "message": "Configurações" },
"navMovies": { "message": "Filmes" }, "navMovies": { "message": "Filmes" },
"navSeries": { "message": "Séries" }, "navSeries": { "message": "Séries" },
"navProviders": { "message": "Provedores" }, "navProviders": { "message": "Provedores" },
"navPhotos": { "message": "Fotos" }, "navPhotos": { "message": "Fotos" },
"navStats": { "message": "Estatísticas" }, "navStats": { "message": "Estatísticas" },
"navFavorites": { "message": "Favoritos" }, "navFavorites": { "message": "Favoritos" },
"navHistory": { "message": "Histórico" }, "navHistory": { "message": "Histórico" },
"navRecommendations": { "message": "Recomendações" }, "navRecommendations": { "message": "Recomendações" },
"navMusic": { "message": "Música" }, "navMusic": { "message": "Música" },
"navM3uGenerator": { "message": "Gerador de M3U" }, "musicFeaturedPlaylists": { "message": "Playlists em Destaque" },
"heroWelcome": { "message": "" }, "musicRecentlyAdded": { "message": "Adicionado Recentemente" },
"heroSubtitle": { "message": "Explore milhares de filmes e séries." }, "navM3uGenerator": { "message": "Gerador M3U" },
"addStream": { "message": "Adicionar stream" }, "heroWelcome": { "message": "" },
"moreInfo": { "message": "Mais informações" }, "heroSubtitle": { "message": "Explore milhares de filmes e séries." },
"popularMovies": { "message": "Filmes populares" }, "addStream": { "message": "Adicionar Stream" },
"allGenres": { "message": "Todos os gêneros" }, "moreInfo": { "message": "Mais informações" },
"allYears": { "message": "Todos os anos" }, "popularMovies": { "message": "Filmes Populares" },
"sortPopular": { "message": "Mais populares" }, "allGenres": { "message": "Todos os gêneros" },
"sortTopRated": { "message": "Mais bem avaliados" }, "allYears": { "message": "Todos os anos" },
"sortRecent": { "message": "Mais recentes" }, "sortPopular": { "message": "Mais populares" },
"loadMore": { "message": "Carregar mais" }, "sortTopRated": { "message": "Melhor avaliados" },
"photosBreadcrumbHome": { "message": "Álbuns" }, "sortRecent": { "message": "Mais recentes" },
"selectServer": { "message": "Selecione um servidor" }, "loadMore": { "message": "Carregar mais" },
"loading": { "message": "Carregando..." }, "photosBreadcrumbHome": { "message": "Álbuns" },
"loadingLibraries": { "message": "Carregando bibliotecas..." }, "selectServer": { "message": "Selecione um servidor" },
"photosEmptyState": { "message": "Nenhum álbum ou foto encontrado." }, "loading": { "message": "Carregando..." },
"photosEmptyStateSub": { "message": "Selecione um servidor ou verifique se você tem uma biblioteca de fotos no Plex." }, "loadingLibraries": { "message": "Carregando bibliotecas..." },
"statsTitle": { "message": "Estatísticas da biblioteca" }, "photosEmptyState": { "message": "Nenhum álbum ou foto encontrado." },
"statsAllTokens": { "message": "Todos os tokens" }, "photosEmptyStateSub": { "message": "Por favor, selecione um servidor ou certifique-se de ter uma biblioteca de fotos no Plex." },
"statsAnalyzing": { "message": "Analisando sua biblioteca..." }, "statsTitle": { "message": "Estatísticas da Biblioteca" },
"statsActiveTokens": { "message": "Tokens ativos" }, "statsAllTokens": { "message": "Todos os Tokens" },
"statsServersFound": { "message": "Servidores encontrados" }, "statsAnalyzing": { "message": "Analisando sua biblioteca..." },
"statsUniqueMovies": { "message": "Filmes únicos" }, "statsActiveTokens": { "message": "Tokens Ativos" },
"statsUniqueSeries": { "message": "Séries únicas" }, "statsServersFound": { "message": "Servidores Encontrados" },
"statsUniqueArtists": { "message": "Artistas únicos" }, "statsUniqueMovies": { "message": "Filmes Únicos" },
"statsTokenServers": { "message": "Servidores de token" }, "statsUniqueSeries": { "message": "Séries Únicas" },
"statsChartMoviesByGenre": { "message": "Conteúdo por gênero (Filmes)" }, "statsUniqueArtists": { "message": "Artistas Únicos" },
"statsChartSeriesByGenre": { "message": "Conteúdo por gênero (Séries)" }, "statsTokenServers": { "message": "Servidores do Token" },
"statsChartByDecade": { "message": "Conteúdo por década" }, "statsChartMoviesByGenre": { "message": "Conteúdo por Gênero (Filmes)" },
"recommendationsTitle": { "message": "Recomendações para você" }, "statsChartSeriesByGenre": { "message": "Conteúdo por Gênero (Séries)" },
"historyTitle": { "message": "Histórico de visualização" }, "statsChartByDecade": { "message": "Conteúdo por Década" },
"clearHistory": { "message": "Limpar tudo" }, "recommendationsTitle": { "message": "Recomendações para você" },
"consoleTitle": { "message": "Console de verificação do Plex" }, "historyTitle": { "message": "Histórico de Visualização" },
"footerCredit": { "message": "Uma interface para o seu universo Plex." }, "clearHistory": { "message": "Limpar Tudo" },
"closeTrailer": { "message": "Fechar trailer" }, "consoleTitle": { "message": "Console de Escaneamento Plex" },
"close": { "message": "Fechar" }, "footerCredit": { "message": "Uma interface para o seu universo Plex." },
"photoViewer": { "message": "Visualizador de fotos" }, "closeTrailer": { "message": "Fechar trailer" },
"previous": { "message": "Anterior" }, "close": { "message": "Fechar" },
"next": { "message": "Próximo" }, "photoViewer": { "message": "Visualizador de fotos" },
"notificationTemplateText": { "message": "Notificação" }, "previous": { "message": "Anterior" },
"settingsTitleFull": { "message": "Configurações e ajustes" }, "next": { "message": "Próximo" },
"settingsTabGeneral": { "message": "Geral" }, "notificationTemplateText": { "message": "Notificação" },
"settingsTabPlex": { "message": "Plex" }, "settingsTitleFull": { "message": "Configurações e Ajustes" },
"settingsTabJellyfin": { "message": "Jellyfin" }, "settingsTabGeneral": { "message": "Geral" },
"settingsTabPhpGen": { "message": "Gerador de PHP" }, "settingsTabPlex": { "message": "Plex" },
"settingsTabData": { "message": "Dados" }, "settingsTabJellyfin": { "message": "Jellyfin" },
"settingsApiServer": { "message": "Configurações de API e servidor" }, "settingsTabPhpGen": { "message": "Gerador PHP" },
"settingsTmdbApiLabel": { "message": "Chave de API do TMDB (opcional)" }, "settingsTabData": { "message": "Dados" },
"settingsTmdbApiPlaceholder": { "message": "A chave padrão será usada se o campo for deixado em branco" }, "settingsApiServer": { "message": "Configuração de API e Servidor" },
"settingsGoogleApiLabel": { "message": "Chave de API do Google Gemini (opcional)" }, "settingsTmdbApiLabel": { "message": "Chave de API do TMDB (Opcional)" },
"settingsGoogleApiPlaceholder": { "message": "Necessária para usar o assistente de IA" }, "settingsTmdbApiPlaceholder": { "message": "A chave padrão será usada se deixado em branco" },
"settingsRegionLabel": { "message": "Região para descoberta de conteúdo" }, "settingsGoogleApiLabel": { "message": "Chave de API do Google Gemini (Opcional)" },
"allRegions": { "message": "Todas as regiões" }, "settingsGoogleApiPlaceholder": { "message": "Necessária para usar o assistente de IA" },
"settingsPhpUrlLabel": { "message": "URL do servidor para adicionar streams" }, "settingsRegionLabel": { "message": "Região para descoberta de conteúdo" },
"settingsPhpUrlPlaceholder": { "message": "https://seu-servidor.com/caminho/para/script.php" }, "allRegions": { "message": "Todas as regiões" },
"settingsInterface": { "message": "Interface" }, "settingsPhpUrlLabel": { "message": "URL do Servidor para Adicionar Streams" },
"settingsLightTheme": { "message": "Modo claro" }, "settingsPhpUrlPlaceholder": { "message": "https://seu-servidor.com/caminho/para/script.php" },
"settingsShowHero": { "message": "Mostrar seção de boas-vindas 'Hero'" }, "settingsInterface": { "message": "Interface" },
"settingsScanContent": { "message": "Verificação de conteúdo" }, "settingsLightTheme": { "message": "Modo Claro" },
"settingsScanDesc": { "message": "Selecione o que verificar e pressione o botão." }, "settingsShowHero": { "message": "Mostrar seção de boas-vindas 'Hero'" },
"settingsScanMovies": { "message": "Filmes" }, "settingsScanContent": { "message": "Escaneamento de Conteúdo" },
"settingsScanShows": { "message": "Séries" }, "settingsScanDesc": { "message": "Selecione o que escanear e pressione o botão." },
"settingsScanArtists": { "message": "Música" }, "settingsScanMovies": { "message": "Filmes" },
"settingsScanPhotos": { "message": "Fotos" }, "settingsScanShows": { "message": "Séries" },
"settingsSelectAll": { "message": "Selecionar tudo" }, "settingsScanArtists": { "message": "Música" },
"settingsStartScan": { "message": "Iniciar verificação" }, "settingsScanPhotos": { "message": "Fotos" },
"settingsPlexTokens": { "message": "Tokens do Plex" }, "settingsSelectAll": { "message": "Selecionar Tudo" },
"settingsPlexTokensDesc": { "message": "Edite a lista de tokens do Plex (formato JSON)." }, "settingsStartScan": { "message": "Iniciar Escaneamento" },
"settingsSaveTokens": { "message": "Salvar tokens" }, "settingsPlexTokens": { "message": "Tokens do Plex" },
"settingsJellyfinTitle": { "message": "Configurações do Jellyfin" }, "settingsPlexTokensDesc": { "message": "Edite a lista de tokens do Plex (formato JSON)." },
"settingsJellyfinDesc": { "message": "Adicione os detalhes do seu servidor Jellyfin para verificar o conteúdo dele." }, "settingsSaveTokens": { "message": "Salvar Tokens" },
"jellyfinUrlLabel": { "message": "URL do servidor Jellyfin" }, "settingsJellyfinTitle": { "message": "Configuração do Jellyfin" },
"jellyfinUserLabel": { "message": "Nome de usuário" }, "settingsJellyfinDesc": { "message": "Adicione os dados do seu servidor Jellyfin para escanear seu conteúdo." },
"jellyfinPassLabel": { "message": "Senha" }, "jellyfinUrlLabel": { "message": "URL do Servidor Jellyfin" },
"jellyfinConnectAndScan": { "message": "Conectar e verificar" }, "jellyfinUserLabel": { "message": "Nome de Usuário" },
"settingsPhpGenTitle": { "message": "Gerador de script PHP para servidor" }, "jellyfinPassLabel": { "message": "Senha" },
"settingsPhpFileOptions": { "message": "Opções de arquivo" }, "jellyfinConnectAndScan": { "message": "Conectar e Escanear" },
"settingsPhpSavePathLabel": { "message": "Caminho para salvar no servidor" }, "settingsPhpGenTitle": { "message": "Gerador de Script PHP para o Servidor" },
"settingsPhpSavePathPlaceholder": { "message": "Ex: /var/www/html/listas (em branco para a mesma pasta)" }, "settingsPhpFileOptions": { "message": "Opções do Arquivo" },
"settingsPhpFilenameLabel": { "message": "Nome do arquivo" }, "settingsPhpSavePathLabel": { "message": "Caminho de Salvamento no Servidor" },
"settingsPhpFileAction": { "message": "Ação do arquivo" }, "settingsPhpSavePathPlaceholder": { "message": "Ex: /var/www/html/listas (em branco para a mesma pasta)" },
"settingsPhpActionAppend": { "message": "Anexar ao final do arquivo (cumulativo)" }, "settingsPhpFilenameLabel": { "message": "Nome do Arquivo" },
"settingsPhpActionOverwrite": { "message": "Substituir o arquivo (começar do zero)" }, "settingsPhpFileAction": { "message": "Ação sobre o Arquivo" },
"settingsPhpSecurity": { "message": "Segurança (opcional)" }, "settingsPhpActionAppend": { "message": "Adicionar ao final do arquivo (acumulativo)" },
"settingsPhpUseSecretKey": { "message": "Usar chave secreta (recomendado)" }, "settingsPhpActionOverwrite": { "message": "Sobrescrever o arquivo (começar de novo)" },
"settingsPhpSecretKeyPlaceholder": { "message": "Digite uma chave secreta segura" }, "settingsPhpSecurity": { "message": "Segurança (Opcional)" },
"settingsPhpGeneratedCode": { "message": "Código gerado" }, "settingsPhpUseSecretKey": { "message": "Usar chave secreta (Recomendado)" },
"settingsPhpGeneratedPlaceholder": { "message": "O código PHP gerado aparecerá aqui." }, "settingsPhpSecretKeyPlaceholder": { "message": "Digite uma chave secreta segura" },
"settingsGenerateScript": { "message": "Gerar script" }, "settingsPhpGeneratedCode": { "message": "Código Gerado" },
"settingsCopyScript": { "message": "Copiar script" }, "settingsPhpGeneratedPlaceholder": { "message": "O código PHP gerado aparecerá aqui." },
"settingsDataManagement": { "message": "Gerenciamento do banco de dados local" }, "settingsGenerateScript": { "message": "Gerar Script" },
"settingsImportDb": { "message": "Importar banco de dados de um arquivo" }, "settingsCopyScript": { "message": "Copiar Script" },
"settingsExportDb": { "message": "Exportar banco de dados para um arquivo" }, "settingsDataManagement": { "message": "Gerenciamento do Banco de Dados Local" },
"settingsClearContent": { "message": "Limpar dados de conteúdo local" }, "settingsImportDb": { "message": "Importar BD de Arquivo" },
"settingsClearContentDesc": { "message": "Esta ação excluirá filmes, séries e músicas do banco de dados local, mas não afetará seus favoritos ou suas configurações." }, "settingsExportDb": { "message": "Exportar BD para Arquivo" },
"settingsClose": { "message": "Fechar" }, "settingsClearContent": { "message": "Limpar Dados de Conteúdo Local" },
"settingsSave": { "message": "Salvar configurações" }, "settingsClearContentDesc": { "message": "Esta ação excluirá filmes, séries e músicas do banco de dados local, mas não afetará seus favoritos ou configurações." },
"musicSidenavTitle": { "message": "Música do Plex" }, "settingsClose": { "message": "Fechar" },
"musicAllServers": { "message": "Todos os servidores" }, "settingsSave": { "message": "Salvar Configurações" },
"musicSearchArtistPlaceholder": { "message": "Pesquisar um artista..." }, "musicSidenavTitle": { "message": "Música do Plex" },
"musicSearchDiscographyPlaceholder": { "message": "Pesquisar na discografia..." }, "musicAllServers": { "message": "Todos os Servidores" },
"musicNothingPlaying": { "message": "Nada tocando" }, "musicSearchArtistPlaceholder": { "message": "Buscar artista..." },
"musicSelectSong": { "message": "Selecione uma música" }, "musicSearchDiscographyPlaceholder": { "message": "Buscar na discografia..." },
"musicToStart": { "message": "para começar a tocar" }, "musicNothingPlaying": { "message": "Nada tocando" },
"miniplayerDownloadSong": { "message": "Baixar música" }, "musicSelectSong": { "message": "Selecione uma música" },
"miniplayerDownloadAlbum": { "message": "Baixar álbum M3U" }, "musicToStart": { "message": "para começar a tocar" },
"miniplayerVolume": { "message": "Volume" }, "miniplayerDownloadSong": { "message": "Baixar música" },
"miniplayerShuffle": { "message": "Aleatório" }, "miniplayerDownloadAlbum": { "message": "Baixar M3U" },
"miniplayerEqualizer": { "message": "Equalizador" }, "miniplayerVolume": { "message": "Volume" },
"miniplayerOpenList": { "message": "Abrir lista" }, "miniplayerShuffle": { "message": "Aleatório" },
"eqTitle": { "message": "Equalizador gráfico" }, "miniplayerEqualizer": { "message": "Equalizador" },
"eqPresetsLabel": { "message": "Predefinições" }, "miniplayerOpenList": { "message": "Abrir lista" },
"eqPresetFlat": { "message": "Plano" }, "eqTitle": { "message": "Equalizador Gráfico" },
"eqPresetRock": { "message": "Rock" }, "eqPresetsLabel": { "message": "Predefinições" },
"eqPresetPop": { "message": "Pop" }, "eqPresetFlat": { "message": "Plano" },
"eqPresetJazz": { "message": "Jazz" }, "eqPresetRock": { "message": "Rock" },
"eqPresetClassical": { "message": "Clássico" }, "eqPresetPop": { "message": "Pop" },
"eqPresetBassBoost": { "message": "Reforço de graves" }, "eqPresetJazz": { "message": "Jazz" },
"eqPreampLabel": { "message": "Pré-amplificador" }, "eqPresetClassical": { "message": "Clássica" },
"infoModalTitle": { "message": "Informações" }, "eqPresetBassBoost": { "message": "Reforço de Graves" },
"infoModalFieldTitle": { "message": "Título:" }, "eqPreampLabel": { "message": "Pré-amplificador" },
"infoModalFieldArtist": { "message": "Artista:" }, "infoModalTitle": { "message": "Informação" },
"infoModalFieldAlbum": { "message": "Álbum:" }, "infoModalFieldTitle": { "message": "Título:" },
"infoModalFieldSong": { "message": "Música:" }, "infoModalFieldArtist": { "message": "Artista:" },
"infoModalFieldYear": { "message": "Ano:" }, "infoModalFieldAlbum": { "message": "Álbum:" },
"infoModalFieldGenre": { "message": "Gênero:" }, "infoModalFieldSong": { "message": "Música:" },
"lang_en": { "message": "Inglês" }, "infoModalFieldYear": { "message": "Ano:" },
"lang_es": { "message": "Espanhol" }, "infoModalFieldGenre": { "message": "Gênero:" },
"lang_fr": { "message": "Francês" }, "lang_en": { "message": "Inglês" },
"lang_de": { "message": "Alemão" }, "lang_es": { "message": "Espanhol" },
"lang_it": { "message": "Italiano" }, "lang_fr": { "message": "Francês" },
"lang_pt": { "message": "Português" }, "lang_de": { "message": "Alemão" },
"essentialFeaturesNotSupported": { "message": "Seu navegador não suporta recursos essenciais." }, "lang_it": { "message": "Italiano" },
"dbAccessError": { "message": "Erro ao acessar o banco de dados local." }, "lang_pt": { "message": "Português" },
"dbUpdateNeeded": { "message": "O banco de dados precisa ser atualizado, recarregue a página." }, "essentialFeaturesNotSupported": { "message": "Seu navegador não suporta recursos essenciais." },
"dbBlocked": { "message": "Feche outras abas deste aplicativo para continuar." }, "dbAccessError": { "message": "Erro ao acessar o banco de dados local." },
"deletingContentData": { "message": "Excluindo dados de conteúdo local..." }, "dbUpdateNeeded": { "message": "O banco de dados precisa ser atualizado, por favor, recarregue a página." },
"noContentDataToDelete": { "message": "Nenhum dado de conteúdo para excluir." }, "dbBlocked": { "message": "Por favor, feche outras abas desta aplicação para continuar." },
"contentDataDeleted": { "message": "Dados de conteúdo excluídos do IndexedDB." }, "deletingContentData": { "message": "Excluindo dados de conteúdo local..." },
"errorDeletingData": { "message": "Erro ao excluir dados: $message$", "placeholders": { "message": { "content": "$1" } } }, "noContentDataToDelete": { "message": "Não há dados de conteúdo para excluir." },
"aceEditorNotAvailable": { "message": "Editor de texto não disponível." }, "contentDataDeleted": { "message": "Dados de conteúdo excluídos do IndexedDB." },
"errorLoadingTokens": { "message": "Erro ao carregar tokens para edição." }, "errorDeletingData": { "message": "Erro ao excluir dados: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingTokensMessage": { "message": "Erro ao carregar tokens: $message$", "placeholders": { "message": { "content": "$1" } } }, "aceEditorNotAvailable": { "message": "Editor de texto não disponível." },
"aceEditorNotAvailableToSave": { "message": "Editor não disponível para salvar." }, "errorLoadingTokens": { "message": "Erro ao carregar tokens para edição." },
"invalidJsonFormat": { "message": "Formato JSON inválido. Deve ser { \"tokens\": [...] }" }, "errorLoadingTokensMessage": { "message": "Erro ao carregar tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"tokensSaved": { "message": "Tokens salvos com sucesso." }, "aceEditorNotAvailableToSave": { "message": "Editor não disponível para salvar." },
"errorSavingTokens": { "message": "Erro ao salvar tokens: $message$", "placeholders": { "message": { "content": "$1" } } }, "invalidJsonFormat": { "message": "Formato JSON inválido. Deve ser { \"tokens\": [...] }" },
"dbNotAvailable": { "message": "O IndexedDB não está disponível." }, "tokensSaved": { "message": "Tokens salvos com sucesso." },
"dbExported": { "message": "Banco de dados exportado com sucesso." }, "errorSavingTokens": { "message": "Erro ao salvar tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorExportingDb": { "message": "Erro ao exportar o banco de dados: $message$", "placeholders": { "message": { "content": "$1" } } }, "dbNotAvailable": { "message": "IndexedDB não está disponível." },
"invalidJsonFile": { "message": "O arquivo não contém um objeto JSON válido." }, "dbExported": { "message": "Banco de dados exportado com sucesso." },
"noDataToImport": { "message": "O arquivo não contém dados para as seções atuais do banco de dados." }, "errorExportingDb": { "message": "Erro ao exportar banco de dados: $message$", "placeholders": { "message": { "content": "$1" } } },
"dbImported": { "message": "Banco de dados importado com sucesso." }, "invalidJsonFile": { "message": "O arquivo não contém um objeto JSON válido." },
"errorImportingDb": { "message": "Erro ao importar o banco de dados: $message$", "placeholders": { "message": { "content": "$1" } } }, "noDataToImport": { "message": "O arquivo não contém dados para as seções atuais do BD." },
"updatingView": { "message": "Atualizando a visualização com novos dados..." }, "dbImported": { "message": "Banco de dados importado com sucesso." },
"confirmClearContent": { "message": "Tem certeza de que deseja excluir os dados de conteúdo local (filmes, séries, músicas, etc.)? Favoritos e configurações NÃO serão excluídos." }, "errorImportingDb": { "message": "Erro ao importar banco de dados: $message$", "placeholders": { "message": { "content": "$1" } } },
"trailerNotFound": { "message": "Nenhum trailer encontrado para este título." }, "updatingView": { "message": "Atualizando a visualização com os novos dados..." },
"confirmClearHistory": { "message": "Tem certeza de que deseja limpar todo o seu histórico de visualização? Esta ação não pode ser desfeita." }, "confirmClearContent": { "message": "Você tem certeza de que deseja excluir os dados de conteúdo local (Filmes, Séries, Música, etc.)? Favoritos e Configurações NÃO serão excluídos." },
"historyCleared": { "message": "Histórico de visualização limpo." }, "trailerNotFound": { "message": "Nenhum trailer encontrado para este título." },
"historyItemDeleted": { "message": "Item excluído do histórico." }, "confirmClearHistory": { "message": "Você tem certeza de que deseja excluir todo o seu histórico de visualização? Esta ação não pode ser desfeita." },
"errorGeneratingScript": { "message": "Primeiro, gere um script para poder copiá-lo." }, "historyCleared": { "message": "Histórico de visualização limpo." },
"scriptCopied": { "message": "Script PHP copiado para a área de transferência." }, "historyItemDeleted": { "message": "Item excluído do histórico." },
"errorCopyingScript": { "message": "Erro ao copiar o script." }, "errorGeneratingScript": { "message": "Primeiro gere um script para poder copiá-lo." },
"scriptGenerated": { "message": "Script PHP gerado." }, "scriptCopied": { "message": "Script PHP copiado para a área de transferência." },
"errorLoadingAlbum": { "message": "Erro ao carregar o álbum: $message$", "placeholders": { "message": { "content": "$1" } } }, "errorCopyingScript": { "message": "Erro ao copiar o script." },
"noPhotoServerSelected": { "message": "Erro: nenhum servidor de fotos foi selecionado." }, "scriptGenerated": { "message": "Script PHP gerado." },
"loadingGenres": { "message": "Carregando gêneros..." }, "errorLoadingAlbum": { "message": "Erro ao carregar álbum: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingGenres": { "message": "Erro ao carregar" }, "noPhotoServerSelected": { "message": "Erro: Nenhum servidor de fotos foi selecionado." },
"noContentFound": { "message": "Nenhum resultado encontrado." }, "loadingGenres": { "message": "Carregando gêneros..." },
"couldNotLoadContent": { "message": "Não foi possível carregar o conteúdo." }, "errorLoadingGenres": { "message": "Erro ao carregar" },
"noFavorites": { "message": "Você ainda não tem favoritos." }, "noContentFound": { "message": "Nenhum resultado encontrado." },
"errorLoadingFavorites": { "message": "Erro ao carregar os favoritos." }, "couldNotLoadContent": { "message": "Não foi possível carregar o conteúdo." },
"historyEmpty": { "message": "Seu histórico está vazio." }, "noFavorites": { "message": "Você ainda não tem favoritos." },
"historyEmptySub": { "message": "Explore e assista a conteúdo para que ele apareça aqui." }, "errorLoadingFavorites": { "message": "Erro ao carregar favoritos." },
"errorGeneratingRecommendations": { "message": "Erro ao gerar recomendações." }, "historyEmpty": { "message": "Seu histórico está vazio." },
"noRecommendations": { "message": "Precisamos conhecê-lo melhor para dar recomendações!" }, "historyEmptySub": { "message": "Explore e assista a conteúdo para que ele apareça aqui." },
"errorGeneratingStats": { "message": "Erro ao gerar estatísticas." }, "errorGeneratingRecommendations": { "message": "Erro ao gerar recomendações." },
"noServersForToken": { "message": "Nenhum servidor associado encontrado para este token." }, "noRecommendations": { "message": "Precisamos te conhecer melhor para dar recomendações!" },
"searchingActorContent": { "message": "Pesquisando conteúdo de $actorName$", "placeholders": { "actorName": { "content": "$1" } } }, "errorGeneratingStats": { "message": "Erro ao gerar estatísticas." },
"errorLoadingActorContent": { "message": "Não foi possível carregar o conteúdo de $actorName$.", "placeholders": { "actorName": { "content": "$1" } } }, "noServersForToken": { "message": "Nenhum servidor associado encontrado para este token." },
"errorAddingStream": { "message": "Erro ao adicionar stream(s): $message$", "placeholders": { "message": { "content": "$1" } } }, "searchingActorContent": { "message": "Buscando conteúdo de $actorName$", "placeholders": { "actorName": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "A URL do servidor PHP não está configurada. Configure-a nas Configurações." }, "errorLoadingActorContent": { "message": "Não foi possível carregar o conteúdo para $actorName$.", "placeholders": { "actorName": { "content": "$1" } } },
"searchingStreams": { "message": "Pesquisando streams para \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "errorAddingStream": { "message": "Erro ao adicionar stream(s): $message$", "placeholders": { "message": { "content": "$1" } } },
"sendingStreams": { "message": "Enviando $count$ stream(s) para o servidor...", "placeholders": { "count": { "content": "$1" } } }, "phpUrlNotConfigured": { "message": "A URL do servidor PHP não está configurada. Por favor, configure-a nas Configurações." },
"streamAddedSuccess": { "message": "Stream(s) adicionado(s) com sucesso." }, "searchingStreams": { "message": "Buscando streams para \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingM3U": { "message": "Gerando M3U para \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "sendingStreams": { "message": "Enviando $count$ stream(s) para o servidor...", "placeholders": { "count": { "content": "$1" } } },
"m3uDownloaded": { "message": "\"$title$\" baixado.", "placeholders": { "title": { "content": "$1" } } }, "streamAddedSuccess": { "message": "Stream(s) adicionado(s) com sucesso." },
"errorGeneratingM3U": { "message": "Erro ao gerar M3U: $message$", "placeholders": { "message": { "content": "$1" } } }, "generatingM3U": { "message": "Gerando M3U para \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"settingsSavedSuccess": { "message": "Configurações salvas com sucesso." }, "m3uDownloaded": { "message": "\"$title$\" baixado.", "placeholders": { "title": { "content": "$1" } } },
"errorSavingSettings": { "message": "Erro ao salvar as configurações no banco de dados." }, "errorGeneratingM3U": { "message": "Erro ao gerar M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
"languageChangeReload": { "message": "Idioma alterado. O aplicativo será recarregado agora." }, "settingsSavedSuccess": { "message": "Configurações salvas com sucesso." },
"addedToFavorites": { "message": "Adicionado aos favoritos." }, "errorSavingSettings": { "message": "Erro ao salvar as configurações no banco de dados." },
"removedFromFavorites": { "message": "Removido dos favoritos." }, "languageChangeReload": { "message": "Idioma alterado. A aplicação será recarregada agora." },
"plexScanInProgress": { "message": "A verificação do Plex já está em andamento." }, "addedToFavorites": { "message": "Adicionado aos favoritos." },
"plexScanStarting": { "message": "Iniciando a verificação do Plex..." }, "removedFromFavorites": { "message": "Removido dos favoritos." },
"noPlexTokens": { "message": "Nenhum token do Plex configurado." }, "plexScanInProgress": { "message": "O escaneamento do Plex já está em andamento." },
"clearingSections": { "message": "Limpando seções: $sections$", "placeholders": { "sections": { "content": "$1" } } }, "plexScanStarting": { "message": "Iniciando escaneamento do Plex..." },
"initialScanPhaseComplete": { "message": "Fase de verificação inicial concluída." }, "noPlexTokens": { "message": "Não há tokens do Plex configurados." },
"retryPhaseFinished": { "message": "Fase de nova tentativa concluída." }, "clearingSections": { "message": "Limpando seções: $sections$", "placeholders": { "sections": { "content": "$1" } } },
"plexScanFinished": { "message": "Verificação concluída. Atualizando conteúdo..." }, "initialScanPhaseComplete": { "message": "Fase de escaneamento inicial finalizada." },
"scanCancelled": { "message": "Verificação cancelada pelo usuário." }, "retryPhaseFinished": { "message": "Fase de novas tentativas finalizada." },
"scanCancelledInfo": { "message": "Verificação cancelada." }, "plexScanFinished": { "message": "Escaneamento finalizado. Atualizando conteúdo..." },
"errorInitializingMusicPlayer": { "message": "Erro ao inicializar o reprodutor de música." }, "scanCancelled": { "message": "Escaneamento cancelado pelo usuário." },
"criticalErrorLoadingMusic": { "message": "Erro crítico ao carregar os dados de música." }, "scanCancelledInfo": { "message": "Escaneamento cancelado." },
"errorLoadingArtists": { "message": "Erro ao carregar os artistas." }, "errorInitializingMusicPlayer": { "message": "Erro ao inicializar o player de música." },
"dbUnavailableError": { "message": "Erro: banco de dados indisponível." }, "criticalErrorLoadingMusic": { "message": "Erro crítico ao carregar dados de música." },
"updatingMusicData": { "message": "Atualizando dados de música..." }, "errorLoadingArtists": { "message": "Erro ao carregar artistas." },
"musicDataUpdated": { "message": "Dados de música atualizados." }, "dbUnavailableError": { "message": "Erro: Banco de dados não disponível." },
"errorFetchingArtistSongs": { "message": "Erro ao buscar as músicas do artista." }, "updatingMusicData": { "message": "Atualizando dados de música..." },
"errorLoadingSongs": { "message": "Erro ao carregar as músicas." }, "musicDataUpdated": { "message": "Dados de música atualizados." },
"noArtistsFound": { "message": "Nenhum artista encontrado." }, "errorFetchingArtistSongs": { "message": "Erro ao buscar as músicas do artista." },
"shuffleOn": { "message": "Modo aleatório ativado." }, "errorLoadingSongs": { "message": "Erro ao carregar músicas." },
"shuffleOff": { "message": "Modo aleatório desativado." }, "noArtistsFound": { "message": "Nenhum artista encontrado." },
"playbackError": { "message": "Erro de reprodução" }, "shuffleOn": { "message": "Modo aleatório ativado." },
"errorLabel": { "message": "Erro" }, "shuffleOff": { "message": "Modo aleatório desativado." },
"reloadingPage": { "message": "Recarregando a página..." }, "playbackError": { "message": "Erro de reprodução" },
"viewed": { "message": "Visto" }, "errorLabel": { "message": "Erro" },
"local": { "message": "Local" }, "reloadingPage": { "message": "Recarregando a página..." },
"topRatedSort": {"message": "Mais bem avaliados"}, "viewed": { "message": "Visto" },
"recentSort": {"message": "Recentes"}, "local": { "message": "Local" },
"popularSort": {"message": "Populares"}, "topRatedSort": {"message": "Melhor Avaliados"},
"moviesSectionTitle": {"message": "Filmes"}, "recentSort": {"message": "Recentes"},
"seriesSectionTitle": {"message": "Séries"}, "popularSort": {"message": "Populares"},
"searchResultsFor": {"message": "Resultados para \"$query$\"", "placeholders": {"query": {"content": "$1"}}}, "moviesSectionTitle": {"message": "Filmes"},
"contentFrom": {"message": "Conteúdo de $actor$", "placeholders": {"actor": {"content": "$1"}}}, "seriesSectionTitle": {"message": "Séries"},
"explore": {"message": "Explorar"}, "searchResultsFor": {"message": "Resultados para \"$query$\"", "placeholders": {"query": {"content": "$1"}}},
"noGenre": {"message": "Sem categoria"}, "contentFrom": {"message": "Conteúdo de $actor$", "placeholders": {"actor": {"content": "$1"}}},
"synopsis": {"message": "Sinopse"}, "explore": {"message": "Explorar"},
"noSynopsis": {"message": "Nenhuma sinopse disponível."}, "noGenre": {"message": "Sem categoria"},
"director": {"message": "Diretor:"}, "synopsis": {"message": "Sinopse"},
"writer": {"message": "Roteirista:"}, "noSynopsis": {"message": "Nenhuma sinopse disponível."},
"viewOnImdb": {"message": "Ver no IMDb"}, "director": {"message": "Diretor:"},
"watchTrailer": {"message": "Assistir ao trailer"}, "writer": {"message": "Roteirista:"},
"addToFavorites": {"message": "Adicionar aos favoritos"}, "viewOnImdb": {"message": "Ver no IMDb"},
"removeFromFavorites": {"message": "Remover dos favoritos"}, "watchTrailer": {"message": "Trailer"},
"notAvailable": {"message": "Não disponível"}, "addToFavorites": {"message": "Adicionar aos favoritos"},
"mainCast": {"message": "Elenco principal"}, "removeFromFavorites": {"message": "Remover dos favoritos"},
"seasonsAndEpisodes": {"message": "Temporadas e episódios"}, "notAvailable": {"message": "Não disponível"},
"similarContent": {"message": "Conteúdo semelhante"}, "mainCast": {"message": "Elenco Principal"},
"filmography": {"message": "Filmografia"}, "seasonsAndEpisodes": {"message": "Temporadas e Episódios"},
"availableOn": {"message": "Disponível em"}, "similarContent": {"message": "Conteúdo Similar"},
"episodesCount": {"message": "$count$ episódios", "placeholders": {"count": {"content": "$1"}}}, "filmography": {"message": "Filmografia"},
"seasonsCount": {"message": "$count$ temporadas", "placeholders": {"count": {"content": "$1"}}}, "availableOn": {"message": "Disponível em"},
"runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}}, "episodesCount": {"message": "$count$ Episódios", "placeholders": {"count": {"content": "$1"}}},
"noTrailerFound": {"message": "Nenhum trailer encontrado para este título."}, "seasonsCount": {"message": "$count$ Temporadas", "placeholders": {"count": {"content": "$1"}}},
"fatalInitError": {"message": "Erro fatal de inicialização"}, "runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}},
"fatalInitErrorSub": {"message": "Não foi possível carregar o aplicativo."}, "noTrailerFound": {"message": "Nenhum trailer encontrado para este título."},
"invalidStreamInfo": {"message": "Informações inválidas."}, "fatalInitError": {"message": "Erro fatal de inicialização"},
"dbUnavailableForStreams": {"message": "Banco de dados local indisponível."}, "fatalInitErrorSub": {"message": "Não foi possível carregar a aplicação."},
"noPlexServersForStreams": {"message": "Nenhum servidor Plex."}, "invalidStreamInfo": {"message": "Informação de stream inválida."},
"notFoundOnServers": {"message": "\"$query$\" não encontrado nos servidores Plex.", "placeholders": {"query": {"content": "$1"}}}, "dbUnavailableForStreams": {"message": "Banco de dados local não disponível."},
"relativeTime_justNow": { "message": "Agora mesmo" }, "noPlexServersForStreams": {"message": "Nenhum servidor Plex."},
"relativeTime_minutesAgo": { "message": "Há $count$ minutos", "placeholders": { "count": { "content": "$1" } } }, "notFoundOnServers": {"message": "\"$query$\" não encontrado nos servidores Plex.", "placeholders": {"query": {"content": "$1"}}},
"relativeTime_hoursAgo": { "message": "Há $count$ horas", "placeholders": { "count": { "content": "$1" } } }, "relativeTime_justNow": { "message": "Agora mesmo" },
"relativeTime_yesterday": { "message": "Ontem" }, "relativeTime_minutesAgo": { "message": "Há $count$ minutos", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_daysAgo": { "message": "Há $count$ dias", "placeholders": { "count": { "content": "$1" } } }, "relativeTime_hoursAgo": { "message": "Há $count$ horas", "placeholders": { "count": { "content": "$1" } } },
"errorLoadingDetails": { "message": "Erro ao carregar os detalhes" }, "relativeTime_yesterday": { "message": "Ontem" },
"errorLoadingLocalContent": { "message": "Erro ao carregar o conteúdo local." }, "relativeTime_daysAgo": { "message": "Há $count$ dias", "placeholders": { "count": { "content": "$1" } } },
"errorServerResponse": { "message": "Resposta do servidor sem sucesso." }, "errorLoadingDetails": { "message": "Erro ao Carregar os Detalhes" },
"errorPlexApi": { "message": "Erro $status$ da API do Plex.", "placeholders": { "status": { "content": "$1" } } }, "errorLoadingLocalContent": { "message": "Erro ao carregar o conteúdo local." },
"errorParsingPlexXml": { "message": "Erro ao analisar o XML do Plex." }, "errorServerResponse": { "message": "Resposta não bem-sucedida do servidor." },
"untitled": { "message": "Sem título" }, "errorPlexApi": { "message": "Erro $status$ da API do Plex.", "placeholders": { "status": { "content": "$1" } } },
"itemCount": { "message": "$count$ itens", "placeholders": { "count": { "content": "$1" } } }, "errorParsingPlexXml": { "message": "Erro ao analisar o XML do Plex." },
"noPhotoServers": { "message": "Nenhum servidor de fotos" }, "untitled": { "message": "Sem título" },
"jellyfinScanInProgress": { "message": "A verificação do Jellyfin já está em andamento." }, "itemCount": { "message": "$count$ itens", "placeholders": { "count": { "content": "$1" } } },
"jellyfinScanning": { "message": "Verificando o Jellyfin..." }, "noPhotoServers": { "message": "Nenhum servidor de fotos" },
"jellyfinMissingCredentials": { "message": "Preencha a URL e o nome de usuário do Jellyfin." }, "jellyfinScanInProgress": { "message": "O escaneamento do Jellyfin já está em andamento." },
"jellyfinConnecting": { "message": "Conectando-se ao Jellyfin em: $url$", "placeholders": { "url": { "content": "$1" } } }, "jellyfinScanning": { "message": "Escaneando Jellyfin..." },
"jellyfinAuthFailed": { "message": "A autenticação do Jellyfin falhou: $message$", "placeholders": { "message": { "content": "$1" } } }, "jellyfinMissingCredentials": { "message": "Por favor, preencha a URL e o usuário do Jellyfin." },
"jellyfinAuthSuccess": { "message": "Autenticação do Jellyfin bem-sucedida." }, "jellyfinConnecting": { "message": "Conectando ao Jellyfin em: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinFetchingLibraries": { "message": "Buscando bibliotecas..." }, "jellyfinAuthFailed": { "message": "Autenticação do Jellyfin falhou: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinFetchFailed": { "message": "Erro ao buscar bibliotecas: $message$", "placeholders": { "message": { "content": "$1" } } }, "jellyfinAuthSuccess": { "message": "Autenticação do Jellyfin bem-sucedida." },
"jellyfinNoMediaLibraries": { "message": "Nenhuma biblioteca de filmes ou séries encontrada no Jellyfin." }, "jellyfinFetchingLibraries": { "message": "Obtendo bibliotecas..." },
"jellyfinLibrariesFound": { "message": "$count$ biblioteca(s) de mídia encontrada(s).", "placeholders": { "count": { "content": "$1" } } }, "jellyfinFetchFailed": { "message": "Erro ao obter bibliotecas: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Sucesso] '$libraryName' verificada, $count$ títulos adicionados.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } }, "jellyfinNoMediaLibraries": { "message": "Nenhuma biblioteca de filmes ou séries encontrada no Jellyfin." },
"jellyfinLibraryScanFailed": { "message": "Erro ao verificar a biblioteca '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } }, "jellyfinLibrariesFound": { "message": "$count$ biblioteca(s) de mídia encontrada(s).", "placeholders": { "count": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Verificação do Jellyfin concluída. Adicionados $movies$ filmes e $series$ séries.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } }, "jellyfinLibraryScanSuccess": { "message": "[Sucesso] '$libraryName' escaneada, $count$ títulos adicionados.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Credenciais do Jellyfin não configuradas." }, "jellyfinLibraryScanFailed": { "message": "Erro ao escanear a biblioteca '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } },
"notFoundOnJellyfin": { "message": "\"$query$\" não encontrado no Jellyfin.", "placeholders": { "query": { "content": "$1" } } }, "jellyfinScanSuccess": { "message": "Escaneamento do Jellyfin concluído. Adicionados $movies$ filmes e $series$ séries.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" não encontrado em nenhum servidor.", "placeholders": { "query": { "content": "$1" } } }, "noJellyfinCredentials": { "message": "Credenciais do Jellyfin não configuradas." },
"localOnPlex": { "message": "No Plex" }, "notFoundOnJellyfin": { "message": "\"$query$\" não encontrado no Jellyfin.", "placeholders": {"query": {"content": "$1"}}},
"searchOnPlex": { "message": "Pesquisar no Plex" }, "notFoundOnAnyServer": { "message": "\"$query$\" não encontrado em nenhum servidor.", "placeholders": {"query": {"content": "$1"}}},
"jellyfinTitle": { "message": "Conteúdo do Jellyfin" }, "localOnPlex": { "message": "No Plex" },
"noJellyfinContent": { "message": "Nenhum conteúdo do Jellyfin encontrado." }, "searchOnPlex": { "message": "Buscar no Plex" },
"noJellyfinContentSub": { "message": "Verifique se você verificou seu servidor Jellyfin nas configurações." }, "jellyfinTitle": { "message": "Conteúdo do Jellyfin" },
"activityViewerTitle": { "message": "Visualizador de atividades do servidor" }, "noJellyfinContent": { "message": "Nenhum conteúdo do Jellyfin encontrado." },
"activitySelectServer": { "message": "Selecione um servidor" }, "noJellyfinContentSub": { "message": "Certifique-se de ter escaneado seu servidor Jellyfin nas configurações." },
"activityCheckBtn": { "message": "Atualizar" }, "activityViewerTitle": { "message": "Visualizador de Atividade do Servidor" },
"activityNoSessions": { "message": "Nenhuma sessão ativa neste servidor." }, "activitySelectServer": { "message": "Selecione um servidor" },
"activitySessionUser": { "message": "Usuário" }, "activityCheckBtn": { "message": "Atualizar" },
"activitySessionDevice": { "message": "Dispositivo" }, "activityNoSessions": { "message": "Não há sessões ativas neste servidor." },
"activitySessionContent": { "message": "Conteúdo" }, "activitySessionUser": { "message": "Usuário" },
"activitySessionState": { "message": "Estado" }, "activitySessionDevice": { "message": "Dispositivo" },
"activitySessionIdentifier": { "message": "Identificador do cliente" }, "activitySessionContent": { "message": "Conteúdo" },
"activityCopyID": { "message": "Copiar ID" }, "activitySessionState": { "message": "Estado" },
"activityError": { "message": "Não foi possível obter a atividade do servidor." }, "activitySessionIdentifier": { "message": "Identificador do Cliente" },
"activityCopied": { "message": "Identificador copiado para a área de transferência!" }, "activityCopyID": { "message": "Copiar ID" },
"activityCopyError": { "message": "Erro ao copiar o identificador." }, "activityError": { "message": "Não foi possível obter a atividade do servidor." },
"noProvidersFound": { "message": "Nenhum provedor encontrado." }, "activityCopied": { "message": "Identificador copiado para a área de transferência!" },
"availableOnPlex": { "message": "Disponível no Plex" }, "activityCopyError": { "message": "Erro ao copiar o identificador." },
"m3uGeneratorTitle": { "message": "Gerador de listas M3U" }, "noProvidersFound": { "message": "Nenhum provedor encontrado." },
"selectAServer": { "message": "Selecione um servidor..." }, "availableOnPlex": { "message": "Disponível no Plex" },
"downloadM3u": { "message": "Baixar M3U" }, "m3uGeneratorTitle": { "message": "Gerador de Listas M3U" },
"m3uGenerator": { "message": "Gerador de M3U" }, "selectAServer": { "message": "Selecione um servidor..." },
"selectLibraries": { "message": "Selecionar bibliotecas" }, "downloadM3u": { "message": "Baixar M3U" },
"howToUse": { "message": "Como usar" }, "m3uGenerator": { "message": "Gerador M3U" },
"m3uInstruction1": { "message": "Escolha um servidor na lista." }, "selectLibraries": { "message": "Selecionar Bibliotecas" },
"m3uInstruction2": { "message": "Selecione uma ou mais bibliotecas para incluir." }, "howToUse": { "message": "Como Usar" },
"m3uInstruction3": { "message": "Clique no botão de download." }, "m3uInstruction1": { "message": "Escolha um servidor da lista." },
"m3uInstruction4": { "message": "Importe o arquivo .m3u para o seu reprodutor compatível." }, "m3uInstruction2": { "message": "Selecione uma ou mais bibliotecas para incluir." },
"chatOpen": { "message": "Abrir chat" }, "m3uInstruction3": { "message": "Clique no botão de download." },
"chatTitle": { "message": "Assistente de IA" }, "m3uInstruction4": { "message": "Importe o arquivo .m3u no seu player compatível." },
"chatClose": { "message": "X" }, "chatOpen": { "message": "Abrir Chat" },
"chatPlaceholder": { "message": "Digite sua mensagem..." }, "chatTitle": { "message": "Assistente de IA" },
"chatSend": { "message": "➤" }, "chatClose": { "message": "X" },
"chatWelcome": { "message": "Bem-vindo! Eu sou seu assistente CinePlex. Pergunte-me sobre filmes, séries ou qualquer outra coisa que você queira saber." }, "chatPlaceholder": { "message": "Digite sua mensagem..." },
"chatGoogleApiKeyMissing": { "message": "A chave de API do Google Gemini não está configurada. Defina-a nas configurações da extensão para usar o assistente de IA." }, "chatSend": { "message": "➤" },
"chatApiInvalidResponse": { "message": "A API retornou uma resposta inválida. Tente novamente." }, "chatWelcome": { "message": "Bem-vindo! Eu sou seu assistente CinePlex. Pergunte-me sobre filmes, séries ou qualquer outra coisa que queira saber." },
"chatApiError": { "message": "Erro ao se comunicar com o assistente de IA" }, "chatGoogleApiKeyMissing": { "message": "A chave da API do Google Gemini não está configurada. Por favor, configure-a nas configurações da extensão para usar o assistente de IA." },
"downloadAll": { "message": "Baixar tudo" }, "chatApiInvalidResponse": { "message": "A API retornou uma resposta inválida. Por favor, tente novamente." },
"download": { "message": "Baixar" }, "chatApiError": { "message": "Erro ao se comunicar com o assistente de IA" },
"aiToolSearchLibraryDesc": { "message": "Pesquisa na biblioteca Plex do usuário por filmes ou séries por título." }, "downloadAll": { "message": "Baixar tudo" },
"aiToolSearchLibraryQueryParamDesc": { "message": "O título do filme ou série a ser pesquisado." }, "download": { "message": "Baixar" },
"aiToolSearchLibraryTypeParamDesc": { "message": "O tipo de conteúdo a ser pesquisado. Pode ser 'movie' para filmes ou 'series' para séries. (Opcional)." }, "aiToolSearchLibraryDesc": { "message": "Pesquisa na biblioteca Plex do usuário por filmes ou séries por título." },
"aiToolSearchLibraryResolutionParamDesc": { "message": "A resolução de vídeo a ser pesquisada (por exemplo, '4k', '1080p'). (Opcional)." }, "aiToolSearchLibraryQueryParamDesc": { "message": "O título do filme ou série a ser pesquisado." },
"aiToolSearchLibraryContainerParamDesc": { "message": "O formato do contêiner de vídeo a ser pesquisado (por exemplo, 'mkv', 'mp4'). (Opcional)." }, "aiToolSearchLibraryTypeParamDesc": { "message": "O tipo de conteúdo a ser pesquisado. Pode ser 'movie' para filmes ou 'series' para séries. (Opcional)." },
"aiToolNavigateToPageDesc": { "message": "Navega o usuário para uma página específica da interface do aplicativo." }, "aiToolSearchLibraryResolutionParamDesc": { "message": "A resolução de vídeo a ser pesquisada (ex: '4k', '1080p'). (Opcional)." },
"aiToolNavigateToPagePageParamDesc": { "message": "O nome da página para a qual navegar, por exemplo: 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers' ou 'm3u-generator'." }, "aiToolSearchLibraryContainerParamDesc": { "message": "O formato do contêiner de vídeo a ser pesquisado (ex: 'mkv', 'mp4'). (Opcional)." },
"aiToolGetUserStatsDesc": { "message": "Obtém e exibe as estatísticas da biblioteca do usuário, como o número total de filmes, séries e artistas únicos." }, "aiToolNavigateToPageDesc": { "message": "Navega o usuário para uma página específica da interface da aplicação." },
"aiToolShowItemDetailsDesc": { "message": "Exibe a página de detalhes de um filme ou série específica por seu título e tipo." }, "aiToolNavigateToPagePageParamDesc": { "message": "O nome da página para a qual navegar, por exemplo: 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', 'm3u-generator' ou 'music'." },
"aiToolShowItemDetailsTitleParamDesc": { "message": "O título exato do filme ou série." }, "aiToolGetUserStatsDesc": { "message": "Obtém e exibe as estatísticas da biblioteca do usuário, como o número total de filmes, séries e artistas únicos." },
"aiToolShowItemDetailsTypeParamDesc": { "message": "O tipo de conteúdo. Deve ser 'movie' ou 'series'." }, "aiToolShowItemDetailsDesc": { "message": "Exibe a página de detalhes de um filme ou série específica por seu título e tipo." },
"aiToolAddToPlaylistDesc": { "message": "Adiciona um filme ou série à lista de reprodução atual do usuário para transmiti-lo para um servidor PHP configurado." }, "aiToolShowItemDetailsTitleParamDesc": { "message": "O título exato do filme ou série." },
"aiToolAddToPlaylistTitleParamDesc": { "message": "O título do filme ou série a ser adicionado." }, "aiToolShowItemDetailsTypeParamDesc": { "message": "O tipo de conteúdo. Deve ser 'movie' ou 'series'." },
"aiToolAddToPlaylistTypeParamDesc": { "message": "O tipo de conteúdo. Deve ser 'movie' ou 'series'." }, "aiToolAddToPlaylistDesc": { "message": "Adiciona um filme ou série à lista de reprodução atual do usuário para transmitir para um servidor PHP configurado." },
"aiToolCheckAndDownloadDesc": { "message": "Verifica a disponibilidade de uma lista de títulos de filmes ou séries nos servidores locais do usuário e, se encontrados, gera e baixa um arquivo de lista de reprodução M3U com os streams encontrados." }, "aiToolAddToPlaylistTitleParamDesc": { "message": "O título do filme ou série a ser adicionado." },
"aiToolCheckAndDownloadTitlesParamDesc": { "message": "Uma matriz de títulos de filmes ou séries para pesquisar e baixar." }, "aiToolAddToPlaylistTypeParamDesc": { "message": "O tipo de conteúdo. Deve ser 'movie' ou 'series'." },
"aiToolCheckAndDownloadTypeParamDesc": { "message": "O tipo de conteúdo da lista. Deve ser 'movie' ou 'series'." }, "aiToolDownloadSingleMovieM3UDesc": { "message": "Gera e baixa um arquivo de lista de reprodução M3U para um único filme disponível localmente." },
"aiToolCheckAndDownloadFilenameParamDesc": { "message": "O nome do arquivo M3U a ser baixado (por exemplo, 'MinhaLista.m3u'). Se não for fornecido, um nome padrão será usado." }, "aiToolDownloadSingleMovieM3UTitleParamDesc": { "message": "O título do filme para o qual o M3U será gerado." },
"aiToolToggleFavoriteDesc": { "message": "Adiciona ou remove um filme ou série da lista de favoritos do usuário." }, "aiToolDownloadSingleMovieM3UYearParamDesc": { "message": "O ano de lançamento do filme (opcional, para maior precisão)." },
"aiToolToggleFavoriteTitleParamDesc": { "message": "O título do filme ou série." }, "aiToolDownloadSeriesSeasonM3UDesc": { "message": "Gera e baixa um arquivo de lista de reprodução M3U para uma temporada específica de uma série disponível localmente." },
"aiToolToggleFavoriteTypeParamDesc": { "message": "O tipo de conteúdo. Deve ser 'movie' ou 'series'." }, "aiToolDownloadSeriesSeasonM3UTitleParamDesc": { "message": "O título da série." },
"aiToolGetRecommendationsDesc": { "message": "Gera e exibe uma lista de recomendações de filmes ou séries com base no histórico de visualização e nos favoritos do usuário." }, "aiToolDownloadSeriesSeasonM3USeasonParamDesc": { "message": "O número da temporada a ser baixada." },
"aiToolApplyFiltersDesc": { "message": "Aplica filtros à visualização atual de filmes ou séries, permitindo refinar os resultados por tipo, gênero, ano e ordem de classificação." }, "aiToolDownloadSeriesSeasonM3UYearParamDesc": { "message": "O ano de lançamento da série (opcional)." },
"aiToolApplyFiltersTypeParamDesc": { "message": "O tipo de conteúdo ao qual aplicar os filtros. Deve ser 'movie' ou 'series'." }, "aiToolCheckAndDownloadDesc": { "message": "Verifica a disponibilidade de uma lista de títulos de filmes ou séries nos servidores locais do usuário e, se encontrados, gera e baixa um arquivo de lista de reprodução M3U com os streams encontrados." },
"aiToolApplyFiltersGenreParamDesc": { "message": "O nome do gênero pelo qual filtrar (por exemplo, 'Ação', 'Drama')." }, "aiToolCheckAndDownloadTitlesParamDesc": { "message": "Um array de títulos de filmes ou séries para pesquisar e baixar." },
"aiToolApplyFiltersYearParamDesc": { "message": "O ano de lançamento pelo qual filtrar (por exemplo, '2023')." }, "aiToolCheckAndDownloadTypeParamDesc": { "message": "O tipo de conteúdo da lista. Deve ser 'movie' ou 'series'." },
"aiToolApplyFiltersSortParamDesc": { "message": "O critério de classificação para os resultados. Valores válidos: 'popularity.desc' (populares), 'vote_average.desc' (mais bem avaliados), 'release_date.desc' (recentes para filmes) ou 'first_air_date.desc' (recentes para séries)." }, "aiToolCheckAndDownloadFilenameParamDesc": { "message": "O nome do arquivo M3U a ser baixado (ex: 'MinhaLista.m3u'). Se não for fornecido, um nome padrão será usado." },
"aiToolPlayMusicByArtistDesc": { "message": "Abre o reprodutor de música e começa a tocar músicas de um artista específico da biblioteca do usuário." }, "aiToolToggleFavoriteDesc": { "message": "Adiciona ou remove um filme ou série da lista de favoritos do usuário." },
"aiToolPlayMusicByArtistNameParamDesc": { "message": "O nome exato do artista cujas músicas devem ser tocadas." }, "aiToolToggleFavoriteTitleParamDesc": { "message": "O título do filme ou série." },
"aiToolClearChatHistoryDesc": { "message": "Limpa todo o histórico de mensagens da conversa atual com o assistente de IA." }, "aiToolToggleFavoriteTypeParamDesc": { "message": "O tipo de conteúdo. Deve ser 'movie' ou 'series'." },
"aiToolDeleteDatabaseDesc": { "message": "Exclui todo o banco de dados local da extensão, incluindo conteúdo verificado, configurações e favoritos. Esta ação é irreversível e recarregará o aplicativo." }, "aiToolGetRecommendationsDesc": { "message": "Gera e exibe uma lista de recomendações de filmes ou séries com base no histórico de visualização e nos favoritos do usuário." },
"aiToolUpdateAllTokensDesc": { "message": "Inicia uma verificação completa de todos os servidores e bibliotecas Plex associados aos tokens configurados na extensão. Atualiza todos os filmes, séries, artistas e fotos." }, "aiToolApplyFiltersDesc": { "message": "Aplica filtros à visualização atual de filmes ou séries, permitindo refinar os resultados por tipo, gênero, ano и ordem de classificação." },
"aiToolAddPlexTokenDesc": { "message": "Adiciona um novo token X-Plex à configuração da extensão, permitindo que o aplicativo verifique o conteúdo de novos servidores Plex." }, "aiToolApplyFiltersTypeParamDesc": { "message": "O tipo de conteúdo ao qual aplicar os filtros. Deve ser 'movie' ou 'series'." },
"aiToolAddPlexTokenTokenParamDesc": { "message": "A string do token X-Plex a ser adicionada." }, "aiToolApplyFiltersGenreParamDesc": { "message": "O nome do gênero pelo qual filtrar (ex: 'Ação', 'Drama')." },
"aiToolChangeRegionDesc": { "message": "Altera a região usada para a descoberta de conteúdo na API do TMDB. Isso afetará os resultados exibidos nas seções de filmes e séries, bem como os provedores de streaming." }, "aiToolApplyFiltersYearParamDesc": { "message": "O ano de lançamento pelo qual filtrar (ex: '2023')." },
"aiToolChangeRegionRegionParamDesc": { "message": "O código de país de duas letras ISO 3166-1 para a nova região (por exemplo, 'US' para os Estados Unidos, 'ES' para a Espanha, 'MX' para o México)." }, "aiToolApplyFiltersSortParamDesc": { "message": "O critério de ordenação para os resultados. Valores válidos: 'popularity.desc' (populares), 'vote_average.desc' (melhor avaliados), 'release_date.desc' (recentes para filmes) ou 'first_air_date.desc' (recentes para séries)." },
"aiToolClearAllFavoritesDesc": { "message": "Remove todos os filmes e séries que o usuário marcou como favoritos." }, "aiToolListAvailableMusicGenresDesc": { "message": "Lista todos os gêneros musicais únicos disponíveis na biblioteca local do usuário." },
"aiToolClearViewingHistoryDesc": { "message": "Limpa o histórico de visualização do usuário da página de histórico." }, "aiToolSearchMusicByGenreDesc": { "message": "Pesquisa por artistas na biblioteca de música do usuário que pertencem a um gênero específico." },
"aiToolClearRecommendationsViewDesc": { "message": "Limpa a visualização de recomendações e remove as recomendações em cache." }, "aiToolSearchMusicByGenreNameParamDesc": { "message": "O nome do gênero musical a ser pesquisado (ex: 'Rock', 'Pop', 'Jazz')." },
"aiToolSearchNotFound": { "message": "'$query' não encontrado em sua biblioteca.", "placeholders": { "query": { "content": "$1" } } }, "aiToolPlayMusicByArtistDesc": { "message": "Abre o player de música e começa a tocar músicas de um artista específico da biblioteca do usuário." },
"aiToolNavigateSuccess": { "message": "Navegado para a página $page$.", "placeholders": { "page": { "content": "$1" } } }, "aiToolPlayMusicByArtistNameParamDesc": { "message": "O nome exato do artista cujas músicas devem ser tocadas." },
"aiToolNavigateError": { "message": "Erro ao navegar para a página $page$.", "placeholders": { "page": { "content": "$1" } } }, "aiToolClearChatHistoryDesc": { "message": "Limpa todo o histórico de mensagens da conversa atual com o assistente de IA." },
"aiToolStatsError": { "message": "Erro ao obter estatísticas." }, "aiToolDeleteDatabaseDesc": { "message": "Exclui todo o banco de dados local da extensão, incluindo conteúdo escaneado, configurações e favoritos. Esta ação é irreversível e recarregará a aplicação." },
"aiToolItemNotFound": { "message": "Item '$title' não encontrado.", "placeholders": { "title": { "content": "$1" } } }, "aiToolUpdateAllTokensDesc": { "message": "Inicia um escaneamento completo de todos os servidores e bibliotecas do Plex associados aos tokens configurados na extensão. Atualiza todos os filmes, séries, artistas e fotos." },
"aiToolShowItemDetailsSuccess": { "message": "Mostrando detalhes de '$title'.", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenDesc": { "message": "Adiciona um novo token X-Plex à configuração da extensão, permitindo que a aplicação escaneie conteúdo de novos servidores Plex." },
"aiToolAddToPlaylistSuccess": { "message": "Adicionado '$title' à lista de reprodução.", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenTokenParamDesc": { "message": "A string do token X-Plex a ser adicionada." },
"aiToolFavoriteAdded": { "message": "Adicionado '$title' aos favoritos.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionDesc": { "message": "Muda a região usada для descoberta de conteúdo na API do TMDB. Isso afetará os resultados exibidos nas seções de filmes e séries, bem como os provedores de streaming." },
"aiToolFavoriteRemoved": { "message": "Removido '$title' dos favoritos.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionRegionParamDesc": { "message": "O código de país ISO 3166-1 de duas letras para a nova região (ex: 'US' para Estados Unidos, 'BR' para Brasil, 'PT' para Portugal)." },
"aiToolRecommendationsSuccess": { "message": "Mostrando recomendações." }, "aiToolClearAllFavoritesDesc": { "message": "Remove todos os filmes e séries que o usuário marcou como favoritos." },
"aiToolApplyFiltersGenreNotFound": { "message": "Gênero '$genre' não encontrado.", "placeholders": { "genre": { "content": "$1" } } }, "aiToolClearViewingHistoryDesc": { "message": "Limpa o histórico de visualização do usuário da página de histórico." },
"aiToolApplyFiltersSuccess": { "message": "Filtros aplicados com sucesso." }, "aiToolClearRecommendationsViewDesc": { "message": "Limpa a visualização de recomendações e remove as recomendações em cache." },
"aiToolPlayMusicNotReady": { "message": "O reprodutor de música não está pronto. Verifique se a sua biblioteca de música do Plex foi verificada." }, "aiToolSearchNotFound": { "message": "Não foi encontrado '$query' em sua biblioteca.", "placeholders": { "query": { "content": "$1" } } },
"aiToolPlayMusicArtistNotFound": { "message": "Artista '$artist_name' não encontrado.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolNavigateSuccess": { "message": "Navegado para a página de $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolPlayMusicNoSongs": { "message": "Nenhuma música encontrada para '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolNavigateError": { "message": "Erro ao navegar para a página de $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolPlayMusicSuccess": { "message": "Tocando música de '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } }, "aiToolStatsError": { "message": "Erro ao obter estatísticas." },
"aiToolChatHistoryCleared": { "message": "Histórico do chat limpo." }, "aiToolItemNotFound": { "message": "Item '$title' não encontrado.", "placeholders": { "title": { "content": "$1" } } },
"aiToolConfirmDeleteDatabase": { "message": "Tem certeza de que deseja excluir o banco de dados local? Esta ação é irreversível." }, "aiToolShowItemDetailsSuccess": { "message": "Mostrando detalhes de '$title'.", "placeholders": { "title": { "content": "$1" } } },
"aiToolDeleteDatabaseCancelled": { "message": "Exclusão do banco de dados cancelada." }, "aiToolAddToPlaylistSuccess": { "message": "Adicionado '$title' à lista de reprodução.", "placeholders": { "title": { "content": "$1" } } },
"aiToolExecutionError": { "message": "Erro ao executar a ferramenta '$toolName$': $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolFavoriteAdded": { "message": "Adicionado '$title' aos favoritos.", "placeholders": { "title": { "content": "$1" } } },
"aiToolUnknown": { "message": "Ferramenta desconhecida: '$toolName'.", "placeholders": { "toolName": { "content": "$1" } } }, "aiToolFavoriteRemoved": { "message": "Removido '$title' dos favoritos.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoritesCleared": { "message": "Favoritos limpos." }, "aiToolRecommendationsSuccess": { "message": "Mostrando recomendações." },
"aiToolFavoritesClearError": { "message": "Erro ao limpar os favoritos: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolApplyFiltersGenreNotFound": { "message": "Gênero '$genre' não encontrado.", "placeholders": { "genre": { "content": "$1" } } },
"aiToolRecommendationsCleared": { "message": "Recomendações limpas." }, "aiToolApplyFiltersSuccess": { "message": "Filtros aplicados com sucesso." },
"aiToolRecommendationsClearError": { "message": "Erro ao limpar as recomendações: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolSearchMusicByGenreNotFound": { "message": "Não encontrei artistas do gênero '$genre_name' em sua biblioteca.", "placeholders": { "genre_name": { "content": "$1" } } },
"aiToolDatabaseDeleted": { "message": "Banco de dados excluído. A página será recarregada." }, "aiToolPlayMusicNotReady": { "message": "O player de música não está pronto. Certifique-se de que sua biblioteca de música do Plex foi escaneada." },
"aiToolDatabaseDeleteError": { "message": "Erro ao excluir o banco de dados: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolPlayMusicArtistNotFound": { "message": "Artista '$artist_name' não encontrado.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolDatabaseDeleteBlocked": { "message": "A exclusão do banco de dados está bloqueada. Feche outras abas do aplicativo." }, "aiToolPlayMusicNoSongs": { "message": "Nenhuma música encontrada para '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolUpdateAllTokensSuccess": { "message": "Todos os tokens foram atualizados com sucesso." }, "aiToolPlayMusicSuccess": { "message": "Tocando música de '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolUpdateAllTokensError": { "message": "Erro ao atualizar os tokens: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolChatHistoryCleared": { "message": "Histórico do chat limpo." },
"aiToolAddPlexTokenSuccess": { "message": "Token do Plex adicionado com sucesso." }, "aiToolConfirmDeleteDatabase": { "message": "Você tem certeza de que deseja excluir o banco de dados local? Esta ação é irreversível." },
"aiToolAddPlexTokenError": { "message": "Erro ao adicionar o token do Plex: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolDeleteDatabaseCancelled": { "message": "Exclusão do banco de dados cancelada." },
"aiToolChangeRegionSuccess": { "message": "Região alterada para $region$. O conteúdo está sendo atualizado.", "placeholders": { "region": { "content": "$1" } } }, "aiToolExecutionError": { "message": "Erro ao executar a ferramenta '$toolName': $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } },
"aiToolChangeRegionError": { "message": "Erro ao alterar a região: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolUnknown": { "message": "Ferramenta desconhecida: '$toolName'.", "placeholders": { "toolName": { "content": "$1" } } },
"aiToolViewingHistoryCleared": { "message": "Histórico de visualização limpo." }, "aiToolFavoritesCleared": { "message": "Favoritos excluídos." },
"aiToolViewingHistoryClearError": { "message": "Erro ao limpar o histórico de visualização: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolFavoritesClearError": { "message": "Erro ao excluir os favoritos: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiSystemPrompt_v3": { "message": "Você é um assistente especialista em filmes e séries chamado CinePlex. Sua função principal é ajudar os usuários a descobrir conteúdo e interagir com sua biblioteca. Siga estas regras rigorosamente: 1. **NUNCA** finja que realizou uma ação se não usou uma ferramenta para isso. Por exemplo, não diga 'Eu baixei X' se não usou a ferramenta de download. 2. Para solicitações de recomendações ou listas (por exemplo, 'diga-me 5 filmes de terror'), use seu próprio conhecimento para gerar a lista. Apresente-a em formato numerado ou com marcadores. Depois de exibir a lista, pergunte proativamente ao usuário se ele deseja que você verifique a disponibilidade em seus servidores locais e crie um arquivo M3U. 3. **SOMENTE** se o usuário confirmar que deseja verificar ou baixar a lista, use a ferramenta `check_and_download_titles_list`. Não a use sem confirmação explícita. 4. Para qualquer outra ação, como navegar, obter estatísticas, pesquisar um título específico ou filtrar por resolução ou contêiner, use as ferramentas apropriadas. Seja sempre conciso, amigável e eficiente." }, "aiToolRecommendationsCleared": { "message": "Recomendações excluídas." },
"aiToolM3UNoTitlesProvided": { "message": "Forneça uma lista de títulos para criar a lista de reprodução." }, "aiToolRecommendationsClearError": { "message": "Erro ao excluir as recomendações: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolM3UCheckingTitles": { "message": "Verificando os títulos em seus servidores locais..." }, "aiToolDatabaseDeleted": { "message": "Banco de dados excluído. A página será recarregada." },
"aiToolM3UNoLocalMatchesForDownload": { "message": "Não encontrei nenhum dos filmes ou séries da lista em seus servidores locais." }, "aiToolDatabaseDeleteError": { "message": "Erro ao excluir o banco de dados: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolM3UDownloadStarted": { "message": "Pronto! Encontrei $1 de $2 títulos em seus servidores e iniciei o download da lista de reprodução M3U.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } }, "aiToolDatabaseDeleteBlocked": { "message": "A exclusão do banco de dados está bloqueada. Feche outras abas da aplicação." },
"backToProviders": { "message": "Voltar para os provedores" }, "aiToolUpdateAllTokensSuccess": { "message": "Todos os tokens foram atualizados com sucesso." },
"artistsCounterSingle": { "message": "$total$ artista", "placeholders": { "total": { "content": "$1" } } }, "aiToolUpdateAllTokensError": { "message": "Erro ao atualizar os tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Carregando..." }, "aiToolAddPlexTokenSuccess": { "message": "Token do Plex adicionado com sucesso." },
"downloadingSong": { "message": "Iniciando o download de \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolAddPlexTokenError": { "message": "Erro ao adicionar o token do Plex: $message$", "placeholders": { "message": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" baixado.", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionSuccess": { "message": "Região alterada para $region$. O conteúdo está sendo atualizado.", "placeholders": { "region": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Erro ao baixar \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolChangeRegionError": { "message": "Erro ao alterar a região: $message$", "placeholders": { "message": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Gerando M3U para \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } }, "aiToolViewingHistoryCleared": { "message": "Histórico de visualização limpo." },
"albumM3UGenerated": { "message": "M3U para o álbum \"$artist$\" gerado.", "placeholders": { "artist": { "content": "$1" } } }, "aiToolViewingHistoryClearError": { "message": "Erro ao limpar o histórico de visualização: $message$", "placeholders": { "message": { "content": "$1" } } },
"retyingSection": { "message": "Tentando novamente a seção \"$title$\"", "placeholders": { "title": { "content": "$1" } } }, "aiToolM3UDownloadStartedSingle": { "message": "Iniciando o download do M3U para '$movie_title'.", "placeholders": { "movie_title": { "content": "$1" } } },
"retrySuccess": { "message": "[SUCESSO] Nova tentativa de \"$title$\" concluída.", "placeholders": { "title": { "content": "$1" } } }, "aiToolM3UDownloadStartedSeason": { "message": "Iniciando o download do M3U para a temporada $1 de '$2'.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } },
"retryError": { "message": "[ERRO FINAL] A nova tentativa para \"$title$\" falhou: $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolM3UNoTitlesProvided": { "message": "Por favor, forneça uma lista de títulos para criar a lista de reprodução." },
"startingRetryPhase": { "message": "Iniciando a fase de nova tentativa para $count$ seções...", "placeholders": { "count": { "content": "$1" } } }, "aiToolM3UCheckingTitles": { "message": "Verificando os títulos em seus servidores locais..." },
"tokenFoundServers": { "message": "O token $token$... encontrou $count$ servidores.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } }, "aiToolM3UNoLocalMatchesForDownload": { "message": "Não encontrei nenhum dos filmes ou séries da lista em seus servidores locais." },
"errorProcessingToken": { "message": "Erro ao processar o token $token$...: $message$", "placeholders": { "token": { "content": "$1" }, "message": { "content": "$2" } } }, "aiToolM3UDownloadStarted": { "message": "Pronto! Encontrei $1 dos $2 títulos em seus servidores e iniciei o download da lista de reprodução M3U.", "placeholders": { "1": { "content": "$1" }, "2": { "content": "$2" } } },
"plexScanFatalError": { "message": "ERRO FATAL: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiToolTrailerNotFoundSpecific": { "message": "Desculpe, não consegui encontrar um trailer disponível para '$title'.", "placeholders": { "title": { "content": "$1" } } },
"errorDuringScan": { "message": "Erro durante a verificação: $message$", "placeholders": { "message": { "content": "$1" } } }, "aiSystemPrompt_v4": {
"stoppingPlexScan": { "message": "Parando a verificação do Plex..." }, "message": "Você é um assistente virtual integrado a uma extensão do Chrome que interage com servidores Plex e Jellyfin. Sua função principal é ajudar o usuário a pesquisar, gerenciar, reproduzir e baixar conteúdo multimídia, bem como gerenciar configurações personalizadas.\n\nPRIORIDADE MÁXIMA: Sempre que a pergunta do usuário se referir a conteúdo multimídia (filmes, séries, música), VOCÊ DEVE presumir que se refere à sua biblioteca local. Use as ferramentas para pesquisar em seu banco de dados ANTES de pesquisar na web.\n\n🎯 Regras gerais de comportamento:\nResponda sempre de forma clara, concisa e direta. Seja proativo e forneça todas as informações relevantes de uma vez para evitar perguntas de acompanhamento. Por exemplo, ao confirmar a disponibilidade de uma série, inclua os detalhes das temporadas.\n\nCompare a data atual com os resultados da pesquisa do Google quando solicitado por informações externas para garantir que estejam atualizadas.\n\nUse os nomes exatos dos comandos definidos no sistema (function.name) ao chamar as ferramentas.\n\n📦 Funções-chave para conteúdo multimídia:\nPara gerar um M3U para um único filme, use download_single_movie_m3u.\nPara baixar uma temporada específica de uma série, use download_series_season_m3u.\nPara múltiplos títulos (filmes ou séries completas), use sempre check_and_download_titles_list.\nPara pesquisar conteúdo local: search_library.\nPara pesquisar no TMDB: search_tmdb_content.\nPara conteúdo em alta: get_trending_content.\nPara mostrar detalhes de um título: show_item_details.\nPara adicionar à lista de reprodução PHP: add_to_playlist.\nPara verificar a disponibilidade local: check_local_availability.\nSe uma série estiver disponível localmente, informe quantas temporadas existem e em quais servidores usando get_local_series_seasons.\nPara ver recomendações: get_recommendations.\nPara aplicar filtros: apply_filters.\nPara ver histórico ou favoritos: view_history, view_favorites.\nPara marcar como favorito: toggle_favorite.\nPara reproduzir trailer: play_trailer.\n\n🎵 Funções de música:\nSe o usuário pedir recomendações de gêneros musicais de forma geral (ex: 'recomende-me um gênero para me animar'), primeiro use list_available_music_genres para ver quais gêneros ele tem e baseie sua recomendação nessa lista.\nPara listar todos os gêneros musicais disponíveis na biblioteca: list_available_music_genres.\nPara pesquisar artistas por gênero: search_music_by_genre.\nPara tocar músicas por título e/ou artista: play_song.\nPara tocar música de um artista: play_music_by_artist.\n\n🧰 Funções de gerenciamento e configuração:\nPara obter estatísticas do usuário: get_user_stats.\nPara navegar para seções específicas: navigate_to_page.\nPara atualizar tokens: update_all_tokens, add_plex_token.\nPara alterar a região do conteúdo: change_region.\nPara exportar ou importar o banco de dados local: export_local_database, import_local_database.\nPara excluir o banco de dados: delete_database.\nPara limpar favoritos, histórico ou recomendações: clear_all_favorites, clear_viewing_history, clear_recommendations_view.\nPara alternar o modo claro/escuro: toggle_light_mode.\nPara mostrar ou ocultar a seção hero: toggle_hero_section.\n\n⚠ Considerações adicionais:\nDê prioridade ao conteúdo disponível localmente. Use check_local_availability antes de mostrar opções de reprodução ou download.\nSe uma ação falhar, informe de maneira clara e direta.\nEvite repetir desnecessariamente a solicitação do usuário, a menos que ajude a contextualizar a resposta."
"invalidTokenProvided": { "message": "Token inválido fornecido." }, },
"tokenAlreadyExists": { "message": "O token já existe." }, "backToProviders": { "message": "Voltar aos Provedores" },
"tokenAddedSuccessfully": { "message": "Token adicionado com sucesso." }, "artistsCounterSingle": { "message": "$total$ Artista", "placeholders": { "total": { "content": "$1" } } },
"noStreamsFoundForSelection": { "message": "Nenhum stream encontrado para a seleção." }, "artistsCounterLoading": { "message": "Carregando..." },
"autoplayBlocked": { "message": "Reprodução automática bloqueada." }, "downloadingSong": { "message": "Iniciando o download de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"page": { "message": "Página" }, "songDownloaded": { "message": "\"$title$\" baixado.", "placeholders": { "title": { "content": "$1" } } },
"all": { "message": "Todos" }, "errorDownloadingSong": { "message": "Erro ao baixar \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"userScore": { "message": "Pontuação dos usuários" }, "generatingAlbumM3U": { "message": "Gerando M3U para \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
"duration": { "message": "Duração" }, "albumM3UGenerated": { "message": "M3U para o álbum \"$artist$\" gerado.", "placeholders": { "artist": { "content": "$1" } } },
"min": { "message": "Mín" }, "retyingSection": { "message": "Tentando novamente a seção \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"max": { "message": "Máx" } "retrySuccess": { "message": "[SUCESSO] Nova tentativa de \"$title$\" concluída.", "placeholders": { "title": { "content": "$1" } } },
"retryError": { "message": "[ERRO FINAL] Falha na nova tentativa para \"$title$\": $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } },
"startingRetryPhase": { "message": "Iniciando fase de novas tentativas para $count$ seções...", "placeholders": { "count": { "content": "$1" } } },
"tokenFoundServers": { "message": "Token $token$... encontrou $count$ servidores.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } },
"errorProcessingToken": { "message": "Erro ao processar o token $token$...: $message$", "placeholders": { "token": { "content": "$1" }, "message": { "content": "$2" } } },
"plexScanFatalError": { "message": "ERRO FATAL: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorDuringScan": { "message": "Erro durante o escaneamento: $message$", "placeholders": { "message": { "content": "$1" } } },
"stoppingPlexScan": { "message": "Parando escaneamento do Plex..." },
"invalidTokenProvided": { "message": "Token fornecido inválido." },
"tokenAlreadyExists": { "message": "O token já existe." },
"tokenAddedSuccessfully": { "message": "Token adicionado com sucesso." },
"noStreamsFoundForSelection": { "message": "Nenhum stream encontrado para a seleção." },
"autoplayBlocked": { "message": "Reprodução automática bloqueada." },
"welcomeToCinePlex": { "message": "" },
"page": { "message": "Página" },
"all": { "message": "Tudo" },
"userScore": { "message": "Pontuação" },
"duration": { "message": "Duração" },
"min": { "message": "Mín" },
"max": { "message": "Máx" },
"aiToolFindStreamingProvidersDesc": { "message": "Encontra onde assistir a um filme ou série em serviços de streaming." },
"aiToolFindStreamingProvidersTitleParamDesc": { "message": "O título do filme ou série a ser pesquisado." },
"aiToolFindStreamingProvidersTypeParamDesc": { "message": "O tipo de conteúdo (filme ou série)." },
"aiToolFindStreamingProvidersYearParamDesc": { "message": "O ano de lançamento do conteúdo (opcional)." },
"aiToolNoStreamingProviders": { "message": "Nenhum provedor de streaming encontrado para {title}." },
"aiToolStreamingProvidersFound": { "message": "{title} está disponível nos seguintes serviços: {providers}." },
"aiToolStreamingProviderError": { "message": "Erro ao pesquisar provedores de streaming: {message}." },
"aiToolGetLocalSeriesSeasonsDesc": { "message": "Verifica se uma série de TV está disponível localmente e retorna um detalhamento das temporadas disponíveis em cada servidor." },
"aiToolGetLocalSeriesSeasonsTitleParamDesc": { "message": "O título da série a ser verificada." },
"aiToolGetLocalSeriesSeasonsYearParamDesc": { "message": "O ano de lançamento da série (opcional para maior precisão)." },
"aiToolLocalSeriesNoSeasons": { "message": "A série '$series_title' está em sua biblioteca, mas não foram encontrados detalhes das temporadas.", "placeholders": { "series_title": { "content": "$1" } } },
"artist": { "message": "Artista" },
"tracks": { "message": "faixas" },
"noSongsFound": { "message": "Nenhuma música encontrada para este artista." },
"durationMin": { "message": "Duração (Min)" },
"score": { "message": "Pontuação" },
"searchGenre": { "message": "Buscar gênero..." },
"searchArtists": { "message": "Buscar artistas..." },
"preparingMusicLibrary": { "message": "Preparando sua biblioteca de música..." },
"preparingMusicLibraryDesc": { "message": "Este processo único pode levar alguns minutos se você tiver muitos artistas." },
"artistsProgress": { "message": "0 / 0 artistas" },
"starting": { "message": "Iniciando..." },
"artistName": { "message": "Nome do Artista" },
"playPause": { "message": "Tocar/Pausar" },
"noLocalFilesFound": { "message": "Nenhum arquivo local encontrado para este título." },
"server": { "message": "Servidor" },
"title": { "message": "Título" },
"year": { "message": "Ano" },
"resolution": { "message": "Resolução" },
"size": { "message": "Tamanho" },
"container": { "message": "Contêiner" },
"action": { "message": "Ação" },
"generate": { "message": "Gerar" },
"availableLocalFiles": { "message": "Arquivos Locais Disponíveis" },
"downloadSeason": { "message": "Baixar Temporada" },
"errorLoadingServersM3u": { "message": "Erro ao carregar os servidores para o gerador M3U:" },
"errorFetchingLibraries": { "message": "Erro ao buscar as bibliotecas." },
"selectServerAndLibrary": { "message": "Por favor, selecione um servidor e pelo menos uma biblioteca." },
"generating": { "message": "Gerando..." },
"errorProcessingLibrary": { "message": "Erro ao processar a biblioteca" },
"errorProcessingLibrarySkipping": { "message": "Erro ao processar a biblioteca. Pulando." },
"allLibrariesFailed": { "message": "Todas as bibliotecas selecionadas falharam ao processar." },
"m3uGeneratedWithErrors": { "message": "M3U gerado com alguns erros. Algumas bibliotecas могут estar faltando." },
"m3uDownloadedSuccess": { "message": "Lista de reprodução M3U baixada com sucesso." },
"errorGeneratingM3uFile": { "message": "Erro ao gerar o arquivo M3U." },
"chatSources": { "message": "Fontes" },
"chatUnnamedSource": { "message": "Fonte sem nome" },
"googleApiFailure": { "message": "Falha na chamada da API do Google AI:" }
} }

376
css/base.css Normal file
View File

@ -0,0 +1,376 @@
:root {
--primary: #0a0a0f;
--secondary: #101116;
--accent: #00bfff;
--accent-dark: #0072ff;
--text-primary: #f0f0f5;
--text-secondary: rgba(240, 240, 245, 0.7);
--gradient: linear-gradient(135deg, var(--accent), var(--accent-dark));
--card-bg: rgba(20, 21, 27, 0.8);
--glass: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.1);
--shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
--transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
--success: #4caf50;
--warning: #ffc107;
--danger: #f44336;
--info: #2196f3;
--border-radius-lg: 18px;
--border-radius-md: 14px;
--border-radius-sm: 10px;
--topbar-height: 60px;
--sidebar-width: 240px;
}
body.light-theme {
--primary: #f4f7fa;
--secondary: #ffffff;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--card-bg: #ffffff;
--glass: rgba(0, 0, 0, 0.03);
--glass-border: rgba(0, 0, 0, 0.08);
--shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body.unlocalized {
visibility: hidden;
}
body {
background-color: var(--primary);
color: var(--text-primary);
font-family: 'Montserrat', sans-serif;
line-height: 1.6;
min-height: 100vh;
overflow-x: hidden;
position: relative;
transition: background-color 0.3s, color 0.3s;
}
#main-container {
padding-left: 0;
transition: padding-left 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
body.details-view-active {
overflow: hidden;
}
#particles-js {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: -1;
opacity: 0.25;
}
body.light-theme #particles-js {
opacity: 0.5;
}
@keyframes spin {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (min-width: 992px) {
#main-container.sidebar-open {
padding-left: var(--sidebar-width);
}
}
@media (max-width: 576px) {
:root {
--border-radius-lg: 14px;
--border-radius-md: 10px;
--border-radius-sm: 8px;
--topbar-height: 55px;
}
}
body::-webkit-scrollbar, .item-details::-webkit-scrollbar {
width: 12px;
}
body::-webkit-scrollbar-track, .item-details::-webkit-scrollbar-track {
background: var(--primary);
border-left: 1px solid var(--glass-border);
}
body::-webkit-scrollbar-thumb, .item-details::-webkit-scrollbar-thumb {
background-color: var(--accent-dark);
border-radius: 10px;
border: 3px solid var(--primary);
}
body::-webkit-scrollbar-thumb:hover, .item-details::-webkit-scrollbar-thumb:hover {
background-color: var(--accent);
}
body.light-theme::-webkit-scrollbar-track, body.light-theme .item-details::-webkit-scrollbar-track {
background: var(--secondary);
border-left: 1px solid var(--glass-border);
}
body.light-theme::-webkit-scrollbar-thumb, body.light-theme .item-details::-webkit-scrollbar-thumb {
background-color: #bdc3c7;
border-color: var(--secondary);
}
body.light-theme::-webkit-scrollbar-thumb:hover, body.light-theme .item-details::-webkit-scrollbar-thumb:hover {
background-color: #a3aab1;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.7rem;
padding: 0.7rem 1.8rem;
font-size: 0.9rem;
font-weight: 600;
text-transform: uppercase;
border: none;
border-radius: 50px;
cursor: pointer;
transition: var(--transition);
letter-spacing: 1px;
line-height: 1.5;
position: relative;
overflow: hidden;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
}
.btn i {
line-height: 1;
}
.btn:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.btn-primary {
background: var(--gradient);
color: var(--primary) !important;
box-shadow: 0 6px 20px rgba(0, 224, 255, 0.25);
}
.btn-primary:hover:not(:disabled) {
background: linear-gradient(135deg, var(--accent-dark), var(--accent));
box-shadow: 0 8px 25px rgba(0, 224, 255, 0.35);
transform: translateY(-3px);
}
.btn-secondary {
background: var(--glass);
color: var(--text-primary);
border: 1px solid var(--glass-border);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.btn-secondary:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.15);
transform: translateY(-3px);
color: var(--text-primary);
}
.light-theme .btn-secondary:hover:not(:disabled) {
background: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.1);
}
.btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 1.25rem;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
transition: var(--transition);
}
.btn-icon:hover {
color: var(--accent);
background-color: var(--glass);
}
.spinner {
display: none;
width: 45px;
height: 45px;
border: 5px solid rgba(240, 240, 245, 0.2);
border-top: 5px solid var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1050;
}
.light-theme .spinner {
border: 5px solid rgba(0, 0, 0, 0.1);
border-top: 5px solid var(--accent-dark);
}
#consoleOutput {
border: 1px solid var(--glass-border);
padding: 15px;
margin: 20px 0;
height: 250px;
overflow-y: scroll;
background-color: rgba(10, 10, 15, 0.9);
color: var(--text-secondary);
font-family: monospace;
font-size: 0.85rem;
border-radius: var(--border-radius-md);
display: none;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2);
}
.light-theme #consoleOutput {
background-color: var(--primary);
}
#consoleOutput .console-log-entry {
margin-bottom: 6px;
line-height: 1.4;
word-break: break-word;
white-space: pre-wrap;
}
#consoleOutput .log-time {
color: var(--accent);
margin-right: 8px;
font-weight: 500;
}
#consoleOutput .log-message {
color: var(--text-secondary);
}
#consoleOutput .log-error .log-message {
color: var(--danger);
}
#consoleOutput .log-warning .log-message {
color: var(--warning);
}
#consoleOutput .log-success .log-message {
color: var(--success);
}
.form-control {
background-color: var(--glass);
border: 1px solid var(--glass-border);
color: var(--text-primary);
border-radius: var(--border-radius-sm);
padding: .6rem 1rem;
}
.form-control:focus {
background-color: var(--glass);
color: var(--text-primary);
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(0,224,255,.2);
}
.form-control::placeholder {
color: var(--text-secondary);
}
.form-label {
color: var(--text-primary);
font-weight: 500;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 28px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-switch label {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--glass-border);
transition: .4s;
border-radius: 28px;
}
.toggle-switch label:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
.toggle-switch input:checked + label {
background: var(--gradient);
}
.toggle-switch input:checked + label:before {
transform: translateX(22px);
}
body.unlocalized #main-container,
body.unlocalized footer,
body.unlocalized header,
body.unlocalized .sidebar-nav {
opacity: 0;
visibility: hidden;
}
body.unlocalized #spinner {
display: block;
}
#main-container, footer, header, .sidebar-nav {
transition: opacity 0.4s ease-in-out;
}

332
css/chat.css Normal file
View File

@ -0,0 +1,332 @@
#fab-container {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 1050;
display: flex;
flex-direction: row-reverse;
gap: 1rem;
align-items: flex-end;
transition: bottom 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
body.miniplayer-active #fab-container {
bottom: calc(85px + 2rem);
}
.chat-fab, .fab-btn {
position: relative;
right: auto;
bottom: auto;
}
.chat-fab {
width: 60px;
height: 60px;
background: var(--gradient);
color: var(--primary);
border-radius: 50%;
border: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
cursor: pointer;
box-shadow: 0 5px 20px rgba(0, 224, 255, 0.3);
transition: var(--transition);
}
.chat-fab:hover {
transform: scale(1.1);
box-shadow: 0 8px 25px rgba(0, 224, 255, 0.4);
}
.chat-window {
position: fixed;
bottom: 95px;
right: 2rem;
width: 400px;
height: 520px;
max-width: 90vw;
max-height: 70vh;
background-color: var(--primary);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow);
border: 1px solid var(--glass-border);
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 1051;
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
background: rgba(10, 10, 15, 0.8);
cursor: default;
}
.light-theme .chat-window {
background: rgba(244, 247, 250, 0.8);
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.8rem 1.2rem;
background-color: rgba(255,255,255,0.05);
border-bottom: 1px solid var(--glass-border);
cursor: move;
flex-shrink: 0;
}
.chat-title {
font-family: 'Orbitron', sans-serif;
color: var(--text-primary);
font-size: 1.1rem;
margin: 0;
}
.chat-close-btn {
background: none;
border: none;
color: var(--text-secondary);
font-size: 1.2rem;
cursor: pointer;
transition: var(--transition);
}
.chat-close-btn:hover {
color: var(--accent);
transform: rotate(90deg);
}
.chat-messages {
flex-grow: 1;
padding: 1rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.chat-messages::-webkit-scrollbar {
width: 8px;
}
.chat-messages::-webkit-scrollbar-track {
background: transparent;
}
.chat-messages::-webkit-scrollbar-thumb {
background-color: var(--glass-border);
border-radius: 4px;
}
.message-wrapper {
display: flex;
align-items: flex-end;
gap: 0.75rem;
max-width: 90%;
animation: slide-in-bottom 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
.assistant-wrapper {
align-self: flex-start;
}
.user-wrapper {
align-self: flex-end;
flex-direction: row-reverse;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--gradient);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 0 10px rgba(0, 224, 255, 0.3);
}
.avatar svg {
width: 20px;
height: 20px;
color: var(--primary);
}
.message {
padding: 0.8rem 1.2rem;
border-radius: var(--border-radius-md);
line-height: 1.6;
word-wrap: break-word;
}
.message p {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
}
.user-message {
background: var(--gradient);
color: var(--primary);
border-bottom-right-radius: 4px;
}
.assistant-message {
background-color: var(--secondary);
color: var(--text-primary);
border-bottom-left-radius: 4px;
}
.chat-item-actions {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--glass-border);
}
.chat-action-title {
display: block;
font-weight: 600;
color: var(--accent);
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.chat-action-buttons {
display: flex;
flex-wrap: wrap;
gap: .5rem;
}
.chat-action-buttons button {
background-color: var(--glass);
border: 1px solid var(--glass-border);
color: var(--text-secondary);
padding: .4rem .8rem;
border-radius: 20px;
font-size: .8rem;
cursor: pointer;
transition: var(--transition);
}
.chat-action-buttons button:hover {
background-color: var(--accent);
color: var(--primary);
border-color: var(--accent);
transform: translateY(-2px);
}
.chat-download-all {
margin-top: 1rem;
}
.typing-indicator-bubble {
display: flex;
gap: 6px;
align-items: center;
padding: 14px 16px;
}
.typing-indicator-bubble span {
width: 8px;
height: 8px;
background-color: var(--text-secondary);
border-radius: 50%;
animation: typing-pulse 1.4s infinite;
}
.typing-indicator-bubble span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator-bubble span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing-pulse {
0%, 100% { opacity: 0.2; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1); }
}
#chat-input-form {
display: flex;
padding: 0.8rem;
border-top: 1px solid var(--glass-border);
gap: 0.8rem;
background: linear-gradient(to top, rgba(16, 17, 22, 0.8), rgba(16, 17, 22, 0.5));
align-items: flex-end;
}
.chat-input {
flex-grow: 1;
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 20px;
padding: 0.7rem 1.2rem;
color: var(--text-primary);
resize: none;
font-family: 'Montserrat', sans-serif;
font-size: 0.95rem;
transition: all 0.3s ease;
}
.chat-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 15px rgba(0, 224, 255, 0.2);
background: var(--secondary);
}
.chat-send-btn {
flex-shrink: 0;
background: var(--accent);
color: var(--primary);
border: none;
border-radius: 12px;
width: 42px;
height: 42px;
font-size: 1.1rem;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
}
.chat-send-btn svg {
transition: transform 0.3s ease;
}
.chat-send-btn:hover {
background: var(--accent-dark);
transform: scale(1.1);
box-shadow: 0 0 10px rgba(0, 224, 255, 0.4);
}
.chat-send-btn:hover svg {
transform: translateX(2px) rotate(-15deg);
}
.chat-send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.chat-send-btn:disabled:hover svg {
transform: none;
}
@keyframes slide-in-bottom {
0% {
transform: translateY(20px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@media (max-width: 768px) {
body.miniplayer-active #fab-container {
bottom: calc(110px + 1rem);
}
}
@media (max-width: 480px) {
.chat-window {
width: 100%;
height: 100%;
bottom: 0;
right: 0;
border-radius: 0;
max-height: none;
}
}

371
css/content.css Normal file
View File

@ -0,0 +1,371 @@
.main-content {
padding: 0 2rem 4rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.section-title {
font-family: 'Orbitron', sans-serif;
font-size: clamp(1.6rem, 4vw, 2rem);
font-weight: 600;
position: relative;
padding-bottom: 0.7rem;
}
.section-title::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
height: 4px;
width: 70px;
background: var(--gradient);
border-radius: 3px;
}
.section-subtitle {
font-family: 'Orbitron', sans-serif;
font-size: 1.7rem;
font-weight: 600;
padding-bottom: 0.8rem;
margin-bottom: 2rem;
border-bottom: 1px solid var(--glass-border);
position: relative;
color: var(--text-primary);
}
.filters {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 2rem;
}
.filter-select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
padding: 0.7rem 2.8rem 0.7rem 1.4rem;
font-size: 0.9rem;
color: var(--text-primary);
background-color: var(--secondary);
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23f0f0f5'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 1rem center;
background-size: 1rem;
border: 1px solid var(--glass-border);
border-radius: 50px;
cursor: pointer;
transition: var(--transition);
}
.light-theme .filter-select {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%231f2937'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
}
.filter-select option {
background: var(--primary);
color: var(--text-primary);
border: none;
}
.filter-select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(0, 224, 255, 0.2);
}
#main-view {
min-height: calc(100vh - var(--topbar-height) - 100px);
}
.content-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 1.8rem;
}
.item-card {
background: var(--card-bg);
border-radius: var(--border-radius-lg);
overflow: hidden;
position: relative;
transition: var(--transition);
box-shadow: var(--shadow);
cursor: pointer;
border: 1px solid transparent;
display: flex;
flex-direction: column;
}
.item-card:hover {
transform: translateY(-10px) scale(1.03);
box-shadow: 0 18px 45px rgba(0, 0, 0, 0.5);
border-color: rgba(0, 224, 255, 0.5);
z-index: 10;
}
.item-card .badge {
position: absolute;
top: 1rem;
font-size: 0.7rem;
font-weight: 600;
padding: 0.35rem 0.8rem;
border-radius: 20px;
z-index: 3;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.item-card .top-badge {
left: 1rem;
background: var(--warning);
color: var(--primary);
}
.item-card .available-badge {
right: 1rem;
background: var(--success);
color: var(--primary);
}
.item-poster {
display: block;
width: 100%;
height: auto;
aspect-ratio: 2 / 3;
object-fit: cover;
background-color: var(--secondary);
background-size: cover;
background-position: center;
transition: transform 0.6s cubic-bezier(0.23, 1, 0.32, 1);
}
.item-card:hover .item-poster {
transform: scale(1.08);
}
.item-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to top, rgba(10, 10, 15, 0.9) 0%, transparent 50%);
opacity: 0;
transition: opacity 0.4s ease;
z-index: 1;
display: flex;
align-items: flex-end;
justify-content: center;
}
.item-card:hover .item-overlay {
opacity: 1;
}
.item-info {
padding: 1.2rem;
position: relative;
z-index: 2;
margin-top: auto;
background: var(--card-bg);
transition: var(--transition);
}
.item-title {
font-size: 1.05rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.3s ease;
}
.item-card:hover .item-title {
color: var(--accent);
}
.item-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.85rem;
color: var(--text-secondary);
}
.item-meta span {
display: flex;
align-items: center;
gap: 0.4rem;
}
.item-rating {
font-weight: 600;
}
.item-rating.rating-good {
color: var(--success);
}
.item-rating.rating-ok {
color: var(--warning);
}
.item-rating.rating-bad {
color: var(--danger);
}
.item-actions {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
z-index: 3;
opacity: 0;
pointer-events: none;
transform: translateY(10px);
transition: opacity 0.4s ease, transform 0.4s ease;
}
.item-card:hover .item-actions {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.action-btn {
display: flex;
justify-content: center;
align-items: center;
width: 44px;
height: 44px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
color: var(--text-primary);
border-radius: 50%;
border: 1px solid var(--glass-border);
cursor: pointer;
transition: var(--transition);
font-size: 1rem;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.action-btn:hover:not(:disabled) {
background: var(--accent);
color: var(--primary);
transform: scale(1.1);
border-color: transparent;
box-shadow: 0 6px 15px rgba(0, 224, 255, 0.3);
}
.action-btn.favorites-btn.active {
background: var(--danger);
color: white;
}
.action-btn.favorites-btn.active:hover {
background: #c62828;
}
.action-btn.disabled-btn {
cursor: not-allowed;
opacity: 0.6;
background: rgba(80, 80, 80, 0.3);
}
.action-btn.disabled-btn:hover {
transform: none;
background: rgba(80, 80, 80, 0.3);
color: var(--text-primary);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.recommendations-section {
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius-lg);
padding: 2.5rem;
margin-top: 4rem;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.recommendations-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 2rem;
}
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 4rem 2rem;
background: var(--glass);
border: 1px dashed var(--glass-border);
border-radius: var(--border-radius-lg);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.empty-state i.fas,
.empty-state i.far {
font-size: 3.5rem;
color: var(--accent);
opacity: 0.6;
margin-bottom: 1.5rem;
display: block;
}
.empty-state p.lead {
font-size: 1.3rem;
color: var(--text-primary);
margin-bottom: 0.8rem;
}
.empty-state p.text-muted {
font-size: 1rem;
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
.empty-state .btn {
margin-top: 1rem;
}
@media (max-width: 992px) {
.content-grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 1.5rem;
}
}
@media (max-width: 768px) {
.section-title {
font-size: 1.5rem;
}
.content-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 1.2rem;
}
}
@media (max-width: 576px) {
.content-grid {
grid-template-columns: repeat(auto-fill, minmax(125px, 1fr));
gap: 1rem;
}
}

View File

@ -101,3 +101,47 @@ input[id$="-min"].form-range {
.form-range::-moz-range-thumb:hover { .form-range::-moz-range-thumb:hover {
background-color: var(--accent); background-color: var(--accent);
} }
.filter-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-group label {
font-size: 0.9rem;
color: var(--text-secondary);
white-space: nowrap;
}
.filter-input {
width: 80px;
padding: 0.7rem 1rem;
font-size: 0.9rem;
color: var(--text-primary);
background-color: var(--secondary);
border: 1px solid var(--glass-border);
border-radius: 50px;
transition: var(--transition);
}
.filter-input::placeholder {
color: var(--text-secondary);
opacity: 0.7;
}
.filter-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(0, 224, 255, 0.2);
}
.filter-input::-webkit-outer-spin-button,
.filter-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.filter-input[type=number] {
-moz-appearance: textfield;
}

667
css/details-view.css Normal file
View File

@ -0,0 +1,667 @@
.item-details {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: none;
overflow-y: auto;
background: var(--primary);
z-index: 1035;
clip-path: inset(0 0 0 0);
}
.item-details.active {
display: block;
}
.back-button {
position: absolute;
top: 2rem;
left: 2rem;
z-index: 10;
display: flex;
justify-content: center;
align-items: center;
width: 42px;
height: 42px;
background: var(--glass);
color: var(--text-primary);
border-radius: 50%;
border: 1px solid var(--glass-border);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
cursor: pointer;
transition: var(--transition);
}
.back-button:hover {
background: var(--accent);
color: var(--primary);
transform: scale(1.1);
box-shadow: 0 4px 15px rgba(0, 224, 255, 0.3);
}
.details-backdrop-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 60vh;
overflow: hidden;
z-index: 0;
}
.details-backdrop-img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 20%;
opacity: 0.4;
}
.details-backdrop-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to top, var(--primary) 15%, transparent 50%),
linear-gradient(to right, var(--primary) 10%, transparent 70%);
}
.item-details-container {
max-width: 1200px;
margin: 0 auto;
position: relative;
z-index: 1;
padding: 8rem 2rem 5rem;
}
.item-details-header {
display: flex;
flex-direction: row;
gap: 3rem;
margin-bottom: 3rem;
align-items: flex-start;
}
.item-details-poster-wrapper {
flex-shrink: 0;
width: 100%;
max-width: 320px;
}
.item-details-poster {
display: block;
width: 100%;
height: auto;
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow);
transition: var(--transition);
}
.item-details-poster:hover {
transform: scale(1.03);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
}
#item-details-content {
opacity: 0;
}
.item-details-content {
flex: 1;
min-width: 0;
}
.item-details-title {
font-family: 'Orbitron', sans-serif;
font-size: clamp(2rem, 5vw, 3.2rem);
font-weight: 700;
margin-bottom: 0.5rem;
line-height: 1.2;
}
.item-details-tagline {
font-size: 1.2rem;
font-style: italic;
color: var(--text-secondary);
margin-bottom: 1.5rem;
font-weight: 300;
}
.item-details-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem 1.8rem;
margin-bottom: 1.8rem;
font-size: 0.95rem;
}
.item-details-meta-item {
display: flex;
align-items: center;
gap: 0.6rem;
color: var(--text-secondary);
}
.item-details-meta-item i {
color: var(--accent);
font-size: 1.05rem;
}
.item-details-overview {
font-size: 1.05rem;
margin-bottom: 2rem;
line-height: 1.8;
color: rgba(240, 240, 245, 0.85);
}
.item-details-genres {
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
margin-bottom: 1.8rem;
}
.genre-badge {
padding: 0.5rem 1.1rem;
font-size: 0.8rem;
font-weight: 500;
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 50px;
transition: var(--transition);
cursor: default;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.item-details-crew p {
margin-bottom: 0.5rem;
font-size: 0.95rem;
color: var(--text-secondary);
}
.item-details-crew strong {
color: var(--text-primary);
margin-right: 0.5em;
}
.item-details-external-links a {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--text-secondary);
text-decoration: none;
padding: 0.4rem 0.9rem;
border: 1px solid var(--glass-border);
border-radius: 50px;
font-size: 0.85rem;
background: var(--glass);
transition: var(--transition);
}
.item-details-external-links a:hover {
color: var(--accent);
border-color: var(--accent);
background: rgba(0, 224, 255, 0.1);
}
.item-details-actions {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 2rem;
}
.item-details-section {
margin-bottom: 3.5rem;
}
.cast-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: 1.8rem;
}
.cast-card {
text-align: center;
transition: var(--transition);
cursor: pointer;
background: var(--secondary);
padding: 1rem;
border-radius: var(--border-radius-md);
}
.cast-card:hover {
transform: translateY(-8px);
background: var(--glass);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
}
.cast-photo {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
margin: 0 auto 1rem auto;
border: 3px solid var(--glass-border);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
transition: var(--transition);
}
.cast-card:hover .cast-photo {
transform: scale(1.05);
border-color: var(--accent);
box-shadow: 0 6px 18px rgba(0, 224, 255, 0.25);
}
.cast-name {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.3rem;
color: var(--text-primary);
}
.cast-character {
font-size: 0.85rem;
color: var(--text-secondary);
}
.similar-items-grid, .filmography-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 1.5rem;
}
.similar-item-card {
background: transparent;
border-radius: var(--border-radius-md);
overflow: hidden;
transition: var(--transition);
cursor: pointer;
position: relative;
}
.similar-item-card:hover {
transform: translateY(-8px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}
.similar-item-card:hover .similar-item-poster {
transform: scale(1.05);
}
.similar-item-poster {
display: block;
width: 100%;
height: auto;
border-radius: var(--border-radius-md);
aspect-ratio: 2 / 3;
object-fit: cover;
transition: transform 0.4s ease;
}
.similar-item-info {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 0.8rem;
background: linear-gradient(to top, rgba(10, 10, 15, 0.95) 0%, transparent 100%);
border-bottom-left-radius: var(--border-radius-md);
border-bottom-right-radius: var(--border-radius-md);
}
.similar-item-title {
color: var(--text-primary);
font-size: 0.9rem;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.seasons-tabs {
background: var(--card-bg);
border-radius: var(--border-radius-lg);
border: 1px solid var(--glass-border);
overflow: hidden;
}
.season-tabs-list {
display: flex;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border-bottom: 1px solid var(--glass-border);
padding: 0.5rem 1.5rem 0;
margin-bottom: 1rem;
}
.season-tabs-list::-webkit-scrollbar {
height: 8px;
}
.season-tabs-list::-webkit-scrollbar-thumb {
background-color: var(--accent);
border-radius: 4px;
}
.season-tabs-list::-webkit-scrollbar-track {
background: transparent;
}
.season-tab-btn {
background: transparent;
border: none;
color: var(--text-secondary);
padding: 1rem 1.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
white-space: nowrap;
position: relative;
border-bottom: 3px solid transparent;
}
.season-tab-btn.is-local::after {
content: '';
position: absolute;
top: 12px;
right: 12px;
width: 8px;
height: 8px;
background-color: var(--success);
border-radius: 50%;
border: 1px solid var(--primary);
box-shadow: 0 0 5px var(--success);
}
.season-tab-btn:hover {
color: var(--text-primary);
}
.season-tab-btn.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
.episodes-container {
padding: 1.5rem;
}
.episodes-content-header {
display: flex;
gap: 1.5rem;
padding-bottom: 1.5rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--glass-border);
}
.episode-content-details {
flex-grow: 1;
}
.season-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.season-meta {
display: flex;
gap: 1.5rem;
font-size: 0.9rem;
color: var(--text-secondary);
margin-bottom: 0.8rem;
}
.season-meta span {
display: flex;
align-items: center;
gap: 0.5rem;
}
.season-overview {
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-secondary);
}
.season-header-actions {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.season-local-badge {
background: var(--gradient);
color: var(--primary);
font-size: 0.7rem;
padding: 0.4rem 1rem;
border-radius: 20px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
display: inline-flex;
align-items: center;
gap: 0.4rem;
box-shadow: 0 0 10px rgba(0, 224, 255, 0.4);
}
.download-season-btn {
width: 42px;
height: 42px;
font-size: 1rem;
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 50%;
color: var(--text-primary);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition);
}
.download-season-btn:hover {
background: var(--accent);
color: var(--primary);
transform: scale(1.1);
}
.episodes-list {
display: grid;
gap: 1rem;
}
.episode-item {
display: grid;
grid-template-columns: 180px 1fr;
gap: 1.5rem;
padding: 1rem;
background: var(--secondary);
border-radius: var(--border-radius-md);
transition: background-color 0.3s ease;
cursor: pointer;
}
.episode-item:hover {
background: var(--glass);
}
.episode-item-image {
width: 100%;
border-radius: var(--border-radius-sm);
aspect-ratio: 16 / 9;
object-fit: cover;
}
.episode-item-content {
display: flex;
flex-direction: column;
}
.episode-item-header {
display: flex;
align-items: baseline;
gap: 1rem;
margin-bottom: 0.5rem;
}
.episode-item-number {
font-size: 1.2rem;
font-weight: 700;
color: var(--accent);
}
.episode-item-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
}
.episode-item-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1.5rem;
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
.episode-item-overview {
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-secondary);
margin-top: 0.5rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.episode-item.expanded .episode-item-overview {
display: block;
-webkit-line-clamp: unset;
}
.watch-providers-section {
margin-top: 2rem;
}
.watch-providers-grid {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
}
.watch-providers-grid .provider-link {
display: block;
transition: var(--transition);
border-radius: var(--border-radius-md);
overflow: hidden;
}
.watch-providers-grid .provider-link:hover {
transform: scale(1.1);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.watch-providers-grid .provider-logo {
display: block;
width: 50px;
height: 50px;
object-fit: cover;
border-radius: var(--border-radius-md);
background-color: var(--glass);
border: 1px solid var(--glass-border);
}
.local-files-list {
background: var(--secondary);
border-radius: var(--border-radius-md);
border: 1px solid var(--glass-border);
padding: 1rem;
overflow-x: auto;
}
.local-files-list table {
width: 100%;
border-collapse: collapse;
}
.local-files-list th, .local-files-list td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--glass-border);
font-size: 0.9rem;
}
.local-files-list th {
font-weight: 600;
color: var(--text-primary);
position: sticky;
top: 0;
background: var(--secondary);
}
.local-files-list td {
color: var(--text-secondary);
}
.local-files-list tbody tr:last-child td {
border-bottom: none;
}
.local-files-list tbody tr:hover {
background-color: var(--glass);
}
.local-files-list .btn-sm {
padding: 0.25rem 0.6rem;
font-size: 0.8rem;
}
@media (max-width: 992px) {
.item-details-header {
flex-direction: column;
align-items: center;
text-align: center;
}
.item-details-container { padding-top: calc(5vh + var(--topbar-height)); }
.item-details-poster-wrapper { max-width: 350px; margin: 0 auto 1rem; }
.item-details-content { text-align: center; }
.item-details-meta, .item-details-genres, .item-details-crew, .item-details-actions, .item-details-external-links {
justify-content: center;
}
}
@media (max-width: 768px) {
.item-details-title { font-size: 2rem; }
.cast-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 1.5rem; }
.similar-items-grid, .filmography-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 1rem; }
.back-button { top: 1rem; left: 1rem; width: 38px; height: 38px; }
.item-details-poster-wrapper { max-width: 280px; }
.episodes-content-header { flex-direction: column; }
.season-header-actions { flex-direction: row; justify-content: center; width: 100%; }
.episode-item {
grid-template-columns: 1fr;
}
}
@media (max-width: 576px) {
.item-details-poster-wrapper { max-width: 240px; }
.item-details-title { font-size: 1.8rem; }
.cast-grid { grid-template-columns: repeat(auto-fill, minmax(95px, 1fr)); gap: 1rem; }
.cast-photo { width: 80px; height: 80px; }
.similar-items-grid, .filmography-grid { grid-template-columns: repeat(2, 1fr); gap: 1rem; }
}
@media (min-width: 992px) {
.back-button {
left: calc(var(--sidebar-width) + 2rem);
}
}

80
css/footer.css Normal file
View File

@ -0,0 +1,80 @@
.footer {
background: var(--secondary);
padding: 1.5rem 2rem;
border-top: 1px solid var(--glass-border);
margin-top: 0;
}
.footer .container {
max-width: 1200px;
margin: 0 auto;
padding: 0;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1.5rem;
}
.footer-logo-link {
text-decoration: none;
}
.footer-logo-text {
font-family: 'Orbitron', sans-serif;
font-size: 1.5rem;
font-weight: 700;
letter-spacing: 1px;
background: var(--gradient);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
transition: var(--transition);
display: inline-block;
}
.footer-logo-link:hover .footer-logo-text {
transform: scale(1.03);
filter: brightness(1.1);
}
.footer-links {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
justify-content: center;
}
.footer-link {
color: var(--text-secondary);
text-decoration: none;
transition: var(--transition);
font-size: 0.9rem;
font-weight: 500;
}
.footer-link:hover {
color: var(--accent);
transform: translateY(-2px);
}
.footer-credit {
font-size: 0.9rem;
color: var(--text-secondary);
opacity: 0.8;
margin: 0;
}
@media (max-width: 768px) {
.footer-content {
flex-direction: column;
gap: 1.2rem;
}
.footer {
padding: 2rem 1rem;
}
}

141
css/header.css Normal file
View File

@ -0,0 +1,141 @@
#main-view {
padding-top: var(--topbar-height);
}
.top-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: var(--topbar-height);
padding: 0 1.5rem;
background: rgba(10, 10, 15, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--glass-border);
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 1030;
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.3s ease, box-shadow 0.3s ease;
}
body.light-theme .top-bar {
background: rgba(255, 255, 255, 0.85);
}
body.details-view-active .top-bar {
transform: translateY(-110%);
}
.top-bar-left, .top-bar-right {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.top-bar-center {
flex-grow: 1;
display: flex;
justify-content: center;
}
.logo {
font-family: 'Orbitron', sans-serif;
font-size: 1.8rem;
font-weight: 700;
letter-spacing: 1px;
background: var(--gradient);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
transition: var(--transition);
cursor: pointer;
padding: 0.5rem 0;
}
.logo:hover {
transform: scale(1.03);
filter: brightness(1.1);
}
.search-bar {
position: relative;
width: 100%;
max-width: 450px;
}
.search-input {
width: 100%;
padding: 0.6rem 1.2rem 0.6rem 2.6rem;
font-size: 0.9rem;
color: var(--text-primary);
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 50px;
transition: var(--transition);
}
.search-input::placeholder {
color: var(--text-secondary);
}
.search-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(0, 224, 255, 0.2);
}
.search-icon {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
transition: var(--transition);
font-size: 0.9rem;
}
.search-input:focus+.search-icon {
color: var(--accent);
}
@media (max-width: 768px) {
.top-bar-center {
display: none;
}
.top-bar {
justify-content: space-between;
}
.logo {
margin-left: 0.5rem;
}
.search-bar {
margin: 1rem;
position: absolute;
top: var(--topbar-height);
left: 0;
right: 0;
width: auto;
z-index: 1025;
padding: 0 1rem;
display: block;
max-width: none;
}
}
@media (max-width: 576px) {
.top-bar {
padding: 0 0.8rem;
}
.logo {
font-size: 1.5rem;
}
.btn-icon {
width: 36px;
height: 36px;
font-size: 1.1rem;
}
}

139
css/hero.css Normal file
View File

@ -0,0 +1,139 @@
.hero {
display: flex;
align-items: flex-end;
position: relative;
height: 70vh;
min-height: 550px;
max-height: 800px;
overflow: hidden;
background-color: var(--primary);
margin-bottom: 0;
}
.hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to top, var(--primary) 5%, rgba(0, 0, 0, 0.8) 40%, rgba(0, 0, 0, 0.4) 70%, transparent 100%),
linear-gradient(to right, var(--primary) 10%, transparent 70%);
z-index: 1;
}
.hero.no-overlay::before {
display: none;
}
.hero-background-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
}
.hero-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-position: center 20%;
opacity: 0;
transform: scale(1.1);
}
.hero-content {
position: relative;
z-index: 2;
max-width: 700px;
padding: 0 2rem 4rem;
opacity: 0;
}
.hero-title {
font-family: 'Orbitron', sans-serif;
font-size: clamp(2.5rem, 5vw, 3.8rem);
font-weight: 700;
line-height: 1.15;
margin-bottom: 1rem;
text-shadow: 0 3px 15px rgba(0, 0, 0, 0.4);
color: var(--text-primary);
}
.hero-subtitle {
font-size: clamp(1rem, 2vw, 1.2rem);
font-weight: 400;
color: var(--text-secondary);
margin-bottom: 1.5rem;
max-width: 550px;
}
.hero-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.8rem 1.5rem;
margin-bottom: 2rem;
font-size: 0.95rem;
color: var(--text-secondary);
}
.hero-meta-item {
display: flex;
align-items: center;
gap: 0.6rem;
}
.hero-meta-item i {
color: var(--accent);
font-size: 1.05rem;
}
.hero-buttons {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.hero.loading .hero-content {
opacity: 0;
}
.hero:not(.loading) .hero-content {
opacity: 1;
}
@media (max-width: 992px) {
.hero {
min-height: 450px;
height: 60vh;
}
}
@media (max-width: 768px) {
.hero {
height: auto;
min-height: unset;
padding-bottom: 3rem;
margin-bottom: 2rem;
}
.hero-content {
padding: 0 1rem 2rem;
}
.hero-title {
font-size: clamp(2rem, 7vw, 2.8rem);
}
.hero-subtitle {
font-size: clamp(0.9rem, 3vw, 1rem);
}
}

118
css/history.css Normal file
View File

@ -0,0 +1,118 @@
#history-section .section-header {
align-items: center;
}
#history-section .btn-danger.btn-sm {
padding: 0.4rem 1rem;
font-size: 0.8rem;
font-weight: 500;
}
#history-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.history-item {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 1rem;
background: var(--card-bg);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius-md);
transition: var(--transition);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.history-item:hover {
background: rgba(0, 224, 255, 0.08);
transform: translateX(8px);
border-color: var(--accent);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.history-poster {
width: 60px;
height: 90px;
object-fit: cover;
border-radius: var(--border-radius-sm);
flex-shrink: 0;
cursor: pointer;
}
.history-info {
flex: 1;
min-width: 0;
cursor: pointer;
}
.history-title-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.3rem;
}
.history-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-primary);
flex-grow: 1;
}
.badge.local-badge-history {
background-color: var(--success);
color: var(--primary);
font-size: 0.7rem;
padding: 0.25rem 0.6rem;
border-radius: 20px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
flex-shrink: 0;
}
.history-meta {
font-size: 0.85rem;
color: var(--text-secondary);
}
.history-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
.history-actions .action-btn {
width: 36px;
height: 36px;
font-size: 0.9rem;
background: var(--glass);
border: 1px solid transparent;
}
.history-actions .action-btn:hover {
border-color: transparent;
}
.history-actions .delete-btn:hover {
background: var(--danger);
}
@media (max-width: 576px) {
.history-item {
flex-direction: column;
align-items: flex-start;
}
.history-actions {
width: 100%;
justify-content: space-around;
}
}

231
css/m3u-generator.css Normal file
View File

@ -0,0 +1,231 @@
.m3u-animated-item {
opacity: 0;
transform: translateY(20px);
}
#m3u-generator-section {
max-width: 1400px;
margin: 0 auto;
}
.m3u-container {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2.5rem;
margin-top: 2rem;
}
.m3u-config-panel {
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius-lg);
padding: 2rem;
box-shadow: var(--shadow);
}
.m3u-step {
margin-bottom: 2.5rem;
}
.m3u-step:last-child {
margin-bottom: 0;
}
.m3u-step-header {
display: flex;
align-items: center;
margin-bottom: 1.5rem;
}
.m3u-step-number {
font-size: 1.2rem;
font-weight: 700;
color: var(--primary);
background: var(--gradient);
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
}
.m3u-step-title {
font-family: 'Orbitron', sans-serif;
font-size: 1.4rem;
font-weight: 600;
color: var(--text-primary);
}
#m3u-server-select {
width: 100%;
}
#m3u-libraries-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
max-height: 400px;
overflow-y: auto;
padding-right: 1rem;
}
#m3u-libraries-container::-webkit-scrollbar {
width: 8px;
}
#m3u-libraries-container::-webkit-scrollbar-track {
background: transparent;
}
#m3u-libraries-container::-webkit-scrollbar-thumb {
background-color: var(--glass-border);
border-radius: 8px;
}
#m3u-libraries-container .form-check {
background: rgba(0, 0, 0, 0.15);
padding: 0.8rem 1rem;
border-radius: var(--border-radius-md);
transition: background-color 0.2s;
display: flex;
align-items: center;
cursor: pointer;
}
#m3u-libraries-container .form-check:hover {
background: rgba(0, 0, 0, 0.25);
}
#m3u-libraries-container .form-check-input {
width: 1.1em;
height: 1.1em;
margin-right: 0.8rem;
background-color: var(--secondary);
border: 1px solid var(--glass-border);
flex-shrink: 0;
}
#m3u-libraries-container .form-check-label {
font-weight: 500;
cursor: pointer;
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.2;
}
#m3u-libraries-container .form-check-input:checked {
background-color: var(--accent);
border-color: var(--accent);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%230a0a0f' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e");
}
#m3u-libraries-container .form-check-label i {
color: var(--text-secondary);
width: 20px;
text-align: center;
margin-right: 0.5rem;
transition: color 0.2s;
}
#m3u-libraries-container .form-check-input:checked + .form-check-label i {
color: var(--accent);
}
.m3u-info-panel {
background: var(--card-bg);
border-radius: var(--border-radius-lg);
padding: 2rem;
text-align: center;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.m3u-info-title {
font-family: 'Orbitron', sans-serif;
font-size: 1.4rem;
margin-bottom: 1.5rem;
color: white;
}
.m3u-instructions {
text-align: left;
margin-left: 1.5rem;
color: var(--text-secondary);
font-size: 0.95rem;
margin-bottom: 2rem;
}
.m3u-instructions li {
margin-bottom: 1rem;
}
#download-m3u-btn {
width: 100%;
padding: 1rem;
font-size: 1rem;
border-radius: var(--border-radius-md);
transition: all 0.3s ease;
}
#download-m3u-btn:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-lg);
}
#download-m3u-btn span {
margin-left: 0.5rem;
}
#m3u-libraries-loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 150px;
color: var(--text-secondary);
}
#m3u-libraries-loader .spinner-border {
width: 3rem;
height: 3rem;
margin-bottom: 1rem;
}
.light-theme .m3u-config-panel {
background: var(--secondary);
}
.light-theme #m3u-libraries-container .form-check {
background: rgba(0, 0, 0, 0.03);
}
.light-theme #m3u-libraries-container .form-check:hover {
background: rgba(0, 0, 0, 0.06);
}
.light-theme #m3u-libraries-container .form-check-input {
background-color: #e9ecef;
border-color: #ced4da;
}
.light-theme #m3u-libraries-container .form-check-input:checked {
background-color: var(--accent-dark);
border-color: var(--accent-dark);
}
.light-theme #m3u-libraries-container .form-check-label i {
color: var(--text-secondary);
}
.light-theme #m3u-libraries-container .form-check-input:checked + .form-check-label i {
color: var(--accent-dark);
}
@media (max-width: 992px) {
.m3u-container {
grid-template-columns: 1fr;
}
}

File diff suppressed because it is too large Load Diff

452
css/modals.css Normal file
View File

@ -0,0 +1,452 @@
.lightbox {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(10, 10, 15, 0.96);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 2000;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.4s ease;
}
.lightbox.active {
display: flex;
opacity: 1;
}
.lightbox-content {
position: relative;
width: 90%;
max-width: 960px;
background: var(--secondary);
padding: 1rem;
border-radius: var(--border-radius-lg);
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.5);
border: 1px solid var(--glass-border);
transform: scale(0.95);
transition: transform 0.4s ease;
}
.lightbox.active .lightbox-content {
transform: scale(1);
}
.lightbox-close {
position: absolute;
top: -15px;
right: -15px;
width: 38px;
height: 38px;
display: flex;
justify-content: center;
align-items: center;
background: var(--accent);
color: var(--primary);
border: none;
border-radius: 50%;
font-size: 1.1rem;
cursor: pointer;
transition: var(--transition);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.lightbox-close:hover {
transform: scale(1.1) rotate(90deg);
background: var(--accent-dark);
box-shadow: 0 6px 15px rgba(0, 224, 255, 0.4);
}
.video-container {
position: relative;
padding-bottom: 56.25%;
height: 0;
overflow: hidden;
border-radius: var(--border-radius-md);
}
.video-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
.notification {
position: relative;
min-width: 280px;
max-width: 350px;
margin-bottom: 1rem;
padding: 1.1rem 1.5rem;
border-radius: var(--border-radius-md);
background: var(--secondary);
color: var(--text-primary);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
border: 1px solid var(--glass-border);
border-left-width: 5px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transform: translateX(120%);
opacity: 0;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s ease;
overflow: hidden;
}
.notification.show {
transform: translateX(0);
opacity: 1;
}
.notification-content {
display: flex;
align-items: center;
gap: 1rem;
}
.notification i.fas {
font-size: 1.2rem;
line-height: 1;
}
.notification span {
flex: 1;
}
.notification.success {
border-left-color: var(--success);
}
.notification.error {
border-left-color: var(--danger);
}
.notification.info {
border-left-color: var(--info);
}
.notification.warning {
border-left-color: var(--warning);
}
.light-theme .notification {
color: var(--text-primary);
}
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(3px);
-webkit-backdrop-filter: blur(3px);
}
.modal-content {
background-color: var(--secondary);
color: var(--text-primary);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow);
overflow: hidden;
}
.modal-header {
border-bottom: 1px solid var(--glass-border);
background: rgba(255, 255, 255, 0.03);
}
.modal-title {
color: var(--accent);
font-family: 'Orbitron', sans-serif;
font-size: 1.3rem;
}
.modal-footer {
border-top: 1px solid var(--glass-border);
background: rgba(10, 10, 15, 0.5);
padding: 1rem;
}
.modal .btn-close {
filter: invert(1) grayscale(100%) brightness(150%) opacity(0.8);
transition: transform 0.3s ease;
}
.modal .btn-close:hover {
filter: invert(1) grayscale(100%) brightness(200%) opacity(1);
transform: rotate(90deg);
}
.light-theme .modal-content {
background-color: #f9f9f9; /* Lighter background for light theme */
}
.light-theme .modal-header, .light-theme .modal-footer {
background-color: var(--primary);
}
#settingsModal .nav-tabs {
border-bottom: 1px solid var(--glass-border);
padding: 0.5rem 1rem 0;
background-color: rgba(20, 21, 27, 0.9); /* Slightly lighter background */
color: var(--text-primary); /* Use primary text color for better contrast */
}
.light-theme #settingsModal .nav-tabs {
background-color: var(--secondary);
color: var(--text-primary);
}
#settingsModal .nav-tabs .nav-link {
border: none;
color: var(--text-secondary);
border-bottom: 3px solid transparent;
transition: var(--transition);
padding: 0.8rem 1.2rem;
font-weight: 500;
}
#settingsModal .nav-tabs .nav-link:hover {
color: var(--text-primary);
border-bottom-color: var(--glass-border);
}
#settingsModal .nav-tabs .nav-link.active {
color: var(--accent);
background-color: transparent;
border-bottom-color: var(--accent);
font-weight: 600;
}
#settingsModal .modal-body {
max-height: 70vh;
overflow-y: auto;
padding: 1rem;
}
#settingsModal .tab-content label {
font-weight: 500;
}
#settingsModal .tab-content p,
#settingsModal .tab-content .text-muted {
color: var(--text-secondary);
}
#settingsModal .tab-content h5 {
color: var(--text-primary);
}
#settingsModal .tab-content input[type="checkbox"] {
margin-right: 0.6rem;
transform: scale(1.1);
accent-color: var(--accent);
}
#editor {
height: 300px;
width: 100%;
border-radius: var(--border-radius-md);
border: 1px solid var(--glass-border);
font-family: monospace;
}
#photo-lightbox {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(10, 10, 15, 0.96);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 2050;
display: none;
justify-content: center;
align-items: center;
opacity: 0;
}
#photo-lightbox.active {
display: flex;
}
.photo-lightbox-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.photo-lightbox-img {
max-width: 90vw;
max-height: 85vh;
object-fit: contain;
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
border-radius: var(--border-radius-sm);
}
.photo-lightbox-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(255,255,255,0.1);
border: 1px solid var(--glass-border);
color: var(--text-primary);
width: 50px;
height: 70px;
border-radius: var(--border-radius-sm);
font-size: 1.5rem;
cursor: pointer;
transition: var(--transition);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.photo-lightbox-btn:hover {
background: var(--accent);
color: var(--primary);
transform: translateY(-50%) scale(1.05);
}
#photo-lightbox-prev {
left: 2vw;
}
#photo-lightbox-next {
right: 2vw;
}
#photo-lightbox-close {
position: absolute;
top: 2rem;
right: 2rem;
width: 42px;
height: 42px;
top: 20px;
right: 20px;
}
.photo-lightbox-caption {
position: absolute;
bottom: 2vh;
left: 50%;
transform: translateX(-50%);
background: rgba(10,10,15,0.8);
color: var(--text-primary);
padding: 0.8rem 1.5rem;
border-radius: var(--border-radius-md);
font-size: 1rem;
text-align: center;
max-width: 70vw;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#activityViewerModal .modal-body {
max-height: 70vh;
overflow-y: auto;
}
.session-card {
display: flex;
gap: 1.5rem;
padding: 1.2rem;
background-color: var(--glass);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius-md);
margin-bottom: 1rem;
transition: var(--transition);
}
.session-card:hover {
background-color: rgba(255, 255, 255, 0.08);
border-color: var(--accent);
}
.session-poster {
width: 80px;
height: 120px;
object-fit: cover;
border-radius: var(--border-radius-sm);
flex-shrink: 0;
}
.session-info {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: 1rem;
}
.session-details p {
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
color: var(--text-secondary);
}
.session-details strong {
color: var(--text-primary);
font-weight: 600;
}
.session-identifier {
margin-top: auto;
}
.session-identifier label {
font-size: 0.8rem;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 0.25rem;
}
#settingsModal .modal-body::-webkit-scrollbar {
width: 8px;
}
#settingsModal .modal-body::-webkit-scrollbar-track {
background: var(--secondary);
}
#settingsModal .modal-body::-webkit-scrollbar-thumb {
background-color: var(--accent-dark);
border-radius: 4px;
}
#settingsModal .modal-body::-webkit-scrollbar-thumb:hover {
background-color: var(--accent);
}
.light-theme #settingsModal .modal-body::-webkit-scrollbar-track {
background: var(--primary);
}
.light-theme #settingsModal .modal-body::-webkit-scrollbar-thumb {
background-color: #bdc3c7;
}
.light-theme #settingsModal .modal-body::-webkit-scrollbar-thumb:hover {
background-color: #a3aab1;
}
.session-identifier .input-group .form-control {
background-color: var(--primary) !important;
font-family: monospace;
font-size: 0.85rem;
}
#activity-results .empty-state {
padding: 2rem;
margin-top: 1rem;
}
#settingsModal .modal-body {
max-height: 70vh;
overflow-y: auto;
padding: 1rem;
}

908
css/music-player.css Normal file
View File

@ -0,0 +1,908 @@
#musicPlayerContainer {
position: fixed;
top: 0;
left: 0;
width: 320px;
height: 100%;
background: var(--secondary);
box-shadow: 5px 0 35px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
z-index: 1040;
border-right: 1px solid var(--glass-border);
transform: translateX(-100%);
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), height 0.3s ease;
}
body.miniplayer-active #musicPlayerContainer {
height: calc(100% - 85px);
}
.sidenav {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background: transparent;
position: relative;
}
.sidenav-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.7rem 1.2rem;
background: var(--primary);
color: var(--text-primary);
border-bottom: 1px solid var(--glass-border);
flex-shrink: 0;
position: relative;
z-index: 3;
}
.sidenav-header h4 {
margin: 0;
font-family: 'Orbitron', sans-serif;
font-size: 1.4rem;
color: var(--accent);
line-height: 1;
}
.sidenav-header button {
background: none;
border: none;
color: var(--text-secondary);
font-size: 1.4rem;
cursor: pointer;
padding: 0.5rem;
line-height: 1;
transition: color 0.3s ease, transform 0.3s ease;
}
.sidenav-header button:hover {
color: var(--accent);
transform: rotate(90deg);
}
.music-panel {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
min-height: 0;
position: absolute;
top: 57px;
left: 0;
width: 100%;
height: calc(100% - 57px);
background-color: var(--secondary);
}
#artistListContainer {
z-index: 2;
transform: translateX(0);
}
#songListContainer {
z-index: 3;
transform: translateX(100%);
opacity: 0;
visibility: hidden;
}
.panel-controls {
padding: 1rem;
border-bottom: 1px solid var(--glass-border);
flex-shrink: 0;
}
.search-wrapper {
position: relative;
}
.search-wrapper i {
position: absolute;
left: 1.1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
font-size: 0.9em;
}
.search-wrapper input,
.search-wrapper-songs input {
width: 100%;
padding: 0.7rem 1.3rem 0.7rem 2.5rem;
font-size: 0.9rem;
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 50px;
color: var(--text-primary);
transition: var(--transition);
}
.search-wrapper input:focus,
.search-wrapper-songs input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(0, 224, 255, 0.2);
background: rgba(0, 224, 255, 0.08);
}
.artist-grid {
padding: 0.5rem;
display: grid;
grid-template-columns: 1fr;
gap: 0.25rem;
overflow-y: auto;
flex-grow: 1;
}
.artist-card {
background: transparent;
border-radius: var(--border-radius-sm);
border: 1px solid transparent;
overflow: hidden;
cursor: pointer;
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
display: flex;
flex-direction: row;
align-items: center;
padding: 0.6rem 0.75rem;
gap: 1rem;
}
.artist-card:hover {
background: var(--glass);
}
.artist-card.current-artist {
background: rgba(0, 224, 255, 0.1);
border-color: rgba(0, 224, 255, 0.2);
}
.artist-thumb-wrapper {
background-color: var(--primary);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: 50%;
border: 1px solid var(--glass-border);
}
.artist-thumb {
width: 100%;
height: 100%;
object-fit: cover;
}
.artist-thumb-placeholder {
font-size: 1.5rem;
color: var(--text-secondary);
}
.artist-card-title {
padding: 0;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
transition: color 0.2s ease-in-out;
}
.artist-card:hover .artist-card-title,
.artist-card.current-artist .artist-card-title {
color: var(--text-primary);
}
.pagination-controls {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
border-top: 1px solid var(--glass-border);
flex-shrink: 0;
}
.pagination-controls #artistCounter {
font-size: 0.85rem;
color: var(--text-secondary);
font-weight: 500;
}
.btn-icon-sm {
background: var(--glass);
border: 1px solid var(--glass-border);
color: var(--text-secondary);
width: 32px;
height: 32px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
transition: var(--transition);
}
.btn-icon-sm:hover {
background-color: var(--accent);
color: var(--primary);
border-color: var(--accent);
}
.custom-select {
position: relative;
width: 100%;
margin-bottom: 1rem;
}
.select-selected {
background-color: var(--glass);
color: var(--text-primary);
padding: 0.7rem 2.5rem 0.7rem 1.3rem;
border: 1px solid var(--glass-border);
border-radius: 50px;
cursor: pointer;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
transition: var(--transition);
}
.select-selected:hover {
border-color: var(--accent);
}
.select-items {
position: absolute;
background-color: var(--primary);
top: 105%;
left: 0;
right: 0;
z-index: 99;
border-radius: var(--border-radius-md);
border: 1px solid var(--glass-border);
overflow-y: auto;
box-shadow: 0 8px 20px rgba(0,0,0,0.3);
max-height: 250px;
}
.select-hide { display: none; }
.select-option {
color: var(--text-secondary);
padding: 0.8rem 1.3rem;
cursor: pointer;
user-select: none;
}
.select-option:hover {
background-color: var(--glass);
color: var(--text-primary);
}
.artist-grid::-webkit-scrollbar,
.select-items::-webkit-scrollbar,
.song-list::-webkit-scrollbar {
width: 8px;
}
.artist-grid::-webkit-scrollbar-thumb,
.select-items::-webkit-scrollbar-thumb,
.song-list::-webkit-scrollbar-thumb {
background-color: var(--accent);
border-radius: 4px;
}
.artist-grid::-webkit-scrollbar-track,
.select-items::-webkit-scrollbar-track,
.song-list::-webkit-scrollbar-track {
background: transparent;
}
.song-list-controls {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
}
.back-btn-icon {
flex-shrink: 0;
}
#artist-header-info {
display: flex;
align-items: center;
gap: 1rem;
overflow: hidden;
flex-grow: 1;
justify-content: center;
}
#artist-header-thumb {
width: 45px;
height: 45px;
object-fit: cover;
border-radius: 50%;
border: 2px solid var(--glass-border);
}
#artist-header-title {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search-wrapper-songs {
position: relative;
padding: 0 1rem 1rem;
border-bottom: 1px solid var(--glass-border);
}
.song-list {
flex-grow: 1;
overflow-y: auto;
padding: 1rem;
}
.album-group {
margin-bottom: 1.5rem;
}
.album-group-title {
font-size: 0.8rem;
font-weight: 600;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 0.8rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--glass-border);
}
.song-item {
display: flex;
align-items: center;
padding: 0.6rem 0.5rem;
border-radius: var(--border-radius-sm);
cursor: pointer;
transition: background-color 0.2s ease;
}
.song-item:hover {
background: var(--glass);
}
.song-item.current-song {
background: var(--accent);
}
.song-item.current-song .song-number,
.song-item.current-song .item-title {
color: var(--primary) !important;
}
.song-number {
font-size: 0.9rem;
color: var(--text-secondary);
width: 2rem;
text-align: center;
flex-shrink: 0;
}
.song-details {
flex-grow: 1;
overflow: hidden;
}
.song-details .item-title {
font-size: 0.9rem;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.song-item .play-icon {
color: var(--text-secondary);
opacity: 0;
transition: opacity 0.2s ease;
}
@keyframes spin-song {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.song-item .loading-spinner {
width: 16px;
height: 16px;
border: 2px solid var(--text-secondary);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin-song 0.8s linear infinite;
}
.song-item:hover .play-icon,
.song-item.current-song .play-icon {
opacity: 1;
}
.song-item.current-song .play-icon {
color: var(--primary);
}
.list-item-empty {
padding: 2rem 1rem;
text-align: center;
color: var(--text-secondary);
font-style: italic;
background-color: transparent;
border: 1px dashed var(--glass-border);
border-radius: var(--border-radius-md);
}
#side-nav-now-playing {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: var(--primary);
border-top: 1px solid var(--glass-border);
flex-shrink: 0;
cursor: pointer;
}
#side-nav-now-playing .details {
overflow: hidden;
}
#side-nav-now-playing img {
width: 45px;
height: 45px;
border-radius: var(--border-radius-sm);
flex-shrink: 0;
}
#side-nav-now-playing p {
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#side-nav-track-title {
font-weight: 600;
}
#side-nav-track-artist {
font-size: 0.8rem;
color: var(--text-secondary);
}
#side-nav-play-pause {
margin-left: auto;
flex-shrink: 0;
font-size: 1.2rem;
}
#miniplayer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 85px;
padding: 0 1.5rem;
display: grid;
grid-template-columns: minmax(200px, 1fr) 2fr minmax(200px, 1fr);
gap: 1.5rem;
align-items: center;
z-index: 1045;
color: var(--text-primary);
transform: translateY(110%);
}
.miniplayer-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(16, 17, 22, 0.8);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
border-top: 1px solid var(--glass-border);
z-index: -1;
}
.player-left-info {
display: flex;
align-items: center;
gap: 1rem;
overflow: hidden;
min-width: 0;
}
.album-cover {
width: 55px;
height: 55px;
border-radius: var(--border-radius-sm);
flex-shrink: 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
object-fit: cover;
cursor: pointer;
transition: transform 0.3s ease;
}
#trackInfo .details {
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
gap: 0.1rem;
}
#trackTitle, #trackArtist {
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#trackTitle { font-weight: 600; }
#trackArtist { font-size: 0.8rem; color: var(--text-secondary); }
.player-center-controls {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
min-width: 250px;
}
#player-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 0.8rem;
}
.control-btn {
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
width: 38px;
height: 38px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
}
.control-btn:hover {
color: var(--accent);
background: var(--glass);
}
.control-btn.active { color: var(--accent); }
.control-btn.play-pause-main {
font-size: 1.5rem;
width: 48px;
height: 48px;
background: var(--accent);
color: var(--primary);
}
.control-btn.play-pause-main:hover {
transform: scale(1.1);
box-shadow: 0 0 15px rgba(0, 224, 255, 0.4);
}
#closeMiniplayerBtn {
color: var(--text-secondary);
}
#closeMiniplayerBtn:hover {
color: var(--accent);
}
.fab-btn {
position: relative;
width: 60px;
height: 60px;
border-radius: 50%;
background-color: var(--accent);
color: var(--primary);
border: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease;
z-index: 1030;
}
.fab-btn:hover {
transform: scale(1.1);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
}
.time-and-progress {
display: flex;
align-items: center;
width: 100%;
gap: 1rem;
}
.time-label { font-size: 0.75rem; color: var(--text-secondary); }
#progressBarContainer {
flex-grow: 1;
height: 5px;
background-color: rgba(255, 255, 255, 0.15);
border-radius: 2.5px;
cursor: pointer;
position: relative;
transition: height 0.2s ease;
}
#progressBarContainer:hover {
height: 8px;
}
#seek-hover-bar {
position: absolute;
height: 100%;
background-color: rgba(255, 255, 255, 0.25);
border-radius: inherit;
width: 0%;
}
#played-bar {
position: relative;
height: 100%;
background: var(--accent);
border-radius: inherit;
width: 0%;
}
#progress-handle {
position: absolute;
top: 50%;
transform: translate(-50%, -50%) scale(0);
width: 14px;
height: 14px;
background-color: var(--text-primary);
border-radius: 50%;
transition: transform 0.2s ease;
}
#progressBarContainer:hover #progress-handle {
transform: translate(-50%, -50%) scale(1);
}
.player-right-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 0.5rem;
}
#volumeControl { position: relative; }
.volume-slider-wrapper {
position: absolute;
top: 50%;
left: calc(100% + 15px);
transform: translateY(-50%);
background: var(--secondary);
padding: 0.5rem 1rem;
border-radius: var(--border-radius-md);
border: 1px solid var(--glass-border);
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.volume-slider-wrapper.active {
opacity: 1;
visibility: visible;
left: calc(100% + 5px);
}
#volumeSlider {
-webkit-appearance: none;
appearance: none;
width: 100px;
height: 8px;
background: var(--glass);
border-radius: 4px;
}
#volumeSlider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
background: var(--accent);
border-radius: 50%;
cursor: pointer;
}
#audioPlayer { display: none; }
body.miniplayer-active { padding-bottom: 85px; }
@media (max-width: 768px) {
body.miniplayer-active { padding-bottom: 110px; }
body.miniplayer-active #musicPlayerContainer {
height: calc(100% - 110px);
}
#miniplayer {
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
grid-template-areas:
"info actions"
"center center";
height: 110px;
padding: 0.5rem 1rem;
gap: 0.5rem;
}
.player-left-info { grid-area: info; }
.player-center-controls { grid-area: center; padding: 0 1rem; }
.player-right-actions { grid-area: actions; justify-content: flex-end; }
#downloadBtn, #downloadAlbumBtn, #eqBtn, #volumeControl { display: none; }
}
@media (max-width: 576px) {
#player-controls { gap: 1.5rem; }
}
#equalizer-panel {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(100%);
width: 520px;
background-color: var(--secondary);
border-top: 1px solid var(--glass-border);
border-left: 1px solid var(--glass-border);
border-right: 1px solid var(--glass-border);
border-radius: 12px 12px 0 0;
box-shadow: 0 -5px 25px rgba(0,0,0,0.3);
z-index: 100;
overflow: hidden;
display: none;
font-family: 'Montserrat', sans-serif;
}
.equalizer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background-color: var(--primary);
color: var(--text-primary);
border-bottom: 1px solid var(--glass-border);
}
.equalizer-header h5 {
margin: 0;
font-weight: 600;
font-family: 'Orbitron', sans-serif;
}
.close-btn {
background: none;
border: none;
color: var(--text-secondary);
font-size: 1.2rem;
cursor: pointer;
transition: color 0.2s;
}
.close-btn:hover { color: var(--text-primary); }
.equalizer-top-bar {
display: flex;
justify-content: space-around;
align-items: center;
padding: 15px;
border-bottom: 1px solid var(--glass-border);
gap: 20px;
}
.control-group {
display: flex;
align-items: center;
gap: 10px;
}
.control-group label {
font-size: 0.8rem;
color: var(--text-secondary);
font-weight: 500;
white-space: nowrap;
}
.control-group.preamp {
flex-grow: 1;
}
.equalizer-bands-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px 15px;
padding: 20px 15px;
}
.band {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.band label {
font-size: 0.75rem;
color: var(--text-secondary);
font-weight: 500;
}
.eq-slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 6px;
background: var(--glass);
outline: none;
border-radius: 3px;
cursor: pointer;
transition: opacity 0.2s;
}
.eq-slider::-webkit-slider-runnable-track {
width: 100%;
height: 6px;
cursor: pointer;
background: var(--glass);
border-radius: 3px;
}
.eq-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
background: var(--accent);
border: 3px solid var(--secondary);
border-radius: 50%;
cursor: pointer;
margin-top: -6px;
transition: background 0.2s, box-shadow 0.2s;
box-shadow: 0 0 5px rgba(0, 224, 255, 0.4);
}
.eq-slider:hover::-webkit-slider-thumb {
background: var(--accent);
box-shadow: 0 0 10px rgba(0, 224, 255, 0.6);
}
.eq-slider::-moz-range-track {
width: 100%;
height: 6px;
cursor: pointer;
background: var(--glass);
border-radius: 3px;
}
.eq-slider::-moz-range-thumb {
width: 18px;
height: 18px;
background: var(--accent);
border: 3px solid var(--secondary);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 5px rgba(0, 224, 255, 0.4);
}
.slider-value {
font-size: 0.7rem;
color: var(--text-secondary);
background: var(--primary);
padding: 2px 5px;
border-radius: 3px;
min-width: 35px;
text-align: center;
}
.custom-select-sm {
background-color: var(--primary);
color: var(--text-primary);
border: 1px solid var(--glass-border);
border-radius: 4px;
padding: 5px 8px;
font-size: 0.8rem;
}
.visualizer-container {
height: 80px;
background-color: var(--primary);
border-top: 1px solid var(--glass-border);
padding: 0;
margin: 0;
overflow: hidden;
}
#visualizer-canvas {
width: 100%;
height: 100%;
display: block;
}

426
css/music.css Normal file
View File

@ -0,0 +1,426 @@
#music-section {
padding: 2rem;
animation: fadeIn 0.5s ease-in-out;
}
#music-section .section-header {
margin-bottom: 2rem;
}
.music-controls {
display: flex;
gap: 1rem;
align-items: center;
}
.music-search-bar {
position: relative;
min-width: 280px;
}
.music-search-bar .search-input {
width: 100%;
}
.genre-card {
background: var(--gradient);
border-radius: var(--border-radius-md);
padding: 2rem 1rem;
text-align: center;
color: var(--primary);
font-size: 1.2rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
box-shadow: 0 5px 15px rgba(0, 224, 255, 0.2);
text-transform: capitalize;
}
.genre-card:hover {
transform: translateY(-5px) scale(1.05);
box-shadow: 0 10px 25px rgba(0, 224, 255, 0.3);
}
.artist-card-spotify {
background: transparent;
border-radius: var(--border-radius-md);
padding: 1rem;
text-align: center;
transition: background-color 0.3s ease;
cursor: pointer;
position: relative;
}
.artist-card-spotify:hover {
background-color: var(--glass);
}
.artist-card-spotify.current-artist {
background-color: var(--glass);
}
.artist-card-img-container {
position: relative;
width: 100%;
padding-top: 100%;
margin-bottom: 1rem;
}
.artist-card-img {
position: absolute;
top: 0;
left: 0;
width: 90%;
height: 90%;
object-fit: cover;
border-radius: 50%;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
}
.artist-card-spotify:hover .artist-card-img {
transform: scale(1.05);
}
.artist-card-play-btn {
position: absolute;
bottom: 0;
right: 0;
width: 48px;
height: 48px;
background-color: var(--accent);
color: var(--primary);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
opacity: 0;
transform: translateY(10px) scale(0.8);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.artist-card-spotify:hover .artist-card-play-btn {
opacity: 1;
transform: translateY(0) scale(1);
}
.artist-card-info {
padding: 0;
}
.artist-card-title-spotify {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 0.25rem 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.artist-card-subtitle {
font-size: 0.85rem;
color: var(--text-secondary);
text-transform: capitalize;
}
.song-list-header-spotify {
display: flex;
align-items: flex-end;
gap: 1.5rem;
padding: 2rem;
margin-bottom: 2rem;
position: relative;
min-height: 300px;
border-radius: var(--border-radius-lg);
overflow: hidden;
transition: background 0.5s ease;
}
#back-to-artists-btn {
position: absolute;
top: 1rem;
left: 1rem;
z-index: 2;
background: rgba(0,0,0,0.3);
}
#back-to-artists-btn:hover {
background: rgba(0,0,0,0.5);
}
.artist-header-thumb-spotify {
width: 180px;
height: 180px;
border-radius: 50%;
object-fit: cover;
box-shadow: 0 8px 30px rgba(0,0,0,0.5);
z-index: 1;
}
.artist-header-info-spotify {
z-index: 1;
text-shadow: 0 2px 10px rgba(0,0,0,0.5);
}
.artist-header-type {
font-weight: 700;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.artist-header-info-spotify h1 {
font-size: clamp(2.5rem, 5vw, 4.5rem);
font-weight: 900;
margin: 0;
line-height: 1.1;
font-family: 'Orbitron', sans-serif;
}
.album-group-container-spotify {
margin-bottom: 2.5rem;
}
.album-group-header-spotify {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
padding: 0 1rem;
}
.album-play-btn {
width: 42px;
height: 42px;
background-color: var(--accent);
color: var(--primary);
border: none;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 1.1rem;
transition: transform 0.2s ease, box-shadow 0.2s ease;
margin-left: auto;
}
.album-play-btn:hover {
transform: scale(1.1);
box-shadow: 0 4px 15px rgba(0, 224, 255, 0.4);
}
.album-info-spotify {
display: flex;
flex-direction: column;
}
.album-track-count {
font-size: 0.8rem;
color: var(--text-secondary);
}
.album-group-cover-art-spotify {
width: 50px;
height: 50px;
object-fit: cover;
border-radius: var(--border-radius-sm);
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
.album-group-title-spotify {
font-size: 1.2rem;
font-weight: 600;
}
.song-list-grid {
display: grid;
grid-template-columns: 1fr;
gap: 0.25rem;
}
.song-list-grid-header,
.song-grid-row {
display: grid;
grid-template-columns: 40px 1fr 60px;
gap: 1rem;
align-items: center;
padding: 0.75rem 1rem;
border-radius: var(--border-radius-sm);
transition: background-color 0.2s ease-in-out;
}
.song-list-grid-header {
color: var(--text-secondary);
font-size: 0.8rem;
text-transform: uppercase;
border-bottom: 1px solid var(--glass-border);
padding-bottom: 0.5rem;
margin-bottom: 0.5rem;
}
.song-grid-row {
cursor: pointer;
}
.song-grid-row:hover {
background-color: var(--glass);
}
.song-grid-row:hover .track-number {
display: none;
}
.song-grid-row:hover .play-icon-spotify {
display: block;
}
.song-grid-row.current-song {
background-color: rgba(0, 224, 255, 0.1);
}
.song-grid-row.current-song .song-title-spotify { color: var(--accent); }
.song-grid-row.current-song .track-number { display: none; }
.song-grid-row.current-song .play-icon-spotify { display: none; }
.song-grid-row.current-song .playing-indicator { display: flex; }
.song-index {
color: var(--text-secondary);
text-align: center;
position: relative;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.play-icon-spotify {
color: var(--text-primary);
display: none;
}
.playing-indicator {
display: none;
height: 16px;
align-items: flex-end;
gap: 2px;
}
.playing-indicator div {
width: 3px;
background-color: var(--accent);
animation-duration: 1.2s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
animation-name: music-wave;
}
.playing-indicator div:nth-child(1) { height: 10px; animation-delay: -1.2s; }
.playing-indicator div:nth-child(2) { height: 16px; animation-delay: -1.0s; }
.playing-indicator div:nth-child(3) { height: 6px; animation-delay: -0.8s; }
@keyframes music-wave {
50% { height: 2px; }
}
.song-title-artist {
display: flex;
flex-direction: column;
}
.song-title-spotify {
font-weight: 500;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.song-duration-header,
.song-duration {
text-align: right;
color: var(--text-secondary);
font-size: 0.9rem;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
#music-classification-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(10, 10, 15, 0.95);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
color: var(--text-primary);
padding: 2rem;
padding-bottom: 15vh;
}
.classification-content {
max-width: 500px;
}
.classification-icon {
font-size: 5rem;
color: var(--accent);
margin-bottom: 1.5rem;
animation: spin-vinyl 4s linear infinite;
}
@keyframes spin-vinyl {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.classification-title {
font-family: 'Orbitron', sans-serif;
font-size: 1.8rem;
margin-bottom: 0.5rem;
}
.classification-subtitle {
color: var(--text-secondary);
margin-bottom: 2rem;
font-size: 0.95rem;
}
.classification-progress-bar {
width: 100%;
height: 10px;
background-color: var(--glass);
border-radius: 5px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.classification-progress-fill {
width: 0%;
height: 100%;
background: var(--gradient);
border-radius: 5px;
transition: width 0.3s ease-in-out;
}
.classification-progress-text {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 1.5rem;
font-weight: 500;
}
.classification-status-text {
min-height: 1.5em;
font-style: italic;
color: var(--accent);
transition: opacity 0.3s;
}

150
css/photos.css Normal file
View File

@ -0,0 +1,150 @@
#photos-section {
padding-top: 2rem;
}
.photos-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
padding: 1.5rem;
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius-lg);
}
#photos-breadcrumb {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0;
margin: 0;
list-style: none;
font-size: 0.95rem;
}
#photos-breadcrumb .breadcrumb-item a {
color: var(--text-secondary);
text-decoration: none;
transition: var(--transition);
padding: 0.3rem 0.6rem;
border-radius: var(--border-radius-sm);
}
#photos-breadcrumb .breadcrumb-item a:hover {
color: var(--accent);
background-color: var(--glass);
}
#photos-breadcrumb .breadcrumb-item.active {
color: var(--text-primary);
font-weight: 600;
}
#photos-breadcrumb .breadcrumb-divider {
color: var(--text-secondary);
opacity: 0.5;
margin: 0 0.3rem;
}
#photos-token-select {
min-width: 250px;
}
#photos-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.8rem;
}
.photo-card, .album-card {
background: var(--card-bg);
border-radius: var(--border-radius-lg);
overflow: hidden;
position: relative;
transition: var(--transition);
box-shadow: var(--shadow);
cursor: pointer;
border: 1px solid transparent;
aspect-ratio: 1 / 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 1rem;
}
.photo-card:hover, .album-card:hover {
transform: translateY(-10px) scale(1.03);
box-shadow: 0 18px 45px rgba(0, 0, 0, 0.5);
border-color: rgba(0, 224, 255, 0.5);
z-index: 10;
}
.album-card-icon {
font-size: 4rem;
color: var(--accent);
margin-bottom: 1rem;
transition: transform 0.4s ease;
}
.album-card:hover .album-card-icon {
transform: scale(1.1);
}
.album-card-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
}
.album-card-meta {
font-size: 0.85rem;
color: var(--text-secondary);
margin-top: 0.3rem;
}
.photo-card {
padding: 0;
justify-content: flex-end;
}
.photo-card-img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.6s cubic-bezier(0.23, 1, 0.32, 1);
}
.photo-card:hover .photo-card-img {
transform: scale(1.08);
}
.photo-card-caption {
position: relative;
z-index: 1;
width: 100%;
padding: 0.8rem;
background: linear-gradient(to top, rgba(10, 10, 15, 0.95) 20%, transparent 100%);
color: var(--text-primary);
font-size: 0.9rem;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
opacity: 0;
transform: translateY(10px);
transition: var(--transition);
}
.photo-card:hover .photo-card-caption {
opacity: 1;
transform: translateY(0);
}

90
css/providers.css Normal file
View File

@ -0,0 +1,90 @@
.providers-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 30px;
padding: 30px;
perspective: 1000px;
}
.provider-card {
position: relative;
width: 120px;
height: 120px;
background-color: transparent;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.4s ease, box-shadow 0.4s ease;
box-shadow: 0 0 0px rgba(0, 0, 0, 0.3);
overflow: hidden;
border: none;
}
.provider-card:hover {
transform: scale(1.1) translateY(-5px);
box-shadow: 0 0 25px rgba(0, 224, 255, 0.7);
}
.provider-logo {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0;
transition: filter 0.3s ease;
}
.provider-card:hover .provider-logo {
filter: brightness(1.1);
}
.provider-name {
display: none;
}
.provider-card.available {
box-shadow: 0 0 15px #00e0ff;
}
.provider-card.available::after {
content: '\f00c';
font-family: 'Font Awesome 5 Free';
font-weight: 900;
position: absolute;
top: -5px;
right: -5px;
background-color: #00e0ff;
color: #121212;
border-radius: 50%;
width: 25px;
height: 25px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
box-shadow: 0 0 10px rgba(0, 224, 255, 0.5);
z-index: 11;
}
.provider-tooltip {
position: absolute;
bottom: -35px;
background-color: #00e0ff;
color: #121212;
padding: 5px 12px;
border-radius: 15px;
font-size: 14px;
font-weight: 600;
white-space: nowrap;
opacity: 0;
transform: translateY(10px);
transition: opacity 0.3s ease, transform 0.3s ease;
pointer-events: none;
z-index: 10;
}
.provider-card:hover .provider-tooltip {
opacity: 1;
transform: translateY(0);
}

109
css/settings.css Normal file
View File

@ -0,0 +1,109 @@
#settings-section .settings-container {
display: flex;
background: var(--card-bg);
border-radius: var(--border-radius-lg);
border: 1px solid var(--glass-border);
overflow: hidden;
min-height: 70vh;
}
.settings-nav {
flex: 0 0 220px;
background: rgba(0,0,0,0.1);
padding: 1.5rem 0.5rem;
border-right: 1px solid var(--glass-border);
}
.settings-nav .nav-item {
display: flex;
align-items: center;
padding: 0.8rem 1.2rem;
border-radius: var(--border-radius-md);
color: var(--text-secondary);
text-decoration: none;
margin-bottom: 0.5rem;
transition: background-color 0.2s ease, color 0.2s ease;
cursor: pointer;
font-weight: 500;
}
.settings-nav .nav-item:hover {
background-color: var(--glass);
color: var(--text-primary);
}
.settings-nav .nav-item.active {
background-color: var(--accent);
color: var(--primary);
font-weight: 600;
box-shadow: 0 4px 15px rgba(0, 224, 255, 0.2);
}
.settings-nav .nav-item i {
width: 24px;
text-align: center;
}
.settings-content-wrapper {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.settings-content {
padding: 2.5rem;
flex-grow: 1;
overflow-y: auto;
}
.settings-content .tab-pane {
display: none;
}
.settings-content .tab-pane.active {
display: block;
animation: fadeIn 0.4s ease-in-out;
}
.settings-footer {
padding: 1.5rem 2.5rem;
margin-top: auto;
border-top: 1px solid var(--glass-border);
display: flex;
justify-content: flex-end;
gap: 1rem;
background: rgba(0,0,0,0.1);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 992px) {
#settings-section .settings-container {
flex-direction: column;
}
.settings-nav {
flex: 0 0 auto;
border-right: none;
border-bottom: 1px solid var(--glass-border);
display: flex;
overflow-x: auto;
padding: 0.5rem;
}
.settings-nav::-webkit-scrollbar { height: 4px; }
.settings-nav::-webkit-scrollbar-thumb { background: var(--glass-border); border-radius: 2px; }
.settings-nav .nav-item {
flex: 0 0 auto;
margin-bottom: 0;
margin-right: 0.5rem;
}
.settings-content {
padding: 1.5rem;
}
}

90
css/sidebar.css Normal file
View File

@ -0,0 +1,90 @@
.sidebar-nav {
position: fixed;
top: var(--topbar-height);
left: 0;
width: var(--sidebar-width);
height: calc(100vh - var(--topbar-height));
background: var(--secondary);
z-index: 1020;
border-right: 1px solid var(--glass-border);
transform: translateX(-100%);
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
padding: 1.5rem 0;
}
body.light-theme .sidebar-nav {
background: #eef2f7;
}
.sidebar-nav.open {
transform: translateX(0);
}
.sidebar-menu {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar-menu .nav-link {
display: flex;
align-items: center;
gap: 1.2rem;
color: var(--text-secondary);
font-weight: 500;
padding: 0.9rem 1.8rem;
transition: var(--transition);
border-left: 4px solid transparent;
}
.sidebar-menu .nav-link i {
font-size: 1.1rem;
width: 20px;
text-align: center;
}
.sidebar-menu .nav-link:hover {
color: var(--text-primary);
background-color: var(--glass);
}
.sidebar-menu .nav-link.active {
color: var(--accent);
font-weight: 600;
background: var(--glass);
border-left-color: var(--accent);
}
@media (min-width: 992px) {
.sidebar-nav {
transform: translateX(0);
}
#main-container {
padding-left: var(--sidebar-width);
}
}
@media (max-width: 991px) {
#sidebar-toggle {
display: inline-flex;
}
}
@media (max-width: 576px) {
.sidebar-nav {
width: 100%;
transform: translateX(-105%);
}
}
body.sidebar-open .sidebar-nav {
transform: translateX(0);
}
body.sidebar-collapsed .sidebar-nav {
transform: translateX(-100%);
}
body.sidebar-collapsed #main-container {
padding-left: 0;
}

138
css/stats.css Normal file
View File

@ -0,0 +1,138 @@
#stats-section {
padding-top: 2rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
margin-top: 2rem;
}
.stats-card {
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius-lg);
padding: 2rem;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: var(--transition);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.stats-card:hover {
transform: translateY(-8px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
border-color: var(--accent);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.stat-icon {
font-size: 2.2rem;
color: var(--accent);
background: var(--gradient);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
opacity: 0.8;
}
.stat-label {
font-size: 1.1rem;
font-weight: 500;
color: var(--text-secondary);
text-align: right;
}
.stat-value {
font-family: 'Orbitron', sans-serif;
font-size: clamp(2.5rem, 5vw, 3.5rem);
font-weight: 700;
color: var(--text-primary);
line-height: 1;
text-align: right;
}
.chart-container {
background: var(--card-bg);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius-lg);
padding: 2rem;
box-shadow: var(--shadow);
grid-column: span 1;
}
.chart-container.full-width {
grid-column: 1 / -1;
}
.chart-title {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 1.5rem;
color: var(--text-primary);
text-align: center;
}
.chart-container canvas {
max-height: 400px;
width: 100% !important;
}
#stats-filters {
display: flex;
justify-content: flex-end;
margin-bottom: 2rem;
}
.token-details-card {
grid-column: 1 / -1;
display: none;
}
.token-details-card .card-body {
padding: 1.5rem;
}
.token-details-card .server-list {
list-style: none;
padding: 0;
margin: 0;
max-height: 200px;
overflow-y: auto;
}
.token-details-card .server-list li {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--glass-border);
font-size: 0.95rem;
color: var(--text-secondary);
}
.token-details-card .server-list li:last-child {
border-bottom: none;
}
.token-details-card .server-list strong {
color: var(--text-primary);
margin-right: 0.5rem;
}
@media (max-width: 768px) {
.stats-card {
padding: 1.5rem;
}
}
@media (max-width: 576px) {
.stats-grid {
grid-template-columns: 1fr;
}
}

View File

@ -1,7 +1,7 @@
import { state } from './state.js'; import { state } from './state.js';
import { getFromDB, clearStore, addItemsToStore } from './db.js'; import { getFromDB, clearStore, addItemsToStore, exportDatabase, importDatabase } from './db.js';
import { showNotification, _ } from './utils.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 { switchView, showItemDetails, addStreamToList, downloadM3U, generateStatistics, toggleFavorite, loadRecommendations, applyFilters as applyUIFilters, clearAllFavorites, clearRecommendations, loadInitialContent, loadContent, clearAllHistory, applyTheme, applyHeroVisibility, getTrailerKey, showTrailer, renderGrid, showActorDetails, isContentAvailableLocally } from './ui.js';
import { fetchTMDB } from './api.js'; import { fetchTMDB } from './api.js';
import { updateAllTokens, addPlexToken } from './plex.js'; import { updateAllTokens, addPlexToken } from './plex.js';
import { config } from './config.js'; import { config } from './config.js';
@ -36,7 +36,7 @@ export class AITools {
properties: { properties: {
page: { page: {
type: 'string', type: 'string',
enum: ['movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', 'm3u-generator'], enum: ['movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', 'm3u-generator', 'music'],
description: _('aiToolNavigateToPagePageParamDesc') description: _('aiToolNavigateToPagePageParamDesc')
} }
}, },
@ -72,6 +72,31 @@ export class AITools {
required: ['title', 'type'] required: ['title', 'type']
} }
}, },
{
name: 'download_single_movie_m3u',
description: _('aiToolDownloadSingleMovieM3UDesc'),
parameters: {
type: 'object',
properties: {
movie_title: { type: 'string', description: _('aiToolDownloadSingleMovieM3UTitleParamDesc') },
year: { type: 'string', description: _('aiToolDownloadSingleMovieM3UYearParamDesc') }
},
required: ['movie_title']
}
},
{
name: 'download_series_season_m3u',
description: _('aiToolDownloadSeriesSeasonM3UDesc'),
parameters: {
type: 'object',
properties: {
series_title: { type: 'string', description: _('aiToolDownloadSeriesSeasonM3UTitleParamDesc') },
season_number: { type: 'number', description: _('aiToolDownloadSeriesSeasonM3USeasonParamDesc') },
year: { type: 'string', description: _('aiToolDownloadSeriesSeasonM3UYearParamDesc') }
},
required: ['series_title', 'season_number']
}
},
{ {
name: 'check_and_download_titles_list', name: 'check_and_download_titles_list',
description: _('aiToolCheckAndDownloadDesc'), description: _('aiToolCheckAndDownloadDesc'),
@ -124,6 +149,22 @@ export class AITools {
required: ['type'] required: ['type']
} }
}, },
{
name: 'list_available_music_genres',
description: _('aiToolListAvailableMusicGenresDesc'),
parameters: { type: 'object', properties: {} }
},
{
name: 'search_music_by_genre',
description: _('aiToolSearchMusicByGenreDesc'),
parameters: {
type: 'object',
properties: {
genre_name: { type: 'string', description: _('aiToolSearchMusicByGenreNameParamDesc') }
},
required: ['genre_name']
}
},
{ {
name: 'play_music_by_artist', name: 'play_music_by_artist',
description: _('aiToolPlayMusicByArtistDesc'), description: _('aiToolPlayMusicByArtistDesc'),
@ -186,6 +227,132 @@ export class AITools {
name: 'clear_recommendations_view', name: 'clear_recommendations_view',
description: _('aiToolClearRecommendationsViewDesc'), description: _('aiToolClearRecommendationsViewDesc'),
parameters: { type: 'object', properties: {} } parameters: { type: 'object', properties: {} }
},
{
name: 'view_history',
description: _('aiToolViewHistoryDesc'),
parameters: { type: 'object', properties: {} }
},
{
name: 'view_favorites',
description: _('aiToolViewFavoritesDesc'),
parameters: { type: 'object', properties: {} }
},
{
name: 'play_song',
description: _('aiToolPlaySongDesc'),
parameters: {
type: 'object',
properties: {
title: { type: 'string', description: _('aiToolPlaySongTitleParamDesc') },
artist: { type: 'string', description: _('aiToolPlaySongArtistParamDesc') }
},
required: ['title']
}
},
{
name: 'toggle_light_mode',
description: _('aiToolToggleLightModeDesc'),
parameters: { type: 'object', properties: {} }
},
{
name: 'toggle_hero_section',
description: _('aiToolToggleHeroSectionDesc'),
parameters: { type: 'object', properties: {} }
},
{
name: 'export_local_database',
description: _('aiToolExportLocalDatabaseDesc'),
parameters: { type: 'object', properties: {} }
},
{
name: 'import_local_database',
description: _('aiToolImportLocalDatabaseDesc'),
parameters: { type: 'object', properties: {} }
},
{
name: 'search_tmdb_content',
description: _('aiToolSearchTmdbContentDesc'),
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: _('aiToolSearchTmdbContentQueryParamDesc') },
type: { type: 'string', enum: ['movie', 'series', 'person', 'all'], description: _('aiToolSearchTmdbContentTypeParamDesc') }
},
required: ['query']
}
},
{
name: 'get_trending_content',
description: _('aiToolGetTrendingContentDesc'),
parameters: {
type: 'object',
properties: {
type: { type: 'string', enum: ['movie', 'series', 'all'], description: _('aiToolGetTrendingContentTypeParamDesc') }
},
required: ['type']
}
},
{
name: 'show_actor_details',
description: _('aiToolShowActorDetailsDesc'),
parameters: {
type: 'object',
properties: {
actor_name: { type: 'string', description: _('aiToolShowActorDetailsNameParamDesc') }
},
required: ['actor_name']
}
},
{
name: 'play_trailer',
description: _('aiToolPlayTrailerDesc'),
parameters: {
type: 'object',
properties: {
title: { type: 'string', description: _('aiToolPlayTrailerTitleParamDesc') },
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolPlayTrailerTypeParamDesc') }
},
required: ['title', 'type']
}
},
{
name: 'check_local_availability',
description: _('aiToolCheckLocalAvailabilityDesc'),
parameters: {
type: 'object',
properties: {
title: { type: 'string', description: _('aiToolCheckLocalAvailabilityTitleParamDesc') },
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolCheckLocalAvailabilityTypeParamDesc') },
year: { type: 'string', description: _('aiToolCheckLocalAvailabilityYearParamDesc') }
},
required: ['title', 'type']
}
},
{
name: 'get_local_series_seasons',
description: _('aiToolGetLocalSeriesSeasonsDesc'),
parameters: {
type: 'object',
properties: {
series_title: { type: 'string', description: _('aiToolGetLocalSeriesSeasonsTitleParamDesc') },
year: { type: 'string', description: _('aiToolGetLocalSeriesSeasonsYearParamDesc') }
},
required: ['series_title']
}
},
{
name: 'find_streaming_providers',
description: _('aiToolFindStreamingProvidersDesc'),
parameters: {
type: 'object',
properties: {
title: { type: 'string', description: _('aiToolFindStreamingProvidersTitleParamDesc') },
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolFindStreamingProvidersTypeParamDesc') },
year: { type: 'string', description: _('aiToolFindStreamingProvidersYearParamDesc') }
},
required: ['title', 'type']
}
} }
]; ];
} }
@ -193,9 +360,14 @@ export class AITools {
async "search_library"({ query, type, resolution, container }) { async "search_library"({ query, type, resolution, container }) {
const movieEntries = await getFromDB('movies'); const movieEntries = await getFromDB('movies');
const seriesEntries = await getFromDB('series'); const seriesEntries = await getFromDB('series');
const jellyfinMovieEntries = await getFromDB('jellyfin_movies');
const jellyfinSeriesEntries = await getFromDB('jellyfin_series');
const allContent = [ const allContent = [
...movieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))), ...movieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))),
...seriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' }))) ...seriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' }))),
...jellyfinMovieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))),
...jellyfinSeriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' })))
]; ];
let results = allContent; let results = allContent;
@ -233,13 +405,25 @@ export class AITools {
async "get_user_stats"() { async "get_user_stats"() {
try { try {
const movieItems = (await getFromDB('movies')).flatMap(s => s.titulos); const movieItemsPlex = (await getFromDB('movies')).flatMap(s => s.titulos);
const seriesItems = (await getFromDB('series')).flatMap(s => s.titulos); const seriesItemsPlex = (await getFromDB('series')).flatMap(s => s.titulos);
const artistItems = (await getFromDB('artists')).flatMap(s => s.titulos); const artistItemsPlex = (await getFromDB('artists')).flatMap(s => s.titulos);
const movieItemsJellyfin = (await getFromDB('jellyfin_movies')).flatMap(s => s.titulos);
const seriesItemsJellyfin = (await getFromDB('jellyfin_series')).flatMap(s => s.titulos);
const allMovieTitles = new Set([...movieItemsPlex.map(item => item.title), ...movieItemsJellyfin.map(item => item.title)]);
const allSeriesTitles = new Set([...seriesItemsPlex.map(item => item.title), ...seriesItemsJellyfin.map(item => item.title)]);
const allArtistTitles = new Set(artistItemsPlex.map(item => item.title));
const plexConnections = await getFromDB('conexiones_locales');
const jellyfinConnections = await getFromDB('jellyfin_settings');
const stats = { const stats = {
totalMovies: new Set(movieItems.map(item => item.title)).size, totalMovies: allMovieTitles.size,
totalSeries: new Set(seriesItems.map(item => item.title)).size, totalSeries: allSeriesTitles.size,
totalArtists: new Set(artistItems.map(item => item.title)).size, totalArtists: allArtistTitles.size,
plexServers: plexConnections.length,
jellyfinServers: jellyfinConnections.length
}; };
switchView('stats'); switchView('stats');
return JSON.stringify({ success: true, stats }); return JSON.stringify({ success: true, stats });
@ -253,6 +437,7 @@ export class AITools {
if (!content) { if (!content) {
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) }); return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
} }
state.aiTriggeredDetails = true;
showItemDetails(Number(content.id), content.type); showItemDetails(Number(content.id), content.type);
return JSON.stringify({ success: true, message: _('aiToolShowItemDetailsSuccess', title) }); return JSON.stringify({ success: true, message: _('aiToolShowItemDetailsSuccess', title) });
} }
@ -266,6 +451,25 @@ export class AITools {
return JSON.stringify({ success: true, message: _('aiToolAddToPlaylistSuccess', title) }); return JSON.stringify({ success: true, message: _('aiToolAddToPlaylistSuccess', title) });
} }
async "download_single_movie_m3u"({ movie_title, year }) {
try {
downloadM3U([{ title: movie_title, type: 'movie', year: year }], null, movie_title);
return JSON.stringify({ success: true, message: _('aiToolM3UDownloadStartedSingle', movie_title) });
} catch (error) {
return JSON.stringify({ success: false, message: `Error generating M3U for ${movie_title}: ${error.message}` });
}
}
async "download_series_season_m3u"({ series_title, season_number, year }) {
try {
const filename = `${series_title.replace(/[^a-z0-9]/gi, '_')}_S${season_number}`;
downloadM3U([{ title: series_title, type: 'tv', year: year, seasonNumber: season_number }], null, filename);
return JSON.stringify({ success: true, message: _('aiToolM3UDownloadStartedSeason', [String(season_number), series_title]) });
} catch (error) {
return JSON.stringify({ success: false, message: `Error generating M3U for ${series_title} Season ${season_number}: ${error.message}` });
}
}
async "check_and_download_titles_list"({ titles, type, filename }) { async "check_and_download_titles_list"({ titles, type, filename }) {
try { try {
if (!titles || titles.length === 0) { if (!titles || titles.length === 0) {
@ -344,6 +548,41 @@ export class AITools {
return JSON.stringify({ success: true, message: _('aiToolApplyFiltersSuccess') }); return JSON.stringify({ success: true, message: _('aiToolApplyFiltersSuccess') });
} }
async "list_available_music_genres"() {
if (!state.musicPlayer || !state.musicPlayer.isReady) {
return JSON.stringify({ success: false, message: _('aiToolPlayMusicNotReady') });
}
const allArtists = state.musicPlayer._generateFullArtistListForToken('all');
const genreSet = new Set();
allArtists.forEach(artist => {
if (artist.genres && artist.genres.length > 0) {
artist.genres.forEach(g => genreSet.add(g));
}
});
const sortedGenres = Array.from(genreSet).sort((a, b) => a.localeCompare(b));
if (sortedGenres.length === 0) {
return JSON.stringify({ success: true, genres: [], message: "No genres found in the user's library." });
}
return JSON.stringify({ success: true, genres: sortedGenres });
}
async "search_music_by_genre"({ genre_name }) {
if (!state.musicPlayer || !state.musicPlayer.isReady) {
return JSON.stringify({ success: false, message: _('aiToolPlayMusicNotReady') });
}
const allArtists = state.musicPlayer._generateFullArtistListForToken('all');
const searchTerm = genre_name.toLowerCase().trim();
const matchingArtists = allArtists.filter(artist =>
artist.genres && artist.genres.some(g => g.toLowerCase().includes(searchTerm))
);
if (matchingArtists.length === 0) {
return JSON.stringify({ success: false, message: _('aiToolSearchMusicByGenreNotFound', genre_name) });
}
const artistNames = matchingArtists.map(a => a.title);
switchView('music');
return JSON.stringify({ success: true, count: artistNames.length, artists: artistNames });
}
async "play_music_by_artist"({ artist_name }) { async "play_music_by_artist"({ artist_name }) {
if (!state.musicPlayer || !state.musicPlayer.isReady) { if (!state.musicPlayer || !state.musicPlayer.isReady) {
return JSON.stringify({ success: false, message: _('aiToolPlayMusicNotReady') }); return JSON.stringify({ success: false, message: _('aiToolPlayMusicNotReady') });
@ -459,17 +698,251 @@ export class AITools {
} }
} }
async "view_history"() {
switchView('history');
return JSON.stringify({ success: true, message: _('aiToolNavigatedToHistory') });
}
async "view_favorites"() {
switchView('favorites');
return JSON.stringify({ success: true, message: _('aiToolNavigatedToFavorites') });
}
async "play_song"({ title, artist }) {
if (!state.musicPlayer || !state.musicPlayer.isReady) {
return JSON.stringify({ success: false, message: _('aiToolPlayMusicNotReady') });
}
const song = await this.findSongLocally(title, artist);
if (!song) {
return JSON.stringify({ success: false, message: _('aiToolSongNotFound', title) });
}
state.musicPlayer.showPlayer();
state.musicPlayer.cancionesActuales = [song];
state.musicPlayer.playSong(0);
return JSON.stringify({ success: true, message: _('aiToolPlayingSong', [title, artist || _('unknownArtist')]) });
}
async "toggle_light_mode"() {
state.settings.theme = state.settings.theme === 'light' ? 'dark' : 'light';
await addItemsToStore('settings', [{ id: 'user_settings', ...state.settings }]);
applyTheme(state.settings.theme);
return JSON.stringify({ success: true, message: state.settings.theme === 'light' ? _('aiToolLightModeOn') : _('aiToolLightModeOff') });
}
async "toggle_hero_section"() {
state.settings.showHero = !state.settings.showHero;
await addItemsToStore('settings', [{ id: 'user_settings', ...state.settings }]);
applyHeroVisibility(state.settings.showHero);
return JSON.stringify({ success: true, message: state.settings.showHero ? _('aiToolHeroOn') : _('aiToolHeroOff') });
}
async "export_local_database"() {
try {
await exportDatabase();
return JSON.stringify({ success: true, message: _('aiToolDatabaseExported') });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolDatabaseExportError', error.message) });
}
}
async "import_local_database"() {
showNotification(_('aiToolImportPrompt'), 'info', 5000);
return JSON.stringify({ success: true, message: _('aiToolImportInitiated') });
}
async "search_tmdb_content"({ query, type = 'all' }) {
try {
const searchEndpoint = type === 'all' ? `search/multi?query=${encodeURIComponent(query)}` : `search/${type}?query=${encodeURIComponent(query)}`;
const searchResults = await fetchTMDB(searchEndpoint);
if (!searchResults || !searchResults.results || searchResults.results.length === 0) {
return JSON.stringify({ success: false, message: _('aiToolTmdbSearchNoResults', query) });
}
const filteredResults = searchResults.results.filter(item => item.media_type !== 'person').slice(0, 10);
if (filteredResults.length === 0) {
return JSON.stringify({ success: false, message: _('aiToolTmdbSearchNoMoviesOrSeries', query) });
}
renderGrid(filteredResults, false);
switchView('search');
state.currentParams.query = query;
return JSON.stringify({ success: true, message: _('aiToolTmdbSearchSuccess', [filteredResults.length, query]), results: filteredResults.map(i => i.title || i.name) });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolTmdbSearchError', error.message) });
}
}
async "get_trending_content"({ type = 'all' }) {
try {
const trendingEndpoint = `trending/${type}/day`;
const trendingResults = await fetchTMDB(trendingEndpoint);
if (!trendingResults || !trendingResults.results || trendingResults.results.length === 0) {
return JSON.stringify({ success: false, message: _('aiToolTrendingNoResults') });
}
renderGrid(trendingResults.results.filter(item => item.media_type !== 'person').slice(0, 10), false);
switchView(type === 'movie' ? 'movies' : (type === 'tv' ? 'series' : 'home'));
return JSON.stringify({ success: true, message: _('aiToolTrendingSuccess', trendingResults.results.length) });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolTrendingError', error.message) });
}
}
async "show_actor_details"({ actor_name }) {
try {
const searchResults = await fetchTMDB(`search/person?query=${encodeURIComponent(actor_name)}`);
if (!searchResults || !searchResults.results || searchResults.results.length === 0) {
return JSON.stringify({ success: false, message: _('aiToolActorNotFound', actor_name) });
}
const actor = searchResults.results[0];
showActorDetails(actor.id);
return JSON.stringify({ success: true, message: _('aiToolShowActorDetailsSuccess', actor.name) });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolShowActorDetailsError', error.message) });
}
}
async "play_trailer"({ title, type }) {
try {
const content = await this.findTmdbContent(title, type);
if (!content) {
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
}
const trailerKey = await getTrailerKey(content.id, content.type);
if (!trailerKey) {
return JSON.stringify({ success: false, message: _('aiToolTrailerNotFound') });
}
showTrailer(trailerKey);
return JSON.stringify({ success: true, message: _('aiToolPlayingTrailer', title) });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolPlayTrailerError', error.message) });
}
}
async "check_local_availability"({ title, type, year }) {
const available = isContentAvailableLocally(title, type, year);
return JSON.stringify({ success: true, available: available, message: available ? _('aiToolAvailableLocally', title) : _('aiToolNotAvailableLocally', title) });
}
async "get_local_series_seasons"({ series_title, year }) {
const seriesDetails = await this.findLocalSeriesDetails(series_title, year);
if (seriesDetails.length === 0) {
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', series_title) });
}
return JSON.stringify({
success: true,
found: true,
servers: seriesDetails
});
}
async "find_streaming_providers"({ title, type, year }) {
const available = isContentAvailableLocally(title, type, year);
if (available) {
return JSON.stringify({ success: true, available: true, message: _('aiToolAvailableLocally', title) });
}
try {
const searchResults = await fetchTMDB(`search/${type}?query=${encodeURIComponent(title)}&year=${year || ''}`);
if (!searchResults || !searchResults.results || searchResults.results.length === 0) {
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
}
const item = searchResults.results[0];
const providersResult = await fetchTMDB(`${type}/${item.id}/watch/providers`);
const region = state.settings.watchRegion || 'US';
const providers = providersResult.results[region];
if (!providers || !providers.flatrate) {
return JSON.stringify({ success: false, available: false, message: _('aiToolNoStreamingProviders', title) });
}
const providerNames = providers.flatrate.map(p => p.provider_name).join(', ');
return JSON.stringify({ success: true, available: false, providers: providerNames, message: _('aiToolStreamingProvidersFound', [title, providerNames]) });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolStreamingProviderError', error.message) });
}
}
async findLocalContent(title, type) { async findLocalContent(title, type) {
const movieEntries = await getFromDB('movies'); const movieEntries = await getFromDB('movies');
const seriesEntries = await getFromDB('series'); const seriesEntries = await getFromDB('series');
const jellyfinMovieEntries = await getFromDB('jellyfin_movies');
const jellyfinSeriesEntries = await getFromDB('jellyfin_series');
const allContent = [ const allContent = [
...movieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))), ...movieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))),
...seriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' }))) ...seriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' }))),
...jellyfinMovieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))),
...jellyfinSeriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' })))
]; ];
const searchTerm = title.toLowerCase().trim(); const searchTerm = title.toLowerCase().trim();
return allContent.find(item => item.title.toLowerCase() === searchTerm && (!type || item.type === type)); return allContent.find(item => item.title.toLowerCase() === searchTerm && (!type || item.type === type));
} }
async findLocalSeriesDetails(title, year) {
const allSeriesSources = [...(await getFromDB('series')), ...(await getFromDB('jellyfin_series'))];
const normalizedTitle = title.toLowerCase().trim();
const serverDetails = [];
for (const server of allSeriesSources) {
if (server && server.titulos) {
const foundSeries = server.titulos.find(s =>
s.title.toLowerCase().trim() === normalizedTitle &&
(!year || s.year == year)
);
if (foundSeries) {
const seasons = (foundSeries.seasons || []).map(season => ({
season_number: parseInt(season.index, 10),
episode_count: parseInt(season.episodeCount, 10)
})).sort((a, b) => a.season_number - b.season_number);
serverDetails.push({
serverName: server.serverName || server.nombre || 'Servidor Desconocido',
season_count: seasons.length,
seasons: seasons
});
}
}
}
return serverDetails;
}
async findSongLocally(title, artist) {
const allArtistsData = await getFromDB('artists');
for (const serverData of allArtistsData) {
if (serverData && Array.isArray(serverData.titulos)) {
for (const artistItem of serverData.titulos) {
let songs;
if (artistItem.isJellyfin) {
songs = await state.musicPlayer.getArtistSongs({
id: artistItem.id,
isJellyfin: true,
serverUrl: artistItem.serverUrl,
userId: artistItem.userId,
token: artistItem.token
});
} else {
songs = await state.musicPlayer.getArtistSongs({
id: artistItem.id,
isJellyfin: false,
token: artistItem.token,
protocolo: serverData.protocolo,
ip: serverData.ip,
puerto: serverData.puerto
});
}
const foundSong = songs.find(song =>
song.titulo.toLowerCase().includes(title.toLowerCase()) &&
(!artist || song.artista.toLowerCase().includes(artist.toLowerCase()))
);
if (foundSong) return foundSong;
}
}
}
return null;
}
async findTmdbContent(title, type) { async findTmdbContent(title, type) {
try { try {
const searchResults = await fetchTMDB(`search/${type}?query=${encodeURIComponent(title)}`); const searchResults = await fetchTMDB(`search/${type}?query=${encodeURIComponent(title)}`);

View File

@ -15,7 +15,6 @@ export async function fetchTMDB(endpoint, params = {}, signal) {
finalParams.set('language', lang); finalParams.set('language', lang);
finalParams.set('watch_region', region); finalParams.set('watch_region', region);
// Añadir filtros de puntuación y duración
if (params.minScore) finalParams.set('vote_average.gte', params.minScore); if (params.minScore) finalParams.set('vote_average.gte', params.minScore);
if (params.maxScore) finalParams.set('vote_average.lte', params.maxScore); if (params.maxScore) finalParams.set('vote_average.lte', params.maxScore);
if (params.minDuration) finalParams.set('with_runtime.gte', params.minDuration); if (params.minDuration) finalParams.set('with_runtime.gte', params.minDuration);
@ -92,7 +91,8 @@ export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artista
genre: Array.from(track.querySelectorAll("Genre")).map(g => g.getAttribute('tag')).join(', ') || '', genre: Array.from(track.querySelectorAll("Genre")).map(g => g.getAttribute('tag')).join(', ') || '',
index: parseInt(track.getAttribute("index") || 0, 10), index: parseInt(track.getAttribute("index") || 0, 10),
albumIndex: parseInt(track.getAttribute("parentIndex") || 0, 10), albumIndex: parseInt(track.getAttribute("parentIndex") || 0, 10),
trackIndex: parseInt(track.getAttribute("index") || 0, 10) trackIndex: parseInt(track.getAttribute("index") || 0, 10),
duration: track.getAttribute("duration")
}; };
}).filter(track => track !== null); }).filter(track => track !== null);
@ -146,7 +146,8 @@ export async function getMusicUrlsFromJellyfin(serverUrl, userId, token, artistI
genre: track.Genres?.join(', ') || '', genre: track.Genres?.join(', ') || '',
index: track.IndexNumber || 0, index: track.IndexNumber || 0,
albumIndex: album.IndexNumber || 0, albumIndex: album.IndexNumber || 0,
trackIndex: track.IndexNumber || 0 trackIndex: track.IndexNumber || 0,
duration: source.RunTimeTicks / 10000
}; };
}).filter(track => track !== null); }).filter(track => track !== null);
allTracks.push(...albumTracks); allTracks.push(...albumTracks);
@ -167,12 +168,15 @@ export async function getMusicUrlsFromJellyfin(serverUrl, userId, token, artistI
} }
} }
export async function fetchAllStreamsFromPlex(busqueda, tipoContenido, year = null) { export async function fetchAllStreamsFromPlex(busqueda, tipoContenido, year = null, seasonNumber = null, serverName = null) {
if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') }; if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') };
if (!state.db) return { success: false, streams: [], message: _('dbUnavailableForStreams') }; if (!state.db) return { success: false, streams: [], message: _('dbUnavailableForStreams') };
const plexSearchType = tipoContenido === 'movie' ? '1' : '2'; const plexSearchType = tipoContenido === 'movie' ? '1' : '2';
const servers = await getFromDB('conexiones_locales'); let servers = await getFromDB('conexiones_locales');
if(serverName){
servers = servers.filter(s => s.nombre === serverName);
}
if (!servers || servers.length === 0) return { success: false, streams: [], message: _('noPlexServersForStreams') }; if (!servers || servers.length === 0) return { success: false, streams: [], message: _('noPlexServersForStreams') };
const searchTasks = servers.map(async (server) => { const searchTasks = servers.map(async (server) => {
@ -203,12 +207,12 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido, year = nu
if (exactMatch) { if (exactMatch) {
videosToProcess = [exactMatch]; videosToProcess = [exactMatch];
} }
} else { }
if (videosToProcess.length === 0) {
const exactMatch = videos.find(v => v.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase()); const exactMatch = videos.find(v => v.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase());
if (exactMatch) { if (exactMatch) {
videosToProcess = [exactMatch]; videosToProcess = [exactMatch];
} else {
videosToProcess = videos;
} }
} }
@ -243,10 +247,6 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido, year = nu
directoryToProcess = directories.find(d => d.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase()); directoryToProcess = directories.find(d => d.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase());
} }
if (!directoryToProcess && directories.length > 0) {
directoryToProcess = directories[0];
}
if (directoryToProcess && directoryToProcess.getAttribute("ratingKey")) { if (directoryToProcess && directoryToProcess.getAttribute("ratingKey")) {
const serieKey = directoryToProcess.getAttribute("ratingKey"); const serieKey = directoryToProcess.getAttribute("ratingKey");
const serieTitulo = directoryToProcess.getAttribute("title") || busqueda; const serieTitulo = directoryToProcess.getAttribute("title") || busqueda;
@ -258,7 +258,11 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido, year = nu
const leavesData = await leavesResponse.text(); const leavesData = await leavesResponse.text();
const leavesXml = parser.parseFromString(leavesData, "text/xml"); const leavesXml = parser.parseFromString(leavesData, "text/xml");
if (!leavesXml.querySelector('parsererror')) { if (!leavesXml.querySelector('parsererror')) {
const episodes = Array.from(leavesXml.querySelectorAll("Video")); let episodes = Array.from(leavesXml.querySelectorAll("Video"));
if (seasonNumber) {
episodes = episodes.filter(ep => ep.getAttribute("parentIndex") == seasonNumber);
}
episodes.sort((a, b) => { episodes.sort((a, b) => {
const seasonA = parseInt(a.getAttribute("parentIndex") || 0, 10); const seasonA = parseInt(a.getAttribute("parentIndex") || 0, 10);
@ -278,12 +282,13 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido, year = nu
const streamUrl = `${protocolo}://${ip}:${puerto}${part.getAttribute("key")}?X-Plex-Token=${token}`; const streamUrl = `${protocolo}://${ip}:${puerto}${part.getAttribute("key")}?X-Plex-Token=${token}`;
const groupTitle = `${serieTitulo}${serieYear ? ` (${serieYear})` : ''} - Temporada ${seasonNum}`.replace(/"/g, "'"); const groupTitle = `${serieTitulo}${serieYear ? ` (${serieYear})` : ''} - Temporada ${seasonNum}`.replace(/"/g, "'");
const extinfName = `${serieTitulo} T${seasonNum}E${episodeNum} ${episodeTitle}`; const extinfName = `${serieTitulo} T${seasonNum}E${episodeNum} ${episodeTitle}`;
const displayName = `T${seasonNum}E${episodeNum} ${episodeTitle}`;
const logoUrl = episode.getAttribute("grandparentThumb") || episode.getAttribute("parentThumb") || episode.getAttribute("thumb"); const logoUrl = episode.getAttribute("grandparentThumb") || episode.getAttribute("parentThumb") || episode.getAttribute("thumb");
const fullLogoUrl = logoUrl ? `${protocolo}://${ip}:${puerto}${logoUrl}?X-Plex-Token=${token}` : ''; const fullLogoUrl = logoUrl ? `${protocolo}://${ip}:${puerto}${logoUrl}?X-Plex-Token=${token}` : '';
serverStreams.push({ serverStreams.push({
url: streamUrl, url: streamUrl,
title: extinfName, title: extinfName,
extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${fullLogoUrl}" group-title="${groupTitle}",${extinfName}` extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${fullLogoUrl}" group-title="${groupTitle}",${displayName}`
}); });
} }
}); });
@ -323,7 +328,7 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido, year = nu
} }
} }
export async function fetchAllStreamsFromJellyfin(busqueda, tipoContenido) { export async function fetchAllStreamsFromJellyfin(busqueda, tipoContenido, year = null, seasonNumber = null) {
if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') }; if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') };
const { url, userId, apiKey } = state.jellyfinSettings; const { url, userId, apiKey } = state.jellyfinSettings;
@ -366,7 +371,12 @@ export async function fetchAllStreamsFromJellyfin(busqueda, tipoContenido) {
if (!episodesResponse.ok) throw new Error(`Error obteniendo episodios: ${episodesResponse.status}`); if (!episodesResponse.ok) throw new Error(`Error obteniendo episodios: ${episodesResponse.status}`);
const episodesData = await episodesResponse.json(); const episodesData = await episodesResponse.json();
const sortedEpisodes = episodesData.Items.sort((a, b) => { let episodes = episodesData.Items;
if (seasonNumber) {
episodes = episodes.filter(ep => ep.ParentIndexNumber == seasonNumber);
}
const sortedEpisodes = episodes.sort((a, b) => {
if (a.ParentIndexNumber !== b.ParentIndexNumber) return (a.ParentIndexNumber || 0) - (b.ParentIndexNumber || 0); if (a.ParentIndexNumber !== b.ParentIndexNumber) return (a.ParentIndexNumber || 0) - (b.ParentIndexNumber || 0);
return (a.IndexNumber || 0) - (b.IndexNumber || 0); return (a.IndexNumber || 0) - (b.IndexNumber || 0);
}); });
@ -378,11 +388,12 @@ export async function fetchAllStreamsFromJellyfin(busqueda, tipoContenido) {
const episodeTitle = ep.Name || 'Episodio'; const episodeTitle = ep.Name || 'Episodio';
const groupTitle = `${itemName} - Temporada ${seasonNum}`.replace(/"/g, "'"); const groupTitle = `${itemName} - Temporada ${seasonNum}`.replace(/"/g, "'");
const extinfName = `${itemName} T${seasonNum}E${episodeNum} ${episodeTitle}`; const extinfName = `${itemName} T${seasonNum}E${episodeNum} ${episodeTitle}`;
const displayName = `T${seasonNum}E${episodeNum} ${episodeTitle}`;
streams.push({ streams.push({
url: streamUrl, url: streamUrl,
title: extinfName, title: extinfName,
extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${posterUrl}" group-title="${groupTitle}",${extinfName}` extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${posterUrl}" group-title="${groupTitle}",${displayName}`
}); });
}); });
} }
@ -395,11 +406,33 @@ export async function fetchAllStreamsFromJellyfin(busqueda, tipoContenido) {
} }
} }
export async function fetchAllAvailableStreams(title, type, year = null) { export async function fetchAllAvailableStreams(title, type, year = null, seasonNumber = null, serverName = null, sourceType = null) {
const plexPromise = fetchAllStreamsFromPlex(title, type, year); const promises = [];
const jellyfinPromise = fetchAllStreamsFromJellyfin(title, type); // Jellyfin no usa 'year' en su signature, lo he quitado para que no cause error si se le pasa.
const results = await Promise.allSettled([plexPromise, jellyfinPromise]); if (serverName) {
if (sourceType === 'plex' || !sourceType) { // Assume plex if sourceType is not defined
promises.push(fetchAllStreamsFromPlex(title, type, year, seasonNumber, serverName));
}
if (sourceType === 'jellyfin' || !sourceType) {
// We need a way to check if the serverName belongs to Jellyfin.
// For now, let's assume if it's not found in Plex, it might be Jellyfin.
// This part of the logic might need refinement if there are multiple Jellyfin servers.
const plexServers = await getFromDB('conexiones_locales');
if (!plexServers.some(s => s.nombre === serverName)){
promises.push(fetchAllStreamsFromJellyfin(title, type, year, seasonNumber));
}
}
} else {
const plexPromise = fetchAllStreamsFromPlex(title, type, year, seasonNumber);
const jellyfinPromise = fetchAllStreamsFromJellyfin(title, type, year, seasonNumber);
promises.push(plexPromise, jellyfinPromise);
}
if (promises.length === 0) {
return { success: false, streams: [], message: _('notFoundOnAnyServer', title) };
}
const results = await Promise.allSettled(promises);
let allStreams = []; let allStreams = [];
const errorMessages = []; const errorMessages = [];
@ -425,4 +458,28 @@ export async function fetchAllAvailableStreams(title, type, year = null) {
} else { } else {
return { success: false, streams: [], message: errorMessages.join('; ') || _('notFoundOnAnyServer', title) }; return { success: false, streams: [], message: errorMessages.join('; ') || _('notFoundOnAnyServer', title) };
} }
}
export async function fetchArtistGenresFromLastFM(artist) {
if (!config.lastFmApiKey) {
return [];
}
const url = `https://ws.audioscrobbler.com/2.0/?method=artist.gettoptags&artist=${encodeURIComponent(artist)}&api_key=${config.lastFmApiKey}&format=json`;
try {
const response = await fetchWithTimeout(url, {}, 5000);
if (!response.ok) {
return [];
}
const data = await response.json();
if (data.toptags && data.toptags.tag) {
const tags = Array.isArray(data.toptags.tag) ? data.toptags.tag : [data.toptags.tag];
return tags
.sort((a, b) => b.count - a.count)
.slice(0, 4)
.map(tag => tag.name.charAt(0).toUpperCase() + tag.name.slice(1));
}
return [];
} catch (error) {
return [];
}
} }

View File

@ -73,7 +73,9 @@ export class Chat {
gsap.to(this.dom.window, { opacity: 0, scale: 0.9, y: 20, duration: 0.3, ease: 'power2.in', onComplete: () => { gsap.to(this.dom.window, { opacity: 0, scale: 0.9, y: 20, duration: 0.3, ease: 'power2.in', onComplete: () => {
this.dom.window.style.display = 'none'; this.dom.window.style.display = 'none';
}}); }});
gsap.fromTo(this.dom.fab, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)', delay: 0.2 }); if (!document.body.classList.contains('miniplayer-active')) {
gsap.fromTo(this.dom.fab, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)', delay: 0.2 });
}
} }
async sendMessage() { async sendMessage() {
@ -89,22 +91,20 @@ export class Chat {
this.addTypingIndicator(); this.addTypingIndicator();
try { try {
const response = await this.getAIResponseWithTools(); await this.getAIResponseWithTools();
this.removeTypingIndicator();
if (response) {
this.addMessage(response, 'assistant');
this.conversationHistory.push({ role: 'model', parts: [{ text: response }] });
}
} catch (error) { } catch (error) {
this.removeTypingIndicator();
this.addMessage(error.message, 'assistant', true); this.addMessage(error.message, 'assistant', true);
} finally { } finally {
this.dom.sendBtn.disabled = false; this.dom.sendBtn.disabled = false;
this.removeTypingIndicator();
} }
} }
formatMessageText(text) {
const markdownLinkRegex = /\[([^\]]+?)\]\((https?:\/\/[^\s)]+?)\)/g;
return text.replace(markdownLinkRegex, `<a href="$2" target="_blank" rel="noopener">$1</a>`);
}
addMessage(text, sender, isError = false, toolName = null) { addMessage(text, sender, isError = false, toolName = null) {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = `message-wrapper ${sender}-wrapper`; wrapper.className = `message-wrapper ${sender}-wrapper`;
@ -120,7 +120,7 @@ export class Chat {
if (sender === 'assistant') { 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>'; 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 { } 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>'; 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-5 5zm-2.5-5h5v2h-5v-2z"/></svg>';
} }
avatar.innerHTML = icon; avatar.innerHTML = icon;
wrapper.appendChild(avatar); wrapper.appendChild(avatar);
@ -128,9 +128,9 @@ export class Chat {
const p = document.createElement('p'); const p = document.createElement('p');
if (sender === 'tool-call' || sender === 'tool-result') { if (sender === 'tool-call' || sender === 'tool-result') {
p.innerHTML = `<strong>${toolName}:</strong> ${text}`; p.innerHTML = `<strong>${toolName}:</strong> ${this.formatMessageText(text)}`;
} else { } else {
p.textContent = text; p.innerHTML = this.formatMessageText(text);
} }
messageEl.appendChild(p); messageEl.appendChild(p);
@ -163,86 +163,111 @@ export class Chat {
if (indicator) indicator.remove(); if (indicator) indicator.remove();
} }
async getAIResponseWithTools() { formatCitations(chunks) {
if (!chunks || chunks.length === 0) return '';
let citationText = '\n\n**' + _('chatSources') + ':**\n';
chunks.forEach((chunk, index) => {
if (chunk.web && chunk.web.uri) {
const title = chunk.web.title || _('chatUnnamedSource');
citationText += `[${index + 1}] [${title}](${chunk.web.uri})\n`;
}
});
return citationText;
}
async getAIResponseWithTools(isRecursiveCall = false) {
const apiKey = state.settings.googleApiKey; const apiKey = state.settings.googleApiKey;
if (!apiKey) { if (!apiKey) {
return _('chatGoogleApiKeyMissing'); this.removeTypingIndicator();
const errorMessage = _('chatGoogleApiKeyMissing');
this.addMessage(errorMessage, 'assistant');
this.conversationHistory.push({ role: 'model', parts: [{ text: errorMessage }] });
return;
} }
const systemPrompt = _('aiSystemPrompt_v3');
const tools = [{
functionDeclarations: this.aiTools.toolDefinitions
}];
const model = "gemini-2.5-flash"; const model = "gemini-2.5-flash";
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`; const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
const currentDate = new Date().toLocaleDateString(_('appLocaleCode'), {
year: 'numeric',
month: 'long',
day: 'numeric'
});
const systemPrompt = _('aiSystemPrompt_v3', [currentDate]);
const functionTools = [{ functionDeclarations: this.aiTools.toolDefinitions }];
const googleSearchTool = [{ googleSearch: {} }];
try { try {
const response = await fetch(url, { if (!isRecursiveCall) {
const functionCallResponse = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey },
body: JSON.stringify({
contents: this.conversationHistory,
tools: functionTools,
system_instruction: { parts: [{ text: systemPrompt }] }
})
});
if (!functionCallResponse.ok) throw new Error((await functionCallResponse.json()).error.message);
const functionCallData = await functionCallResponse.json();
const candidate = functionCallData.candidates[0];
if (candidate && candidate.content.parts[0].functionCall) {
const part = candidate.content.parts[0];
this.conversationHistory.push(candidate.content);
const toolCall = {
id: `call_${Date.now()}`,
function: { name: part.functionCall.name, arguments: part.functionCall.args },
};
const toolResult = await this.aiTools.executeTool(toolCall);
this.conversationHistory.push({
role: 'tool',
parts: [{ functionResponse: { name: part.functionCall.name, response: JSON.parse(toolResult) } }]
});
await this.getAIResponseWithTools(true);
return;
}
}
const finalResponse = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey },
'Content-Type': 'application/json',
'x-goog-api-key': apiKey
},
body: JSON.stringify({ body: JSON.stringify({
contents: this.conversationHistory, contents: this.conversationHistory,
tools: tools, tools: isRecursiveCall ? undefined : googleSearchTool,
system_instruction: { system_instruction: { parts: [{ text: systemPrompt }] }
parts: [{ text: systemPrompt }]
}
}) })
}); });
if (!response.ok) { if (!finalResponse.ok) throw new Error((await finalResponse.json()).error.message);
const errorData = await response.json().catch(() => ({}));
const errorMessage = errorData?.error?.message || `Error HTTP ${response.status}`; const finalData = await finalResponse.json();
throw new Error(errorMessage); if (!finalData.candidates || !finalData.candidates.length) throw new Error(_('chatApiInvalidResponse'));
const finalCandidate = finalData.candidates[0];
let aiResponseText = finalCandidate.content.parts[0].text;
if (finalCandidate.groundingMetadata && finalCandidate.groundingMetadata.groundingChunks) {
aiResponseText += this.formatCitations(finalCandidate.groundingMetadata.groundingChunks);
} }
const data = await response.json(); this.removeTypingIndicator();
this.addMessage(aiResponseText, 'assistant');
if (!data.candidates || !data.candidates.length || !data.candidates[0].content || !data.candidates[0].content.parts) { this.conversationHistory.push({ role: 'model', parts: [{ text: aiResponseText }] });
console.error("Respuesta inesperada de la API:", data);
throw new Error(_('chatApiInvalidResponse'));
}
const candidate = data.candidates[0];
const part = candidate.content.parts[0];
if (part.functionCall) {
this.conversationHistory.push(candidate.content);
const toolCall = {
id: `call_${Date.now()}`,
function: {
name: part.functionCall.name,
arguments: part.functionCall.args,
},
};
const toolResult = await this.aiTools.executeTool(toolCall);
this.conversationHistory.push({
role: 'tool',
parts: [{
functionResponse: {
name: part.functionCall.name,
response: JSON.parse(toolResult)
}
}]
});
return await this.getAIResponseWithTools();
} else if (part.text) {
return part.text;
} else {
return _('chatApiNoTextResponse');
}
} catch (error) { } catch (error) {
console.error('Fallo en la llamada a la API de Google AI:', error); this.removeTypingIndicator();
console.error(_('googleApiFailure'), error);
const errorMessage = _('chatApiError') + `: ${error.message}`; const errorMessage = _('chatApiError') + `: ${error.message}`;
showNotification(errorMessage, 'error'); showNotification(errorMessage, 'error');
return errorMessage; this.addMessage(errorMessage, 'assistant', true);
} }
} }

View File

@ -1,5 +1,10 @@
export const config = { export let config = {
defaultApiKey: '4e44d9029b1270a757cddc766a1bcb63', defaultApiKey: '4e44d9029b1270a757cddc766a1bcb63',
lastFmApiKey: '1182c6cd210f9ac366127a6bbe63902a',
dbName: 'PlexDB', dbName: 'PlexDB',
dbVersion: 9, dbVersion: 9,
}; };
if (typeof chrome === 'undefined' || !chrome.storage) {
console.warn('Running outside of Chrome extension, using default config.');
}

View File

@ -4,6 +4,25 @@ import { showNotification, emitirEventoActualizacion, mostrarSpinner, ocultarSpi
export function initDB() { export function initDB() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (typeof chrome === 'undefined' || !chrome.storage) {
console.warn('IndexedDB not available, using mock DB.');
state.db = {
objectStoreNames: [],
transaction: () => ({
objectStore: () => ({
clear: () => ({
onsuccess: () => {},
onerror: () => {}
}),
put: () => ({
onsuccess: () => {},
onerror: () => {}
})
})
})
};
return resolve();
}
if (!window.indexedDB) { if (!window.indexedDB) {
showNotification(_("essentialFeaturesNotSupported"), "warning"); showNotification(_("essentialFeaturesNotSupported"), "warning");
return reject("IndexedDB not supported"); return reject("IndexedDB not supported");

View File

@ -1,5 +1,5 @@
import { state } from './state.js'; import { state } from './state.js';
import { switchView, resetView, showMainView, showItemDetails, showActorDetails, applyFilters, searchByActor, loadContent, toggleFavorite, addStreamToList, downloadM3U, showTrailer, closeTrailer, openSettingsModal, saveSettings, updateSectionTitle, generateStatistics, loadFavorites, loadLocalContent, phpScriptGenerator, initPhotosView, handlePhotoGridClick, handlePhotoTokenChange, showNextPhoto, showPrevPhoto, closePhotoLightbox, activateSettingsTab, deleteHistoryItem, clearAllHistory, getTrailerKey, initializeHeroSection } from './ui.js'; import { switchView, resetView, showMainView, showItemDetails, showActorDetails, applyFilters, searchByActor, loadContent, toggleFavorite, addStreamToList, downloadM3U, showTrailer, closeTrailer, saveSettings, updateSectionTitle, generateStatistics, loadFavorites, loadLocalContent, phpScriptGenerator, initPhotosView, handlePhotoGridClick, handlePhotoTokenChange, showNextPhoto, showPrevPhoto, closePhotoLightbox, activateSettingsTab, deleteHistoryItem, clearAllHistory, getTrailerKey, initializeHeroSection, downloadM3UForSeason, goBack } from './ui.js';
import { loadProviderContent, changeProviderPage, backToProviders } from './providers.js'; import { loadProviderContent, changeProviderPage, backToProviders } from './providers.js';
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';
@ -45,6 +45,14 @@ export function setupEventListeners() {
} }
}); });
document.querySelectorAll('#openMusicPlayerMobile, #openMusicPlayerDesktop').forEach(btn => {
btn.addEventListener('click', () => {
if (state.musicPlayer) {
state.musicPlayer.showPlayer();
}
});
});
document.getElementById('nav-movies').addEventListener('click', (e) => { e.preventDefault(); switchView('movies'); }); document.getElementById('nav-movies').addEventListener('click', (e) => { e.preventDefault(); switchView('movies'); });
document.getElementById('nav-series').addEventListener('click', (e) => { e.preventDefault(); switchView('series'); }); document.getElementById('nav-series').addEventListener('click', (e) => { e.preventDefault(); switchView('series'); });
document.getElementById('nav-providers').addEventListener('click', (e) => { e.preventDefault(); switchView('providers'); }); document.getElementById('nav-providers').addEventListener('click', (e) => { e.preventDefault(); switchView('providers'); });
@ -53,6 +61,7 @@ export function setupEventListeners() {
document.getElementById('nav-favorites').addEventListener('click', (e) => { e.preventDefault(); switchView('favorites'); }); document.getElementById('nav-favorites').addEventListener('click', (e) => { e.preventDefault(); switchView('favorites'); });
document.getElementById('nav-history').addEventListener('click', (e) => { e.preventDefault(); switchView('history'); }); document.getElementById('nav-history').addEventListener('click', (e) => { e.preventDefault(); switchView('history'); });
document.getElementById('nav-recommendations').addEventListener('click', (e) => { e.preventDefault(); switchView('recommendations'); }); document.getElementById('nav-recommendations').addEventListener('click', (e) => { e.preventDefault(); switchView('recommendations'); });
document.getElementById('nav-music').addEventListener('click', (e) => { e.preventDefault(); switchView('music'); });
document.getElementById('nav-m3u-generator').addEventListener('click', (e) => { e.preventDefault(); switchView('m3u-generator'); }); document.getElementById('nav-m3u-generator').addEventListener('click', (e) => { e.preventDefault(); switchView('m3u-generator'); });
document.getElementById('reset-view-btn').addEventListener('click', (e) => { e.preventDefault(); resetView(); }); document.getElementById('reset-view-btn').addEventListener('click', (e) => { e.preventDefault(); resetView(); });
@ -63,13 +72,6 @@ export function setupEventListeners() {
document.getElementById('footer-favorites').addEventListener('click', (e) => { e.preventDefault(); switchView('favorites'); }); document.getElementById('footer-favorites').addEventListener('click', (e) => { e.preventDefault(); switchView('favorites'); });
document.getElementById('activity-viewer-btn').addEventListener('click', () => state.activityViewer.show()); document.getElementById('activity-viewer-btn').addEventListener('click', () => state.activityViewer.show());
document.getElementById('load-more').addEventListener('click', () => {
if (!state.isLoading) {
state.currentPage++;
loadContent(true);
}
});
document.getElementById('search-input').addEventListener('keyup', debounce(async (e) => { document.getElementById('search-input').addEventListener('keyup', debounce(async (e) => {
const query = e.target.value.trim(); const query = e.target.value.trim();
@ -93,7 +95,7 @@ export function setupEventListeners() {
document.getElementById('genre-filter').addEventListener('change', applyFilters); document.getElementById('genre-filter').addEventListener('change', applyFilters);
document.getElementById('year-filter').addEventListener('change', applyFilters); document.getElementById('year-filter').addEventListener('change', applyFilters);
document.getElementById('sort-filter').addEventListener('change', applyFilters); document.getElementById('sort-filter').addEventListener('change', applyFilters);
// Filter Popover Logic
const durationBtn = document.getElementById('duration-filter-btn'); const durationBtn = document.getElementById('duration-filter-btn');
const scoreBtn = document.getElementById('score-filter-btn'); const scoreBtn = document.getElementById('score-filter-btn');
const durationPopover = document.getElementById('duration-popover'); const durationPopover = document.getElementById('duration-popover');
@ -104,11 +106,9 @@ export function setupEventListeners() {
button.addEventListener('click', (event) => { button.addEventListener('click', (event) => {
event.stopPropagation(); event.stopPropagation();
const isVisible = popover.style.display === 'block'; const isVisible = popover.style.display === 'block';
// Close all popovers
document.querySelectorAll('.filter-popover').forEach(p => { document.querySelectorAll('.filter-popover').forEach(p => {
if (p !== popover) p.style.display = 'none'; if (p !== popover) p.style.display = 'none';
}); });
// Toggle current popover
popover.style.display = isVisible ? 'none' : 'block'; popover.style.display = isVisible ? 'none' : 'block';
}); });
}; };
@ -116,14 +116,12 @@ export function setupEventListeners() {
setupPopover(durationBtn, durationPopover); setupPopover(durationBtn, durationPopover);
setupPopover(scoreBtn, scorePopover); setupPopover(scoreBtn, scorePopover);
// Close popovers when clicking outside
window.addEventListener('click', (event) => { window.addEventListener('click', (event) => {
if (!event.target.closest('.filter-popover') && !event.target.closest('#duration-filter-btn') && !event.target.closest('#score-filter-btn')) { if (!event.target.closest('.filter-popover') && !event.target.closest('#duration-filter-btn') && !event.target.closest('#score-filter-btn')) {
document.querySelectorAll('.filter-popover').forEach(p => p.style.display = 'none'); document.querySelectorAll('.filter-popover').forEach(p => p.style.display = 'none');
} }
}); });
// Range Slider Logic
function setupRangeSlider(minId, maxId, fillId, minValueId, maxValueId) { function setupRangeSlider(minId, maxId, fillId, minValueId, maxValueId) {
const minSlider = document.getElementById(minId); const minSlider = document.getElementById(minId);
const maxSlider = document.getElementById(maxId); const maxSlider = document.getElementById(maxId);
@ -162,7 +160,7 @@ export function setupEventListeners() {
minSlider.addEventListener('change', applyFilters); minSlider.addEventListener('change', applyFilters);
maxSlider.addEventListener('change', applyFilters); maxSlider.addEventListener('change', applyFilters);
updateFill(); // Initial call updateFill();
} }
setupRangeSlider('duration-min', 'duration-max', 'duration-fill', 'duration-min-value', 'duration-max-value'); setupRangeSlider('duration-min', 'duration-max', 'duration-fill', 'duration-min-value', 'duration-max-value');
@ -189,7 +187,7 @@ export function setupEventListeners() {
document.getElementById('main-view').addEventListener('click', handleMainViewClick); document.getElementById('main-view').addEventListener('click', handleMainViewClick);
document.getElementById('item-details-view').addEventListener('click', handleDetailsClick); document.getElementById('item-details-view').addEventListener('click', handleDetailsClick);
document.getElementById('settings-btn').addEventListener('click', openSettingsModal); document.getElementById('settings-btn').addEventListener('click', () => switchView('settings'));
document.getElementById('import-db-btn').addEventListener('click', async () => { document.getElementById('import-db-btn').addEventListener('click', async () => {
const input = document.createElement('input'); const input = document.createElement('input');
@ -199,7 +197,7 @@ export function setupEventListeners() {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
await importDatabase(file); await importDatabase(file);
bootstrap.Modal.getInstance(document.getElementById('settingsModal'))?.hide(); resetView();
} }
}; };
input.click(); input.click();
@ -211,7 +209,6 @@ export function setupEventListeners() {
.filter(v => v && v !== 'on'); .filter(v => v && v !== 'on');
if (selectedTypes.length > 0) { if (selectedTypes.length > 0) {
bootstrap.Modal.getInstance(document.getElementById('settingsModal')).hide();
document.getElementById('consoleOutputContainer').style.display = 'block'; document.getElementById('consoleOutputContainer').style.display = 'block';
document.getElementById('consoleOutput').style.display = 'block'; document.getElementById('consoleOutput').style.display = 'block';
startPlexScan(selectedTypes); startPlexScan(selectedTypes);
@ -228,12 +225,13 @@ export function setupEventListeners() {
document.getElementById('saveTokensBtn').addEventListener('click', saveTokensFromEditor); document.getElementById('saveTokensBtn').addEventListener('click', saveTokensFromEditor);
document.getElementById('exportDbBtn').addEventListener('click', exportDatabase); document.getElementById('exportDbBtn').addEventListener('click', exportDatabase);
document.getElementById('saveSettingsBtn').addEventListener('click', saveSettings); document.getElementById('saveSettingsBtn').addEventListener('click', async () => {
await saveSettings();
document.getElementById('settingsModal').addEventListener('shown.bs.modal', () => { goBack();
loadTokensToEditor();
activateSettingsTab('general');
}); });
document.getElementById('cancelSettingsBtn').addEventListener('click', goBack);
document.getElementById('updateAll').addEventListener('change', (e) => { document.getElementById('updateAll').addEventListener('change', (e) => {
document.querySelectorAll('#plex input[type="checkbox"]').forEach(cb => { document.querySelectorAll('#plex input[type="checkbox"]').forEach(cb => {
@ -341,7 +339,101 @@ export function setupEventListeners() {
const audioPlayer = document.getElementById('audioPlayer'); const audioPlayer = document.getElementById('audioPlayer');
audioPlayer.addEventListener('play', initializeEqualizer, { once: true }); audioPlayer.addEventListener('play', initializeEqualizer, { once: true });
document.getElementById('generatePhpScriptBtn').addEventListener('click', phpScriptGenerator.generatePhpScript);
document.getElementById('copyPhpScriptBtn').addEventListener('click', phpScriptGenerator.copyScript);
phpScriptGenerator.init(); phpScriptGenerator.init();
// Initialize Spatial Navigation
window.SpatialNavigation.init();
window.SpatialNavigation.makeFocusable();
// Check if the user agent indicates an Android TV device
const isAndroidTV = navigator.userAgent.toLowerCase().includes('android tv');
/**
* Determines the navigation direction based on the key event.
* @param {KeyboardEvent} event - The key event.
* @param {boolean} isAndroidTV - Whether the device is an Android TV.
* @returns {string|null} - The navigation direction ('up', 'down', 'left', 'right') or null if no direction key is pressed.
*/
function getDirectionFromKeyEvent(event, isAndroidTV) {
let direction = null;
if (isAndroidTV) {
// Android TV key codes
switch (event.keyCode) {
case 19: // Up
direction = 'up';
break;
case 20: // Down
direction = 'down';
break;
case 21: // Left
direction = 'left';
break;
case 22: // Right
direction = 'right';
break;
case 85: // Media Play/Pause
case 179: // Media Play/Pause
state.musicPlayer.togglePlayPause();
break;
case 88: // Media Stop
case 178: // Media Stop
//TODO: Implement stop for music player (no stop function available)
break;
case 90: // Media Previous
case 177: // Media Previous
state.musicPlayer.playPrevious();
break;
case 86: // Media Next
case 176: // Media Next
state.musicPlayer.playNext();
break;
}
} else {
// Standard arrow keys
switch (event.key) {
case 'ArrowUp':
direction = 'up';
break;
case 'ArrowDown':
direction = 'down';
break;
case 'ArrowLeft':
direction = 'left';
break;
case 'ArrowRight':
direction = 'right';
break;
}
}
return direction;
}
// Add a keydown event listener to handle spatial navigation
document.addEventListener('keydown', function(event) {
const direction = getDirectionFromKeyEvent(event, isAndroidTV);
if (direction) {
window.SpatialNavigation.move(direction);
}
});
// TODO: Further testing and configuration may be required on Android TV and other smart TV platforms.
// TODO: Add voice control for the Gemini assistant as a future enhancement.
window.addEventListener('scroll', debounce(async () => {
if (state.isLoading || state.currentPage >= state.totalPages) return;
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 200) {
state.currentPage++;
await loadContent(true);
}
}, 100));
} }
function handleMainViewClick(e) { function handleMainViewClick(e) {
@ -456,16 +548,23 @@ async function handleDetailsClick(e) {
} }
const addStreamBtn = e.target.closest('.play-btn'); const addStreamBtn = e.target.closest('.play-btn');
if (addStreamBtn) { if (addStreamBtn) {
const { title, type } = addStreamBtn.dataset; const { title, type, year } = addStreamBtn.dataset;
addStreamToList(title, type, addStreamBtn); addStreamToList(title, type, year, addStreamBtn);
return; return;
} }
const downloadM3uBtn = e.target.closest('.download-btn'); const downloadM3uBtn = e.target.closest('.download-btn');
if (downloadM3uBtn) { if (downloadM3uBtn) {
const { title, type } = downloadM3uBtn.dataset; const { title, type, year, serverName, sourceType } = downloadM3uBtn.dataset;
downloadM3U(title, type, downloadM3uBtn); downloadM3U([{ title, type, year, serverName, sourceType }], downloadM3uBtn, title);
return; return;
} }
}
const downloadSeasonBtn = e.target.closest('.download-season-btn');
if (downloadSeasonBtn) {
e.stopPropagation();
downloadM3UForSeason(downloadSeasonBtn);
return;
}
}

View File

@ -1,4 +1,16 @@
function localizeHtmlPage() { function localizeHtmlPage() {
if (typeof chrome === 'undefined' || !chrome.i18n) {
// Mock chrome.i18n API if not available (e.g., running in a regular browser)
const script = document.createElement('script');
script.src = 'js/i18n-mock.js';
script.onload = function() {
console.log('Loaded mock i18n API');
localizeHtmlPage(); // Re-run localization after mock is loaded
};
document.head.appendChild(script);
return; // Exit to prevent running without the mock
}
const i18nRegex = /__MSG_(\w+)__/g; const i18nRegex = /__MSG_(\w+)__/g;
function replaceMsg(match, p1) { function replaceMsg(match, p1) {
@ -34,7 +46,6 @@ function localizeHtmlPage() {
document.documentElement.lang = chrome.i18n.getUILanguage().split('-')[0]; document.documentElement.lang = chrome.i18n.getUILanguage().split('-')[0];
document.title = document.title.replace(i18nRegex, replaceMsg); document.title = document.title.replace(i18nRegex, replaceMsg);
document.body.classList.remove('unlocalized');
} }
document.addEventListener('DOMContentLoaded', localizeHtmlPage); document.addEventListener('DOMContentLoaded', localizeHtmlPage);

View File

@ -38,7 +38,7 @@ document.addEventListener('DOMContentLoaded', () => {
m3uServerSelect.appendChild(option); m3uServerSelect.appendChild(option);
}); });
} catch (error) { } catch (error) {
console.error('Error loading servers for M3U generator:', error); console.error(_('errorLoadingServersM3u'), error);
} }
} }
@ -106,8 +106,8 @@ document.addEventListener('DOMContentLoaded', () => {
gsap.from(checkboxes, { opacity: 0, y: 20, stagger: 0.05, duration: 0.3 }); gsap.from(checkboxes, { opacity: 0, y: 20, stagger: 0.05, duration: 0.3 });
downloadM3uBtn.disabled = m3uLibrariesContainer.querySelectorAll('input:checked').length === 0; downloadM3uBtn.disabled = m3uLibrariesContainer.querySelectorAll('input:checked').length === 0;
} catch (error) { } catch (error) {
console.error('Error fetching libraries:', error); console.error(_('errorFetchingLibraries'), error);
showNotification('Error fetching libraries.', 'error'); showNotification(_('errorFetchingLibraries'), 'error');
} finally { } finally {
m3uLibrariesLoader.style.display = 'none'; // Hide loader m3uLibrariesLoader.style.display = 'none'; // Hide loader
} }
@ -120,12 +120,12 @@ document.addEventListener('DOMContentLoaded', () => {
const selectedLibraries = Array.from(m3uLibrariesContainer.querySelectorAll('input:checked')).map(input => input.value); const selectedLibraries = Array.from(m3uLibrariesContainer.querySelectorAll('input:checked')).map(input => input.value);
if (!serverId || selectedLibraries.length === 0) { if (!serverId || selectedLibraries.length === 0) {
showNotification('Please select a server and at least one library.', 'error'); showNotification(_('selectServerAndLibrary'), 'error');
return; return;
} }
downloadM3uBtn.disabled = true; downloadM3uBtn.disabled = true;
downloadM3uBtn.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Generating...`; downloadM3uBtn.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ${_('generating')}`;
try { try {
const servers = await getServers(); const servers = await getServers();
@ -169,13 +169,13 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} catch (error) { } catch (error) {
hasErrors = true; hasErrors = true;
console.error(`Error processing library ${libraryKey}:`, error); console.error(`${_('errorProcessingLibrary')} ${libraryKey}:`, error);
showNotification(`Error processing library ${libraryKey}. Skipping.`, 'warning'); showNotification(`${_('errorProcessingLibrarySkipping')} ${libraryKey}.`, 'warning');
} }
} }
if (m3uContent.split('\n').length <= 2 && hasErrors) { if (m3uContent.split('\n').length <= 2 && hasErrors) {
throw new Error("All selected libraries failed to process."); throw new Error(_("allLibrariesFailed"));
} }
const blob = new Blob([m3uContent], { type: 'audio/x-mpegurl;charset=utf-8' }); const blob = new Blob([m3uContent], { type: 'audio/x-mpegurl;charset=utf-8' });
@ -189,13 +189,13 @@ document.addEventListener('DOMContentLoaded', () => {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
if (hasErrors) { if (hasErrors) {
showNotification('M3U generated with some errors. Some libraries may be missing.', 'warning'); showNotification(_('m3uGeneratedWithErrors'), 'warning');
} else { } else {
showNotification('M3U playlist downloaded successfully.', 'success'); showNotification(_('m3uDownloadedSuccess'), 'success');
} }
} catch (error) { } catch (error) {
console.error('Error generating M3U file:', error); console.error(_('errorGeneratingM3uFile'), error);
showNotification('Error generating M3U file.', 'error'); showNotification(_('errorGeneratingM3uFile'), 'error');
} finally { } finally {
downloadM3uBtn.disabled = false; downloadM3uBtn.disabled = false;
downloadM3uBtn.innerHTML = `<i class="fas fa-download"></i> __MSG_downloadM3u__`; downloadM3uBtn.innerHTML = `<i class="fas fa-download"></i> __MSG_downloadM3u__`;

36
js/m3u-utils.js Normal file
View File

@ -0,0 +1,36 @@
import { showNotification, _ } from './utils.js';
export async function generateAndDownloadSingleM3U(mediaItem) {
try {
if (!mediaItem || !mediaItem.streamUrl || !mediaItem.title) {
showNotification(_('invalidMediaItemForM3U'), 'error');
return;
}
const duration = mediaItem.duration ? Math.round(mediaItem.duration / 1000) : -1;
const tvgLogo = mediaItem.thumb ? `tvg-logo="${mediaItem.thumb}"` : '';
const groupTitle = mediaItem.seriesTitle && mediaItem.seasonNumber
? `group-title="${mediaItem.seriesTitle} - Season ${mediaItem.seasonNumber}"`
: `group-title="${mediaItem.title}'`; // Fallback to title for group-title
let m3uContent = '#EXTM3U\n';
m3uContent += `#EXTINF:${duration} ${tvgLogo} ${groupTitle},${mediaItem.title}\n`;
m3uContent += `${mediaItem.streamUrl}\n`;
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 = `${mediaItem.title}.m3u`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showNotification(_('m3uDownloadedSuccess'), 'success');
} catch (error) {
console.error(_('errorGeneratingM3uFile'), error);
showNotification(_('errorGeneratingM3uFile'), 'error');
}
}

View File

@ -10,6 +10,10 @@ import { loadInitialContent, initializeFavorites, initializeUserData, loadLocalC
import { showNotification, _ } from './utils.js'; import { showNotification, _ } from './utils.js';
async function loadSettings() { async function loadSettings() {
if (typeof chrome === 'undefined' || !chrome.storage) {
console.warn('Running outside of Chrome extension, skipping settings load from DB.');
return;
}
try { try {
const settingsData = await getFromDB('settings'); const settingsData = await getFromDB('settings');
if (settingsData && settingsData.length > 0) { if (settingsData && settingsData.length > 0) {
@ -50,18 +54,32 @@ async function loadSettings() {
} }
} }
function debugLog(message) {
const debugOutput = document.getElementById('debug-output');
if (debugOutput) {
const time = new Date().toLocaleTimeString();
debugOutput.innerHTML += `<div>[${time}] ${message}</div>`;
}
}
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
try { try {
await initDB(); await initDB();
await loadSettings(); await loadSettings();
applyTheme(state.settings.theme); applyTheme('dark');
applyHeroVisibility(state.settings.showHero); applyHeroVisibility(state.settings.showHero);
gsap.registerPlugin(ScrollTrigger); if (typeof gsap !== 'undefined') {
gsap.registerPlugin(ScrollTrigger);
} else {
console.warn('GSAP not available.');
}
state.musicPlayer = new MusicPlayer(); if (typeof chrome !== 'undefined' && chrome.storage) {
state.musicPlayer.setDB(state.db); state.musicPlayer = new MusicPlayer();
state.musicPlayer.setDB(state.db);
}
state.activityViewer = new ActivityViewer(document.getElementById('activityViewerModal')); state.activityViewer = new ActivityViewer(document.getElementById('activityViewerModal'));
state.chat = new Chat(); state.chat = new Chat();
@ -74,10 +92,18 @@ document.addEventListener('DOMContentLoaded', async () => {
setupEventListeners(); setupEventListeners();
initializeThirdPartyLibs(); initializeThirdPartyLibs();
document.body.classList.remove('unlocalized');
} catch (error) { } catch (error) {
console.error("Fatal Initialization failed:", error); console.error("--- FATAL INIT ERROR ---");
console.error("Error object:", error);
if (error) {
console.error("Error message:", error.message);
console.error("Error stack:", error.stack);
}
showNotification(_("fatalInitError"), "error"); showNotification(_("fatalInitError"), "error");
document.getElementById('main-container').innerHTML = `<div class="container text-center mt-5 pt-5"><h1 class="text-danger">${_("fatalInitError")}</h1><p>${_("fatalInitErrorSub")}</p></div>`; document.getElementById('main-container').innerHTML = `<div class="container text-center mt-5 pt-5"><h1 class="text-danger">${_("fatalInitError")}</h1><p>${_("fatalInitErrorSub")}</p></div>`;
document.body.classList.remove('unlocalized');
} }
}); });
@ -89,6 +115,10 @@ function initializeThirdPartyLibs() {
"retina_detect": true "retina_detect": true
}); });
} }
if (typeof chrome === 'undefined' || !chrome.storage) {
console.warn('Running outside of Chrome extension, skipping initializeThirdPartyLibs.');
return;
}
if (typeof ace !== 'undefined') { if (typeof ace !== 'undefined') {
try { try {

514
js/music.js Normal file
View File

@ -0,0 +1,514 @@
import { state } from './state.js';
import { debounce, _, showNotification } from './utils.js';
import { fetchArtistGenresFromLastFM } from './api.js';
import { getFromDB, addItemsToStore } from './db.js';
let musicSection, genreGrid, artistGrid, songListView, searchInput, serverFilter, loadMoreBtn, backBtn, genreSearchInput;
let isInitialized = false;
let isClassifying = false;
let currentPage = 0;
const ARTISTS_PER_PAGE = 20;
let currentMusicView = 'genres';
let selectedGenre = null;
let genreScrollPosition = 0;
export function initMusicView() {
if (typeof chrome === 'undefined' || !chrome.storage) {
console.warn('Running outside of Chrome extension, skipping initMusicView.');
return;
}
musicSection = document.getElementById('music-section');
if (!musicSection) return;
if (state.musicPlayer && state.musicPlayer.isReady) {
setupMusicView();
} else {
const initialGrid = document.getElementById('music-section-genre-grid');
if (initialGrid) initialGrid.innerHTML = `<div class="col-12 text-center mt-5"><div class="spinner"></div><p class="mt-2">${_('loading')}</p></div>`;
const checkReady = setInterval(() => {
if (state.musicPlayer && state.musicPlayer.isReady) {
clearInterval(checkReady);
setupMusicView();
}
}, 150);
}
}
async function setupMusicView() {
genreGrid = document.getElementById('music-section-genre-grid');
artistGrid = document.getElementById('music-section-artist-grid');
songListView = document.getElementById('music-section-song-list');
searchInput = document.getElementById('music-section-search-input');
serverFilter = document.getElementById('music-server-filter');
loadMoreBtn = document.getElementById('music-load-more-btn');
backBtn = document.getElementById('music-back-btn');
genreSearchInput = document.getElementById('genre-search-input');
if (!genreGrid || !artistGrid || !songListView || !searchInput || !serverFilter || !loadMoreBtn || !backBtn || !genreSearchInput) return;
if (!isInitialized) {
serverFilter.addEventListener('change', () => {
currentPage = 0;
renderArtists();
});
searchInput.addEventListener('input', debounce(() => {
currentPage = 0;
renderArtists();
}, 300));
genreSearchInput.addEventListener('input', debounce(() => {
renderGenres();
}, 300));
loadMoreBtn.addEventListener('click', () => {
currentPage++;
renderArtists(true);
});
backBtn.addEventListener('click', navigateBack);
genreGrid.addEventListener('click', handleGenreClick);
artistGrid.addEventListener('click', handleArtistClick);
isInitialized = true;
}
if (isClassifying) {
const overlay = document.getElementById('music-classification-overlay');
overlay.style.display = 'flex';
} else {
await checkAndClassifyArtists();
}
currentMusicView = 'genres';
selectedGenre = null;
await renderMusicView();
}
async function checkAndClassifyArtists() {
if (isClassifying) {
const overlay = document.getElementById('music-classification-overlay');
overlay.style.display = 'flex';
return;
}
const allServers = await getFromDB('artists');
let artistsToClassify = [];
allServers.forEach(server => {
if (server.titulos) {
server.titulos.forEach(artist => {
if (!artist.hasOwnProperty('genres')) {
artistsToClassify.push(artist);
}
});
}
});
if (artistsToClassify.length > 0) {
isClassifying = true;
const overlay = document.getElementById('music-classification-overlay');
overlay.style.display = 'flex';
const totalArtists = artistsToClassify.length;
try {
for (let i = 0; i < artistsToClassify.length; i += 5) {
const batch = artistsToClassify.slice(i, i + 5);
const promises = batch.map(artist =>
fetchArtistGenresFromLastFM(artist.title).then(genres => ({ artist, genres }))
);
await Promise.all(promises.map(p => p.then(({ artist, genres }) => {
artist.genres = genres.length > 0 ? genres : ['Varios'];
})));
const processedCount = Math.min(i + batch.length, totalArtists);
const percentage = Math.round((processedCount / totalArtists) * 100);
const nextArtist = artistsToClassify[processedCount];
updateClassificationProgress(percentage, processedCount, totalArtists, nextArtist ? nextArtist.title : null);
}
await addItemsToStore('artists', allServers);
await state.musicPlayer.loadMusicData();
showNotification('Clasificación de música completada.', 'success');
} catch (error) {
showNotification('Ocurrió un error durante la clasificación de artistas.', 'error');
} finally {
isClassifying = false;
gsap.to(overlay, {
autoAlpha: 0,
duration: 0.5,
onComplete: () => {
overlay.style.display = 'none';
}
});
}
}
}
function updateClassificationProgress(percentage, processedCount, totalArtists, nextArtistTitle) {
const progressFill = document.getElementById('classification-progress-fill');
const percentageText = document.getElementById('classification-percentage');
const progressDetails = document.getElementById('classification-progress-details');
const statusText = document.getElementById('classification-status-text');
if (progressFill) progressFill.style.width = `${percentage}%`;
if (percentageText) percentageText.textContent = `${percentage}%`;
if (progressDetails) progressDetails.textContent = `${processedCount} / ${totalArtists} artistas`;
if (statusText) {
if (nextArtistTitle) {
statusText.textContent = `Analizando: ${nextArtistTitle}...`;
} else {
statusText.textContent = `Finalizando...`;
}
}
}
async function renderMusicView() {
genreGrid.style.display = 'none';
artistGrid.style.display = 'none';
songListView.style.display = 'none';
serverFilter.style.display = 'none';
searchInput.parentElement.style.display = 'none';
genreSearchInput.parentElement.style.display = 'none';
backBtn.style.display = 'none';
loadMoreBtn.style.display = 'none';
switch (currentMusicView) {
case 'genres':
genreSearchInput.parentElement.style.display = 'flex';
await renderGenres();
break;
case 'artists':
backBtn.style.display = 'inline-flex';
serverFilter.style.display = 'block';
searchInput.parentElement.style.display = 'flex';
renderArtists();
break;
case 'songs':
break;
}
}
async function renderGenres() {
currentMusicView = 'genres';
genreGrid.style.display = 'grid';
genreGrid.innerHTML = '';
const allArtists = state.musicPlayer._generateFullArtistListForToken('all');
const genreSet = new Set();
allArtists.forEach(artist => {
if (artist.genres && artist.genres.length > 0) {
artist.genres.forEach(g => genreSet.add(g));
}
});
let sortedGenres = Array.from(genreSet).sort((a, b) => a.localeCompare(b));
const filter = genreSearchInput.value.toLowerCase();
if (filter) {
sortedGenres = sortedGenres.filter(genre => genre.toLowerCase().includes(filter));
}
if (sortedGenres.length === 0) {
genreGrid.innerHTML = `<div class="empty-state"><i class="fas fa-microphone-slash"></i><p>${_('noArtistsFound')}</p></div>`;
return;
}
const fragment = document.createDocumentFragment();
sortedGenres.forEach(genre => {
const card = document.createElement('div');
card.className = 'genre-card';
card.textContent = genre;
card.dataset.genre = genre;
fragment.appendChild(card);
});
genreGrid.appendChild(fragment);
}
function handleGenreClick(e) {
const card = e.target.closest('.genre-card');
if (card && card.dataset.genre) {
genreScrollPosition = window.scrollY;
window.scrollTo(0, 0);
selectedGenre = card.dataset.genre;
currentMusicView = 'artists';
currentPage = 0;
populateServerFilter();
renderMusicView();
}
}
async function navigateBack() {
if (currentMusicView === 'songs') {
currentMusicView = 'artists';
await renderMusicView();
} else if (currentMusicView === 'artists') {
selectedGenre = null;
currentMusicView = 'genres';
await renderMusicView();
requestAnimationFrame(() => {
window.scrollTo(0, genreScrollPosition);
});
}
}
function populateServerFilter() {
if (!serverFilter || !state.musicPlayer) return;
const currentVal = serverFilter.value;
serverFilter.innerHTML = `<option value="all">${_('musicAllServers')}</option>`;
state.musicPlayer.tokens.forEach(token => {
const option = document.createElement('option');
option.value = token.value;
option.textContent = token.name;
serverFilter.appendChild(option);
});
if (currentVal) serverFilter.value = currentVal;
}
function renderArtists(append = false) {
if (!artistGrid || !state.musicPlayer) return;
artistGrid.style.display = 'grid';
const musicPlayer = state.musicPlayer;
const selectedServer = serverFilter.value;
const filter = searchInput.value.toLowerCase();
let allArtists = musicPlayer._generateFullArtistListForToken(selectedServer);
if (selectedGenre) {
allArtists = allArtists.filter(artist => artist.genres && artist.genres.map(g => g.toLowerCase()).includes(selectedGenre.toLowerCase()));
}
const filteredArtists = filter
? allArtists.filter(artist => artist.title.toLowerCase().includes(filter))
: allArtists;
if (!append) {
artistGrid.innerHTML = '';
currentPage = 0;
}
if (filteredArtists.length === 0 && !append) {
artistGrid.innerHTML = `<div class="empty-state" style="grid-column: 1 / -1;"><i class="fas fa-microphone-slash"></i><p>${_('noArtistsFound')}</p></div>`;
loadMoreBtn.style.display = 'none';
return;
}
const startIndex = currentPage * ARTISTS_PER_PAGE;
const endIndex = startIndex + ARTISTS_PER_PAGE;
const artistsToRender = filteredArtists.slice(startIndex, endIndex);
const fragment = document.createDocumentFragment();
artistsToRender.forEach(artist => {
const card = createMusicCard(artist.title, artist.serverName, artist.thumb, artist.id, artist.isJellyfin, artist.token, artist.protocolo, artist.ip, artist.puerto);
fragment.appendChild(card);
});
artistGrid.appendChild(fragment);
if (endIndex < filteredArtists.length) {
loadMoreBtn.style.display = 'block';
} else {
loadMoreBtn.style.display = 'none';
}
if (musicPlayer) {
musicPlayer.markCurrentArtist();
}
}
async function handleArtistClick(e) {
const card = e.target.closest('.artist-card-spotify');
if (card && card.dataset.id) {
currentMusicView = 'songs';
const artistId = card.dataset.id;
const musicPlayer = state.musicPlayer;
if (musicPlayer) {
const fullList = musicPlayer._generateFullArtistListForToken(serverFilter.value);
const artistData = fullList.find(a => a.id == artistId);
if (artistData) {
const songs = await musicPlayer.getArtistSongs(artistData);
renderSongList(artistData, songs);
}
}
}
}
async function renderSongList(artistData, songs) {
artistGrid.style.display = 'none';
genreGrid.style.display = 'none';
searchInput.parentElement.style.display = 'none';
serverFilter.style.display = 'none';
loadMoreBtn.style.display = 'none';
backBtn.style.display = 'none';
songListView.style.display = 'block';
songListView.innerHTML = '';
let thumbUrl = 'img/no-profile.png';
if (artistData.thumb) {
if (artistData.isJellyfin) {
thumbUrl = artistData.thumb;
} else {
thumbUrl = `${artistData.protocolo}://${artistData.ip}:${artistData.puerto}${artistData.thumb}?X-Plex-Token=${artistData.token}`;
}
}
const header = document.createElement('div');
header.className = 'song-list-header-spotify';
header.innerHTML = `
<button id="backToArtistsViewBtn" class="btn-icon"><i class="fas fa-arrow-left"></i></button>
<img src="${thumbUrl}" class="artist-header-thumb-spotify" crossorigin="anonymous" onerror="this.onerror=null;this.src='img/no-profile.png';">
<div class="artist-header-info-spotify">
<span class="artist-header-type">${_('artist')}</span>
<h1>${artistData.title}</h1>
</div>
`;
songListView.appendChild(header);
const img = header.querySelector('.artist-header-thumb-spotify');
img.onload = () => {
if(img.src.includes('no-profile.png')) return;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const data = ctx.getImageData(0, 0, 1, 1).data;
const color = `rgba(${data[0]}, ${data[1]}, ${data[2]}, 0.5)`;
const darkColor = `rgba(${Math.round(data[0] * 0.3)}, ${Math.round(data[1] * 0.3)}, ${Math.round(data[2] * 0.3)}, 0.8)`;
header.style.background = `linear-gradient(135deg, ${color} 0%, ${darkColor} 100%)`;
};
const songListContainer = document.createElement('div');
songListContainer.className = 'song-list-container-spotify';
songListView.appendChild(songListContainer);
songListContainer.addEventListener('click', e => {
const row = e.target.closest('.song-grid-row');
if(row) {
const songId = row.dataset.id;
const originalIndex = songs.findIndex(s => s.id === songId);
if(originalIndex !== -1) {
state.musicPlayer.cancionesActuales = songs;
state.musicPlayer.playSong(originalIndex);
}
}
const albumPlayBtn = e.target.closest('.album-play-btn');
if(albumPlayBtn) {
const albumTitle = albumPlayBtn.dataset.album;
const albumSongs = songs.filter(s => s.album === albumTitle);
if(albumSongs.length > 0) {
state.musicPlayer.cancionesActuales = albumSongs;
state.musicPlayer.playSong(0);
}
}
});
if (songs.length === 0) {
songListContainer.innerHTML = `<div class="empty-state"><i class="fas fa-music"></i><p>${_('noSongsFound')}</p></div>`;
return;
}
const albums = songs.reduce((acc, song) => {
const albumTitle = song.album || 'Unknown Album';
if (!acc[albumTitle]) {
acc[albumTitle] = {
cover: song.cover,
songs: []
};
}
acc[albumTitle].songs.push(song);
return acc;
}, {});
for (const albumTitle in albums) {
const album = albums[albumTitle];
const albumContainer = document.createElement('div');
albumContainer.className = 'album-group-container-spotify';
albumContainer.innerHTML = `
<div class="album-group-header-spotify">
<img src="${album.cover || 'img/no-poster.png'}" class="album-group-cover-art-spotify" onerror="this.onerror=null;this.src='img/no-poster.png';">
<div class="album-info-spotify">
<h3 class="album-group-title-spotify">${albumTitle}</h3>
<span class="album-track-count">${album.songs.length} ${_('tracks')}</span>
</div>
<button class="album-play-btn" data-album="${albumTitle}">
<i class="fas fa-play"></i>
</button>
</div>
<div class="song-list-grid">
<div class="song-list-grid-header">
<div>#</div>
<div>${_('infoModalFieldTitle')}</div>
<div class="song-duration-header"><i class="far fa-clock"></i></div>
</div>
</div>
`;
const songsGrid = albumContainer.querySelector('.song-list-grid');
album.songs.forEach(song => {
const songRow = document.createElement('div');
songRow.className = 'song-grid-row';
songRow.dataset.id = song.id;
const originalIndex = songs.findIndex(s => s.id === song.id);
songRow.innerHTML = `
<div class="song-index">
<span class="track-number">${song.index || originalIndex + 1}</span>
<i class="fas fa-play play-icon-spotify"></i>
<div class="playing-indicator"><div></div><div></div><div></div></div>
</div>
<div class="song-title-artist">
<span class="song-title-spotify">${song.titulo}</span>
</div>
<div class="song-duration">${song.duration ? state.musicPlayer.formatTime(song.duration / 1000) : '--:--'}</div>
`;
songsGrid.appendChild(songRow);
});
songListContainer.appendChild(albumContainer);
}
document.getElementById('backToArtistsViewBtn').addEventListener('click', navigateBack);
if (state.musicPlayer) {
state.musicPlayer.markCurrentSong();
}
}
function createMusicCard(title, subtitle, imageUrl, artistId, isJellyfin, token, protocolo, ip, puerto) {
const card = document.createElement('div');
card.className = 'artist-card-spotify';
card.dataset.id = artistId;
let thumbUrl = 'img/no-profile.png';
if (imageUrl) {
if (isJellyfin === 'true' || isJellyfin === true) {
thumbUrl = imageUrl;
} else {
thumbUrl = `${protocolo}://${ip}:${puerto}${imageUrl}?X-Plex-Token=${token}`;
}
}
card.innerHTML = `
<div class="artist-card-img-container">
<img src="${thumbUrl}" class="artist-card-img" alt="${title}" loading="lazy" onerror="this.onerror=null;this.src='img/no-profile.png';">
<div class="artist-card-play-btn">
<i class="fas fa-play"></i>
</div>
</div>
<div class="artist-card-info">
<p class="artist-card-title-spotify">${title}</p>
<p class="artist-card-subtitle">${_('artist')}</p>
</div>
`;
return card;
}

View File

@ -82,15 +82,13 @@ export class MusicPlayer {
} }
setupEventHandlers() { setupEventHandlers() {
document.querySelectorAll('#openMusicPlayerMobile, #openMusicPlayerDesktop, #openMusicPlayerFromMiniplayer').forEach(btn => { document.getElementById('openMusicPlayerFromMiniplayer').addEventListener('click', () => this.togglePlayerVisibility());
btn.addEventListener('click', () => this.togglePlayerVisibility());
});
document.getElementById('closeSideNavBtn').addEventListener('click', () => this.hidePlayer()); document.getElementById('closeSideNavBtn').addEventListener('click', () => this.hidePlayer());
document.getElementById('closeMiniplayerBtn').addEventListener('click', () => this.closeMiniplayer()); document.getElementById('closeMiniplayerBtn').addEventListener('click', () => this.closeMiniplayer());
document.getElementById('fab-music-player').addEventListener('click', () => this.openMiniplayer()); document.getElementById('fab-music-player').addEventListener('click', () => this.openMiniplayer());
document.getElementById('searchArtist').addEventListener("input", debounce(() => this.filterArtists(), 300)); document.getElementById('searchArtist').addEventListener("input", debounce(() => this.filterArtists(), 300));
document.getElementById('searchSong').addEventListener("input", debounce(() => this.filterSongs(), 300)); document.getElementById('searchSong').addEventListener("input", debounce(() => this.filterSongs(), 300));
document.getElementById('backBtn').addEventListener('click', () => this.showArtistList()); document.getElementById('backBtnSidenav').addEventListener('click', () => this.showArtistList());
document.getElementById('playPauseBtn').addEventListener('click', () => this.togglePlayPause()); document.getElementById('playPauseBtn').addEventListener('click', () => this.togglePlayPause());
document.getElementById('nextBtn').addEventListener('click', () => this.playNext()); document.getElementById('nextBtn').addEventListener('click', () => this.playNext());
document.getElementById('prevBtn').addEventListener('click', () => this.playPrevious()); document.getElementById('prevBtn').addEventListener('click', () => this.playPrevious());
@ -358,6 +356,7 @@ export class MusicPlayer {
id: artista.id, id: artista.id,
title: artista.title || 'Artista Desconocido', title: artista.title || 'Artista Desconocido',
thumb: artista.thumb, thumb: artista.thumb,
genres: artista.genres,
isJellyfin: servidor.isJellyfin || false, isJellyfin: servidor.isJellyfin || false,
serverName: servidor.serverName || 'Servidor Desconocido', serverName: servidor.serverName || 'Servidor Desconocido',
}; };
@ -780,21 +779,21 @@ export class MusicPlayer {
markCurrentSong() { markCurrentSong() {
if (!this.isReady) return; if (!this.isReady) return;
document.querySelectorAll(".song-item").forEach(item => { document.querySelectorAll(".song-item, .song-grid-row").forEach(item => {
item.classList.remove("current-song"); item.classList.remove("current-song");
}); });
if (this.currentSongId !== null) { if (this.currentSongId !== null) {
const songItem = document.querySelector(`.song-item[data-id='${this.currentSongId}']`); const songItems = document.querySelectorAll(`.song-item[data-id='${this.currentSongId}'], .song-grid-row[data-id='${this.currentSongId}']`);
if (songItem) { songItems.forEach(item => {
songItem.classList.add("current-song"); item.classList.add("current-song");
} });
} }
} }
markCurrentArtist() { markCurrentArtist() {
if (!this.isReady) return; if (!this.isReady) return;
const targetArtistId = this.currentSongArtistId; const targetArtistId = this.currentSongArtistId;
document.querySelectorAll(".artist-card").forEach(card => { document.querySelectorAll(".artist-card, .artist-card-spotify").forEach(card => {
if (targetArtistId != null && card.dataset.id == targetArtistId) { if (targetArtistId != null && card.dataset.id == targetArtistId) {
card.classList.add("current-artist"); card.classList.add("current-artist");
} else { } else {

View File

@ -44,7 +44,10 @@ export async function fetchLibraries(accessToken, publicUrl, localUrl) {
export async function fetchLibraryContents(accessToken, publicUrl, localUrl, libraryKey, libraryType, timeout = 7000) { export async function fetchLibraryContents(accessToken, publicUrl, localUrl, libraryKey, libraryType, timeout = 7000) {
const endpoint = libraryType === 'show' ? 'allLeaves' : 'all'; const endpoint = libraryType === 'show' ? 'allLeaves' : 'all';
const url = `${localUrl || publicUrl}/library/sections/${libraryKey}/${endpoint}?X-Plex-Token=${accessToken}`; let url = `${localUrl || publicUrl}/library/sections/${libraryKey}/${endpoint}?X-Plex-Token=${accessToken}`;
if (libraryType === 'show') {
url = `${localUrl || publicUrl}/library/sections/${libraryKey}/all?X-Plex-Token=${accessToken}`;
}
const contentXml = await fetchSectionContent(url, new AbortController().signal, timeout); const contentXml = await fetchSectionContent(url, new AbortController().signal, timeout);
const items = []; const items = [];
const baseUrl = localUrl || publicUrl; const baseUrl = localUrl || publicUrl;
@ -74,23 +77,26 @@ export async function fetchLibraryContents(accessToken, publicUrl, localUrl, lib
}); });
}; };
processItems('Video', 'video'); // Handles both movies and episodes processItems('Video', 'video');
processItems('Track', 'music'); // Handles music processItems('Track', 'music');
return items; return items;
} }
async function parseAndStoreSectionItems(contentXml, storeName, serverData) { async function parseAndStoreSectionItems(contentXml, storeName, serverData, allShowsMetadataXml = null) {
const type = storeName === 'movies' ? 'movie' : (storeName === 'series' ? 'show' : (storeName === 'artists' ? 'artist' : 'photo')); const type = storeName === 'movies' ? 'movie' : (storeName === 'series' ? 'show' : (storeName === 'artists' ? 'artist' : 'photo'));
let items; let items = [];
if (type === 'movie' || type === 'show') { if (type === 'movie') {
const itemSelector = type === 'movie' ? 'Video' : 'Directory'; const itemElements = Array.from(contentXml.querySelectorAll('Video'));
items = Array.from(contentXml.querySelectorAll(itemSelector)).map(el => { items = itemElements.map(el => {
const media = el.querySelector('Media'); const media = el?.querySelector('Media');
const part = media?.querySelector('Part'); const part = media?.querySelector('Part');
const container = part?.getAttribute('container'); const container = part?.getAttribute('container');
const videoResolution = media?.getAttribute('videoResolution'); const videoResolution = media?.getAttribute('videoResolution');
const size = part?.getAttribute('size');
const audioStream = media?.querySelector('Stream[streamType="2"]');
const audioLanguage = audioStream?.getAttribute('language') || audioStream?.getAttribute('languageTag');
return { return {
id: el.getAttribute('ratingKey'), id: el.getAttribute('ratingKey'),
@ -99,7 +105,29 @@ async function parseAndStoreSectionItems(contentXml, storeName, serverData) {
genre: el.querySelector('Genre')?.getAttribute('tag') || _('noGenre'), genre: el.querySelector('Genre')?.getAttribute('tag') || _('noGenre'),
type: type, type: type,
container: container, container: container,
resolution: videoResolution resolution: videoResolution,
size: size,
audioLanguage: audioLanguage
};
});
} else if (type === 'show' && allShowsMetadataXml) {
const itemElements = Array.from(allShowsMetadataXml.querySelectorAll('Directory[type="show"]'));
items = itemElements.map(el => {
const seasonElements = Array.from(el.querySelectorAll('Directory[type="season"]'));
const seasons = seasonElements.map(seasonEl => ({
id: seasonEl.getAttribute('ratingKey'),
index: seasonEl.getAttribute('index'),
title: seasonEl.getAttribute('title'),
episodeCount: seasonEl.getAttribute('leafCount')
}));
return {
id: el.getAttribute('ratingKey'),
title: el.getAttribute('title'),
year: el.getAttribute('year'),
genre: el.querySelector('Genre')?.getAttribute('tag') || _('noGenre'),
type: type,
seasons: seasons,
}; };
}); });
} else if (type === 'artist' || type === 'photo') { } else if (type === 'artist' || type === 'photo') {
@ -164,10 +192,21 @@ async function processServer(device, token, tipos, signal) {
const sectionTitle = dir.getAttribute('title'); const sectionTitle = dir.getAttribute('title');
logToConsole(` Procesando sección: "${sectionTitle}" (Tipo: ${type})`); logToConsole(` Procesando sección: "${sectionTitle}" (Tipo: ${type})`);
const contentUrl = `${protocol}://${address}:${port}/library/sections/${sectionId}/all?X-Plex-Token=${accessToken}`; const contentUrl = `${protocol}://${address}:${port}/library/sections/${sectionId}/all?X-Plex-Token=${accessToken}`;
try { try {
const contentXml = await fetchSectionContent(contentUrl, signal); const contentXml = await fetchSectionContent(contentUrl, signal);
const serverData = { ...connectionData, sectionTitle }; const serverData = { ...connectionData, sectionTitle };
await parseAndStoreSectionItems(contentXml, storeName, serverData); let allShowsMetadataXml = null;
if (type === 'show') {
const showKeys = Array.from(contentXml.querySelectorAll('Directory[type="show"]')).map(d => d.getAttribute('ratingKey')).join(',');
if (showKeys) {
const metadataUrl = `${protocol}://${address}:${port}/library/metadata/${showKeys}?includeChildren=1&X-Plex-Token=${accessToken}`;
allShowsMetadataXml = await fetchSectionContent(metadataUrl, signal, 20000);
}
}
await parseAndStoreSectionItems(contentXml, storeName, serverData, allShowsMetadataXml);
} catch (e) { } catch (e) {
if (e instanceof TimeoutError) { if (e instanceof TimeoutError) {
logToConsole(` [REINTENTO PENDIENTE] La sección "${sectionTitle}" tardó demasiado en responder.`); logToConsole(` [REINTENTO PENDIENTE] La sección "${sectionTitle}" tardó demasiado en responder.`);
@ -203,8 +242,28 @@ async function processRetryQueue(signal) {
const { url, storeName, serverData } = item; const { url, storeName, serverData } = item;
logToConsole(_('retyingSection', serverData.sectionTitle)); logToConsole(_('retyingSection', serverData.sectionTitle));
try { try {
// Fetch the initial content (list of items)
const contentXml = await fetchSectionContent(url, signal, 30000); const contentXml = await fetchSectionContent(url, signal, 30000);
await parseAndStoreSectionItems(contentXml, storeName, serverData); let allShowsMetadataXml = null;
// *** INICIO DE LA CORRECCIÓN ***
// Si la sección es de series, necesitamos hacer la segunda petición para obtener los metadatos detallados
if (storeName === 'series') {
const showKeys = Array.from(contentXml.querySelectorAll('Directory[type="show"]'))
.map(d => d.getAttribute('ratingKey'))
.join(',');
if (showKeys) {
const { protocolo, ip, puerto, token } = serverData;
const metadataUrl = `${protocolo}://${ip}:${puerto}/library/metadata/${showKeys}?includeChildren=1&X-Plex-Token=${token}`;
logToConsole(` Reintentando obtener metadatos de series para "${serverData.sectionTitle}"...`);
// Aumentamos el timeout para esta petición que puede ser más pesada
allShowsMetadataXml = await fetchSectionContent(metadataUrl, signal, 45000);
}
}
// *** FIN DE LA CORRECCIÓN ***
// Pasamos el XML de metadatos (si existe) a la función de parseo
await parseAndStoreSectionItems(contentXml, storeName, serverData, allShowsMetadataXml);
logToConsole(_('retrySuccess', serverData.sectionTitle)); logToConsole(_('retrySuccess', serverData.sectionTitle));
} catch (e) { } catch (e) {
logToConsole(_('retryError', [serverData.sectionTitle, e.message])); logToConsole(_('retryError', [serverData.sectionTitle, e.message]));
@ -214,7 +273,12 @@ async function processRetryQueue(signal) {
logToConsole(_('retryPhaseFinished')); logToConsole(_('retryPhaseFinished'));
} }
export async function startPlexScan(tipos) { export async function startPlexScan(tipos) {
if (typeof chrome === 'undefined' || !chrome.storage) {
console.warn('Running outside of Chrome extension, skipping startPlexScan.');
return;
}
if (state.isScanningPlex) { if (state.isScanningPlex) {
showNotification(_('plexScanInProgress'), "warning"); showNotification(_('plexScanInProgress'), "warning");
return; return;
@ -305,4 +369,4 @@ export async function addPlexToken(token) {
} catch (error) { } catch (error) {
showNotification(_('errorAddingToken', error.message), 'error'); showNotification(_('errorAddingToken', error.message), 'error');
} }
} }

View File

@ -19,6 +19,10 @@ export async function getAvailableProviders(type, region) {
} }
export async function fetchAllProviders(region) { export async function fetchAllProviders(region) {
if (typeof chrome === 'undefined' || !chrome.storage) {
console.warn('Running outside of Chrome extension, skipping fetchAllProviders.');
return [];
}
const [movieProviders, tvProviders] = await Promise.all([ const [movieProviders, tvProviders] = await Promise.all([
getAvailableProviders('movie', region), getAvailableProviders('movie', region),
getAvailableProviders('tv', region) getAvailableProviders('tv', region)
@ -33,6 +37,10 @@ export async function fetchAllProviders(region) {
} }
export async function getRegions() { export async function getRegions() {
if (typeof chrome === 'undefined' || !chrome.storage) {
console.warn('Running outside of Chrome extension, skipping getRegions.');
return [];
}
try { try {
const regions = await fetchTMDB('watch/providers/regions'); const regions = await fetchTMDB('watch/providers/regions');
return regions.results || []; return regions.results || [];

View File

@ -2,7 +2,9 @@ import { config } from './config.js';
export const state = { export const state = {
currentPage: 1, currentPage: 1,
totalPages: 1,
currentView: 'movies', currentView: 'movies',
viewStack: ['home'],
currentParams: { contentType: 'movie', page: 1, query: '', genre: '', sort: 'popularity.desc', year: '' }, currentParams: { contentType: 'movie', page: 1, query: '', genre: '', sort: 'popularity.desc', year: '' },
settings: { settings: {
id: 'user_settings', id: 'user_settings',
@ -63,4 +65,5 @@ export const state = {
currentPhotoLightboxIndex: 0, currentPhotoLightboxIndex: 0,
providerContentPage: 1, providerContentPage: 1,
providerContentTotalPages: 1, providerContentTotalPages: 1,
aiTriggeredDetails: false,
}; };

609
js/ui.js
View File

@ -1,11 +1,110 @@
import { state } from './state.js'; import { state } from './state.js';
import { fetchTMDB, fetchAllAvailableStreams } from './api.js'; import { fetchTMDB, fetchAllAvailableStreams } from './api.js';
import { showNotification, getRelativeTime, fetchWithTimeout, _ } from './utils.js'; import { showNotification, getRelativeTime, fetchWithTimeout, _, formatBytes } from './utils.js';
import { getFromDB, addItemsToStore } from './db.js'; import { getFromDB, addItemsToStore, loadTokensToEditor } from './db.js';
import { getAvailableProviders, renderProviders, getRegions, fetchAllProviders, resetProvidersView } from './providers.js'; import { getAvailableProviders, renderProviders, getRegions, fetchAllProviders, resetProvidersView } from './providers.js';
import { initMusicView } from './music.js';
let charts = {}; let charts = {};
function getLocalMovieDetails(title, year) {
if (!title || !year) return null;
const normalizedTitle = title.toLowerCase().trim().replace(/\s+/g, ' ');
const yearStr = String(year);
const allMovies = [...state.localMovies, ...state.jellyfinMovies];
for (const server of allMovies) {
if (server && Array.isArray(server.titulos)) {
const found = server.titulos.find(item =>
item.title.toLowerCase().trim().replace(/\s+/g, ' ') === normalizedTitle &&
item.year == yearStr
);
if (found) return found;
}
}
return null;
}
function getLocalFilesForMovie(title, year) {
const matchingFiles = [];
if (!title) return matchingFiles;
const normalizedTitle = title.toLowerCase().trim().replace(/\s+/g, ' ');
const yearStr = String(year);
const sources = [...state.localMovies, ...state.jellyfinMovies];
sources.forEach(server => {
if (server && Array.isArray(server.titulos)) {
server.titulos.forEach(item => {
if (item.title.toLowerCase().trim().replace(/\s+/g, ' ') === normalizedTitle && item.year == yearStr) {
matchingFiles.push({
...item,
serverName: server.serverName || server.nombre,
});
}
});
}
});
return matchingFiles;
}
function renderLocalFilesList(container, movies) {
if (!container) return;
if (!movies || movies.length === 0) {
container.innerHTML = `<p class="text-muted text-center">${_('noLocalFilesFound')}</p>`;
return;
}
const resolutionOrder = { '4k': 5, '2160p': 5, '1440p': 4, '1080p': 3, '720p': 2, '480p': 1, 'sd': 0 };
const getResolutionValue = (resolution) => {
if (!resolution) return -1;
const res = resolution.toLowerCase();
for (const key in resolutionOrder) {
if (res.includes(key)) {
return resolutionOrder[key];
}
}
const parsed = parseInt(res);
if (!isNaN(parsed)) {
if (parsed >= 2160) return 5;
if (parsed >= 1440) return 4;
if (parsed >= 1080) return 3;
if (parsed >= 720) return 2;
if (parsed >= 480) return 1;
}
return -1;
};
movies.sort((a, b) => {
const resA = getResolutionValue(a.resolution);
const resB = getResolutionValue(b.resolution);
return resB - resA;
});
let html = '<div class="local-files-list"><table>';
html += `<thead><tr><th>${_('server')}</th><th>${_('title')}</th><th>${_('year')}</th><th>${_('resolution')}</th><th>${_('size')}</th><th>${_('container')}</th><th>${_('action')}</th></tr></thead>`;
html += '<tbody>';
movies.forEach(movie => {
html += `
<tr>
<td>${movie.serverName || 'N/A'}</td>
<td>${movie.title}</td>
<td>${movie.year || 'N/A'}</td>
<td>${movie.resolution || 'N/A'}</td>
<td>${movie.size ? formatBytes(movie.size) : 'N/A'}</td>
<td>${movie.container || 'N/A'}</td>
<td>
<button class="btn btn-sm btn-primary download-btn" data-title="${movie.title}" data-type="movie" data-year="${movie.year}" data-server-name="${movie.serverName}">
<i class="fas fa-link me-1"></i> ${_('generate')}
</button>
</td>
</tr>
`;
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
export async function loadInitialContent() { export async function loadInitialContent() {
await Promise.all([loadGenres(), loadYears(), loadRegions()]); await Promise.all([loadGenres(), loadYears(), loadRegions()]);
resetView(); resetView();
@ -13,6 +112,10 @@ export async function loadInitialContent() {
} }
async function loadRegions() { async function loadRegions() {
if (typeof chrome === 'undefined' || !chrome.storage) {
console.warn('Running outside of Chrome extension, skipping loadRegions.');
return;
}
const select = document.getElementById('region-filter'); const select = document.getElementById('region-filter');
if (!select) return; if (!select) return;
select.innerHTML = `<option value="">${_('loadingRegions')}</option>`; select.innerHTML = `<option value="">${_('loadingRegions')}</option>`;
@ -43,15 +146,19 @@ export function initializeUserData() {
const savedPrefs = localStorage.getItem('cineplex_userPreferences'); const savedPrefs = localStorage.getItem('cineplex_userPreferences');
state.userPreferences = savedPrefs ? JSON.parse(savedPrefs) : { genres: {}, keywords: {}, ratings: [], cast: {}, crew: {} }; state.userPreferences = savedPrefs ? JSON.parse(savedPrefs) : { genres: {}, keywords: {}, ratings: [], cast: {}, crew: {} };
} catch { } catch {
state.userPreferences = { genres: {}, keywords: {}, ratings: {}, cast: {}, crew: {} }; state.userPreferences = { genres: {}, keywords: {}, ratings: [], cast: {}, crew: {} };
} }
} }
export async function loadLocalContent() { export async function loadLocalContent() {
if (typeof chrome === 'undefined' || !chrome.storage) {
console.warn('Running outside of Chrome extension, skipping loadLocalContent.');
return;
}
if (!state.db) return; if (!state.db) return;
try { try {
const [movies, series, artists, photos, jfMovies, jfSeries] = await Promise.all([ const [movies, series, artists, photos, jfMovies, jfSeries] = await Promise.all([
getFromDB('movies'), getFromDB('movies'),
getFromDB('series'), getFromDB('series'),
getFromDB('artists'), getFromDB('artists'),
getFromDB('photos'), getFromDB('photos'),
@ -115,6 +222,8 @@ export function resetView() {
document.getElementById('photos-section').style.display = 'none'; document.getElementById('photos-section').style.display = 'none';
document.getElementById('providers-section').style.display = 'none'; document.getElementById('providers-section').style.display = 'none';
document.getElementById('m3u-generator-section').style.display = 'none'; document.getElementById('m3u-generator-section').style.display = 'none';
document.getElementById('music-section').style.display = 'none';
document.getElementById('settings-section').style.display = 'none';
if (heroSection) { if (heroSection) {
if (state.settings.showHero) { if (state.settings.showHero) {
@ -136,6 +245,7 @@ export function resetView() {
gsap.set(heroBg1, { backgroundImage: 'url(img/hero-def.png)', autoAlpha: 1, scale: 1 }); gsap.set(heroBg1, { backgroundImage: 'url(img/hero-def.png)', autoAlpha: 1, scale: 1 });
gsap.set(heroBg2, { autoAlpha: 0 }); gsap.set(heroBg2, { autoAlpha: 0 });
gsap.set(heroContent, { autoAlpha: 1 });
heroSection.classList.add('no-overlay'); heroSection.classList.add('no-overlay');
initializeHeroSection(); initializeHeroSection();
@ -146,6 +256,7 @@ export function resetView() {
} }
state.currentView = 'home'; state.currentView = 'home';
state.viewStack = ['home'];
updateActiveNav('home'); updateActiveNav('home');
updateSectionTitle(); updateSectionTitle();
} }
@ -153,6 +264,19 @@ export function resetView() {
export function switchView(viewType) { export function switchView(viewType) {
if (state.isLoading) return; if (state.isLoading) return;
if (viewType === 'home') {
resetView();
return;
}
if (viewType !== state.currentView) {
if (viewType === 'settings') {
state.viewStack.push(viewType);
} else {
state.viewStack = [viewType];
}
}
resetProvidersView(); resetProvidersView();
const heroSection = document.getElementById('hero-section'); const heroSection = document.getElementById('hero-section');
@ -200,7 +324,7 @@ export function switchView(viewType) {
} }
state.lastScrollPosition = 0; state.lastScrollPosition = 0;
const allSections = ['content-section', 'stats-section', 'history-section', 'recommendations-section', 'photos-section', 'providers-section', 'm3u-generator-section']; const allSections = ['content-section', 'stats-section', 'history-section', 'recommendations-section', 'photos-section', 'providers-section', 'm3u-generator-section', 'music-section', 'settings-section'];
allSections.forEach(id => { allSections.forEach(id => {
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) el.style.display = 'none'; if (el) el.style.display = 'none';
@ -229,7 +353,7 @@ export function switchView(viewType) {
const contentSection = document.getElementById('content-section'); const contentSection = document.getElementById('content-section');
switch(viewType) { switch(viewType) {
case 'movies': case 'movies':
case 'series': case 'series':
case 'search': case 'search':
@ -266,10 +390,18 @@ export function switchView(viewType) {
case 'providers': case 'providers':
if(providersSection) providersSection.style.display = 'block'; if(providersSection) providersSection.style.display = 'block';
break; break;
case 'music':
const musicSection = document.getElementById('music-section');
if(musicSection) musicSection.style.display = 'block';
break;
case 'm3u-generator': case 'm3u-generator':
const m3uSection = document.getElementById('m3u-generator-section'); const m3uSection = document.getElementById('m3u-generator-section');
if(m3uSection) m3uSection.style.display = 'block'; if(m3uSection) m3uSection.style.display = 'block';
break; break;
case 'settings':
const settingsSection = document.getElementById('settings-section');
if(settingsSection) settingsSection.style.display = 'block';
break;
} }
updateActiveNav(viewType); updateActiveNav(viewType);
@ -277,7 +409,6 @@ export function switchView(viewType) {
window.scrollTo({ top: targetScrollTop, behavior: 'auto' }); window.scrollTo({ top: targetScrollTop, behavior: 'auto' });
// This is the second part of the fix: don't call loadContent for 'search'
switch(viewType) { switch(viewType) {
case 'movies': case 'movies':
case 'series': case 'series':
@ -303,7 +434,12 @@ export function switchView(viewType) {
break; break;
case 'search': case 'search':
case 'm3u-generator': case 'm3u-generator':
// Do nothing, the respective event listeners will trigger the content load. break;
case 'music':
initMusicView();
break;
case 'settings':
initSettingsView();
break; break;
} }
@ -313,7 +449,46 @@ export function switchView(viewType) {
} }
} }
function updateActiveNav(activeView) { function initSettingsView() {
document.getElementById('tmdbApiKey').value = state.settings.apiKey;
document.getElementById('googleApiKey').value = state.settings.googleApiKey || '';
document.getElementById('phpScriptUrl').value = state.settings.phpScriptUrl || '';
document.getElementById('lightModeToggle').checked = state.settings.theme === 'light';
document.getElementById('showHeroToggle').checked = state.settings.showHero;
document.getElementById('jellyfinServerUrl').value = state.jellyfinSettings.url || '';
document.getElementById('jellyfinUsername').value = state.jellyfinSettings.username || '';
document.getElementById('jellyfinPassword').value = state.jellyfinSettings.password || '';
document.getElementById('phpSecretKeyCheck').checked = state.settings.phpUseSecretKey;
document.getElementById('phpSecretKey').value = state.settings.phpSecretKey || '';
document.getElementById('phpSavePath').value = state.settings.phpSavePath || '';
document.getElementById('phpFilename').value = state.settings.phpFilename || 'CinePlex_Playlist.m3u';
document.getElementById('phpFileActionAppend').checked = state.settings.phpFileAction === 'append';
document.getElementById('phpFileActionOverwrite').checked = state.settings.phpFileAction === 'overwrite';
const navContainer = document.querySelector('.settings-nav');
if(navContainer && !navContainer.dataset.listenerAttached) {
navContainer.addEventListener('click', (e) => {
const navItem = e.target.closest('.nav-item');
if (navItem) {
e.preventDefault();
const tabId = navItem.dataset.tab;
activateSettingsTab(tabId);
}
});
navContainer.dataset.listenerAttached = 'true';
}
document.querySelectorAll('.settings-content .tab-pane').forEach(pane => {
pane.classList.remove('fade', 'show');
});
activateSettingsTab('general');
loadTokensToEditor();
}
export function updateActiveNav(activeView) {
document.querySelectorAll('.nav-link, .footer-link').forEach(link => link.classList.remove('active')); document.querySelectorAll('.nav-link, .footer-link').forEach(link => link.classList.remove('active'));
if (activeView === 'home') return; if (activeView === 'home') return;
let navId = (activeView === 'search') ? (state.currentParams.contentType === 'movie' ? 'movies' : 'series') : activeView; let navId = (activeView === 'search') ? (state.currentParams.contentType === 'movie' ? 'movies' : 'series') : activeView;
@ -492,9 +667,19 @@ export async function loadContent(append = false) {
if (state.currentParams.maxDuration) endpoint += `&with_runtime.lte=${state.currentParams.maxDuration}`; if (state.currentParams.maxDuration) endpoint += `&with_runtime.lte=${state.currentParams.maxDuration}`;
} }
const data = await fetchTMDB(endpoint, {}, signal); let data = await fetchTMDB(endpoint, {}, signal);
state.totalPages = data.total_pages;
if (type === 'series') {
const localSeries = await getFromDB('series');
if (localSeries && localSeries.length > 0) {
const localSeriesTitles = localSeries.flatMap(s => s.titulos).map(t => t.title);
data.results = data.results.filter(item => localSeriesTitles.includes(item.name));
} else {
data.results = [];
}
}
renderGrid(data.results, append); renderGrid(data.results, append);
if (loadMoreButton) loadMoreButton.style.display = (data.page < data.total_pages) ? 'block' : 'none'; if (loadMoreButton) loadMoreButton.style.display = 'none';
if (!append) setupScrollEffects(); if (!append) setupScrollEffects();
} catch (error) { } catch (error) {
@ -510,10 +695,10 @@ export async function loadContent(append = false) {
} }
} }
function isContentAvailableLocally(title, type, year) { export function isContentAvailableLocally(title, type, year) {
if (!title || !type) return false; if (!title || !type) return false;
const normalize = (str) => str.toLowerCase().trim().replace(/\s+/g, ' '); const normalize = (str) => str ? str.toLowerCase().trim().replace(/\s+/g, ' ') : '';
const normalizedTitle = normalize(title); const normalizedTitle = normalize(title);
const yearKey = year ? String(year).slice(0, 4) : 'any'; const yearKey = year ? String(year).slice(0, 4) : 'any';
@ -560,6 +745,7 @@ export function renderGrid(items, append = false, gridId = 'content-grid', anima
card.dataset.id = item.id; card.dataset.id = item.id;
card.dataset.type = 'person'; card.dataset.type = 'person';
card.dataset.name = item.name; card.dataset.name = item.name;
card.dataset.snFocusable = "true";
card.innerHTML = ` card.innerHTML = `
<img src="${item.profile_path ? `https://image.tmdb.org/t/p/w500${item.profile_path}` : 'img/no-profile.png'}" class="item-poster" loading="lazy" alt="${item.name}"> <img src="${item.profile_path ? `https://image.tmdb.org/t/p/w500${item.profile_path}` : 'img/no-profile.png'}" class="item-poster" loading="lazy" alt="${item.name}">
<div class="item-overlay"> <div class="item-overlay">
@ -595,24 +781,26 @@ export function renderGrid(items, append = false, gridId = 'content-grid', anima
card.dataset.id = item.id; card.dataset.id = item.id;
card.dataset.type = finalItemType; card.dataset.type = finalItemType;
card.dataset.year = year; card.dataset.year = year;
card.dataset.snFocusable = "true";
card.innerHTML = ` card.innerHTML = `
<img src="${posterPath}" class="item-poster" loading="lazy" alt="${title}"> <img src="${posterPath}" class="item-poster" loading="lazy" alt="${title}">
${voteAvg >= 7.8 ? '<span class="badge top-badge">TOP</span>' : ''} ${voteAvg >= 7.8 ? '<span class="badge top-badge">TOP</span>' : ''}
${isAvailable ? `<span class="badge available-badge"><i class="fas fa-check-circle"></i> ${_('local')}</span>` : ''} ${isAvailable ? `<span class="badge available-badge"><i class="fas fa-check-circle"></i> ${_('local')}</span>` : ''}
<div class="item-overlay"> <div class="item-overlay">
<div class="item-actions"> <div class="item-actions">
<button class="action-btn info-btn" title="${_('moreInfo')}"><i class="fas fa-info-circle fa-lg"></i></button> <button class="action-btn info-btn" title="${_('moreInfo')}" data-sn-focusable="true"><i class="fas fa-info-circle fa-lg"></i></button>
<button class="action-btn favorites-btn ${isFavorite ? 'active' : ''}" title="${isFavorite ? _('removeFromFavorites') : _('addToFavorites')}"><i class="fas ${isFavorite ? 'fa-heart-broken' : 'fa-heart'} fa-lg"></i></button> <button class="action-btn favorites-btn ${isFavorite ? 'active' : ''}" title="${isFavorite ? _('removeFromFavorites') : _('addToFavorites')}" data-sn-focusable="true"><i class="fas ${isFavorite ? 'fa-heart-broken' : 'fa-heart'} fa-lg"></i></button>
${isAvailable ? ` ${isAvailable ? `
<button class="action-btn download-btn" data-title="${title}" data-type="${finalItemType}" data-year="${year}" title="${_('miniplayerDownloadAlbum')}"><i class="fas fa-download fa-lg"></i></button> <button class="action-btn download-btn" data-title="${title}" data-type="${finalItemType}" data-year="${year}" title="${_('miniplayerDownloadAlbum')}" data-sn-focusable="true"><i class="fas fa-download fa-lg"></i></button>
<button class="action-btn play-btn" data-title="${title}" data-type="${finalItemType}" data-year="${year}" title="${_('addStream')}"><i class="fas fa-plus-circle fa-lg"></i></button> <button class="action-btn play-btn" data-title="${title}" data-type="${finalItemType}" data-year="${year}" title="${_('addStream')}" data-sn-focusable="true"><i class="fas fa-plus-circle fa-lg"></i></button>
` : `<button class="action-btn disabled-btn" disabled title="${_('notAvailable')}"><i class="fas fa-times-circle fa-lg text-muted"></i></button>`} ` : `<button class="action-btn disabled-btn" disabled title="${_('notAvailable')}" data-sn-focusable="false"><i class="fas fa-times-circle fa-lg text-muted"></i></button>`}
</div> </div>
</div> </div>
<div class="item-info"> <div class="item-info">
<h3 class="item-title" title="${title}">${title}</h3> <h3 class="item-title" title="${title}">${title}</h3>
<div class="item-meta"> <div class="item-meta">
<span><i class="fas fa-calendar-alt me-1"></i>${year}</span> <span><i class="fas fa-calendar-alt me-1"></i>${year}</span>
${!isMovie && item.number_of_seasons ? `<span><i class="fas fa-tv me-1"></i>${_('seasonsCount', String(item.number_of_seasons))}</span>` : ''}
${voteAvg !== 'N/A' ? `<span class="item-rating ms-2 ${ratingClass}"><i class="fas fa-star me-1"></i> ${voteAvg}</span>` : ''} ${voteAvg !== 'N/A' ? `<span class="item-rating ms-2 ${ratingClass}"><i class="fas fa-star me-1"></i> ${voteAvg}</span>` : ''}
</div> </div>
</div> </div>
@ -632,6 +820,19 @@ export function showMainView() {
if (!detailsView || !mainView || !detailsContent) return; if (!detailsView || !mainView || !detailsContent) return;
if (state.aiTriggeredDetails) {
detailsView.classList.remove('active');
detailsView.style.display = 'none';
document.body.classList.remove('details-view-active');
mainView.style.display = 'block';
mainView.style.opacity = '1';
window.scrollTo({ top: state.lastScrollPosition, behavior: 'auto' });
state.currentItemId = null;
state.currentItemType = null;
state.aiTriggeredDetails = false;
return;
}
if (!state.lastClickedCardElement) { if (!state.lastClickedCardElement) {
detailsView.classList.remove('active'); detailsView.classList.remove('active');
document.body.classList.remove('details-view-active'); document.body.classList.remove('details-view-active');
@ -748,7 +949,7 @@ export async function showItemDetails(itemId, contentType) {
if (!isMovie && item.seasons && item.seasons.length > 0) { if (!isMovie && item.seasons && item.seasons.length > 0) {
const seasonPromises = item.seasons const seasonPromises = item.seasons
.filter(s => s.season_number > 0) .filter(s => s.season_number > 0)
.map(s => fetchTMDB(`${contentType}/${itemId}/season/${s.season_number}`).catch(() => null)); .map(s => fetchTMDB(`${contentType}/${itemId}/season/${s.season_number}?append_to_response=images`).catch(() => null));
const seasonsData = await Promise.all(seasonPromises); const seasonsData = await Promise.all(seasonPromises);
item.seasons_with_episodes = seasonsData.filter(s => s !== null); item.seasons_with_episodes = seasonsData.filter(s => s !== null);
} }
@ -763,6 +964,37 @@ export async function showItemDetails(itemId, contentType) {
} }
} }
const findAndAggregateLocalSeries = (seriesTitleToFind, seriesYearToFind) => {
const normalizedTitle = seriesTitleToFind.toLowerCase().trim().replace(/\s+/g, ' ');
const allSeriesSources = [...state.localSeries, ...state.jellyfinSeries];
let aggregatedSeries = null;
for (const server of allSeriesSources) {
if (server && Array.isArray(server.titulos)) {
const found = server.titulos.find(s =>
s.title.toLowerCase().trim().replace(/\s+/g, ' ') === normalizedTitle &&
s.year == seriesYearToFind
);
if (found) {
if (!aggregatedSeries) {
aggregatedSeries = { ...found, seasons: [] };
}
if (found.seasons) {
aggregatedSeries.seasons.push(...found.seasons);
}
}
}
}
if (aggregatedSeries && aggregatedSeries.seasons.length > 0) {
const uniqueSeasons = [...new Map(aggregatedSeries.seasons.map(s => [s.index, s])).values()];
aggregatedSeries.seasons = uniqueSeasons;
}
return aggregatedSeries;
};
async function renderItemDetails(item) { async function renderItemDetails(item) {
const detailsContent = document.getElementById('item-details-content'); const detailsContent = document.getElementById('item-details-content');
if (!detailsContent) return; if (!detailsContent) return;
@ -777,14 +1009,18 @@ 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 = isContentAvailableLocally(title, state.currentItemType); const isAvailable = isContentAvailableLocally(title, state.currentItemType, year);
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 localDetails = isAvailable ? getLocalMovieDetails(title, year) : null;
const imdbId = item.external_ids?.imdb_id; const imdbId = item.external_ids?.imdb_id;
const crew = (isMovie ? item.credits?.crew : item.aggregate_credits?.crew) || []; const crew = (isMovie ? item.credits?.crew : item.aggregate_credits?.crew) || [];
const director = crew.find(c => c.job === 'Director'); const director = crew.find(c => c.job === 'Director');
const writer = crew.find(c => c.job === 'Screenplay' || c.job === 'Writer' || c.job === 'Story'); const writer = crew.find(c => c.job === 'Screenplay' || c.job === 'Writer' || c.job === 'Story');
const localSeriesData = isMovie ? null : findAndAggregateLocalSeries(title, year);
const localSeasonNumbers = new Set((localSeriesData?.seasons || []).map(s => Number(s.index)));
detailsContent.innerHTML = ` detailsContent.innerHTML = `
${backdropPath ? `<div class="details-backdrop-container"><img src="${backdropPath}" class="details-backdrop-img"><div class="details-backdrop-overlay"></div></div>` : ''} ${backdropPath ? `<div class="details-backdrop-container"><img src="${backdropPath}" class="details-backdrop-img"><div class="details-backdrop-overlay"></div></div>` : ''}
<div class="item-details-container"> <div class="item-details-container">
@ -798,6 +1034,10 @@ async function renderItemDetails(item) {
<span class="item-details-meta-item"><i class="fas fa-calendar-alt"></i> ${year}</span> <span class="item-details-meta-item"><i class="fas fa-calendar-alt"></i> ${year}</span>
${isMovie && item.runtime ? `<span class="item-details-meta-item"><i class="fas fa-clock"></i> ${_('runtimeMinutes', String(item.runtime))}</span>` : ''} ${isMovie && item.runtime ? `<span class="item-details-meta-item"><i class="fas fa-clock"></i> ${_('runtimeMinutes', String(item.runtime))}</span>` : ''}
${!isMovie && item.number_of_seasons ? `<span class="item-details-meta-item"><i class="fas fa-tv"></i> ${_('seasonsCount', String(item.number_of_seasons))}</span>` : ''} ${!isMovie && item.number_of_seasons ? `<span class="item-details-meta-item"><i class="fas fa-tv"></i> ${_('seasonsCount', String(item.number_of_seasons))}</span>` : ''}
${localDetails?.resolution ? `<span class="item-details-meta-item"><i class="fas fa-film"></i> ${localDetails.resolution}</span>` : ''}
${localDetails?.container ? `<span class="item-details-meta-item"><i class="fas fa-box-open"></i> ${localDetails.container}</span>` : ''}
${localDetails?.audioLanguage ? `<span class="item-details-meta-item"><i class="fas fa-volume-up"></i> ${localDetails.audioLanguage.toUpperCase()}</span>` : ''}
${localDetails?.size ? `<span class="item-details-meta-item"><i class="fas fa-hdd"></i> ${formatBytes(localDetails.size)}</span>` : ''}
</div> </div>
<div class="item-details-genres mb-3">${genres.map(g => `<span class="genre-badge">${g.name}</span>`).join('')}</div> <div class="item-details-genres mb-3">${genres.map(g => `<span class="genre-badge">${g.name}</span>`).join('')}</div>
<div class="item-details-crew"> <div class="item-details-crew">
@ -812,13 +1052,35 @@ async function renderItemDetails(item) {
${trailer ? `<button class="btn btn-outline-light trailer-btn me-2" data-trailer-key="${trailer.key}"><i class="fab fa-youtube me-1"></i> ${_('watchTrailer')}</button>` : ''} ${trailer ? `<button class="btn btn-outline-light trailer-btn me-2" data-trailer-key="${trailer.key}"><i class="fab fa-youtube me-1"></i> ${_('watchTrailer')}</button>` : ''}
<button class="btn ${isFavorite ? 'btn-danger' : 'btn-outline-danger'} favorites-btn me-2" data-id="${item.id}" data-type="${state.currentItemType}"><i class="fas ${isFavorite ? 'fa-heart-broken' : 'fa-heart'} me-1"></i> ${isFavorite ? _('removeFromFavorites') : _('addToFavorites')}</button> <button class="btn ${isFavorite ? 'btn-danger' : 'btn-outline-danger'} favorites-btn me-2" data-id="${item.id}" data-type="${state.currentItemType}"><i class="fas ${isFavorite ? 'fa-heart-broken' : 'fa-heart'} me-1"></i> ${isFavorite ? _('removeFromFavorites') : _('addToFavorites')}</button>
${isAvailable ? ` ${isAvailable ? `
<button class="btn btn-success play-btn me-2" data-title="${title}" data-type="${state.currentItemType}"><i class="fas fa-plus-circle me-1"></i> ${_('addStream')}</button> <button class="btn btn-success play-btn me-2" data-title="${title}" data-type="${state.currentItemType}" data-year="${year}"><i class="fas fa-plus-circle me-1"></i> ${_('addStream')}</button>
<button class="btn btn-info download-btn" data-title="${title}" data-type="${state.currentItemType}"><i class="fas fa-download me-1"></i> ${_('miniplayerDownloadAlbum')}</button> <button class="btn btn-info download-btn" data-title="${title}" data-type="${state.currentItemType}" data-year="${year}"><i class="fas fa-download me-1"></i> ${_('miniplayerDownloadAlbum')}</button>
` : `<button class="btn btn-secondary" disabled><i class="fas fa-times-circle me-1"></i> ${_('notAvailable')}</button>`} ` : `<button class="btn btn-secondary" disabled><i class="fas fa-times-circle me-1"></i> ${_('notAvailable')}</button>`}
</div> </div>
</div> </div>
</div> </div>
${isMovie ? `
<div class="item-details-section">
<h3 class="section-subtitle">${_('availableLocalFiles')}</h3>
<div id="local-files-list-container"></div>
</div>
` : ''}
${!isMovie && item.seasons_with_episodes && item.seasons_with_episodes.length > 0 ? `
<div class="item-details-section">
<h3 class="section-subtitle">${_('seasonsAndEpisodes')}</h3>
<div class="seasons-tabs" id="seasons-navigator">
<div class="season-tabs-list">
${item.seasons_with_episodes.map((season, index) => {
const isSeasonAvailable = localSeasonNumbers.has(season.season_number);
return `<button class="season-tab-btn ${isSeasonAvailable ? 'is-local' : ''}" data-season-index="${index}">${season.name}</button>`;
}).join('')}
</div>
<div class="episodes-container"></div>
</div>
</div>
` : ''}
<div class="item-details-section"> <div class="item-details-section">
${(isMovie ? item.credits?.cast?.length > 0 : item.aggregate_credits?.cast?.length > 0) ? ` ${(isMovie ? item.credits?.cast?.length > 0 : item.aggregate_credits?.cast?.length > 0) ? `
<h3 class="section-subtitle">${_('mainCast')}</h3> <h3 class="section-subtitle">${_('mainCast')}</h3>
@ -832,49 +1094,6 @@ async function renderItemDetails(item) {
`).join('')} `).join('')}
</div>` : ''} </div>` : ''}
</div> </div>
${!isMovie && item.seasons_with_episodes && item.seasons_with_episodes.length > 0 ? `
<div class="item-details-section">
<h3 class="section-subtitle">${_('seasonsAndEpisodes')}</h3>
<div class="accordion seasons-accordion" id="seasonsAccordion">
${item.seasons_with_episodes.map((season, index) => `
<div class="accordion-item">
<h2 class="accordion-header" id="heading-season-${season.id}">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-season-${season.id}" aria-expanded="false" aria-controls="collapse-season-${season.id}">
<div class="season-info">
<img src="${season.poster_path ? `https://image.tmdb.org/t/p/w200${season.poster_path}` : posterPath}" class="season-poster">
<div class="season-details">
<span class="season-title">${season.name}</span>
<div class="season-meta">
<span><i class="fas fa-calendar-alt"></i> ${season.air_date ? new Date(season.air_date).getFullYear() : 'N/A'}</span>
<span><i class="fas fa-list-ol"></i> ${_('episodesCount', String(season.episodes.length))}</span>
</div>
<p class="season-overview d-none d-md-block">${(season.overview || '').substring(0,120)}${season.overview && season.overview.length > 120 ? '...' : ''}</p>
</div>
</div>
</button>
</h2>
<div id="collapse-season-${season.id}" class="accordion-collapse collapse" aria-labelledby="heading-season-${season.id}" data-bs-parent="#seasonsAccordion">
<div class="accordion-body season-episodes">
${season.episodes.map(ep => `
<div class="episode-card">
<span class="episode-number">${ep.episode_number}</span>
<div class="episode-info">
<h5 class="episode-title">${ep.name}</h5>
<div class="episode-meta">
<span><i class="far fa-calendar-alt"></i> ${ep.air_date || 'N/A'}</span>
<span><i class="far fa-star"></i> ${ep.vote_average.toFixed(1)}/10</span>
</div>
<p class="episode-overview">${ep.overview || _('noSynopsis')}</p>
</div>
</div>
`).join('')}
</div>
</div>
</div>
`).join('')}
</div>
</div>
` : ''}
<div class="item-details-section"> <div class="item-details-section">
${item.combined_credits && item.combined_credits.cast && item.combined_credits.cast.length > 0 ? ` ${item.combined_credits && item.combined_credits.cast && item.combined_credits.cast.length > 0 ? `
@ -907,6 +1126,104 @@ async function renderItemDetails(item) {
</div> </div>
</div> </div>
`; `;
if (isMovie) {
const matchingFiles = getLocalFilesForMovie(title, year);
renderLocalFilesList(document.getElementById('local-files-list-container'), matchingFiles);
}
if (!isMovie && item.seasons_with_episodes && item.seasons_with_episodes.length > 0) {
setupSeasonNavigator(item, localSeriesData, title, year, posterPath);
}
}
function renderSeasonDetails(season, localSeriesData, seriesTitle, seriesYear, seriesPosterPath) {
const contentDiv = document.querySelector('.episodes-container');
if (!contentDiv) return;
const localSeasonNumbers = new Set((localSeriesData?.seasons || []).map(s => Number(s.index)));
const isSeasonAvailable = localSeasonNumbers.has(season.season_number);
contentDiv.innerHTML = `
<div class="episodes-content-header">
<img src="${season.poster_path ? `https://image.tmdb.org/t/p/w200${season.poster_path}` : seriesPosterPath}" class="season-poster">
<div class="episode-content-details">
<h4 class="season-title">${season.name}</h4>
<div class="season-meta">
<span><i class="fas fa-calendar-alt"></i> ${season.air_date ? new Date(season.air_date).getFullYear() : 'N/A'}</span>
<span><i class="fas fa-list-ol"></i> ${_('episodesCount', String(season.episodes.length))}</span>
</div>
<p class="season-overview">${season.overview || ''}</p>
</div>
${isSeasonAvailable ? `
<div class="season-header-actions">
<span class="badge season-local-badge"><i class="fas fa-check-circle"></i> ${_('local')}</span>
<button class="download-season-btn" title="${_('downloadSeason')} ${season.season_number}" data-series-title="${seriesTitle}" data-season-number="${season.season_number}" data-year="${seriesYear}">
<i class="fas fa-download"></i>
</button>
</div>
` : ''}
</div>
<div class="episodes-list">
${season.episodes.map(ep => `
<div class="episode-item">
<img class="episode-item-image" src="${ep.still_path ? `https://image.tmdb.org/t/p/w300${ep.still_path}` : 'img/no-poster.png'}">
<div class="episode-item-content">
<div class="episode-item-header">
<span class="episode-item-number">${ep.episode_number}</span>
<h5 class="episode-item-title">${ep.name}</h5>
</div>
<div class="episode-item-meta">
<span><i class="far fa-calendar-alt"></i> ${ep.air_date || 'N/A'}</span>
<span><i class="far fa-star"></i> ${ep.vote_average.toFixed(1)}/10</span>
</div>
<div class="episode-item-overview">
<p>${ep.overview || _('noSynopsis')}</p>
</div>
</div>
</div>
`).join('')}
</div>
`;
contentDiv.querySelectorAll('.episode-item').forEach(item => {
item.addEventListener('click', () => {
item.classList.toggle('expanded');
});
});
}
function setupSeasonNavigator(item, localSeriesData, seriesTitle, seriesYear, seriesPosterPath) {
const navigator = document.getElementById('seasons-navigator');
if (!navigator) return;
const seasonTabs = navigator.querySelectorAll('.season-tab-btn');
const selectSeason = (index) => {
seasonTabs.forEach((tab, i) => {
tab.classList.toggle('active', i === index);
});
const seasonData = item.seasons_with_episodes[index];
renderSeasonDetails(seasonData, localSeriesData, seriesTitle, seriesYear, seriesPosterPath);
};
seasonTabs.forEach((tab, index) => {
tab.addEventListener('click', () => selectSeason(index));
});
let initialSeasonIndex = 0;
if (localSeriesData && localSeriesData.seasons) {
const firstLocalSeasonIndex = item.seasons_with_episodes.findIndex(s_tmdb =>
localSeriesData.seasons.some(s_local => Number(s_local.index) === s_tmdb.season_number)
);
if (firstLocalSeasonIndex !== -1) {
initialSeasonIndex = firstLocalSeasonIndex;
}
}
if (seasonTabs.length > 0) {
selectSeason(initialSeasonIndex);
}
} }
function renderWatchProviders(item) { function renderWatchProviders(item) {
@ -1375,6 +1692,10 @@ function animateValue(id, start, end, duration) {
} }
export async function initializeHeroSection() { export async function initializeHeroSection() {
if (typeof chrome === 'undefined' || !chrome.storage) {
console.warn('Running outside of Chrome extension, skipping initializeHeroSection.');
return;
}
const heroSection = document.getElementById('hero-section'); const heroSection = document.getElementById('hero-section');
if (!heroSection || heroSection.style.display === 'none' || !state.settings.showHero) return; if (!heroSection || heroSection.style.display === 'none' || !state.settings.showHero) return;
@ -1389,21 +1710,21 @@ export async function initializeHeroSection() {
const bg1 = document.querySelector('.hero-background-1'); const bg1 = document.querySelector('.hero-background-1');
const bg2 = document.querySelector('.hero-background-2'); const bg2 = document.querySelector('.hero-background-2');
const content = document.querySelector('.hero-content'); const heroContent = document.querySelector('.hero-content');
if (!bg1 || !bg2 || !content) return; if (!bg1 || !bg2 || !heroContent) return;
const heroButtons = content.querySelector('.hero-buttons'); const heroButtons = heroContent.querySelector('.hero-buttons');
content.querySelector('.hero-title').textContent = _('welcomeToCinePlex'); heroContent.querySelector('.hero-title').textContent = _('welcomeToCinePlex');
content.querySelector('.hero-subtitle').textContent = _('welcomeSubtitle'); heroContent.querySelector('.hero-subtitle').textContent = _('welcomeSubtitle');
content.querySelector('#hero-rating').innerHTML = ''; heroContent.querySelector('#hero-rating').innerHTML = '';
content.querySelector('#hero-year').innerHTML = ''; heroContent.querySelector('#hero-year').innerHTML = '';
content.querySelector('#hero-extra').innerHTML = ''; heroContent.querySelector('#hero-extra').innerHTML = '';
if (heroButtons) heroButtons.style.display = 'none'; if (heroButtons) heroButtons.style.display = 'none';
heroSection.classList.add('no-overlay'); heroSection.classList.add('no-overlay');
gsap.set(bg1, { backgroundImage: `url(img/hero-def.png)`, autoAlpha: 1, scale: 1 }); gsap.set(bg1, { backgroundImage: `url(img/hero-def.png)`, autoAlpha: 1, scale: 1 });
gsap.set(bg2, { autoAlpha: 0 }); gsap.set(bg2, { autoAlpha: 0 });
gsap.set(content, { autoAlpha: 1 }); gsap.set(heroContent, { autoAlpha: 1 });
heroSection.classList.remove('loading'); heroSection.classList.remove('loading');
state.heroLoadTimeoutId = setTimeout(() => { state.heroLoadTimeoutId = setTimeout(() => {
@ -1438,10 +1759,10 @@ export async function initializeHeroSection() {
updateHeroContent(item); updateHeroContent(item);
const heroElements = [ const heroElements = [
content.querySelector('.hero-title'), heroContent.querySelector('.hero-title'),
content.querySelector('.hero-subtitle'), heroContent.querySelector('.hero-subtitle'),
...content.querySelectorAll('.hero-meta-item'), ...heroContent.querySelectorAll('.hero-meta-item'),
content.querySelector('.hero-buttons') heroContent.querySelector('.hero-buttons')
]; ];
const tl = gsap.timeline({ const tl = gsap.timeline({
@ -1578,7 +1899,7 @@ export async function downloadM3U(items, buttonElement = null, customFilename =
for (const item of itemsArray) { for (const item of itemsArray) {
try { try {
showNotification(_('searchingStreams', item.title), 'info'); showNotification(_('searchingStreams', item.title), 'info');
const streamData = await fetchAllAvailableStreams(item.title, item.type, item.year); const streamData = await fetchAllAvailableStreams(item.title, item.type, item.year, item.seasonNumber, item.serverName, item.sourceType);
if (streamData.success && streamData.streams.length > 0) { if (streamData.success && streamData.streams.length > 0) {
streamsFound += streamData.streams.length; streamsFound += streamData.streams.length;
streamData.streams.forEach(stream => { streamData.streams.forEach(stream => {
@ -1602,7 +1923,7 @@ export async function downloadM3U(items, buttonElement = null, customFilename =
showNotification(_('m3uDownloaded', collectiveTitle), 'success'); showNotification(_('m3uDownloaded', collectiveTitle), 'success');
} catch (error) { } catch (error) {
showNotification(_('errorGeneratingM3U', error.message), "error"); showNotification(_('errorGeneratingM3U', error.message), "error");
} // The missing closing brace was here. Added it below. }
finally { finally {
state.isDownloadingM3U = false; state.isDownloadingM3U = false;
if (buttonElement && originalButtonContent) { if (buttonElement && originalButtonContent) {
@ -1648,26 +1969,18 @@ function setupScrollEffects() {
} }
export function activateSettingsTab(tabId) { export function activateSettingsTab(tabId) {
const tabButtons = document.querySelectorAll('#settingsTabs .nav-link'); const navContainer = document.querySelector('.settings-nav');
const tabPanes = document.querySelectorAll('#settingsTabsContent .tab-pane'); const contentContainer = document.querySelector('.settings-content');
if (!navContainer || !contentContainer) return;
tabButtons.forEach(button => { const activeNavItem = navContainer.querySelector(`.nav-item[data-tab="${tabId}"]`);
if (button.id === `${tabId}-tab`) { const activePane = contentContainer.querySelector(`.tab-pane[id="${tabId}"]`);
button.classList.add('active');
button.setAttribute('aria-selected', 'true');
} else {
button.classList.remove('active');
button.setAttribute('aria-selected', 'false');
}
});
tabPanes.forEach(pane => { navContainer.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
if (pane.id === tabId) { contentContainer.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active'));
pane.classList.add('show', 'active');
} else { if (activeNavItem) activeNavItem.classList.add('active');
pane.classList.remove('show', 'active'); if (activePane) activePane.classList.add('active');
}
});
} }
export function openSettingsModal() { export function openSettingsModal() {
@ -1688,6 +2001,23 @@ export function openSettingsModal() {
document.getElementById('phpFileActionAppend').checked = state.settings.phpFileAction === 'append'; document.getElementById('phpFileActionAppend').checked = state.settings.phpFileAction === 'append';
document.getElementById('phpFileActionOverwrite').checked = state.settings.phpFileAction === 'overwrite'; document.getElementById('phpFileActionOverwrite').checked = state.settings.phpFileAction === 'overwrite';
const navContainer = document.querySelector('.settings-nav');
if(navContainer && !navContainer.dataset.listenerAttached) {
navContainer.addEventListener('click', (e) => {
const navItem = e.target.closest('.nav-item');
if (navItem) {
e.preventDefault();
const tabId = navItem.dataset.tab;
activateSettingsTab(tabId);
}
});
navContainer.dataset.listenerAttached = 'true';
}
document.querySelectorAll('.settings-content .tab-pane').forEach(pane => {
pane.classList.remove('fade', 'show');
});
activateSettingsTab('general'); activateSettingsTab('general');
const modal = new bootstrap.Modal(document.getElementById('settingsModal')); const modal = new bootstrap.Modal(document.getElementById('settingsModal'));
@ -1742,6 +2072,16 @@ export async function saveSettings() {
} }
} }
export function goBack() {
if (state.viewStack.length > 1) {
state.viewStack.pop();
const previousView = state.viewStack[state.viewStack.length - 1];
switchView(previousView);
} else {
resetView();
}
}
export function applyTheme(theme) { export function applyTheme(theme) {
document.body.classList.toggle('light-theme', theme === 'light'); document.body.classList.toggle('light-theme', theme === 'light');
} }
@ -1755,17 +2095,17 @@ export const phpScriptGenerator = (() => {
let dom = {}; let dom = {};
function cacheDom() { function cacheDom() {
const settingsModal = document.getElementById('settingsModal'); const settingsSection = document.getElementById('settings-section');
if (!settingsModal) return false; if (!settingsSection) return false;
dom.secretKeyCheck = settingsModal.querySelector('#phpSecretKeyCheck'); dom.secretKeyCheck = settingsSection.querySelector('#phpSecretKeyCheck');
dom.secretKey = settingsModal.querySelector('#phpSecretKey'); dom.secretKey = settingsSection.querySelector('#phpSecretKey');
dom.savePath = settingsModal.querySelector('#phpSavePath'); dom.savePath = settingsSection.querySelector('#phpSavePath');
dom.filename = settingsModal.querySelector('#phpFilename'); dom.filename = settingsSection.querySelector('#phpFilename');
dom.fileActionAppendRadio = settingsModal.querySelector('#phpFileActionAppend'); dom.fileActionAppendRadio = settingsSection.querySelector('#phpFileActionAppend');
dom.generatedCode = settingsModal.querySelector('#generatedPhpCode'); dom.generatedCode = settingsSection.querySelector('#generatedPhpCode');
dom.generateBtn = settingsModal.querySelector('#generatePhpScriptBtn'); dom.generateBtn = settingsSection.querySelector('#generatePhpScriptBtn');
dom.copyBtn = settingsModal.querySelector('#copyPhpScriptBtn'); dom.copyBtn = settingsSection.querySelector('#copyPhpScriptBtn');
return dom.generateBtn && dom.copyBtn; return dom.generateBtn && dom.copyBtn;
} }
@ -1774,8 +2114,6 @@ export const phpScriptGenerator = (() => {
if (!cacheDom()) { if (!cacheDom()) {
return; return;
} }
dom.generateBtn.addEventListener('click', generatePhpScript);
dom.copyBtn.addEventListener('click', copyScript);
} }
function generatePhpScript() { function generatePhpScript() {
@ -1801,7 +2139,7 @@ function generatePhpScript() {
script += `\n$auth_key = isset($_SERVER['HTTP_X_SECRET_KEY']) ? $_SERVER['HTTP_X_SECRET_KEY'] : '';\nif (!defined('SECRET_KEY') || SECRET_KEY === '' || $auth_key !== SECRET_KEY) {\n sendResponse(false, 'Acceso no autorizado. Clave secreta inválida o no proporcionada.', '', 403);\n}\n`; script += `\n$auth_key = isset($_SERVER['HTTP_X_SECRET_KEY']) ? $_SERVER['HTTP_X_SECRET_KEY'] : '';\nif (!defined('SECRET_KEY') || SECRET_KEY === '' || $auth_key !== SECRET_KEY) {\n sendResponse(false, 'Acceso no autorizado. Clave secreta inválida o no proporcionada.', '', 403);\n}\n`;
} }
script += `\n$json_data = file_get_contents('php://input');\n$data = json_decode($json_data, true);\n\nif (json_last_error() !== JSON_ERROR_NONE) {\n sendResponse(false, 'Error: Datos JSON inválidos.');\n}\n\nif (!isset($data['streams']) || !is_array($data['streams']) || empty($data['streams'])) {\n sendResponse(false, 'Error: El JSON debe contener un array "streams" no vacío.');\n}\n\n$save_dir = SAVE_DIRECTORY !== '' ? rtrim(SAVE_DIRECTORY, '/\\') : __DIR__;\n\nif (!is_dir($save_dir) || !is_writable($save_dir)) {\n sendResponse(false, 'Error del servidor: El directorio de destino no existe o no tiene permisos de escritura.', '', 500);\n}\n\n$safe_filename = preg_replace('/[^\\w\\s._-]/', '', basename(FILENAME));\n$safe_filename = preg_replace('/\\s+/', '_', $safe_filename);\n$target_path = $save_dir . DIRECTORY_SEPARATOR . $safe_filename;\n\n$content_to_write = "";\n\nif (FILE_ACTION_APPEND) {\n $file_exists = file_exists($target_path);\n if (!$file_exists) {\n $content_to_write .= "#EXTM3U\\n";\n }\n foreach ($data['streams'] as $stream) {\n if (isset($stream['extinf'], $stream['url'])) {\n $content_to_write .= trim($stream['extinf']) . "\\n";\n $content_to_write .= trim($stream['url']) . "\\n";\n }\n }\n if (file_put_contents($target_path, $content_to_write, FILE_APPEND | LOCK_EX) !== false) {\n sendResponse(true, 'Streams añadidos correctamente al archivo.', $safe_filename, 200);\n } else {\n sendResponse(false, 'Error del servidor: No se pudo añadir contenido al archivo.', '', 500);\n }\n} else {\n $content_to_write = "#EXTM3U\\n";\n foreach ($data['streams'] as $stream) {\n if (isset($stream['extinf'], $stream['url'])) {\n $content_to_write .= trim($stream['extinf']) . "\\n";\n $content_to_write .= trim($stream['url']) . "\\n";\n }\n }\n if (file_put_contents($target_path, $content_to_write, LOCK_EX) !== false) {\n sendResponse(true, 'El archivo de streams ha sido reemplazado.', $safe_filename, 200);\n } else {\n sendResponse(false, 'Error del servidor: No se pudo escribir en el archivo.', '', 500);\n }\n}\n?>`; script += `\n$json_data = file_get_contents('php://input');\n$data = json_decode($json_data, true);\n\nif (json_last_error() !== JSON_ERROR_NONE) {\n sendResponse(false, 'Error: Datos JSON inválidos.');\n}\n\nif (!isset($data['streams']) || !is_array($data['streams']) || empty($data['streams'])) {\n sendResponse(false, 'Error: El JSON debe contener un array "streams" no vacío.');\n}\n\n$save_dir = SAVE_DIRECTORY !== '' ? rtrim(SAVE_DIRECTORY, '/\\\\') : __DIR__;\n\nif (!is_dir($save_dir) || !is_writable($save_dir)) {\n sendResponse(false, 'Error del servidor: El directorio de destino no existe o no tiene permisos de escritura.', '', 500);\n}\n\n$safe_filename = preg_replace('/[^\\w\\s._-]/', '', basename(FILENAME));\n$safe_filename = preg_replace('/\\s+/', '_', $safe_filename);\n$target_path = $save_dir . DIRECTORY_SEPARATOR . $safe_filename;\n\n$content_to_write = "";\n\nif (FILE_ACTION_APPEND) {\n $file_exists = file_exists($target_path);\n if (!$file_exists) {\n $content_to_write .= "#EXTM3U\\n";\n }\n foreach ($data['streams'] as $stream) {\n if (isset($stream['extinf'], $stream['url'])) {\n $content_to_write .= trim($stream['extinf']) . "\\n";\n $content_to_write .= trim($stream['url']) . "\\n";\n }\n }\n if (file_put_contents($target_path, $content_to_write, FILE_APPEND | LOCK_EX) !== false) {\n sendResponse(true, 'Streams añadidos correctamente al archivo.', $safe_filename, 200);\n } else {\n sendResponse(false, 'Error del servidor: No se pudo añadir contenido al archivo.', '', 500);\n }\n} else {\n $content_to_write = "#EXTM3U\\n";\n foreach ($data['streams'] as $stream) {\n if (isset($stream['extinf'], $stream['url'])) {\n $content_to_write .= trim($stream['extinf']) . "\\n";\n $content_to_write .= trim($stream['url']) . "\\n";\n }\n }\n if (file_put_contents($target_path, $content_to_write, LOCK_EX) !== false) {\n sendResponse(true, 'El archivo de streams ha sido reemplazado.', $safe_filename, 200);\n } else {\n sendResponse(false, 'Error del servidor: No se pudo escribir en el archivo.', '', 500);\n }\n}\n?>`;
dom.generatedCode.value = script; dom.generatedCode.value = script;
showNotification(_("scriptGenerated"), "success"); showNotification(_("scriptGenerated"), "success");
@ -1819,7 +2157,7 @@ function generatePhpScript() {
}); });
} }
return { init }; return { init, generatePhpScript, copyScript };
})(); })();
export function initPhotosView() { export function initPhotosView() {
@ -2328,4 +2666,51 @@ export async function searchByActor(actorId, actorName) {
grid.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('errorLoadingActorContent', actorName)}</p></div>`; grid.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('errorLoadingActorContent', actorName)}</p></div>`;
} }
} }
}
export async function downloadM3UForSeason(buttonElement) {
if (!buttonElement) return;
const { seriesTitle, seasonNumber, year } = buttonElement.dataset;
if (state.isDownloadingM3U) return;
state.isDownloadingM3U = true;
let originalButtonContent = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>`;
const filename = `${seriesTitle.replace(/[^a-z0-9]/gi, '_')}_Season_${seasonNumber}`;
showNotification(_('generatingM3U', filename), "info");
try {
const streamData = await fetchAllAvailableStreams(seriesTitle, 'tv', year, seasonNumber);
if (!streamData.success || streamData.streams.length === 0) {
throw new Error(_('noStreamsFoundForSelection'));
}
let m3uContent = "#EXTM3U\n";
streamData.streams.forEach(stream => {
m3uContent += `${stream.extinf}\n${stream.url}\n`;
});
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 = `${filename}.m3u`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showNotification(_('m3uDownloaded', filename), 'success');
} catch (error) {
showNotification(_('errorGeneratingM3U', error.message), "error");
} finally {
state.isDownloadingM3U = false;
if (buttonElement) {
buttonElement.innerHTML = originalButtonContent;
buttonElement.disabled = false;
}
}
} }

View File

@ -135,4 +135,13 @@ export async function fetchWithTimeout(url, options, timeout = 7000) {
} }
}); });
}); });
}
export function formatBytes(bytes, decimals = 2) {
if (!+bytes) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
} }

1264
lib/spatial_navigation.js Normal file

File diff suppressed because it is too large Load Diff

506
plex.html
View File

@ -11,8 +11,9 @@
<link <link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Orbitron:wght@500;600;700&display=swap" href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Orbitron:wght@500;600;700&display=swap"
rel="stylesheet"> rel="stylesheet">
<link rel="stylesheet" href="css/main.css"> <link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/custom-filters.css"> <link rel="stylesheet" href="css/music.css">
<script src="lib/spatial_navigation.js"></script>
</head> </head>
<body class="unlocalized"> <body class="unlocalized">
@ -25,39 +26,40 @@
</div> </div>
<div class="top-bar-center"> <div class="top-bar-center">
<div class="search-bar"> <div class="search-bar">
<input type="text" class="search-input" id="search-input" placeholder="__MSG_searchPlaceholder__"> <input type="text" class="search-input" id="search-input" placeholder="__MSG_searchPlaceholder__" data-sn-focusable="true">
<i class="fas fa-search search-icon"></i> <i class="fas fa-search search-icon"></i>
</div> </div>
</div> </div>
<div class="top-bar-right"> <div class="top-bar-right">
<button id="activity-viewer-btn" class="btn-icon" title="__MSG_activityViewerTitle__"> <button id="activity-viewer-btn" class="btn-icon" title="__MSG_activityViewerTitle__" data-sn-focusable="true">
<i class="fas fa-desktop"></i> <i class="fas fa-desktop"></i>
</button> </button>
<button id="openMusicPlayerDesktop" class="btn-icon" title="__MSG_openMusicPlayer__"> <button id="openMusicPlayerDesktop" class="btn-icon" title="__MSG_openMusicPlayer__" data-sn-focusable="true">
<i class="fas fa-music"></i> <i class="fas fa-music"></i>
</button> </button>
<button id="settings-btn" class="btn-icon" title="__MSG_settings__"> <button id="settings-btn" class="btn-icon" title="__MSG_settings__" data-sn-focusable="true">
<i class="fas fa-cog"></i> <i class="fas fa-cog"></i>
</button> </button>
</div> </div>
</header> </header>
<nav class="sidebar-nav" id="sidebar-nav" role="navigation"> <nav class="sidebar-nav" id="sidebar-nav" role="navigation" data-sn-container="true">
<ul class="sidebar-menu"> <ul class="sidebar-menu">
<li><a class="nav-link active" href="#" id="nav-movies" role="button"><i class="fas fa-film"></i><span>__MSG_navMovies__</span></a></li> <li><a class="nav-link active" href="#" id="nav-movies" role="button" data-sn-focusable="true"><i class="fas fa-film"></i><span>__MSG_navMovies__</span></a></li>
<li><a class="nav-link" href="#" id="nav-series" role="button"><i class="fas fa-tv"></i><span>__MSG_navSeries__</span></a></li> <li><a class="nav-link" href="#" id="nav-series" role="button" data-sn-focusable="true"><i class="fas fa-tv"></i><span>__MSG_navSeries__</span></a></li>
<li><a class="nav-link" href="#" id="nav-providers" role="button"><i class="fas fa-broadcast-tower"></i><span>__MSG_navProviders__</span></a></li> <li><a class="nav-link" href="#" id="nav-providers" role="button" data-sn-focusable="true"><i class="fas fa-broadcast-tower"></i><span>__MSG_navProviders__</span></a></li>
<li><a class="nav-link" href="#" id="nav-photos" role="button"><i class="fas fa-images"></i><span>__MSG_navPhotos__</span></a></li> <li><a class="nav-link" href="#" id="nav-photos" role="button" data-sn-focusable="true"><i class="fas fa-images"></i><span>__MSG_navPhotos__</span></a></li>
<li><a class="nav-link" href="#" id="nav-stats" role="button"><i class="fas fa-chart-pie"></i><span>__MSG_navStats__</span></a></li> <li><a class="nav-link" href="#" id="nav-stats" role="button" data-sn-focusable="true"><i class="fas fa-chart-pie"></i><span>__MSG_navStats__</span></a></li>
<li><a class="nav-link" href="#" id="nav-favorites" role="button"><i class="fas fa-heart"></i><span>__MSG_navFavorites__</span></a></li> <li><a class="nav-link" href="#" id="nav-favorites" role="button" data-sn-focusable="true"><i class="fas fa-heart"></i><span>__MSG_navFavorites__</span></a></li>
<li><a class="nav-link" href="#" id="nav-history" role="button"><i class="fas fa-history"></i><span>__MSG_navHistory__</span></a></li> <li><a class="nav-link" href="#" id="nav-history" role="button" data-sn-focusable="true"><i class="fas fa-history"></i><span>__MSG_navHistory__</span></a></li>
<li><a class="nav-link" href="#" id="nav-recommendations" role="button"><i class="fas fa-magic"></i><span>__MSG_navRecommendations__</span></a></li> <li><a class="nav-link" href="#" id="nav-recommendations" role="button" data-sn-focusable="true"><i class="fas fa-magic"></i><span>__MSG_navRecommendations__</span></a></li>
<li><a class="nav-link d-lg-none" href="#" id="openMusicPlayerMobile" role="button"><i class="fas fa-music"></i><span>__MSG_navMusic__</span></a></li> <li><a class="nav-link d-lg-none" href="#" id="openMusicPlayerMobile" role="button" data-sn-focusable="true"><i class="fas fa-music"></i><span>__MSG_navMusic__</span></a></li>
<li><a class="nav-link" href="#" id="nav-m3u-generator" role="button"><i class="fas fa-list-ul"></i><span>__MSG_navM3uGenerator__</span></a></li> <li><a class="nav-link" href="#" id="nav-music" role="button" data-sn-focusable="true"><i class="fas fa-compact-disc"></i><span>__MSG_navMusic__</span></a></li>
<li><a class="nav-link" href="#" id="nav-m3u-generator" role="button" data-sn-focusable="true"><i class="fas fa-list-ul"></i><span>__MSG_navM3uGenerator__</span></a></li>
</ul> </ul>
</nav> </nav>
<div id="main-container" role="main"> <div id="main-container" role="main" data-sn-container="true">
<div id="main-view"> <div id="main-view">
<section class="hero" id="hero-section"> <section class="hero" id="hero-section">
<div class="hero-background-container"> <div class="hero-background-container">
@ -98,7 +100,7 @@
<option id="sort-release-date" value="release_date.desc">__MSG_sortRecent__</option> <option id="sort-release-date" value="release_date.desc">__MSG_sortRecent__</option>
</select> </select>
<div class="filter-popover-wrapper"> <div class="filter-popover-wrapper">
<button type="button" class="btn btn-secondary" id="duration-filter-btn" data-i18n="duration_min">Duración (Min)</button> <button type="button" class="btn btn-secondary" id="duration-filter-btn" data-i18n="duration_min">__MSG_durationMin__</button>
<div id="duration-popover" class="filter-popover"> <div id="duration-popover" class="filter-popover">
<div class="range-slider-container"> <div class="range-slider-container">
<div class="slider-track"></div> <div class="slider-track"></div>
@ -112,7 +114,7 @@
</div> </div>
</div> </div>
<div class="filter-popover-wrapper"> <div class="filter-popover-wrapper">
<button type="button" class="btn btn-secondary" id="score-filter-btn" data-i18n="score">Puntuación</button> <button type="button" class="btn btn-secondary" id="score-filter-btn" data-i18n="score">__MSG_score__</button>
<div id="score-popover" class="filter-popover"> <div id="score-popover" class="filter-popover">
<div class="range-slider-container"> <div class="range-slider-container">
<div class="slider-track"></div> <div class="slider-track"></div>
@ -252,6 +254,47 @@
</div> </div>
</div> </div>
</section> </section>
<section id="music-section" style="display: none;">
<div class="section-header">
<h2 class="section-title" data-i18n="navMusic">__MSG_navMusic__</h2>
<div class="music-controls">
<button id="music-back-btn" class="btn-icon" style="display: none;"><i class="fas fa-arrow-left"></i></button>
<select id="music-server-filter" class="filter-select" style="display: none;"></select>
<div class="music-search-bar" id="genre-search-container" style="display: none;">
<input type="text" id="genre-search-input" class="search-input" placeholder="__MSG_searchGenre__">
<i class="fas fa-search search-icon"></i>
</div>
<div class="music-search-bar" style="display: none;">
<input type="text" id="music-section-search-input" class="search-input" placeholder="__MSG_searchArtists__">
<i class="fas fa-search search-icon"></i>
</div>
</div>
</div>
<div id="music-section-content-wrapper" style="position: relative;">
<div id="music-section-genre-grid" class="content-grid"></div>
<div id="music-section-artist-grid" class="content-grid" style="display: none;"></div>
<div class="text-center mt-4"><button id="music-load-more-btn" class="btn btn-primary" style="display: none;">__MSG_loadMore__</button></div>
<div id="music-section-song-list" style="display: none;"></div>
<div id="music-classification-overlay" style="display: none;">
<div class="classification-content">
<div class="classification-icon">
<i class="fas fa-compact-disc"></i>
</div>
<h3 class="classification-title">__MSG_preparingMusicLibrary__</h3>
<p class="classification-subtitle">__MSG_preparingMusicLibraryDesc__</p>
<div class="classification-progress-bar">
<div id="classification-progress-fill" class="classification-progress-fill"></div>
</div>
<div class="classification-progress-text">
<span id="classification-percentage">0%</span>
<span id="classification-progress-details">__MSG_artistsProgress__</span>
</div>
<p id="classification-status-text" class="classification-status-text">__MSG_starting__</p>
</div>
</div>
</div>
</section>
<section id="recommendations-section" style="display: none;"> <section id="recommendations-section" style="display: none;">
<div class="section-header"> <div class="section-header">
<h2 class="section-title">__MSG_recommendationsTitle__</h2> <h2 class="section-title">__MSG_recommendationsTitle__</h2>
@ -267,47 +310,197 @@
</section> </section>
<section id="m3u-generator-section" class="content-section"> <section id="m3u-generator-section" class="content-section">
<div class="section-header"> <div class="section-header">
<h2 class="section-title">__MSG_m3uGenerator__</h2> <h2 class="section-title">__MSG_m3uGenerator__</h2>
</div> </div>
<div class="m3u-container"> <div class="m3u-container">
<div class="m3u-config-panel m3u-animated-item"> <div class="m3u-config-panel m3u-animated-item">
<div class="m3u-step"> <div class="m3u-step">
<div class="m3u-step-header"> <div class="m3u-step-header">
<span class="m3u-step-number">1</span> <span class="m3u-step-number">1</span>
<h3 class="m3u-step-title">__MSG_selectServer__</h3> <h3 class="m3u-step-title">__MSG_selectServer__</h3>
</div> </div>
<select id="m3u-server-select" class="filter-select"></select> <select id="m3u-server-select" class="filter-select"></select>
</div> </div>
<div id="m3u-libraries-step" class="m3u-step" style="display: none;"> <div id="m3u-libraries-step" class="m3u-step" style="display: none;">
<div class="m3u-step-header"> <div class="m3u-step-header">
<span class="m3u-step-number">2</span> <span class="m3u-step-number">2</span>
<h3 class="m3u-step-title">__MSG_selectLibraries__</h3> <h3 class="m3u-step-title">__MSG_selectLibraries__</h3>
</div> </div>
<div id="m3u-libraries-container"></div> <div id="m3u-libraries-container"></div>
<div id="m3u-libraries-loader" class="text-center py-4" style="display: none;"> <div id="m3u-libraries-loader" class="text-center py-4" style="display: none;">
<div class="spinner-border text-primary" role="status"> <div class="spinner-border text-primary" role="status">
<span class="visually-hidden">__MSG_loading__</span> <span class="visually-hidden">__MSG_loading__</span>
</div> </div>
<p class="mt-3 lead">__MSG_loadingLibraries__</p> <p class="mt-3 lead">__MSG_loadingLibraries__</p>
</div> </div>
</div> </div>
</div> </div>
<div class="m3u-info-panel m3u-animated-item"> <div class="m3u-info-panel m3u-animated-item">
<h3 class="m3u-info-title">__MSG_howToUse__</h3> <h3 class="m3u-info-title">__MSG_howToUse__</h3>
<ol class="m3u-instructions"> <ol class="m3u-instructions">
<li>__MSG_m3uInstruction1__</li> <li>__MSG_m3uInstruction1__</li>
<li>__MSG_m3uInstruction2__</li> <li>__MSG_m3uInstruction2__</li>
<li>__MSG_m3uInstruction3__</li> <li>__MSG_m3uInstruction3__</li>
<li>__MSG_m3uInstruction4__</li> <li>__MSG_m3uInstruction4__</li>
</ol> </ol>
<button id="download-m3u-btn" class="btn btn-primary" disabled> <button id="download-m3u-btn" class="btn btn-primary" disabled>
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
<span>__MSG_downloadM3u__</span> <span>__MSG_downloadM3u__</span>
</button> </button>
</div> </div>
</div> </div>
</section> </section>
<section id="settings-section" style="display: none;">
<div class="settings-container">
<div class="settings-nav">
<a class="nav-item active" data-tab="general"><i class="fas fa-sliders-h fa-fw me-2"></i>__MSG_settingsTabGeneral__</a>
<a class="nav-item" data-tab="plex"><i class="fab fa-plex fa-fw me-2"></i>__MSG_settingsTabPlex__</a>
<a class="nav-item" data-tab="jellyfin"><i class="fas fa-jellyfish fa-fw me-2"></i>__MSG_settingsTabJellyfin__</a>
<a class="nav-item" data-tab="php-gen"><i class="fab fa-php fa-fw me-2"></i>__MSG_settingsTabPhpGen__</a>
<a class="nav-item" data-tab="data"><i class="fas fa-database fa-fw me-2"></i>__MSG_settingsTabData__</a>
</div>
<div class="settings-content-wrapper">
<div class="settings-content">
<div class="tab-pane active" id="general">
<h5 class="mb-3">__MSG_settingsApiServer__</h5>
<div class="mb-3">
<label for="tmdbApiKey" class="form-label">__MSG_settingsTmdbApiLabel__</label>
<input type="password" class="form-control" id="tmdbApiKey" placeholder="__MSG_settingsTmdbApiPlaceholder__">
</div>
<div class="mb-3">
<label for="googleApiKey" class="form-label">__MSG_settingsGoogleApiLabel__</label>
<input type="password" class="form-control" id="googleApiKey" placeholder="__MSG_settingsGoogleApiPlaceholder__">
</div>
<div class="mb-3">
<label for="region-filter" class="form-label">__MSG_settingsRegionLabel__</label>
<select class="form-control filter-select" id="region-filter">
<option value="">__MSG_allRegions__</option>
</select>
</div>
<div class="mb-3">
<label for="phpScriptUrl" class="form-label">__MSG_settingsPhpUrlLabel__</label>
<input type="url" class="form-control" id="phpScriptUrl" placeholder="__MSG_settingsPhpUrlPlaceholder__">
</div>
<h5 class="mt-4 mb-3">__MSG_settingsInterface__</h5>
<div class="d-flex justify-content-between align-items-center mb-3">
<label for="lightModeToggle" class="form-label mb-0">__MSG_settingsLightTheme__</label>
<div class="toggle-switch">
<input type="checkbox" id="lightModeToggle">
<label for="lightModeToggle"></label>
</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<label for="showHeroToggle" class="form-label mb-0">__MSG_settingsShowHero__</label>
<div class="toggle-switch">
<input type="checkbox" id="showHeroToggle" checked>
<label for="showHeroToggle"></label>
</div>
</div>
</div>
<div class="tab-pane" id="plex">
<div class="row">
<div class="col-md-5">
<h5 class="mb-3">__MSG_settingsScanContent__</h5>
<p class="small text-muted mb-3">__MSG_settingsScanDesc__</p>
<label class="d-block mb-2"><input type="checkbox" id="updateMovies" value="movies"> __MSG_settingsScanMovies__</label>
<label class="d-block mb-2"><input type="checkbox" id="updateShows" value="series"> __MSG_settingsScanShows__</label>
<label class="d-block mb-2"><input type="checkbox" id="updateArtists" value="artists"> __MSG_settingsScanArtists__</label>
<label class="d-block mb-2"><input type="checkbox" id="updatePhotos" value="photos"> __MSG_settingsScanPhotos__</label>
<hr class="my-3">
<label class="d-block mb-3"><input type="checkbox" id="updateAll"> __MSG_settingsSelectAll__</label>
<button type="button" class="btn btn-primary w-100" id="confirmScanBtn" data-sn-focusable="true"><i class="fas fa-sync-alt me-1"></i> __MSG_settingsStartScan__</button>
</div>
<div class="col-md-7">
<h5 class="mb-3">__MSG_settingsPlexTokens__</h5>
<p class="small text-muted mb-2">__MSG_settingsPlexTokensDesc__</p>
<div id="editor"></div>
<button type="button" class="btn btn-success mt-3 w-100" id="saveTokensBtn" data-sn-focusable="true"><i class="fas fa-save me-1"></i> __MSG_settingsSaveTokens__</button>
</div>
</div>
</div>
<div class="tab-pane" id="jellyfin">
<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" data-sn-focusable="true"><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" id="php-gen">
<h5 class="mb-3">__MSG_settingsPhpGenTitle__</h5>
<div class="row">
<div class="col-lg-5">
<h6>__MSG_settingsPhpFileOptions__</h6>
<div class="mb-3">
<label for="phpSavePath" class="form-label">__MSG_settingsPhpSavePathLabel__</label>
<input type="text" id="phpSavePath" class="form-control form-control-sm" placeholder="__MSG_settingsPhpSavePathPlaceholder__">
</div>
<div class="mb-3">
<label for="phpFilename" class="form-label">__MSG_settingsPhpFilenameLabel__</label>
<input type="text" id="phpFilename" class="form-control form-control-sm" value="CinePlex_Playlist.m3u">
</div>
<h6 class="mt-3">__MSG_settingsPhpFileAction__</h6>
<div class="form-check">
<input class="form-check-input" type="radio" name="phpFileAction" id="phpFileActionAppend" checked>
<label class="form-check-label" for="phpFileActionAppend">__MSG_settingsPhpActionAppend__</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="radio" name="phpFileAction" id="phpFileActionOverwrite">
<label class="form-check-label" for="phpFileActionOverwrite">__MSG_settingsPhpActionOverwrite__</label>
</div>
<h6>__MSG_settingsPhpSecurity__</h6>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="phpSecretKeyCheck">
<label class="form-check-label" for="phpSecretKeyCheck">__MSG_settingsPhpUseSecretKey__</label>
</div>
<div class="mb-3">
<input type="text" id="phpSecretKey" class="form-control form-control-sm" placeholder="__MSG_settingsPhpSecretKeyPlaceholder__">
</div>
</div>
<div class="col-lg-7">
<h6>__MSG_settingsPhpGeneratedCode__</h6>
<textarea id="generatedPhpCode" class="form-control" rows="12" readonly placeholder="__MSG_settingsPhpGeneratedPlaceholder__"></textarea>
<div class="d-grid gap-2 mt-2">
<button class="btn btn-primary" id="generatePhpScriptBtn" data-sn-focusable="true"><i class="fas fa-cogs me-2"></i>__MSG_settingsGenerateScript__</button>
<button class="btn btn-secondary" id="copyPhpScriptBtn" data-sn-focusable="true"><i class="fas fa-copy me-2"></i>__MSG_settingsCopyScript__</button>
</div>
</div>
</div>
</div>
<div class="tab-pane" id="data">
<h5 class="mb-3">__MSG_settingsDataManagement__</h5>
<div class="d-grid gap-3">
<button type="button" class="btn btn-info" id="import-db-btn"><i class="fas fa-file-import me-2"></i>__MSG_settingsImportDb__</button>
<button type="button" class="btn btn-info" id="exportDbBtn" data-sn-focusable="true"><i class="fas fa-file-export me-2"></i>__MSG_settingsExportDb__</button>
<hr>
<button type="button" class="btn btn-danger" id="clearDataBtn" data-sn-focusable="true"><i class="fas fa-trash me-2"></i>__MSG_settingsClearContent__</button>
<p class="small text-muted text-center mt-2">__MSG_settingsClearContentDesc__</p>
</div>
</div>
</div>
<div class="settings-footer">
<button type="button" class="btn btn-secondary" id="cancelSettingsBtn">__MSG_settingsClose__</button>
<button type="button" class="btn btn-primary" id="saveSettingsBtn"><i class="fas fa-save me-1"></i> __MSG_settingsSave__</button>
</div>
</div>
</div>
</section>
<div id="consoleOutputContainer" class="mt-5" style="display: none;"> <div id="consoleOutputContainer" class="mt-5" style="display: none;">
<h3 class="section-subtitle mt-4">__MSG_consoleTitle__</h3> <h3 class="section-subtitle mt-4">__MSG_consoleTitle__</h3>
@ -392,188 +585,6 @@
</div> </div>
</div> </div>
<div class="modal fade" id="settingsModal" tabindex="-1" aria-labelledby="settingsModalLabel" aria-hidden="true" role="dialog" aria-modal="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="settingsModalLabel"><i class="fas fa-cog me-2"></i>__MSG_settingsTitleFull__</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="__MSG_close__"></button>
</div>
<div class="modal-body p-0">
<ul class="nav nav-tabs" id="settingsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="general-tab" data-bs-toggle="tab" data-bs-target="#general" type="button" role="tab" aria-controls="general" aria-selected="true">
<i class="fas fa-sliders-h me-2"></i>__MSG_settingsTabGeneral__
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="plex-tab" data-bs-toggle="tab" data-bs-target="#plex" type="button" role="tab" aria-controls="plex" aria-selected="false">
<i class="fas fa-server me-2"></i>__MSG_settingsTabPlex__
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="jellyfin-tab" data-bs-toggle="tab" data-bs-target="#jellyfin" type="button" role="tab" aria-controls="jellyfin" aria-selected="false">
<i class="fas fa-database me-2"></i>__MSG_settingsTabJellyfin__
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="php-gen-tab" data-bs-toggle="tab" data-bs-target="#php-gen" type="button" role="tab" aria-controls="php-gen" aria-selected="false">
<i class="fab fa-php me-2"></i>__MSG_settingsTabPhpGen__
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="data-tab" data-bs-toggle="tab" data-bs-target="#data" type="button" role="tab" aria-controls="data" aria-selected="false">
<i class="fas fa-database me-2"></i>__MSG_settingsTabData__
</button>
</li>
</ul>
<div class="tab-content p-4" id="settingsTabsContent">
<div class="tab-pane fade show active" id="general" role="tabpanel" aria-labelledby="general-tab">
<h5 class="mb-3">__MSG_settingsApiServer__</h5>
<div class="mb-3">
<label for="tmdbApiKey" class="form-label">__MSG_settingsTmdbApiLabel__</label>
<input type="password" class="form-control" id="tmdbApiKey" placeholder="__MSG_settingsTmdbApiPlaceholder__">
</div>
<div class="mb-3">
<label for="googleApiKey" class="form-label">__MSG_settingsGoogleApiLabel__</label>
<input type="password" class="form-control" id="googleApiKey" placeholder="__MSG_settingsGoogleApiPlaceholder__">
</div>
<div class="mb-3">
<label for="region-filter" class="form-label">__MSG_settingsRegionLabel__</label>
<select class="form-control filter-select" id="region-filter">
<option value="">__MSG_allRegions__</option>
</select>
</div>
<div class="mb-3">
<label for="phpScriptUrl" class="form-label">__MSG_settingsPhpUrlLabel__</label>
<input type="url" class="form-control" id="phpScriptUrl" placeholder="__MSG_settingsPhpUrlPlaceholder__">
</div>
<h5 class="mt-4 mb-3">__MSG_settingsInterface__</h5>
<div class="d-flex justify-content-between align-items-center mb-3">
<label for="lightModeToggle" class="form-label mb-0">__MSG_settingsLightTheme__</label>
<div class="toggle-switch">
<input type="checkbox" id="lightModeToggle">
<label for="lightModeToggle"></label>
</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<label for="showHeroToggle" class="form-label mb-0">__MSG_settingsShowHero__</label>
<div class="toggle-switch">
<input type="checkbox" id="showHeroToggle" checked>
<label for="showHeroToggle"></label>
</div>
</div>
</div>
<div class="tab-pane fade" id="plex" role="tabpanel" aria-labelledby="plex-tab">
<div class="row">
<div class="col-md-5">
<h5 class="mb-3">__MSG_settingsScanContent__</h5>
<p class="small text-muted mb-3">__MSG_settingsScanDesc__</p>
<label class="d-block mb-2"><input type="checkbox" id="updateMovies" value="movies"> __MSG_settingsScanMovies__</label>
<label class="d-block mb-2"><input type="checkbox" id="updateShows" value="series"> __MSG_settingsScanShows__</label>
<label class="d-block mb-2"><input type="checkbox" id="updateArtists" value="artists"> __MSG_settingsScanArtists__</label>
<label class="d-block mb-2"><input type="checkbox" id="updatePhotos" value="photos"> __MSG_settingsScanPhotos__</label>
<hr class="my-3">
<label class="d-block mb-3"><input type="checkbox" id="updateAll"> __MSG_settingsSelectAll__</label>
<button type="button" class="btn btn-primary w-100" id="confirmScanBtn"><i class="fas fa-sync-alt me-1"></i> __MSG_settingsStartScan__</button>
</div>
<div class="col-md-7">
<h5 class="mb-3">__MSG_settingsPlexTokens__</h5>
<p class="small text-muted mb-2">__MSG_settingsPlexTokensDesc__</p>
<div id="editor"></div>
<button type="button" class="btn btn-success mt-3 w-100" id="saveTokensBtn"><i class="fas fa-save me-1"></i> __MSG_settingsSaveTokens__</button>
</div>
</div>
</div>
<div class="tab-pane fade" id="jellyfin" role="tabpanel" aria-labelledby="jellyfin-tab">
<h5 class="mb-3">__MSG_settingsJellyfinTitle__</h5>
<p class="small text-muted mb-3">__MSG_settingsJellyfinDesc__</p>
<div class="mb-3">
<label for="jellyfinServerUrl" class="form-label">__MSG_jellyfinUrlLabel__</label>
<input type="url" class="form-control" id="jellyfinServerUrl" placeholder="http://192.168.1.10:8096">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="jellyfinUsername" class="form-label">__MSG_jellyfinUserLabel__</label>
<input type="text" class="form-control" id="jellyfinUsername">
</div>
<div class="col-md-6 mb-3">
<label for="jellyfinPassword" class="form-label">__MSG_jellyfinPassLabel__</label>
<input type="password" class="form-control" id="jellyfinPassword">
</div>
</div>
<div class="d-grid">
<button type="button" class="btn btn-primary" id="jellyfinScanBtn"><i class="fas fa-search-plus me-2"></i>__MSG_jellyfinConnectAndScan__</button>
</div>
<div id="jellyfinScanStatus" class="mt-3"></div>
</div>
<div class="tab-pane fade" id="php-gen" role="tabpanel" aria-labelledby="php-gen-tab">
<h5 class="mb-3">__MSG_settingsPhpGenTitle__</h5>
<div class="row">
<div class="col-lg-5">
<h6>__MSG_settingsPhpFileOptions__</h6>
<div class="mb-3">
<label for="phpSavePath" class="form-label">__MSG_settingsPhpSavePathLabel__</label>
<input type="text" id="phpSavePath" class="form-control form-control-sm" placeholder="__MSG_settingsPhpSavePathPlaceholder__">
</div>
<div class="mb-3">
<label for="phpFilename" class="form-label">__MSG_settingsPhpFilenameLabel__</label>
<input type="text" id="phpFilename" class="form-control form-control-sm" value="CinePlex_Playlist.m3u">
</div>
<h6 class="mt-3">__MSG_settingsPhpFileAction__</h6>
<div class="form-check">
<input class="form-check-input" type="radio" name="phpFileAction" id="phpFileActionAppend" checked>
<label class="form-check-label" for="phpFileActionAppend">
__MSG_settingsPhpActionAppend__
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="radio" name="phpFileAction" id="phpFileActionOverwrite">
<label class="form-check-label" for="phpFileActionOverwrite">
__MSG_settingsPhpActionOverwrite__
</label>
</div>
<h6>__MSG_settingsPhpSecurity__</h6>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="phpSecretKeyCheck">
<label class="form-check-label" for="phpSecretKeyCheck">__MSG_settingsPhpUseSecretKey__</label>
</div>
<div class="mb-3">
<input type="text" id="phpSecretKey" class="form-control form-control-sm" placeholder="__MSG_settingsPhpSecretKeyPlaceholder__">
</div>
</div>
<div class="col-lg-7">
<h6>__MSG_settingsPhpGeneratedCode__</h6>
<textarea id="generatedPhpCode" class="form-control" rows="12" readonly placeholder="__MSG_settingsPhpGeneratedPlaceholder__"></textarea>
<div class="d-grid gap-2 mt-2">
<button class="btn btn-primary" id="generatePhpScriptBtn"><i class="fas fa-cogs me-2"></i>__MSG_settingsGenerateScript__</button>
<button class="btn btn-secondary" id="copyPhpScriptBtn"><i class="fas fa-copy me-2"></i>__MSG_settingsCopyScript__</button>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="data" role="tabpanel" aria-labelledby="data-tab">
<h5 class="mb-3">__MSG_settingsDataManagement__</h5>
<div class="d-grid gap-3">
<button type="button" class="btn btn-info" id="import-db-btn"><i class="fas fa-file-import me-2"></i>__MSG_settingsImportDb__</button>
<button type="button" class="btn btn-info" id="exportDbBtn"><i class="fas fa-file-export me-2"></i>__MSG_settingsExportDb__</button>
<hr>
<button type="button" class="btn btn-danger" id="clearDataBtn"><i class="fas fa-trash me-2"></i>__MSG_settingsClearContent__</button>
<p class="small text-muted text-center mt-2">__MSG_settingsClearContentDesc__</p>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">__MSG_settingsClose__</button>
<button type="button" class="btn btn-primary" id="saveSettingsBtn"><i class="fas fa-save me-1"></i> __MSG_settingsSave__</button>
</div>
</div>
</div>
</div>
<div id="musicPlayerContainer"> <div id="musicPlayerContainer">
<div class="sidenav"> <div class="sidenav">
<div class="sidenav-header"> <div class="sidenav-header">
@ -605,10 +616,10 @@
<div id="songListContainer" class="music-panel"> <div id="songListContainer" class="music-panel">
<div class="panel-controls song-list-controls"> <div class="panel-controls song-list-controls">
<button id="backBtn" class="btn-icon back-btn-icon"><i class="fas fa-arrow-left"></i></button> <button id="backBtnSidenav" class="btn-icon back-btn-icon"><i class="fas fa-arrow-left"></i></button>
<div id="artist-header-info"> <div id="artist-header-info">
<img id="artist-header-thumb" src="img/no-profile.png" alt="Artista"> <img id="artist-header-thumb" src="img/no-profile.png" alt="__MSG_artist__">
<h5 id="artist-header-title">Nombre del Artista</h5> <h5 id="artist-header-title">__MSG_artistName__</h5>
</div> </div>
</div> </div>
<div class="search-wrapper search-wrapper-songs"> <div class="search-wrapper search-wrapper-songs">
@ -649,7 +660,7 @@
<div class="player-center-controls"> <div class="player-center-controls">
<div id="player-controls"> <div id="player-controls">
<button id="prevBtn" class="control-btn" title="__MSG_previous__"><i class="fas fa-step-backward"></i></button> <button id="prevBtn" class="control-btn" title="__MSG_previous__"><i class="fas fa-step-backward"></i></button>
<button id="playPauseBtn" class="control-btn play-pause-main" title="Reproducir/Pausar"><i class="fas fa-play"></i></button> <button id="playPauseBtn" class="control-btn play-pause-main" title="__MSG_playPause__"><i class="fas fa-play"></i></button>
<button id="nextBtn" class="control-btn" title="__MSG_next__"><i class="fas fa-step-forward"></i></button> <button id="nextBtn" class="control-btn" title="__MSG_next__"><i class="fas fa-step-forward"></i></button>
</div> </div>
<div class="time-and-progress"> <div class="time-and-progress">
@ -823,6 +834,7 @@
<script type="module" src="js/m3u-generator.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/ai-tools.js"></script>
<script type="module" src="js/chat.js"></script> <script type="module" src="js/chat.js"></script>
<script type="module" src="js/music.js"></script>
</body> </body>