Compare commits

...

29 Commits
v1.0 ... main

Author SHA1 Message Date
ed6cb69add add 2025-08-02 09:57:26 +02:00
b348ffc529 Actualizar README.md 2025-07-30 19:32:47 +00:00
c4d80d31eb change OpenAI to Gemini (Assitant) 2025-07-30 20:47:55 +02:00
2449a07cae change model AI 2025-07-30 11:48:41 +02:00
c64cc0f23f fix 2025-07-30 11:34:30 +02:00
5e7664677e feat(chat, i18n, m3u): Mejoras en la experiencia de usuario del chat y M3U
Este commit introduce varias mejoras en la experiencia del usuario, incluyendo un chatbot más intuitivo y con mejor feedback visual, notificaciones para la generación de M3U y un soporte de internacionalización ampliado.

Cambios principales:

- Indicador de "trabajando" en el chat: Se ha añadido un indicador de "escribiendo..." que se muestra mientras se ejecutan las herramientas del chat, mejorando la percepción de que el asistente está procesando la petición.
- Traducciones: Se han añadido nuevas claves de i18n para el asistente de IA y las herramientas M3U en español, inglés, francés, italiano, portugués y alemán.
- Cierre del chat: Se ha corregido un error que provocaba que el chatbot se cerrara automáticamente después de ejecutar un comando.
- Mensajes del chat: Se han eliminado los mensajes de "ejecutando herramienta" y "resultado de la herramienta" para una experiencia de chat más limpia.
- Notificaciones M3U: Se han añadido notificaciones visuales para informar al usuario cuando se está generando un archivo M3U.
2025-07-30 11:19:29 +02:00
c5af5483ba fix 2025-07-29 23:27:15 +02:00
020ba6526a fix chat AI 2025-07-29 17:15:23 +02:00
d6957d7959 Asistente IA (openAI - proxyapi.ru) 2025-07-29 15:05:06 +02:00
9c7c57d41f Pre-buffering next song musicplayer 2025-07-28 19:02:06 +02:00
be3b82fe27 minetype fix jellyfin musicplayer 2025-07-28 18:50:25 +02:00
9acd8a1b58 fix db 2025-07-28 15:29:50 +02:00
44e9d78b08 delate 2025-07-28 15:04:42 +02:00
313382894d fix tmdb 2025-07-28 13:59:24 +02:00
eb4bc982de fix tmdb 2025-07-28 13:58:30 +02:00
e614cb387a Jellyfin music integration in MusicPlayer 2025-07-28 00:19:22 +02:00
2a8c3611a4 Eliminar js/constants.js 2025-07-27 10:59:35 +00:00
f6748ab113 Jellyfin integration in M3U generator and statistics 2025-07-27 12:56:26 +02:00
e96e4eb255 fix time generate m3u 2025-07-27 08:47:55 +02:00
aa3622db2d generate m3u 2025-07-26 18:53:03 +02:00
50b9d82f0f add provider 2025-07-25 23:57:03 +02:00
98de6ec451 update 2025-07-15 10:05:44 +02:00
09261a2978 fix playermusic 2025-07-15 09:51:44 +02:00
2345183a6d Eliminar js/constants.js 2025-07-15 07:44:56 +00:00
6d8f3b2ec5 fix hero, playermusic 2025-07-15 09:42:39 +02:00
e6106c149f hero fix 2025-07-13 11:14:42 +02:00
419bfe0ab5 fix fab musicplayer 2025-07-13 00:28:13 +02:00
104d669ac9 session, update playermusic 2025-07-12 12:56:04 +02:00
e988ff15c8 Jellyfin integration. Movies and TV shows only. 2025-07-11 12:10:50 +02:00
40 changed files with 9632 additions and 4759 deletions

View File

@ -1,45 +1,67 @@
# CinePlex: Your Plex on Steroids 🚀
# CinePlex: Your Media Universe on Steroids 🚀
## 🤔 What the Heck Is This? Prepare for Takeoff!
Let's be real. You love your Plex server. It's your own little corner of the digital universe, packed with your movies, shows, music, and photos. But... don't you ever feel like the interface could be... *more*? A little more ✨ **magic and sparkle** ✨?
Let's be real. You love your Plex and Jellyfin servers. They're your own little corner of the digital universe, packed with your movies, shows, music, and photos. But... don't you ever feel like the interface could be... *more*? A little more ✨ **magic and sparkle** ✨?
Well, **CinePlex** is the answer to your digital prayers! It's like you bought your Plex a superhero cape and sent it to a futuristic spa. It's a modern, fast, and stunningly designed interface that sits on top of your Plex servers to give you a visually dazzling experience.
Well, **CinePlex** is the answer to your digital prayers! It's like you bought your media server a superhero cape and sent it to a futuristic spa. It's a modern, fast, and stunningly designed interface that sits on top of your servers to give you a visually dazzling experience.
**In short:** CinePlex is not a streaming service. It's a *radical transformation* for the content **you already own**. It's your personal media universe, but with steroids and a lot more style!
---
## 🌐 We Speak All Languages! (Well, Almost)
Goodbye, language barrier! 👋 We've been working hard in the lab, and now CinePlex is a polyglot. Thanks to the magic of Chrome's `i18n` API, the extension is fully translated and ready to speak your language. Yes, you heard that right! No more excuses not to dive into your favorite content.
**We currently support:** Spanish, English, German, and French. And we're adding more!
---
## ✨ The Feature Loot Drop (What Makes It Awesome!)
Get ready to be amazed by everything CinePlex has under the hood. This isn't just an extension; it's an experience!
* **🎬 Pimped-Out Interface:** Forget boring UIs. We use TheMovieDB's API to bring you high-res posters, spectacular backdrops, gripping synopses, ratings, cast info... all the juicy movie gossip you crave!
* **🗣️ Multilingual Maestro:** Hola! Bonjour! Hallo! CinePlex now speaks your language with full i18n support. No more getting lost in translation your media, your language!
* **📡 Psychic Plex Scanner:** You give it your Plex tokens, and CinePlex goes into full detective mode. It scans your servers, figures out what you *actually* have, and jots it down in its secret notebook (a local IndexedDB database in your browser). It's like having a personal librarian for your media!
* **✅ "Got It" Badge of Honor:** See a movie you want to watch? CinePlex will let you know if you already have it on your server with a neat "Local" badge. No more blind searching!
* **🎶 Music Jukebox 2077:** It's not all about movies. We've built a full-fledged music player that connects directly to your Plex music library. Browse artists, listen to albums, and rock out with a **graphic equalizer and audio visualizer**! Your personal party, guaranteed!
* **🌐 Universal Translator:** Hola! Bonjour! Hallo! CinePlex speaks your language with full `i18n` support. No more getting lost in translation your media, your language!
* **📡 **Multi-Server Fusion Engine (Plex & Jellyfin!):** CinePlex is no longer just for Plex! Now you can connect your Jellyfin servers too. It scans your Plex tokens and Jellyfin credentials, figures out what you *actually* have across all your servers, and unifies it all in one seamless interface. It's your personal librarian for a multi-platform media empire!
* **🤖 **Your Personal AI Assistant (Powered by Gemini):** This is the game-changer! CinePlex includes a built-in AI chat assistant. Ask it to find movies, play music, show you your stats, or even clean up your history. It's like having JARVIS for your media library.
* **✅ "Got It" Badge of Honor:** See a movie you want to watch? CinePlex will let you know if you already have it on any of your connected servers with a neat "Local" badge. No more blind searching!
* **🎶 Music Jukebox 2077:** It's not all about movies. We've built a full-fledged music player that connects to your Plex and Jellyfin music libraries. Browse artists, listen to albums, and rock out with a **graphic equalizer and audio visualizer**! Your personal party, guaranteed!
* **📊 The Nerd Stats Panel:** Ever wondered how many 80s movies you have? Or what your most common genre is? Dive into the statistics panel and get a full breakdown of your media library with amazing charts. Unleash your inner nerd!
* **🖼️ Your Personal Photo Gallery:** Connect to your Plex photo libraries and browse your albums and pictures with a beautiful, integrated lightbox viewer. Relive your memories in style!
* **📜 The Scroll of Power (M3U):** For the power users. Found that series you wanted to watch? Generate an `.m3u` file with all the episode links with a single click. Power in your hands!
* **📜 The Scroll of Power (M3U Generator):** For the power users. Select any server (Plex or Jellyfin), pick the libraries you want, and generate an `.m3u` file with all the direct stream links. Perfect for use in other players like VLC or IPTV apps.
* **🔥 Stream Straight to Your Server:** This is where it gets wild. Configure a simple PHP script on your server, and you can send streams from CinePlex directly to your M3U playlist file with a single click. We even give you a **PHP script generator** to make it foolproof!
* **❤️ Favorites & Goldfish Memory:** Save your favorite movies and shows. Plus, we've got a "History" section so you can remember what you were watching last night before you fell asleep on the couch. Never lose track again!
* **🧠 AI-Powered Recommendations:** Based on your viewing history and favorites, CinePlex will suggest new content you might love. It's like having a personal movie critic living in your browser.
* **🔧 Customization Tuning Shop:** Don't like the dark theme? Switch to light mode! Don't want the giant hero banner? Hide it! Add your own TMDB API key. You're the boss, this is your extension!
* **🔧 Customization Tuning Shop:** Don't like the dark theme? Switch to light mode! Don't want the giant hero banner? Hide it! Add your own TMDB and Google AI API keys. You're the boss, this is your extension!
---
## 🤖 Meet Your AI Assistant: Talk to Your Media!
Click the chat bubble and say hello to your new AI-powered copilot. It's integrated with Google's Gemini and can understand and execute commands within CinePlex. It's not just a search bar; it's a command center.
**Here are a few things you can ask it to do:**
* **Navigate the App:**
* `"Show me my favorite movies."`
* `"Take me to the statistics page."`
* `"Open the M3U generator."`
* **Find Content:**
* `"Do I have the movie 'Inception' on my server?"`
* `"Search for all the 'Star Wars' series."`
* **Control Your Media:**
* `"Play music by Queen."`
* `"Add 'The Matrix' to my favorites."`
* `"I want to download a list of all James Bond movies I have, name the file 007.m3u"`
* **Manage the App:**
* `"Clear my viewing history."`
* `"I need to delete all my favorites."`
* `"Update all my Plex servers."`
Try it out! The possibilities are vast.
---
## 🛠️ Installation and First Steps: Liftoff in 3, 2, 1...!
Get ready for adventure! Getting this beast up and running is easier than finding popcorn at the movies. Follow these simple steps and you'll be navigating your Plex like a Starship captain.
Get ready for adventure! Getting this beast up and running is easier than finding popcorn at the movies. Follow these simple steps and you'll be navigating your media like a Starship captain.
### 1. Installing the Extension: The First Quantum Leap!
@ -56,31 +78,31 @@ Since we're not yet on the Chrome Web Store (but we will be, oh yes!), you'll ha
When you first open CinePlex, it's like a newly built spaceship: impressive, but it needs fuel and coordinates. Let's bring it to life!
1. **Find Your Plex Token: The Master Key to the Universe!** This is the MOST important step. You need your `X-Plex-Token` to let CinePlex talk to your Plex server. The easiest way is to follow the official Plex guide: [Finding an Authentication Token / X-Plex-Token](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/). Don't share this key with anyone, it's yours and yours alone!
1. **Get Your Credentials: The Keys to the Kingdom!**
* **For Plex:** You need your `X-Plex-Token`. The easiest way is to follow the official Plex guide: [Finding an Authentication Token / X-Plex-Token](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/).
* **For Jellyfin:** You'll need your server URL, username, and password.
2. **Open CinePlex Settings: The Control Panel!**
* Click the CinePlex icon in your browser's toolbar to open the application in a new tab.
* Click the **cogwheel icon (⚙️)** in the top-right corner to open the Settings modal. This is where the magic happens!
3. **Add Your Token: Injecting the Fuel!**
* Go to the **Plex** tab.
* You'll see a code editor. Paste your `X-Plex-Token` inside the square brackets `[]`. If you have more than one (how lucky!), separate them with commas. It should look something like this:
3. **Add Your Servers: Injecting the Fuel!**
* **For Plex:** Go to the **Plex** tab. Paste your `X-Plex-Token`(s) inside the code editor.
```json
{
"tokens": [
"YourPlexTokenGoesHere_abc123",
"AnotherTokenIfYouHaveOne_def456"
"YourPlexTokenGoesHere_abc123"
]
}
```
* Click the **"Save Tokens"** button. You've secured the connection!
* **For Jellyfin:** Go to the **Jellyfin** tab. Enter your server URL, username, and password.
* **API Keys (Optional but Recommended):** In the **General** tab, add your free API key from [TheMovieDB](https://www.themoviedb.org/settings/api) for metadata and your [Google AI Studio](https://aistudio.google.com/app/apikey) key to power the AI Assistant.
4. **Start Your First Scan: The Great Exploration!**
* Still in the Plex tab, check the boxes for the content you want to scan (e.g., Movies, Series, Music, Photos).
* Click the big blue **"Start Scan"** button. It's time for CinePlex to discover all your treasures!
* A console will appear at the bottom of the main page, showing you the scanner's progress. Be patient, the first scan can take a few minutes if you have a gigantic library. Rome wasn't built in a day, and your Plex library won't be scanned in a second either!
* In the settings for Plex and/or Jellyfin, click the **"Start Scan"** or **"Connect and Scan"** button. It's time for CinePlex to discover all your treasures!
* A console will appear at the bottom of the main page, showing you the scanner's progress. Be patient, the first scan can take a few minutes if you have a gigantic library.
5. **Enjoy!** Once the scan is complete, the app will automatically refresh. Go back to the main view and start exploring your newly supercharged Plex interface! The galaxy of your content awaits!
5. **Enjoy!** Once the scan is complete, the app will automatically refresh. Go back to the main view and start exploring your newly supercharged media universe!
---
@ -88,19 +110,20 @@ When you first open CinePlex, it's like a newly built spaceship: impressive, but
Want to take your experience to the next level and use the "Add Stream" button? It's like having a magic wand for your content!
1. In the CinePlex settings, go to the **"PHP Generator"** tab.
1. In the CinePlex settings, go to the **"PHP Gen"** tab.
2. Configure the options (like the filename, save path, and security key) and click **"Generate Script"**. Watch as the magic of code materializes!
3. Copy the generated PHP code. It's your secret recipe!
4. Save that code as a `.php` file (e.g., `playlist.php`) and upload it to a web server you control. Your own corner on the web for your streams!
5. Go to the **"General"** tab in settings and paste the public URL to your new script in the **"Stream Server URL"** field. Connecting the dots!
4. Save that code as a `.php` file (e.g., `playlist.php`) and upload it to a web server you control.
5. Go to the **"General"** tab in settings and paste the public URL to your new script in the **"PHP Script URL"** field.
6. Save, and you're ready! Now you can add streams with a single click. It's like teleporting your content wherever you need it!
---
## 🔒 Permissions: What Does CinePlex Need?
## 🔒 Permissions: What Does CinePlex Need and Why?
CinePlex is designed to be as non-intrusive as possible, but it does need a few permissions to work its magic:
* **`storage`**: This allows CinePlex to store your Plex tokens, scanned library data, and your personalized settings directly in your browser's local storage. All your data stays on your machine, safe and sound!
* **`notifications`**: Used to send you helpful notifications, for example, when a scan is complete or if there's an important update.
* **`host_permissions` for `https://*.plex.tv/*`**: This is crucial! It allows CinePlex to communicate directly with your Plex servers to fetch your library information. Without this, CinePlex wouldn't be able to see your awesome media collection.
* **`storage`**: Allows CinePlex to store your server credentials, scanned library data, and your personalized settings directly in your browser's local storage. All your data stays on your machine, safe and sound!
* **`notifications`**: Used to send you helpful notifications, for example, when a scan is complete.
* **`host_permissions` for `https://*.plex.tv/*`**: This is crucial! It allows CinePlex to communicate with the main Plex API to find your servers associated with your token.
* **`host_permissions` for `http://*/*` and `https://*/*`**: **This sounds scary, but it's necessary!** Because your Plex and Jellyfin servers can be on any IP address (e.g., `192.168.1.100`, `my-server.local`, a public domain), the extension needs this broad permission to be able to connect to *your specific server addresses*. **CinePlex ONLY communicates with the server URLs you provide or that it discovers via the official Plex API. It does not read data from any other websites you visit.** This permission is essential for the core functionality of connecting to your personal media servers.

View File

@ -1,7 +1,7 @@
{
"appName": { "message": "CinePlex" },
"appDescription": { "message": "Sucht auf Plex-Servern nach Inhalten und zeigt sie in der Benutzeroberfläche an" },
"appTagline": { "message": "Filme, Serien & Musik" },
"appDescription": { "message": "Sucht Plex-Server nach Inhalten und zeigt sie in der Benutzeroberfläche an" },
"appTagline": { "message": "Filme, Serien und Musik" },
"appLocaleCode": { "message": "de-DE" },
"toggleNavigation": { "message": "Navigation umschalten" },
"searchPlaceholder": { "message": "Suche nach Filmen oder Serien..." },
@ -9,16 +9,18 @@
"settings": { "message": "Einstellungen" },
"navMovies": { "message": "Filme" },
"navSeries": { "message": "Serien" },
"navProviders": { "message": "Anbieter" },
"navPhotos": { "message": "Fotos" },
"navStats": { "message": "Statistiken" },
"navFavorites": { "message": "Favoriten" },
"navHistory": { "message": "Verlauf" },
"navRecommendations": { "message": "Empfehlungen" },
"navMusic": { "message": "Musik" },
"navM3uGenerator": { "message": "M3U-Generator" },
"heroWelcome": { "message": "Willkommen bei CinePlex" },
"heroSubtitle": { "message": "Entdecke Tausende von Filmen und Serien." },
"heroSubtitle": { "message": "Entdecken Sie Tausende von Filmen und Serien." },
"addStream": { "message": "Stream hinzufügen" },
"moreInfo": { "message": "Mehr Infos" },
"moreInfo": { "message": "Mehr Info" },
"popularMovies": { "message": "Beliebte Filme" },
"allGenres": { "message": "Alle Genres" },
"allYears": { "message": "Alle Jahre" },
@ -27,70 +29,80 @@
"sortRecent": { "message": "Neueste" },
"loadMore": { "message": "Mehr laden" },
"photosBreadcrumbHome": { "message": "Alben" },
"selectServer": { "message": "Einen Server auswählen" },
"loading": { "message": "Lädt..." },
"selectServer": { "message": "Wählen Sie einen Server" },
"loading": { "message": "Laden..." },
"loadingLibraries": { "message": "Lade Bibliotheken..." },
"photosEmptyState": { "message": "Keine Alben oder Fotos gefunden." },
"photosEmptyStateSub": { "message": "Bitte wähle einen Server aus oder stelle sicher, dass du eine Fotobibliothek in Plex hast." },
"photosEmptyStateSub": { "message": "Bitte wählen Sie einen Server aus oder stellen Sie sicher, dass Sie eine Fotobibliothek in Plex haben." },
"statsTitle": { "message": "Bibliotheksstatistiken" },
"statsAllTokens": { "message": "Alle Tokens" },
"statsAllTokens": { "message": "Alle Token" },
"statsAnalyzing": { "message": "Analysiere deine Bibliothek..." },
"statsActiveTokens": { "message": "Aktive Tokens" },
"statsActiveTokens": { "message": "Aktive Token" },
"statsServersFound": { "message": "Gefundene Server" },
"statsUniqueMovies": { "message": "Einzigartige Filme" },
"statsUniqueSeries": { "message": "Einzigartige Serien" },
"statsUniqueArtists": { "message": "Einzigartige Künstler" },
"statsTokenServers": { "message": "Server des Tokens" },
"statsTokenServers": { "message": "Token-Server" },
"statsChartMoviesByGenre": { "message": "Inhalt nach Genre (Filme)" },
"statsChartSeriesByGenre": { "message": "Inhalt nach Genre (Serien)" },
"statsChartByDecade": { "message": "Inhalt nach Jahrzehnt" },
"recommendationsTitle": { "message": "Empfehlungen für dich" },
"historyTitle": { "message": "Wiedergabeverlauf" },
"clearHistory": { "message": "Alles löschen" },
"consoleTitle": { "message": "Plex Scan-Konsole" },
"footerCredit": { "message": "Eine Oberfläche für dein Plex-Universum." },
"backButton": { "message": "Zurück" },
"consoleTitle": { "message": "Plex-Scan-Konsole" },
"footerCredit": { "message": "Eine Oberfläche für Ihr Plex-Universum." },
"closeTrailer": { "message": "Trailer schließen" },
"close": { "message": "Schließen" },
"photoViewer": { "message": "Fotoanzeige" },
"photoViewer": { "message": "Fotobetrachter" },
"previous": { "message": "Zurück" },
"next": { "message": "Weiter" },
"notificationTemplateText": { "message": "Benachrichtigung" },
"settingsTitleFull": { "message": "Einstellungen und Konfiguration" },
"settingsTabGeneral": { "message": "Allgemein" },
"settingsTabPlex": { "message": "Plex" },
"settingsTabJellyfin": { "message": "Jellyfin" },
"settingsTabPhpGen": { "message": "PHP-Generator" },
"settingsTabData": { "message": "Daten" },
"settingsApiServer": { "message": "API- und Serverkonfiguration" },
"settingsTmdbApiLabel": { "message": "TMDB API-Schlüssel (Optional)" },
"settingsTmdbApiPlaceholder": { "message": "Verwendet den Standardschlüssel, wenn leer gelassen" },
"settingsTmdbLangLabel": { "message": "Sprache für TMDB & UI" },
"settingsApiServer": { "message": "API- und Server-Einstellungen" },
"settingsTmdbApiLabel": { "message": "TMDB-API-Schlüssel (optional)" },
"settingsTmdbApiPlaceholder": { "message": "Der Standardschlüssel wird verwendet, wenn das Feld leer gelassen wird" },
"settingsGoogleApiLabel": { "message": "Google Gemini-API-Schlüssel (optional)" },
"settingsGoogleApiPlaceholder": { "message": "Wird für die Nutzung des KI-Assistenten benötigt" },
"settingsRegionLabel": { "message": "Region für die Inhaltsentdeckung" },
"allRegions": { "message": "Alle Regionen" },
"settingsPhpUrlLabel": { "message": "Server-URL zum Hinzufügen von Streams" },
"settingsPhpUrlPlaceholder": { "message": "https://dein-server.com/pfad/zum/script.php" },
"settingsInterface": { "message": "Oberfläche" },
"settingsPhpUrlPlaceholder": { "message": "https://ihr-server.com/pfad/zum/skript.php" },
"settingsInterface": { "message": "Benutzeroberfläche" },
"settingsLightTheme": { "message": "Heller Modus" },
"settingsShowHero": { "message": "Willkommensbereich \"Hero\" anzeigen" },
"settingsScanContent": { "message": "Inhaltsscan" },
"settingsScanDesc": { "message": "Wähle aus, was gescannt werden soll, und drücke die Taste." },
"settingsShowHero": { "message": "Willkommensbereich 'Hero' anzeigen" },
"settingsScanContent": { "message": "Inhalt scannen" },
"settingsScanDesc": { "message": "Wählen Sie aus, was gescannt werden soll, und drücken Sie die Taste." },
"settingsScanMovies": { "message": "Filme" },
"settingsScanShows": { "message": "Serien" },
"settingsScanArtists": { "message": "Musik" },
"settingsScanPhotos": { "message": "Fotos" },
"settingsSelectAll": { "message": "Alles auswählen" },
"settingsStartScan": { "message": "Scan starten" },
"settingsPlexTokens": { "message": "Plex-Tokens" },
"settingsPlexTokensDesc": { "message": "Bearbeite die Liste der Plex-Tokens (JSON-Format)." },
"settingsSaveTokens": { "message": "Tokens speichern" },
"settingsPhpGenTitle": { "message": "PHP-Server-Skript-Generator" },
"settingsPlexTokens": { "message": "Plex-Token" },
"settingsPlexTokensDesc": { "message": "Bearbeiten Sie die Liste der Plex-Token (JSON-Format)." },
"settingsSaveTokens": { "message": "Token speichern" },
"settingsJellyfinTitle": { "message": "Jellyfin-Einstellungen" },
"settingsJellyfinDesc": { "message": "Fügen Sie Ihre Jellyfin-Serverdetails hinzu, um deren Inhalte zu scannen." },
"jellyfinUrlLabel": { "message": "Jellyfin-Server-URL" },
"jellyfinUserLabel": { "message": "Benutzername" },
"jellyfinPassLabel": { "message": "Passwort" },
"jellyfinConnectAndScan": { "message": "Verbinden und scannen" },
"settingsPhpGenTitle": { "message": "PHP-Skript-Generator für Server" },
"settingsPhpFileOptions": { "message": "Dateioptionen" },
"settingsPhpSavePathLabel": { "message": "Speicherpfad auf dem Server" },
"settingsPhpSavePathPlaceholder": { "message": "Bsp: /var/www/html/listen (leer für denselben Ordner)" },
"settingsPhpSavePathPlaceholder": { "message": "Bsp.: /var/www/html/listen (leer für denselben Ordner)" },
"settingsPhpFilenameLabel": { "message": "Dateiname" },
"settingsPhpFileAction": { "message": "Dateiaktion" },
"settingsPhpActionAppend": { "message": "An das Ende der Datei anhängen (kumulativ)" },
"settingsPhpActionOverwrite": { "message": "Datei überschreiben (neu beginnen)" },
"settingsPhpSecurity": { "message": "Sicherheit (Optional)" },
"settingsPhpUseSecretKey": { "message": "Geheimschlüssel verwenden (Empfohlen)" },
"settingsPhpSecretKeyPlaceholder": { "message": "Gib einen sicheren Geheimschlüssel ein" },
"settingsPhpActionAppend": { "message": "Am Ende der Datei anfügen (kumulativ)" },
"settingsPhpActionOverwrite": { "message": "Datei überschreiben (von vorne beginnen)" },
"settingsPhpSecurity": { "message": "Sicherheit (optional)" },
"settingsPhpUseSecretKey": { "message": "Geheimschlüssel verwenden (empfohlen)" },
"settingsPhpSecretKeyPlaceholder": { "message": "Geben Sie einen sicheren Geheimschlüssel ein" },
"settingsPhpGeneratedCode": { "message": "Generierter Code" },
"settingsPhpGeneratedPlaceholder": { "message": "Der generierte PHP-Code wird hier angezeigt." },
"settingsGenerateScript": { "message": "Skript generieren" },
@ -99,20 +111,20 @@
"settingsImportDb": { "message": "DB aus Datei importieren" },
"settingsExportDb": { "message": "DB in Datei exportieren" },
"settingsClearContent": { "message": "Lokale Inhaltsdaten löschen" },
"settingsClearContentDesc": { "message": "Diese Aktion löscht Filme, Serien und Musik aus der lokalen Datenbank, hat aber keine Auswirkungen auf deine Favoriten oder Einstellungen." },
"settingsClearContentDesc": { "message": "Diese Aktion löscht Filme, Serien und Musik aus der lokalen Datenbank, hat aber keine Auswirkungen auf Ihre Favoriten oder Ihre Einstellungen." },
"settingsClose": { "message": "Schließen" },
"settingsSave": { "message": "Einstellungen speichern" },
"musicSidenavTitle": { "message": "Plex Musik" },
"musicSidenavTitle": { "message": "Plex-Musik" },
"musicAllServers": { "message": "Alle Server" },
"musicSearchArtistPlaceholder": { "message": "Künstler suchen..." },
"musicSearchArtistPlaceholder": { "message": "Suche nach einem Künstler..." },
"musicSearchDiscographyPlaceholder": { "message": "In Diskografie suchen..." },
"musicNothingPlaying": { "message": "Nichts wird abgespielt" },
"musicSelectSong": { "message": "Wähle ein Lied" },
"musicSelectSong": { "message": "Wählen Sie ein Lied" },
"musicToStart": { "message": "um die Wiedergabe zu starten" },
"miniplayerDownloadSong": { "message": "Lied herunterladen" },
"miniplayerDownloadAlbum": { "message": "M3U-Album herunterladen" },
"miniplayerVolume": { "message": "Lautstärke" },
"miniplayerShuffle": { "message": "Zufall" },
"miniplayerShuffle": { "message": "Zufallswiedergabe" },
"miniplayerEqualizer": { "message": "Equalizer" },
"miniplayerOpenList": { "message": "Liste öffnen" },
"eqTitle": { "message": "Grafischer Equalizer" },
@ -124,7 +136,7 @@
"eqPresetClassical": { "message": "Klassik" },
"eqPresetBassBoost": { "message": "Bass-Boost" },
"eqPreampLabel": { "message": "Vorverstärker" },
"infoModalTitle": { "message": "Information" },
"infoModalTitle": { "message": "Informationen" },
"infoModalFieldTitle": { "message": "Titel:" },
"infoModalFieldArtist": { "message": "Künstler:" },
"infoModalFieldAlbum": { "message": "Album:" },
@ -137,21 +149,21 @@
"lang_de": { "message": "Deutsch" },
"lang_it": { "message": "Italienisch" },
"lang_pt": { "message": "Portugiesisch" },
"essentialFeaturesNotSupported": { "message": "Dein Browser unterstützt wesentliche Funktionen nicht." },
"essentialFeaturesNotSupported": { "message": "Ihr Browser unterstützt keine wesentlichen Funktionen." },
"dbAccessError": { "message": "Fehler beim Zugriff auf die lokale Datenbank." },
"dbUpdateNeeded": { "message": "Die Datenbank muss aktualisiert werden, bitte lade die Seite neu." },
"dbBlocked": { "message": "Bitte schließe andere Tabs dieser Anwendung, um fortzufahren." },
"dbUpdateNeeded": { "message": "Die Datenbank muss aktualisiert werden, bitte laden Sie die Seite neu." },
"dbBlocked": { "message": "Bitte schließen Sie andere Tabs dieser Anwendung, um fortzufahren." },
"deletingContentData": { "message": "Lokale Inhaltsdaten werden gelöscht..." },
"noContentDataToDelete": { "message": "Keine Inhaltsdaten zum Löschen." },
"noContentDataToDelete": { "message": "Keine Inhaltsdaten zum Löschen vorhanden." },
"contentDataDeleted": { "message": "Inhaltsdaten aus IndexedDB gelöscht." },
"errorDeletingData": { "message": "Fehler beim Löschen der Daten: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorDeletingData": { "message": "Fehler beim Löschen von Daten: $message$", "placeholders": { "message": { "content": "$1" } } },
"aceEditorNotAvailable": { "message": "Texteditor nicht verfügbar." },
"errorLoadingTokens": { "message": "Fehler beim Laden der Tokens in den Editor." },
"errorLoadingTokensMessage": { "message": "Fehler beim Laden der Tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingTokens": { "message": "Fehler beim Laden der Token zur Bearbeitung." },
"errorLoadingTokensMessage": { "message": "Fehler beim Laden der Token: $message$", "placeholders": { "message": { "content": "$1" } } },
"aceEditorNotAvailableToSave": { "message": "Editor zum Speichern nicht verfügbar." },
"invalidJsonFormat": { "message": "Ungültiges JSON-Format. Muss { \"tokens\": [...] } sein" },
"tokensSaved": { "message": "Tokens erfolgreich gespeichert." },
"errorSavingTokens": { "message": "Fehler beim Speichern der Tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"invalidJsonFormat": { "message": "Ungültiges JSON-Format. Es muss { \"tokens\": [...] } sein" },
"tokensSaved": { "message": "Token erfolgreich gespeichert." },
"errorSavingTokens": { "message": "Fehler beim Speichern der Token: $message$", "placeholders": { "message": { "content": "$1" } } },
"dbNotAvailable": { "message": "IndexedDB ist nicht verfügbar." },
"dbExported": { "message": "Datenbank erfolgreich exportiert." },
"errorExportingDb": { "message": "Fehler beim Exportieren der Datenbank: $message$", "placeholders": { "message": { "content": "$1" } } },
@ -159,13 +171,13 @@
"noDataToImport": { "message": "Die Datei enthält keine Daten für die aktuellen DB-Abschnitte." },
"dbImported": { "message": "Datenbank erfolgreich importiert." },
"errorImportingDb": { "message": "Fehler beim Importieren der Datenbank: $message$", "placeholders": { "message": { "content": "$1" } } },
"updatingView": { "message": "Ansicht wird mit neuen Daten aktualisiert..." },
"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." },
"trailerNotFound": { "message": "Für diesen Titel wurde kein Trailer gefunden." },
"confirmClearHistory": { "message": "Bist du sicher, dass du deinen gesamten Wiedergabeverlauf löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden." },
"updatingView": { "message": "Ansicht mit neuen Daten wird aktualisiert..." },
"confirmClearContent": { "message": "Sind Sie sicher, dass Sie lokale Inhaltsdaten (Filme, Serien, Musik usw.) löschen möchten? Favoriten und Einstellungen werden NICHT gelöscht." },
"trailerNotFound": { "message": "Kein Trailer für diesen Titel gefunden." },
"confirmClearHistory": { "message": "Sind Sie sicher, dass Sie Ihren gesamten Wiedergabeverlauf löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden." },
"historyCleared": { "message": "Wiedergabeverlauf gelöscht." },
"historyItemDeleted": { "message": "Eintrag aus dem Verlauf gelöscht." },
"errorGeneratingScript": { "message": "Generiere zuerst ein Skript, um es kopieren zu können." },
"historyItemDeleted": { "message": "Element aus dem Verlauf gelöscht." },
"errorGeneratingScript": { "message": "Generieren Sie zuerst ein Skript, um es kopieren zu können." },
"scriptCopied": { "message": "PHP-Skript in die Zwischenablage kopiert." },
"errorCopyingScript": { "message": "Fehler beim Kopieren des Skripts." },
"scriptGenerated": { "message": "PHP-Skript generiert." },
@ -175,116 +187,258 @@
"errorLoadingGenres": { "message": "Fehler beim Laden" },
"noContentFound": { "message": "Keine Ergebnisse gefunden." },
"couldNotLoadContent": { "message": "Inhalt konnte nicht geladen werden." },
"noFavorites": { "message": "Du hast noch keine Favoriten." },
"noFavorites": { "message": "Sie haben noch keine Favoriten." },
"errorLoadingFavorites": { "message": "Fehler beim Laden der Favoriten." },
"historyEmpty": { "message": "Dein Verlauf ist leer." },
"historyEmptySub": { "message": "Durchsuche und schaue Inhalte an, damit sie hier erscheinen." },
"errorGeneratingRecommendations": { "message": "Fehler beim Erstellen von Empfehlungen." },
"noRecommendations": { "message": "Wir müssen dich besser kennenlernen, um dir Empfehlungen geben zu können!" },
"errorGeneratingStats": { "message": "Fehler beim Erstellen von Statistiken." },
"historyEmpty": { "message": "Ihr Verlauf ist leer." },
"historyEmptySub": { "message": "Entdecken und sehen Sie sich Inhalte an, damit sie hier erscheinen." },
"errorGeneratingRecommendations": { "message": "Fehler beim Generieren von Empfehlungen." },
"noRecommendations": { "message": "Wir müssen Sie besser kennenlernen, um Ihnen Empfehlungen geben zu können!" },
"errorGeneratingStats": { "message": "Fehler beim Generieren von Statistiken." },
"noServersForToken": { "message": "Keine zugehörigen Server für dieses Token gefunden." },
"searchingActorContent": { "message": "Suche nach Inhalten von $actorName$", "placeholders": { "actorName": { "content": "$1" } } },
"errorLoadingActorContent": { "message": "Inhalt für $actorName$ konnte nicht geladen werden.", "placeholders": { "actorName": { "content": "$1" } } },
"errorAddingStream": { "message": "Fehler beim Hinzufügen von Stream(s): $message$", "placeholders": { "message": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "Die PHP-Server-URL ist nicht konfiguriert. Bitte richte sie in den Einstellungen ein." },
"phpUrlNotConfigured": { "message": "Die PHP-Server-URL ist nicht konfiguriert. Bitte konfigurieren Sie sie in den Einstellungen." },
"searchingStreams": { "message": "Suche nach Streams für \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"sendingStreams": { "message": "Sende $count$ Stream(s) an den Server...", "placeholders": { "count": { "content": "$1" } } },
"streamAddedSuccess": { "message": "Stream(s) erfolgreich hinzugefügt." },
"generatingM3U": { "message": "Generiere M3U für \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"m3uDownloaded": { "message": "M3U für \"$title$\" heruntergeladen.", "placeholders": { "title": { "content": "$1" } } },
"errorGeneratingM3U": { "message": "Fehler beim Generieren der M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
"m3uDownloaded": { "message": "\"$title$\" heruntergeladen.", "placeholders": { "title": { "content": "$1" } } },
"errorGeneratingM3U": { "message": "Fehler beim Generieren von M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
"settingsSavedSuccess": { "message": "Einstellungen erfolgreich gespeichert." },
"errorSavingSettings": { "message": "Fehler beim Speichern der Einstellungen in der Datenbank." },
"languageChangeReload": { "message": "Sprache geändert. Die Anwendung wird jetzt neu geladen." },
"addedToFavorites": { "message": "Zu Favoriten hinzugefügt." },
"removedFromFavorites": { "message": "Aus Favoriten entfernen." },
"addedToFavorites": { "message": "Zu den Favoriten hinzugefügt." },
"removedFromFavorites": { "message": "Aus den Favoriten entfernt." },
"plexScanInProgress": { "message": "Plex-Scan läuft bereits." },
"plexScanStarting": { "message": "Starte Plex-Scan..." },
"noPlexTokens": { "message": "Keine Plex-Tokens konfiguriert." },
"plexScanStarting": { "message": "Plex-Scan wird gestartet..." },
"noPlexTokens": { "message": "Keine Plex-Token konfiguriert." },
"clearingSections": { "message": "Lösche Abschnitte: $sections$", "placeholders": { "sections": { "content": "$1" } } },
"sectionsCleared": { "message": "Abschnitte gelöscht." },
"tokenFoundServers": { "message": "Token $token$... hat $count$ Server gefunden.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } },
"errorProcessingToken": { "message": "Fehler bei der Verarbeitung von Token $token$...: $message$", "placeholders": { "token": { "content": "$1" }, "message": { "content": "$2" } } },
"initialScanPhaseComplete": { "message": "Initiale Scan-Phase abgeschlossen." },
"initialScanPhaseComplete": { "message": "Erste Scanphase abgeschlossen." },
"retryPhaseFinished": { "message": "Wiederholungsphase abgeschlossen." },
"plexScanFinished": { "message": "Scan abgeschlossen. Aktualisiere Inhalte..." },
"plexScanFatalError": { "message": "FATALER FEHLER: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorDuringScan": { "message": "Fehler während des Scans: $message$", "placeholders": { "message": { "content": "$1" } } },
"plexScanFinished": { "message": "Scan abgeschlossen. Inhalt wird aktualisiert..." },
"scanCancelled": { "message": "Scan vom Benutzer abgebrochen." },
"scanCancelledInfo": { "message": "Scan abgebrochen." },
"retyingSection": { "message": "Wiederhole Abschnitt \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"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" } } },
"noRetriesPending": { "message": "Keine ausstehenden Wiederholungen." },
"startingRetryPhase": { "message": "Starte Wiederholungsphase für $count$ Abschnitte...", "placeholders": { "count": { "content": "$1" } } },
"retryPhaseCancelled": { "message": "Wiederholungsphase abgebrochen." },
"errorInitializingMusicPlayer": { "message": "Fehler beim Initialisieren des Musik-Players." },
"criticalErrorLoadingMusic": { "message": "Kritischer Fehler beim Laden der Musikdaten." },
"errorLoadingArtists": { "message": "Fehler beim Laden der Künstler." },
"dbUnavailableError": { "message": "Fehler: Datenbank nicht verfügbar." },
"updatingMusicData": { "message": "Aktualisiere Musikdaten..." },
"updatingMusicData": { "message": "Musikdaten werden aktualisiert..." },
"musicDataUpdated": { "message": "Musikdaten aktualisiert." },
"errorFetchingArtistSongs": { "message": "Fehler beim Abrufen der Lieder des Künstlers." },
"errorLoadingSongs": { "message": "Fehler beim Laden der Lieder." },
"noArtistsFound": { "message": "Keine Künstler gefunden." },
"artistsCounter": { "message": "$start$-$end$ von $total$", "placeholders": { "start": { "content": "$1" }, "end": { "content": "$2" }, "total": { "content": "$3" } } },
"artistsCounterSingle": { "message": "$total$ Künstler", "placeholders": { "total": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Lädt..." },
"noSongsFound": { "message": "Keine Lieder gefunden." },
"shuffleOn": { "message": "Zufallsmodus aktiviert." },
"shuffleOff": { "message": "Zufallsmodus deaktiviert." },
"downloadingSong": { "message": "Starte Download von \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" heruntergeladen.", "placeholders": { "title": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Fehler beim Herunterladen von \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Generiere M3U für \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
"albumM3UGenerated": { "message": "M3U für Album \"$artist$\" generiert.", "placeholders": { "artist": { "content": "$1" } } },
"shuffleOn": { "message": "Zufallswiedergabe ein." },
"shuffleOff": { "message": "Zufallswiedergabe aus." },
"playbackError": { "message": "Wiedergabefehler" },
"errorLabel": { "message": "Fehler" },
"reloadingPage": { "message": "Seite wird neu geladen..." },
"viewed": { "message": "Gesehen" },
"local": { "message": "Lokal" },
"topRatedSort": {"message": "Bestbewertet"},
"topRatedSort": {"message": "Am besten bewertet"},
"recentSort": {"message": "Neueste"},
"popularSort": {"message": "Beliebte"},
"popularSort": {"message": "Beliebteste"},
"moviesSectionTitle": {"message": "Filme"},
"seriesSectionTitle": {"message": "Serien"},
"searchResultsFor": {"message": "Ergebnisse für \"$query$\"", "placeholders": {"query": {"content": "$1"}}},
"contentFrom": {"message": "Inhalt von $actor$", "placeholders": {"actor": {"content": "$1"}}},
"explore": {"message": "Entdecken"},
"noGenre": {"message": "Unkategorisiert"},
"noGenre": {"message": "Ohne Kategorie"},
"synopsis": {"message": "Zusammenfassung"},
"noSynopsis": {"message": "Keine Zusammenfassung verfügbar."},
"director": {"message": "Regisseur:"},
"writer": {"message": "Autor:"},
"viewOnImdb": {"message": "Auf IMDb ansehen"},
"watchTrailer": {"message": "Trailer ansehen"},
"addToFavorites": {"message": "Zu Favoriten hinzufügen"},
"removeFromFavorites": {"message": "Aus Favoriten entfernen"},
"addToFavorites": {"message": "Zu den Favoriten hinzufügen"},
"removeFromFavorites": {"message": "Aus den Favoriten entfernen"},
"notAvailable": {"message": "Nicht verfügbar"},
"mainCast": {"message": "Hauptbesetzung"},
"seasonsAndEpisodes": {"message": "Staffeln & Episoden"},
"seasonsAndEpisodes": {"message": "Staffeln und Episoden"},
"similarContent": {"message": "Ähnlicher Inhalt"},
"filmography": {"message": "Filmografie"},
"availableOn": {"message": "Verfügbar auf"},
"episodesCount": {"message": "$count$ Episoden", "placeholders": {"count": {"content": "$1"}}},
"seasonsCount": {"message": "$count$ Staffeln", "placeholders": {"count": {"content": "$1"}}},
"runtimeMinutes": {"message": "$count$ Min.", "placeholders": {"count": {"content": "$1"}}},
"noTrailerFound": {"message": "Für diesen Titel wurde kein Trailer gefunden."},
"fatalInitError": {"message": "Fataler Initialisierungsfehler"},
"fatalInitErrorSub": {"message": "Die Anwendung konnte nicht geladen werden."},
"invalidStreamInfo": {"message": "Ungültige Information."},
"dbUnavailableForStreams": {"message": "Lokale Datenbank nicht verfügbar."},
"noPlexServersForStreams": {"message": "Keine Plex-Server."},
"notFoundOnServers": {"message": "\"$query$\" auf Servern nicht gefunden.", "placeholders": {"query": {"content": "$1"}}},
"runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}},
"noTrailerFound": {"message": "Kein Trailer für diesen Titel gefunden."},n "fatalInitError": {"message": "Fataler Initialisierungsfehler"},
"fatalInitErrorSub": {"message": "Die Anwendung konnte nicht geladen werden."},n "invalidStreamInfo": {"message": "Ungültige Informationen."},n "dbUnavailableForStreams": {"message": "Lokale Datenbank nicht verfügbar."},n "noPlexServersForStreams": {"message": "Keine Plex-Server."},n "notFoundOnServers": {"message": "\"$query$\" auf Plex-Servern nicht gefunden.", "placeholders": {"query": {"content": "$1"}}},
"relativeTime_justNow": { "message": "Gerade eben" },
"relativeTime_minutesAgo": { "message": "Vor $count$ Minuten", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_hoursAgo": { "message": "Vor $count$ Stunden", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_yesterday": { "message": "Gestern" },
"relativeTime_daysAgo": { "message": "Vor $count$ Tagen", "placeholders": { "count": { "content": "$1" } } },
"errorLoadingDetails": { "message": "Fehler beim Laden der Details" },
"errorLoadingLocalContent": { "message": "Fehler beim Laden lokaler Inhalte." },
"errorServerResponse": { "message": "Nicht erfolgreiche Antwort vom Server." },
"errorPlexApi": { "message": "Fehler $status$ von der Plex-API.", "placeholders": { "status": { "content": "$1" } } },
"errorLoadingLocalContent": { "message": "Fehler beim Laden des lokalen Inhalts." },
"errorServerResponse": { "message": "Nicht erfolgreiche Serverantwort." },
"errorPlexApi": { "message": "Plex-API-Fehler $status$.", "placeholders": { "status": { "content": "$1" } } },
"errorParsingPlexXml": { "message": "Fehler beim Parsen von Plex-XML." },
"untitled": { "message": "Ohne Titel" },
"itemCount": { "message": "$count$ Elemente", "placeholders": { "count": { "content": "$1" } } },
"noPhotoServers": { "message": "Keine Foto-Server" }
"noPhotoServers": { "message": "Keine Fotoserver" },
"jellyfinScanInProgress": { "message": "Jellyfin-Scan läuft bereits." },
"jellyfinScanning": { "message": "Scanne Jellyfin..." },
"jellyfinMissingCredentials": { "message": "Bitte vervollständigen Sie die Jellyfin-URL und den Benutzernamen." },
"jellyfinConnecting": { "message": "Verbinde mit Jellyfin unter: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinAuthFailed": { "message": "Jellyfin-Authentifizierung fehlgeschlagen: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinAuthSuccess": { "message": "Jellyfin-Authentifizierung erfolgreich." },
"jellyfinFetchingLibraries": { "message": "Rufe Bibliotheken ab..." },
"jellyfinFetchFailed": { "message": "Fehler beim Abrufen der Bibliotheken: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinNoMediaLibraries": { "message": "Keine Film- oder Serienbibliotheken in Jellyfin gefunden." },
"jellyfinLibrariesFound": { "message": "$count$ Medienbibliothek(en) gefunden.", "placeholders": { "count": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Erfolg] '$libraryName' gescannt, $count$ Titel hinzugefügt.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"jellyfinLibraryScanFailed": { "message": "Fehler beim Scannen der Bibliothek '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Jellyfin-Scan abgeschlossen. $movies$ Filme und $series$ Serien hinzugefügt.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Jellyfin-Anmeldeinformationen nicht konfiguriert." },
"notFoundOnJellyfin": { "message": "\"$query$\" auf Jellyfin nicht gefunden.", "placeholders": { "query": { "content": "$1" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" auf keinem Server gefunden.", "placeholders": { "query": { "content": "$1" } } },
"localOnPlex": { "message": "Auf Plex" },
"searchOnPlex": { "message": "Auf Plex suchen" },
"jellyfinTitle": { "message": "Jellyfin-Inhalt" },
"noJellyfinContent": { "message": "Kein Jellyfin-Inhalt gefunden." },
"noJellyfinContentSub": { "message": "Stellen Sie sicher, dass Sie Ihren Jellyfin-Server in den Einstellungen gescannt haben." },
"activityViewerTitle": { "message": "Server-Aktivitätsanzeige" },
"activitySelectServer": { "message": "Wählen Sie einen Server" },
"activityCheckBtn": { "message": "Aktualisieren" },
"activityNoSessions": { "message": "Keine aktiven Sitzungen auf diesem Server." },
"activitySessionUser": { "message": "Benutzer" },
"activitySessionDevice": { "message": "Gerät" },
"activitySessionContent": { "message": "Inhalt" },
"activitySessionState": { "message": "Status" },
"activitySessionIdentifier": { "message": "Client-Identifikator" },
"activityCopyID": { "message": "ID kopieren" },
"activityError": { "message": "Serveraktivität konnte nicht abgerufen werden." },
"activityCopied": { "message": "Identifikator in die Zwischenablage kopiert!" },
"activityCopyError": { "message": "Fehler beim Kopieren des Identifikators." },
"noProvidersFound": { "message": "Keine Anbieter gefunden." },
"availableOnPlex": { "message": "Verfügbar auf Plex" },
"m3uGeneratorTitle": { "message": "M3U-Listen-Generator" },
"selectAServer": { "message": "Wählen Sie einen Server..." },
"downloadM3u": { "message": "M3U herunterladen" },
"m3uGenerator": { "message": "M3U-Generator" },
"selectLibraries": { "message": "Bibliotheken auswählen" },
"howToUse": { "message": "Anwendung" },
"m3uInstruction1": { "message": "Wählen Sie einen Server aus der Liste." },
"m3uInstruction2": { "message": "Wählen Sie eine oder mehrere Bibliotheken aus, die einbezogen werden sollen." },
"m3uInstruction3": { "message": "Klicken Sie auf die Download-Schaltfläche." },
"m3uInstruction4": { "message": "Importieren Sie die .m3u-Datei in Ihren kompatiblen Player." },
"chatOpen": { "message": "Chat öffnen" },
"chatTitle": { "message": "KI-Assistent" },
"chatClose": { "message": "X" },
"chatPlaceholder": { "message": "Geben Sie Ihre Nachricht ein..." },
"chatSend": { "message": "➤" },
"chatWelcome": { "message": "Willkommen! Ich bin Ihr CinePlex-Assistent. Fragen Sie mich nach Filmen, Serien oder allem, was Sie sonst noch wissen möchten." },
"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." },
"chatApiInvalidResponse": { "message": "Die API hat eine ungültige Antwort zurückgegeben. Bitte versuchen Sie es erneut." },
"chatApiError": { "message": "Fehler bei der Kommunikation mit dem KI-Assistenten" },
"downloadAll": { "message": "Alles herunterladen" },
"download": { "message": "Herunterladen" },
"aiToolSearchLibraryDesc": { "message": "Durchsucht die Plex-Bibliothek des Benutzers nach Filmen oder Serien nach Titel." },
"aiToolSearchLibraryQueryParamDesc": { "message": "Der Titel des zu suchenden Films oder der zu suchenden Serie." },
"aiToolSearchLibraryTypeParamDesc": { "message": "Der Typ des zu suchenden Inhalts. Kann 'movie' für Filme oder 'series' für Serien sein. (Optional)." },
"aiToolSearchLibraryResolutionParamDesc": { "message": "Die zu suchende Videoauflösung (z. B. '4k', '1080p'). (Optional)." },
"aiToolSearchLibraryContainerParamDesc": { "message": "Das zu suchende Video-Containerformat (z. B. 'mkv', 'mp4'). (Optional)." },
"aiToolNavigateToPageDesc": { "message": "Navigiert den Benutzer zu einer bestimmten Seite der Anwendungsoberfläche." },
"aiToolNavigateToPagePageParamDesc": { "message": "Der Name der Seite, zu der navigiert werden soll, z. B.: 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers' oder 'm3u-generator'." },
"aiToolGetUserStatsDesc": { "message": "Ruft die Bibliotheksstatistiken des Benutzers ab und zeigt sie an, z. B. die Gesamtzahl der einzigartigen Filme, Serien und Künstler." },
"aiToolShowItemDetailsDesc": { "message": "Zeigt die Detailseite eines bestimmten Films oder einer bestimmten Serie nach Titel und Typ an." },
"aiToolShowItemDetailsTitleParamDesc": { "message": "Der genaue Titel des Films oder der Serie." },
"aiToolShowItemDetailsTypeParamDesc": { "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." },
"aiToolAddToPlaylistTitleParamDesc": { "message": "Der Titel des hinzuzufügenden Films oder der hinzuzufügenden Serie." },
"aiToolAddToPlaylistTypeParamDesc": { "message": "Der Typ des Inhalts. 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." },
"aiToolCheckAndDownloadTitlesParamDesc": { "message": "Ein Array von Film- oder Serientiteln zum Suchen und Herunterladen." },
"aiToolCheckAndDownloadTypeParamDesc": { "message": "Der Inhaltstyp der Liste. Muss 'movie' oder 'series' sein." },
"aiToolCheckAndDownloadFilenameParamDesc": { "message": "Der Name der herunterzuladenden M3U-Datei (z. B. 'MeineListe.m3u'). Wenn nicht angegeben, wird ein Standardname verwendet." },
"aiToolToggleFavoriteDesc": { "message": "Fügt einen Film oder eine Serie zur Favoritenliste des Benutzers hinzu oder entfernt sie daraus." },
"aiToolToggleFavoriteTitleParamDesc": { "message": "Der Titel des Films oder der Serie." },
"aiToolToggleFavoriteTypeParamDesc": { "message": "Der Typ des Inhalts. Muss 'movie' oder 'series' sein." },
"aiToolGetRecommendationsDesc": { "message": "Generiert und zeigt eine Liste von Film- oder Serienempfehlungen basierend auf dem Wiedergabeverlauf und den Favoriten des Benutzers an." },
"aiToolApplyFiltersDesc": { "message": "Wendet Filter auf die aktuelle Ansicht von Filmen oder Serien an, um die Ergebnisse nach Typ, Genre, Jahr und Sortierreihenfolge zu verfeinern." },
"aiToolApplyFiltersTypeParamDesc": { "message": "Der Typ des Inhalts, auf den die Filter angewendet werden sollen. Muss 'movie' oder 'series' sein." },
"aiToolApplyFiltersGenreParamDesc": { "message": "Der Name des Genres, nach dem gefiltert werden soll (z. B. 'Action', 'Drama')." },
"aiToolApplyFiltersYearParamDesc": { "message": "Das Erscheinungsjahr, nach dem gefiltert werden soll (z. B. '2023')." },
"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)." },
"aiToolPlayMusicByArtistDesc": { "message": "Öffnet den Musik-Player und beginnt mit der Wiedergabe von Liedern eines bestimmten Künstlers aus der Bibliothek des Benutzers." },
"aiToolPlayMusicByArtistNameParamDesc": { "message": "Der genaue Name des Künstlers, dessen Lieder abgespielt werden sollen." },
"aiToolClearChatHistoryDesc": { "message": "Löscht den gesamten Nachrichtenverlauf der aktuellen Konversation mit dem KI-Assistenten." },
"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." },
"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." },
"aiToolAddPlexTokenDesc": { "message": "Fügt der Konfiguration der Erweiterung einen neuen X-Plex-Token hinzu, sodass die Anwendung Inhalte von neuen Plex-Servern scannen kann." },
"aiToolAddPlexTokenTokenParamDesc": { "message": "Die hinzuzufügende X-Plex-Token-Zeichenfolge." },
"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." },
"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)." },
"aiToolClearAllFavoritesDesc": { "message": "Entfernt alle Filme und Serien, die der Benutzer als Favoriten markiert hat." },
"aiToolClearViewingHistoryDesc": { "message": "Löscht den Wiedergabeverlauf des Benutzers von der Verlaufsseite." },
"aiToolClearRecommendationsViewDesc": { "message": "Löscht die Empfehlungsansicht und entfernt zwischengespeicherte Empfehlungen." },
"aiToolSearchNotFound": { "message": "'$query' in Ihrer Bibliothek nicht gefunden.", "placeholders": { "query": { "content": "$1" } } },
"aiToolNavigateSuccess": { "message": "Zur Seite $page$ navigiert.", "placeholders": { "page": { "content": "$1" } } },
"aiToolNavigateError": { "message": "Fehler beim Navigieren zur Seite $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolStatsError": { "message": "Fehler beim Abrufen der Statistiken." },
"aiToolItemNotFound": { "message": "Element '$title' nicht gefunden.", "placeholders": { "title": { "content": "$1" } } },
"aiToolShowItemDetailsSuccess": { "message": "Zeige Details für '$title'.", "placeholders": { "title": { "content": "$1" } } },
"aiToolAddToPlaylistSuccess": { "message": "'$title' zur Wiedergabeliste hinzugefügt.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoriteAdded": { "message": "'$title' zu den Favoriten hinzugefügt.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoriteRemoved": { "message": "'$title' aus den Favoriten entfernt.", "placeholders": { "title": { "content": "$1" } } },
"aiToolRecommendationsSuccess": { "message": "Zeige Empfehlungen an." },
"aiToolApplyFiltersGenreNotFound": { "message": "Genre '$genre' nicht gefunden.", "placeholders": { "genre": { "content": "$1" } } },
"aiToolApplyFiltersSuccess": { "message": "Filter erfolgreich angewendet." },
"aiToolPlayMusicNotReady": { "message": "Der Musik-Player ist nicht bereit. Stellen Sie sicher, dass Ihre Plex-Musikbibliothek gescannt wurde." },
"aiToolPlayMusicArtistNotFound": { "message": "Künstler '$artist_name' nicht gefunden.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolPlayMusicNoSongs": { "message": "Keine Lieder für '$artist_name' gefunden.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolPlayMusicSuccess": { "message": "Spiele Musik von '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolChatHistoryCleared": { "message": "Chatverlauf gelöscht." },
"aiToolConfirmDeleteDatabase": { "message": "Sind Sie sicher, dass Sie die lokale Datenbank löschen möchten? Diese Aktion ist irreversibel." },
"aiToolDeleteDatabaseCancelled": { "message": "Löschen der Datenbank abgebrochen." },
"aiToolExecutionError": { "message": "Fehler beim Ausführen des Tools '$toolName': $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } },
"aiToolUnknown": { "message": "Unbekanntes Tool: '$toolName'.", "placeholders": { "toolName": { "content": "$1" } } },
"aiToolFavoritesCleared": { "message": "Favoriten gelöscht." },
"aiToolFavoritesClearError": { "message": "Fehler beim Löschen der Favoriten: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolRecommendationsCleared": { "message": "Empfehlungen gelöscht." },
"aiToolRecommendationsClearError": { "message": "Fehler beim Löschen der Empfehlungen: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolDatabaseDeleted": { "message": "Datenbank gelöscht. Die Seite wird neu geladen." },
"aiToolDatabaseDeleteError": { "message": "Fehler beim Löschen der Datenbank: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolDatabaseDeleteBlocked": { "message": "Das Löschen der Datenbank ist blockiert. Schließen Sie andere Tabs der Anwendung." },
"aiToolUpdateAllTokensSuccess": { "message": "Alle Token wurden erfolgreich aktualisiert." },
"aiToolUpdateAllTokensError": { "message": "Fehler beim Aktualisieren der Token: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolAddPlexTokenSuccess": { "message": "Plex-Token erfolgreich hinzugefügt." },
"aiToolAddPlexTokenError": { "message": "Fehler beim Hinzufügen des Plex-Tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolChangeRegionSuccess": { "message": "Region auf $region$ geändert. Der Inhalt wird aktualisiert.", "placeholders": { "region": { "content": "$1" } } },
"aiToolChangeRegionError": { "message": "Fehler beim Ändern der Region: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolViewingHistoryCleared": { "message": "Wiedergabeverlauf gelöscht." },
"aiToolViewingHistoryClearError": { "message": "Fehler beim Löschen des Wiedergabeverlaufs: $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." },
"aiToolM3UNoTitlesProvided": { "message": "Bitte geben Sie eine Liste von Titeln an, um die Wiedergabeliste zu erstellen." },
"aiToolM3UCheckingTitles": { "message": "Überprüfe die Titel auf Ihren lokalen Servern..." },
"aiToolM3UNoLocalMatchesForDownload": { "message": "Ich habe keinen der Filme oder Serien aus der Liste auf Ihren lokalen Servern gefunden." },
"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" } } },
"backToProviders": { "message": "Zurück zu den Anbietern" },
"artistsCounterSingle": { "message": "$total$ Künstler", "placeholders": { "total": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Laden..." },
"downloadingSong": { "message": "Starte Download von \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" heruntergeladen.", "placeholders": { "title": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Fehler beim Herunterladen von \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Generiere M3U für \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
"albumM3UGenerated": { "message": "M3U für Album \"$artist$\" generiert.", "placeholders": { "artist": { "content": "$1" } } },
"retyingSection": { "message": "Wiederhole Abschnitt \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"retrySuccess": { "message": "[ERFOLG] Wiederholung von \"$title$\" abgeschlossen.", "placeholders": { "title": { "content": "$1" } } },
"retryError": { "message": "[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$... hat $count$ Server gefunden.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } },
"errorProcessingToken": { "message": "Fehler beim Verarbeiten des Tokens $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 angegeben." },
"tokenAlreadyExists": { "message": "Token existiert bereits." },
"tokenAddedSuccessfully": { "message": "Token erfolgreich hinzugefügt." },
"noStreamsFoundForSelection": { "message": "Keine Streams für die Auswahl gefunden." },
"autoplayBlocked": { "message": "Autoplay blockiert." },
"page": { "message": "Seite" },
"all": { "message": "Alle" },
"userScore": { "message": "Benutzerbewertung" },
"duration": { "message": "Dauer" },
"min": { "message": "Min" },
"max": { "message": "Max" }
}

View File

@ -1,7 +1,7 @@
{
"appName": { "message": "CinePlex" },
"appDescription": { "message": "Scans Plex servers to find content and displays it in the interface" },
"appTagline": { "message": "Movies, Series & Music" },
"appDescription": { "message": "Scans Plex servers for content and displays it in the interface" },
"appTagline": { "message": "Movies, Series, and Music" },
"appLocaleCode": { "message": "en-US" },
"toggleNavigation": { "message": "Toggle Navigation" },
"searchPlaceholder": { "message": "Search for movies or series..." },
@ -9,26 +9,29 @@
"settings": { "message": "Settings" },
"navMovies": { "message": "Movies" },
"navSeries": { "message": "Series" },
"navProviders": { "message": "Providers" },
"navPhotos": { "message": "Photos" },
"navStats": { "message": "Statistics" },
"navFavorites": { "message": "Favorites" },
"navHistory": { "message": "History" },
"navRecommendations": { "message": "Recommendations" },
"navMusic": { "message": "Music" },
"navM3uGenerator": { "message": "M3U Generator" },
"heroWelcome": { "message": "Welcome to CinePlex" },
"heroSubtitle": { "message": "Explore thousands of movies and series." },
"addStream": { "message": "Add Stream" },
"moreInfo": { "message": "More Info" },
"moreInfo": { "message": "More info" },
"popularMovies": { "message": "Popular Movies" },
"allGenres": { "message": "All Genres" },
"allYears": { "message": "All Years" },
"sortPopular": { "message": "Most Popular" },
"sortTopRated": { "message": "Top Rated" },
"sortRecent": { "message": "Most Recent" },
"loadMore": { "message": "Load More" },
"allGenres": { "message": "All genres" },
"allYears": { "message": "All years" },
"sortPopular": { "message": "Most popular" },
"sortTopRated": { "message": "Top rated" },
"sortRecent": { "message": "Most recent" },
"loadMore": { "message": "Load more" },
"photosBreadcrumbHome": { "message": "Albums" },
"selectServer": { "message": "Select a server" },
"loading": { "message": "Loading..." },
"loadingLibraries": { "message": "Loading libraries..." },
"photosEmptyState": { "message": "No albums or photos found." },
"photosEmptyStateSub": { "message": "Please select a server or make sure you have a photo library in Plex." },
"statsTitle": { "message": "Library Statistics" },
@ -39,16 +42,15 @@
"statsUniqueMovies": { "message": "Unique Movies" },
"statsUniqueSeries": { "message": "Unique Series" },
"statsUniqueArtists": { "message": "Unique Artists" },
"statsTokenServers": { "message": "Token's Servers" },
"statsTokenServers": { "message": "Token Servers" },
"statsChartMoviesByGenre": { "message": "Content by Genre (Movies)" },
"statsChartSeriesByGenre": { "message": "Content by Genre (Series)" },
"statsChartByDecade": { "message": "Content by Decade" },
"recommendationsTitle": { "message": "Recommendations For You" },
"recommendationsTitle": { "message": "Recommendations for you" },
"historyTitle": { "message": "Viewing History" },
"clearHistory": { "message": "Clear All" },
"consoleTitle": { "message": "Plex Scan Console" },
"footerCredit": { "message": "An interface for your Plex universe." },
"backButton": { "message": "Back" },
"closeTrailer": { "message": "Close trailer" },
"close": { "message": "Close" },
"photoViewer": { "message": "Photo Viewer" },
@ -58,18 +60,22 @@
"settingsTitleFull": { "message": "Settings and Configuration" },
"settingsTabGeneral": { "message": "General" },
"settingsTabPlex": { "message": "Plex" },
"settingsTabJellyfin": { "message": "Jellyfin" },
"settingsTabPhpGen": { "message": "PHP Generator" },
"settingsTabData": { "message": "Data" },
"settingsApiServer": { "message": "API and Server Configuration" },
"settingsApiServer": { "message": "API and Server Settings" },
"settingsTmdbApiLabel": { "message": "TMDB API Key (Optional)" },
"settingsTmdbApiPlaceholder": { "message": "Will use default key if left blank" },
"settingsTmdbLangLabel": { "message": "Language for TMDB & UI" },
"settingsTmdbApiPlaceholder": { "message": "The default key will be used if left blank" },
"settingsGoogleApiLabel": { "message": "Google Gemini API Key (Optional)" },
"settingsGoogleApiPlaceholder": { "message": "Required to use the AI assistant" },
"settingsRegionLabel": { "message": "Region for content discovery" },
"allRegions": { "message": "All regions" },
"settingsPhpUrlLabel": { "message": "Server URL for Adding Streams" },
"settingsPhpUrlPlaceholder": { "message": "https://your-server.com/path/to/script.php" },
"settingsInterface": { "message": "Interface" },
"settingsLightTheme": { "message": "Light Mode" },
"settingsShowHero": { "message": "Show 'Hero' welcome section" },
"settingsScanContent": { "message": "Content Scanning" },
"settingsScanContent": { "message": "Content Scan" },
"settingsScanDesc": { "message": "Select what to scan and press the button." },
"settingsScanMovies": { "message": "Movies" },
"settingsScanShows": { "message": "Series" },
@ -80,14 +86,20 @@
"settingsPlexTokens": { "message": "Plex Tokens" },
"settingsPlexTokensDesc": { "message": "Edit the list of Plex tokens (JSON format)." },
"settingsSaveTokens": { "message": "Save Tokens" },
"settingsPhpGenTitle": { "message": "PHP Server Script Generator" },
"settingsJellyfinTitle": { "message": "Jellyfin Settings" },
"settingsJellyfinDesc": { "message": "Add your Jellyfin server details to scan its content." },
"jellyfinUrlLabel": { "message": "Jellyfin Server URL" },
"jellyfinUserLabel": { "message": "Username" },
"jellyfinPassLabel": { "message": "Password" },
"jellyfinConnectAndScan": { "message": "Connect and Scan" },
"settingsPhpGenTitle": { "message": "PHP Script Generator for Server" },
"settingsPhpFileOptions": { "message": "File Options" },
"settingsPhpSavePathLabel": { "message": "Save Path on Server" },
"settingsPhpSavePathPlaceholder": { "message": "Ex: /var/www/html/lists (blank for same folder)" },
"settingsPhpFilenameLabel": { "message": "Filename" },
"settingsPhpSavePathPlaceholder": { "message": "Ex: /var/www/html/lists (blank for the same folder)" },
"settingsPhpFilenameLabel": { "message": "File Name" },
"settingsPhpFileAction": { "message": "File Action" },
"settingsPhpActionAppend": { "message": "Append to the end of the file (cumulative)" },
"settingsPhpActionOverwrite": { "message": "Overwrite the file (start fresh)" },
"settingsPhpActionOverwrite": { "message": "Overwrite the file (start over)" },
"settingsPhpSecurity": { "message": "Security (Optional)" },
"settingsPhpUseSecretKey": { "message": "Use secret key (Recommended)" },
"settingsPhpSecretKeyPlaceholder": { "message": "Enter a secure secret key" },
@ -99,12 +111,12 @@
"settingsImportDb": { "message": "Import DB from File" },
"settingsExportDb": { "message": "Export DB to File" },
"settingsClearContent": { "message": "Clear Local Content Data" },
"settingsClearContentDesc": { "message": "This action will delete movies, series, and music from the local database, but will not affect your favorites or settings." },
"settingsClearContentDesc": { "message": "This action will delete movies, series, and music from the local database, but will not affect your favorites or your settings." },
"settingsClose": { "message": "Close" },
"settingsSave": { "message": "Save Settings" },
"musicSidenavTitle": { "message": "Plex Music" },
"musicAllServers": { "message": "All Servers" },
"musicSearchArtistPlaceholder": { "message": "Search artist..." },
"musicSearchArtistPlaceholder": { "message": "Search for an artist..." },
"musicSearchDiscographyPlaceholder": { "message": "Search in discography..." },
"musicNothingPlaying": { "message": "Nothing playing" },
"musicSelectSong": { "message": "Select a song" },
@ -123,7 +135,7 @@
"eqPresetJazz": { "message": "Jazz" },
"eqPresetClassical": { "message": "Classical" },
"eqPresetBassBoost": { "message": "Bass Boost" },
"eqPreampLabel": { "message": "Pre-Amp" },
"eqPreampLabel": { "message": "Preamp" },
"infoModalTitle": { "message": "Information" },
"infoModalFieldTitle": { "message": "Title:" },
"infoModalFieldArtist": { "message": "Artist:" },
@ -146,28 +158,28 @@
"contentDataDeleted": { "message": "Content data deleted from IndexedDB." },
"errorDeletingData": { "message": "Error deleting data: $message$", "placeholders": { "message": { "content": "$1" } } },
"aceEditorNotAvailable": { "message": "Text editor not available." },
"errorLoadingTokens": { "message": "Error loading tokens to editor." },
"errorLoadingTokens": { "message": "Error loading tokens for editing." },
"errorLoadingTokensMessage": { "message": "Error loading tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"aceEditorNotAvailableToSave": { "message": "Editor not available for saving." },
"invalidJsonFormat": { "message": "Invalid JSON format. Must be { \"tokens\": [...] }" },
"invalidJsonFormat": { "message": "Invalid JSON format. It must be { \"tokens\": [...] }" },
"tokensSaved": { "message": "Tokens saved successfully." },
"errorSavingTokens": { "message": "Error saving tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"dbNotAvailable": { "message": "IndexedDB is not available." },
"dbExported": { "message": "Database exported successfully." },
"errorExportingDb": { "message": "Error exporting database: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorExportingDb": { "message": "Error exporting the database: $message$", "placeholders": { "message": { "content": "$1" } } },
"invalidJsonFile": { "message": "The file does not contain a valid JSON object." },
"noDataToImport": { "message": "The file does not contain data for the current DB sections." },
"dbImported": { "message": "Database imported successfully." },
"errorImportingDb": { "message": "Error importing database: $message$", "placeholders": { "message": { "content": "$1" } } },
"updatingView": { "message": "Updating view with new data..." },
"errorImportingDb": { "message": "Error importing the database: $message$", "placeholders": { "message": { "content": "$1" } } },
"updatingView": { "message": "Updating the view with new data..." },
"confirmClearContent": { "message": "Are you sure you want to delete local content data (Movies, Series, Music, etc.)? Favorites and Settings will NOT be deleted." },
"trailerNotFound": { "message": "No trailer found for this title." },
"confirmClearHistory": { "message": "Are you sure you want to delete your entire viewing history? This action cannot be undone." },
"confirmClearHistory": { "message": "Are you sure you want to delete all your viewing history? This action cannot be undone." },
"historyCleared": { "message": "Viewing history cleared." },
"historyItemDeleted": { "message": "Item deleted from history." },
"errorGeneratingScript": { "message": "First, generate a script to be able to copy it." },
"errorGeneratingScript": { "message": "First generate a script to be able to copy it." },
"scriptCopied": { "message": "PHP script copied to clipboard." },
"errorCopyingScript": { "message": "Error copying script." },
"errorCopyingScript": { "message": "Error copying the script." },
"scriptGenerated": { "message": "PHP script generated." },
"errorLoadingAlbum": { "message": "Error loading album: $message$", "placeholders": { "message": { "content": "$1" } } },
"noPhotoServerSelected": { "message": "Error: No photo server has been selected." },
@ -178,20 +190,20 @@
"noFavorites": { "message": "You don't have any favorites yet." },
"errorLoadingFavorites": { "message": "Error loading favorites." },
"historyEmpty": { "message": "Your history is empty." },
"historyEmptySub": { "message": "Browse and watch content for it to appear here." },
"historyEmptySub": { "message": "Explore and watch content for it to appear here." },
"errorGeneratingRecommendations": { "message": "Error generating recommendations." },
"noRecommendations": { "message": "We need to know you better to give you recommendations!" },
"noRecommendations": { "message": "We need to get to know you better to give you recommendations!" },
"errorGeneratingStats": { "message": "Error generating statistics." },
"noServersForToken": { "message": "No associated servers found for this token." },
"searchingActorContent": { "message": "Searching for content from $actorName$", "placeholders": { "actorName": { "content": "$1" } } },
"searchingActorContent": { "message": "Searching for content by $actorName$", "placeholders": { "actorName": { "content": "$1" } } },
"errorLoadingActorContent": { "message": "Could not load content for $actorName$.", "placeholders": { "actorName": { "content": "$1" } } },
"errorAddingStream": { "message": "Error adding stream(s): $message$", "placeholders": { "message": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "The PHP server URL is not configured. Please set it up in Settings." },
"searchingStreams": { "message": "Searching for streams for \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "The PHP server URL is not configured. Please configure it in Settings." },
"searchingStreams": { "message": "Searching for streams for \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"sendingStreams": { "message": "Sending $count$ stream(s) to the server...", "placeholders": { "count": { "content": "$1" } } },
"streamAddedSuccess": { "message": "Stream(s) added successfully." },
"generatingM3U": { "message": "Generating M3U for \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
"m3uDownloaded": { "message": "M3U for \"$title$\" downloaded.", "placeholders": { "title": { "content": "$1" } } },
"generatingM3U": { "message": "Generating M3U for \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"m3uDownloaded": { "message": "\"$title$\" downloaded.", "placeholders": { "title": { "content": "$1" } } },
"errorGeneratingM3U": { "message": "Error generating M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
"settingsSavedSuccess": { "message": "Settings saved successfully." },
"errorSavingSettings": { "message": "Error saving settings to the database." },
@ -202,45 +214,25 @@
"plexScanStarting": { "message": "Starting Plex scan..." },
"noPlexTokens": { "message": "No Plex tokens configured." },
"clearingSections": { "message": "Clearing sections: $sections$", "placeholders": { "sections": { "content": "$1" } } },
"sectionsCleared": { "message": "Sections cleared." },
"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" } } },
"initialScanPhaseComplete": { "message": "Initial scan phase completed." },
"initialScanPhaseComplete": { "message": "Initial scan phase finished." },
"retryPhaseFinished": { "message": "Retry phase finished." },
"plexScanFinished": { "message": "Plex scan finished. Updating content..." },
"plexScanFatalError": { "message": "FATAL ERROR: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorDuringScan": { "message": "Error during scan: $message$", "placeholders": { "message": { "content": "$1" } } },
"scanCancelled": { "message": "Scan cancelled by user." },
"plexScanFinished": { "message": "Scan finished. Updating content..." },
"scanCancelled": { "message": "Scan cancelled by the user." },
"scanCancelledInfo": { "message": "Scan cancelled." },
"retyingSection": { "message": "Retrying section \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
"retrySuccess": { "message": "[SUCCESS] Retry of \"$title$\" completed.", "placeholders": { "title": { "content": "$1" } } },
"retryError": { "message": "[FINAL ERROR] Retry failed for \"$title$\": $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } },
"noRetriesPending": { "message": "No pending retries." },
"startingRetryPhase": { "message": "Starting retry phase for $count$ sections...", "placeholders": { "count": { "content": "$1" } } },
"retryPhaseCancelled": { "message": "Retry phase cancelled." },
"errorInitializingMusicPlayer": { "message": "Error initializing music player." },
"errorInitializingMusicPlayer": { "message": "Error initializing the music player." },
"criticalErrorLoadingMusic": { "message": "Critical error loading music data." },
"errorLoadingArtists": { "message": "Error loading artists." },
"dbUnavailableError": { "message": "Error: Database not available." },
"updatingMusicData": { "message": "Updating music data..." },
"musicDataUpdated": { "message": "Music data updated." },
"errorFetchingArtistSongs": { "message": "Error getting artist's songs." },
"errorFetchingArtistSongs": { "message": "Error fetching the artist's songs." },
"errorLoadingSongs": { "message": "Error loading songs." },
"noArtistsFound": { "message": "No artists found." },
"artistsCounter": { "message": "$start$-$end$ of $total$", "placeholders": { "start": { "content": "$1" }, "end": { "content": "$2" }, "total": { "content": "$3" } } },
"artistsCounterSingle": { "message": "$total$ Artists", "placeholders": { "total": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Loading..." },
"noSongsFound": { "message": "No songs found." },
"shuffleOn": { "message": "Shuffle mode enabled." },
"shuffleOff": { "message": "Shuffle mode disabled." },
"downloadingSong": { "message": "Starting download of \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" downloaded.", "placeholders": { "title": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Error downloading \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Generating M3U for \"$artist$\"...", "placeholders": { "artist": { "content": "$1" } } },
"albumM3UGenerated": { "message": "M3U for album \"$artist$\" generated.", "placeholders": { "artist": { "content": "$1" } } },
"shuffleOn": { "message": "Shuffle mode on." },
"shuffleOff": { "message": "Shuffle mode off." },
"playbackError": { "message": "Playback error" },
"errorLabel": { "message": "Error" },
"reloadingPage": { "message": "Reloading page..." },
"reloadingPage": { "message": "Reloading the page..." },
"viewed": { "message": "Viewed" },
"local": { "message": "Local" },
"topRatedSort": {"message": "Top Rated"},
@ -258,22 +250,24 @@
"writer": {"message": "Writer:"},
"viewOnImdb": {"message": "View on IMDb"},
"watchTrailer": {"message": "Watch Trailer"},
"addToFavorites": {"message": "Favorite"},
"removeFromFavorites": {"message": "Unfavorite"},
"notAvailable": {"message": "Not Available"},
"addToFavorites": {"message": "Add to favorites"},
"removeFromFavorites": {"message": "Remove from favorites"},
"notAvailable": {"message": "Not available"},
"mainCast": {"message": "Main Cast"},
"seasonsAndEpisodes": {"message": "Seasons & Episodes"},
"seasonsAndEpisodes": {"message": "Seasons and Episodes"},
"similarContent": {"message": "Similar Content"},
"filmography": {"message": "Filmography"},
"availableOn": {"message": "Available on"},
"episodesCount": {"message": "$count$ Episodes", "placeholders": {"count": {"content": "$1"}}},
"seasonsCount": {"message": "$count$ Seasons", "placeholders": {"count": {"content": "$1"}}},
"runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}},
"noTrailerFound": {"message": "No trailer was found for this title."},
"fatalInitError": {"message": "Fatal Initialization Error"},
"fatalInitErrorSub": {"message": "The application could not be loaded."},
"noTrailerFound": {"message": "No trailer found for this title."},
"fatalInitError": {"message": "Fatal initialization error"},
"fatalInitErrorSub": {"message": "Could not load the application."},
"invalidStreamInfo": {"message": "Invalid information."},
"dbUnavailableForStreams": {"message": "Local database not available."},
"noPlexServersForStreams": {"message": "No Plex servers."},
"notFoundOnServers": {"message": "\"$query$\" not found on servers.", "placeholders": {"query": {"content": "$1"}}},
"notFoundOnServers": {"message": "\"$query$\" not found on Plex servers.", "placeholders": {"query": {"content": "$1"}}},
"relativeTime_justNow": { "message": "Just now" },
"relativeTime_minutesAgo": { "message": "$count$ minutes ago", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_hoursAgo": { "message": "$count$ hours ago", "placeholders": { "count": { "content": "$1" } } },
@ -281,10 +275,175 @@
"relativeTime_daysAgo": { "message": "$count$ days ago", "placeholders": { "count": { "content": "$1" } } },
"errorLoadingDetails": { "message": "Error Loading Details" },
"errorLoadingLocalContent": { "message": "Error loading local content." },
"errorServerResponse": { "message": "Unsuccessful response from server." },
"errorPlexApi": { "message": "Error $status$ from Plex API.", "placeholders": { "status": { "content": "$1" } } },
"errorServerResponse": { "message": "Unsuccessful server response." },
"errorPlexApi": { "message": "Plex API error $status$.", "placeholders": { "status": { "content": "$1" } } },
"errorParsingPlexXml": { "message": "Error parsing Plex XML." },
"untitled": { "message": "Untitled" },
"itemCount": { "message": "$count$ items", "placeholders": { "count": { "content": "$1" } } },
"noPhotoServers": { "message": "No photo servers" }
"noPhotoServers": { "message": "No photo servers" },
"jellyfinScanInProgress": { "message": "Jellyfin scan is already in progress." },
"jellyfinScanning": { "message": "Scanning Jellyfin..." },
"jellyfinMissingCredentials": { "message": "Please complete the Jellyfin URL and username." },
"jellyfinConnecting": { "message": "Connecting to Jellyfin at: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinAuthFailed": { "message": "Jellyfin authentication failed: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinAuthSuccess": { "message": "Jellyfin authentication successful." },
"jellyfinFetchingLibraries": { "message": "Fetching libraries..." },
"jellyfinFetchFailed": { "message": "Error fetching libraries: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinNoMediaLibraries": { "message": "No movie or series libraries found in Jellyfin." },
"jellyfinLibrariesFound": { "message": "$count$ media library(s) found.", "placeholders": { "count": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Success] '$libraryName' scanned, $count$ titles added.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"jellyfinLibraryScanFailed": { "message": "Error scanning library '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Jellyfin scan completed. Added $movies$ movies and $series$ series.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Jellyfin credentials not configured." },
"notFoundOnJellyfin": { "message": "\"$query$\" not found on Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" not found on any server.", "placeholders": { "query": { "content": "$1" } } },
"localOnPlex": { "message": "On Plex" },
"searchOnPlex": { "message": "Search on Plex" },
"jellyfinTitle": { "message": "Jellyfin Content" },
"noJellyfinContent": { "message": "No Jellyfin content found." },
"noJellyfinContentSub": { "message": "Make sure you have scanned your Jellyfin server in the settings." },
"activityViewerTitle": { "message": "Server Activity Viewer" },
"activitySelectServer": { "message": "Select a server" },
"activityCheckBtn": { "message": "Refresh" },
"activityNoSessions": { "message": "No active sessions on this server." },
"activitySessionUser": { "message": "User" },
"activitySessionDevice": { "message": "Device" },
"activitySessionContent": { "message": "Content" },
"activitySessionState": { "message": "State" },
"activitySessionIdentifier": { "message": "Client Identifier" },
"activityCopyID": { "message": "Copy ID" },
"activityError": { "message": "Could not get server activity." },
"activityCopied": { "message": "Identifier copied to clipboard!" },
"activityCopyError": { "message": "Error copying the identifier." },
"noProvidersFound": { "message": "No providers found." },
"availableOnPlex": { "message": "Available on Plex" },
"m3uGeneratorTitle": { "message": "M3U List Generator" },
"selectAServer": { "message": "Select a server..." },
"downloadM3u": { "message": "Download M3U" },
"m3uGenerator": { "message": "M3U Generator" },
"selectLibraries": { "message": "Select Libraries" },
"howToUse": { "message": "How to Use" },
"m3uInstruction1": { "message": "Choose a server from the list." },
"m3uInstruction2": { "message": "Select one or more libraries to include." },
"m3uInstruction3": { "message": "Click the download button." },
"m3uInstruction4": { "message": "Import the .m3u file into your compatible player." },
"chatOpen": { "message": "Open Chat" },
"chatTitle": { "message": "AI Assistant" },
"chatClose": { "message": "X" },
"chatPlaceholder": { "message": "Type your message..." },
"chatSend": { "message": "➤" },
"chatWelcome": { "message": "Welcome! I'm your CinePlex assistant. Ask me about movies, series, or anything else you want to know." },
"chatGoogleApiKeyMissing": { "message": "The Google Gemini API key is not configured. Please set it in the extension settings to use the AI assistant." },
"chatApiInvalidResponse": { "message": "The API returned an invalid response. Please try again." },
"chatApiError": { "message": "Error communicating with the AI assistant" },
"downloadAll": { "message": "Download all" },
"download": { "message": "Download" },
"aiToolSearchLibraryDesc": { "message": "Searches the user's Plex library for movies or series by title." },
"aiToolSearchLibraryQueryParamDesc": { "message": "The title of the movie or series to search for." },
"aiToolSearchLibraryTypeParamDesc": { "message": "The type of content to search for. It can be 'movie' for movies or 'series' for series. (Optional)." },
"aiToolSearchLibraryResolutionParamDesc": { "message": "The video resolution to search for (e.g., '4k', '1080p'). (Optional)." },
"aiToolSearchLibraryContainerParamDesc": { "message": "The video container format to search for (e.g., 'mkv', 'mp4'). (Optional)." },
"aiToolNavigateToPageDesc": { "message": "Navigates the user to a specific page of the application interface." },
"aiToolNavigateToPagePageParamDesc": { "message": "The name of the page to navigate to, e.g.: 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', or 'm3u-generator'." },
"aiToolGetUserStatsDesc": { "message": "Gets and displays the user's library statistics, such as the total number of unique movies, series, and artists." },
"aiToolShowItemDetailsDesc": { "message": "Displays the details page of a specific movie or series by its title and type." },
"aiToolShowItemDetailsTitleParamDesc": { "message": "The exact title of the movie or series." },
"aiToolShowItemDetailsTypeParamDesc": { "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 stream it to a configured PHP server." },
"aiToolAddToPlaylistTitleParamDesc": { "message": "The title of the movie or series to add." },
"aiToolAddToPlaylistTypeParamDesc": { "message": "The type of content. 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." },
"aiToolCheckAndDownloadTitlesParamDesc": { "message": "An array of movie or series titles to search for and download." },
"aiToolCheckAndDownloadTypeParamDesc": { "message": "The content type of the list. It must be 'movie' or 'series'." },
"aiToolCheckAndDownloadFilenameParamDesc": { "message": "The name of the M3U file to download (e.g., 'MyList.m3u'). If not provided, a default name will be used." },
"aiToolToggleFavoriteDesc": { "message": "Adds or removes a movie or series from the user's favorites list." },
"aiToolToggleFavoriteTitleParamDesc": { "message": "The title of the movie or series." },
"aiToolToggleFavoriteTypeParamDesc": { "message": "The type of content. It must be 'movie' or 'series'." },
"aiToolGetRecommendationsDesc": { "message": "Generates and displays a list of movie or series recommendations based on the user's viewing history and favorites." },
"aiToolApplyFiltersDesc": { "message": "Applies filters to the current view of movies or series, allowing to refine the results by type, genre, year, and sort order." },
"aiToolApplyFiltersTypeParamDesc": { "message": "The type of content to apply the filters to. It must be 'movie' or 'series'." },
"aiToolApplyFiltersGenreParamDesc": { "message": "The name of the genre to filter by (e.g., 'Action', 'Drama')." },
"aiToolApplyFiltersYearParamDesc": { "message": "The release year to filter by (e.g., '2023')." },
"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)." },
"aiToolPlayMusicByArtistDesc": { "message": "Opens the music player and starts playing songs by a specific artist from the user's library." },
"aiToolPlayMusicByArtistNameParamDesc": { "message": "The exact name of the artist whose songs are to be played." },
"aiToolClearChatHistoryDesc": { "message": "Clears all message history from the current conversation with the AI assistant." },
"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." },
"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." },
"aiToolAddPlexTokenDesc": { "message": "Adds a new X-Plex token to the extension's configuration, allowing the application to scan content from new Plex servers." },
"aiToolAddPlexTokenTokenParamDesc": { "message": "The X-Plex token string to be added." },
"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." },
"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)." },
"aiToolClearAllFavoritesDesc": { "message": "Removes all movies and series that the user has marked as favorites." },
"aiToolClearViewingHistoryDesc": { "message": "Clears the user's viewing history from the history page." },
"aiToolClearRecommendationsViewDesc": { "message": "Clears the recommendations view and removes cached recommendations." },
"aiToolSearchNotFound": { "message": "'$query$' not found in your library.", "placeholders": { "query": { "content": "$1" } } },
"aiToolNavigateSuccess": { "message": "Navigated to the $page$ page.", "placeholders": { "page": { "content": "$1" } } },
"aiToolNavigateError": { "message": "Error navigating to the $page$ page.", "placeholders": { "page": { "content": "$1" } } },
"aiToolStatsError": { "message": "Error getting statistics." },
"aiToolItemNotFound": { "message": "Item '$title$' not found.", "placeholders": { "title": { "content": "$1" } } },
"aiToolShowItemDetailsSuccess": { "message": "Showing details for '$title$'.", "placeholders": { "title": { "content": "$1" } } },
"aiToolAddToPlaylistSuccess": { "message": "Added '$title$' to the playlist.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoriteAdded": { "message": "Added '$title$' to favorites.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoriteRemoved": { "message": "Removed '$title$' from favorites.", "placeholders": { "title": { "content": "$1" } } },
"aiToolRecommendationsSuccess": { "message": "Showing recommendations." },
"aiToolApplyFiltersGenreNotFound": { "message": "Genre '$genre$' not found.", "placeholders": { "genre": { "content": "$1" } } },
"aiToolApplyFiltersSuccess": { "message": "Filters applied successfully." },
"aiToolPlayMusicNotReady": { "message": "The music player is not ready. Make sure your Plex music library has been scanned." },
"aiToolPlayMusicArtistNotFound": { "message": "Artist '$artist_name$' not found.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolPlayMusicNoSongs": { "message": "No songs found for '$artist_name$'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolPlayMusicSuccess": { "message": "Playing music by '$artist_name$'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolChatHistoryCleared": { "message": "Chat history cleared." },
"aiToolConfirmDeleteDatabase": { "message": "Are you sure you want to delete the local database? This action is irreversible." },
"aiToolDeleteDatabaseCancelled": { "message": "Database deletion cancelled." },
"aiToolExecutionError": { "message": "Error executing tool '$toolName': $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } },
"aiToolUnknown": { "message": "Unknown tool: '$toolName'.", "placeholders": { "toolName": { "content": "$1" } } },
"aiToolFavoritesCleared": { "message": "Favorites cleared." },
"aiToolFavoritesClearError": { "message": "Error clearing favorites: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolRecommendationsCleared": { "message": "Recommendations cleared." },
"aiToolRecommendationsClearError": { "message": "Error clearing recommendations: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolDatabaseDeleted": { "message": "Database deleted. The page will reload." },
"aiToolDatabaseDeleteError": { "message": "Error deleting the database: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolDatabaseDeleteBlocked": { "message": "Database deletion is blocked. Close other tabs of the application." },
"aiToolUpdateAllTokensSuccess": { "message": "All tokens have been updated successfully." },
"aiToolUpdateAllTokensError": { "message": "Error updating tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolAddPlexTokenSuccess": { "message": "Plex token added successfully." },
"aiToolAddPlexTokenError": { "message": "Error adding the Plex token: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolChangeRegionSuccess": { "message": "Region changed to $region$. The content is being updated.", "placeholders": { "region": { "content": "$1" } } },
"aiToolChangeRegionError": { "message": "Error changing the region: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolViewingHistoryCleared": { "message": "Viewing history cleared." },
"aiToolViewingHistoryClearError": { "message": "Error clearing the viewing history: $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." },
"aiToolM3UNoTitlesProvided": { "message": "Please provide a list of titles to create the playlist." },
"aiToolM3UCheckingTitles": { "message": "Checking the titles on your local servers..." },
"aiToolM3UNoLocalMatchesForDownload": { "message": "I haven't found any of the movies or series from the list on your local servers." },
"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" } } },
"backToProviders": { "message": "Back to Providers" },
"artistsCounterSingle": { "message": "$total$ Artist", "placeholders": { "total": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Loading..." },
"downloadingSong": { "message": "Starting download of \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" downloaded.", "placeholders": { "title": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Error downloading \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Generating M3U for \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
"albumM3UGenerated": { "message": "M3U for album \"$artist$\" generated.", "placeholders": { "artist": { "content": "$1" } } },
"retyingSection": { "message": "Retrying section \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"retrySuccess": { "message": "[SUCCESS] Retry of \"$title$\" completed.", "placeholders": { "title": { "content": "$1" } } },
"retryError": { "message": "[FINAL ERROR] Retry failed for \"$title$\": $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": "The token already exists." },
"tokenAddedSuccessfully": { "message": "Token added successfully." },
"noStreamsFoundForSelection": { "message": "No streams found for the selection." },
"autoplayBlocked": { "message": "Autoplay blocked." },
"page": { "message": "Page" },
"all": { "message": "All" },
"userScore": { "message": "User Score" },
"duration": { "message": "Duration" },
"min": { "message": "Min" },
"max": { "message": "Max" }
}

View File

@ -9,13 +9,15 @@
"settings": { "message": "Ajustes" },
"navMovies": { "message": "Películas" },
"navSeries": { "message": "Series" },
"navProviders": { "message": "Proveedores" },
"navPhotos": { "message": "Fotos" },
"navStats": { "message": "Estadísticas" },
"navFavorites": { "message": "Favoritos" },
"navHistory": { "message": "Historial" },
"navRecommendations": { "message": "Recomendaciones" },
"navMusic": { "message": "Música" },
"heroWelcome": { "message": "Bienvenido a CinePlex" },
"navM3uGenerator": { "message": "Generador M3U" },
"heroWelcome": { "message": "" },
"heroSubtitle": { "message": "Explora miles de películas y series." },
"addStream": { "message": "Añadir Stream" },
"moreInfo": { "message": "Más información" },
@ -29,6 +31,7 @@
"photosBreadcrumbHome": { "message": "Álbumes" },
"selectServer": { "message": "Selecciona un servidor" },
"loading": { "message": "Cargando..." },
"loadingLibraries": { "message": "Cargando bibliotecas..." },
"photosEmptyState": { "message": "No se encontraron álbumes ni fotos." },
"photosEmptyStateSub": { "message": "Por favor, selecciona un servidor o asegúrate de tener una biblioteca de fotos en Plex." },
"statsTitle": { "message": "Estadísticas de la Biblioteca" },
@ -48,7 +51,6 @@
"clearHistory": { "message": "Borrar Todo" },
"consoleTitle": { "message": "Consola de Escaneo Plex" },
"footerCredit": { "message": "Una interfaz para tu universo Plex." },
"backButton": { "message": "Atrás" },
"closeTrailer": { "message": "Cerrar tráiler" },
"close": { "message": "Cerrar" },
"photoViewer": { "message": "Visor de fotos" },
@ -58,12 +60,16 @@
"settingsTitleFull": { "message": "Ajustes y Configuración" },
"settingsTabGeneral": { "message": "General" },
"settingsTabPlex": { "message": "Plex" },
"settingsTabJellyfin": { "message": "Jellyfin" },
"settingsTabPhpGen": { "message": "Generador PHP" },
"settingsTabData": { "message": "Datos" },
"settingsApiServer": { "message": "Configuración de API y Servidor" },
"settingsTmdbApiLabel": { "message": "Clave de API de TMDB (Opcional)" },
"settingsTmdbApiPlaceholder": { "message": "Se usará la clave por defecto si se deja en blanco" },
"settingsTmdbLangLabel": { "message": "Idioma para TMDB y la interfaz" },
"settingsGoogleApiLabel": { "message": "Clave de API de Google Gemini (Opcional)" },
"settingsGoogleApiPlaceholder": { "message": "Necesaria para usar el asistente de IA" },
"settingsRegionLabel": { "message": "Región para descubrimiento de contenido" },
"allRegions": { "message": "Todas las regiones" },
"settingsPhpUrlLabel": { "message": "URL del Servidor para Añadir Streams" },
"settingsPhpUrlPlaceholder": { "message": "https://tu-servidor.com/ruta/al/script.php" },
"settingsInterface": { "message": "Interfaz" },
@ -80,6 +86,12 @@
"settingsPlexTokens": { "message": "Tokens de Plex" },
"settingsPlexTokensDesc": { "message": "Edita la lista de tokens de Plex (formato JSON)." },
"settingsSaveTokens": { "message": "Guardar Tokens" },
"settingsJellyfinTitle": { "message": "Configuración de Jellyfin" },
"settingsJellyfinDesc": { "message": "Añade los datos de tu servidor Jellyfin para escanear su contenido." },
"jellyfinUrlLabel": { "message": "URL del Servidor Jellyfin" },
"jellyfinUserLabel": { "message": "Nombre de Usuario" },
"jellyfinPassLabel": { "message": "Contraseña" },
"jellyfinConnectAndScan": { "message": "Conectar y Escanear" },
"settingsPhpGenTitle": { "message": "Generador de Script PHP para el Servidor" },
"settingsPhpFileOptions": { "message": "Opciones del Archivo" },
"settingsPhpSavePathLabel": { "message": "Ruta de Guardado en el Servidor" },
@ -162,7 +174,7 @@
"updatingView": { "message": "Actualizando la vista con los nuevos datos..." },
"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." },
"trailerNotFound": { "message": "No se encontró tráiler para este título." },
"confirmClearHistory": { "message": "¿Estás seguro de que deseas borrar todo tu historial de visualización? Esta acción no se puede deshacer." },
"confirmClearHistory": { "message": "¿Estás seguro de que deseas borrar todo tu historial de visualización? Esta acción no se puede rehacer." },
"historyCleared": { "message": "Historial de visualización borrado." },
"historyItemDeleted": { "message": "Elemento borrado del historial." },
"errorGeneratingScript": { "message": "Primero genera un script para poder copiarlo." },
@ -191,7 +203,7 @@
"sendingStreams": { "message": "Enviando $count$ stream(s) al servidor...", "placeholders": { "count": { "content": "$1" } } },
"streamAddedSuccess": { "message": "Stream(s) añadido(s) con éxito." },
"generatingM3U": { "message": "Generando M3U para \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"m3uDownloaded": { "message": "M3U para \"$title$\" descargado.", "placeholders": { "title": { "content": "$1" } } },
"m3uDownloaded": { "message": "\"$title$\" descargado.", "placeholders": { "title": { "content": "$1" } } },
"errorGeneratingM3U": { "message": "Error al generar M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
"settingsSavedSuccess": { "message": "Ajustes guardados correctamente." },
"errorSavingSettings": { "message": "Error al guardar los ajustes en la base de datos." },
@ -202,22 +214,11 @@
"plexScanStarting": { "message": "Iniciando escaneo Plex..." },
"noPlexTokens": { "message": "No hay tokens de Plex configurados." },
"clearingSections": { "message": "Limpiando secciones: $sections$", "placeholders": { "sections": { "content": "$1" } } },
"sectionsCleared": { "message": "Secciones limpiadas." },
"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" } } },
"initialScanPhaseComplete": { "message": "Fase de escaneo inicial finalizada." },
"retryPhaseFinished": { "message": "Fase de reintentos finalizada." },
"plexScanFinished": { "message": "Escaneo finalizado. Actualizando contenido..." },
"plexScanFatalError": { "message": "ERROR FATAL: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorDuringScan": { "message": "Error durante el escaneo: $message$", "placeholders": { "message": { "content": "$1" } } },
"scanCancelled": { "message": "Escaneo cancelado por el usuario." },
"scanCancelledInfo": { "message": "Escaneo cancelado." },
"retyingSection": { "message": "Reintentando sección \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"retrySuccess": { "message": "[ÉXITO] Reintento de \"$title$\" completado.", "placeholders": { "title": { "content": "$1" } } },
"retryError": { "message": "[ERROR FINAL] Falló el reintento para \"$title$\": $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } },
"noRetriesPending": { "message": "No hay reintentos pendientes." },
"startingRetryPhase": { "message": "Iniciando fase de reintentos para $count$ secciones...", "placeholders": { "count": { "content": "$1" } } },
"retryPhaseCancelled": { "message": "Fase de reintentos cancelada." },
"errorInitializingMusicPlayer": { "message": "Error inicializando el reproductor de música." },
"criticalErrorLoadingMusic": { "message": "Error crítico al cargar datos de música." },
"errorLoadingArtists": { "message": "Error al cargar artistas." },
@ -227,17 +228,8 @@
"errorFetchingArtistSongs": { "message": "Error al obtener las canciones del artista." },
"errorLoadingSongs": { "message": "Error cargando canciones." },
"noArtistsFound": { "message": "No se encontraron artistas." },
"artistsCounter": { "message": "$start$-$end$ de $total$", "placeholders": { "start": { "content": "$1" }, "end": { "content": "$2" }, "total": { "content": "$3" } } },
"artistsCounterSingle": { "message": "$total$ Artistas", "placeholders": { "total": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Cargando..." },
"noSongsFound": { "message": "No se encontraron canciones." },
"shuffleOn": { "message": "Modo aleatorio activado." },
"shuffleOff": { "message": "Modo aleatorio desactivado." },
"downloadingSong": { "message": "Iniciando descarga de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"songDownloaded": { "message": "\"\"$title$\" descargado.", "placeholders": { "title": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Error al descargar \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Generando M3U para \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
"albumM3UGenerated": { "message": "M3U para el álbum \"$artist$\" generado.", "placeholders": { "artist": { "content": "$1" } } },
"playbackError": { "message": "Error de reproducción" },
"errorLabel": { "message": "Error" },
"reloadingPage": { "message": "Recargando la página..." },
@ -264,6 +256,8 @@
"mainCast": {"message": "Reparto Principal"},
"seasonsAndEpisodes": {"message": "Temporadas y Episodios"},
"similarContent": {"message": "Contenido Similar"},
"filmography": {"message": "Filmografía"},
"availableOn": {"message": "Disponible en"},
"episodesCount": {"message": "$count$ Episodios", "placeholders": {"count": {"content": "$1"}}},
"seasonsCount": {"message": "$count$ Temporadas", "placeholders": {"count": {"content": "$1"}}},
"runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}},
@ -273,7 +267,7 @@
"invalidStreamInfo": {"message": "Información inválida."},
"dbUnavailableForStreams": {"message": "Base de datos local no disponible."},
"noPlexServersForStreams": {"message": "No hay servidores Plex."},
"notFoundOnServers": {"message": "No se encontró \"$query$\" en los servidores.", "placeholders": {"query": {"content": "$1"}}},
"notFoundOnServers": {"message": "No se encontró \"$query$\" en los servidores de Plex.", "placeholders": {"query": {"content": "$1"}}},
"relativeTime_justNow": { "message": "Ahora mismo" },
"relativeTime_minutesAgo": { "message": "Hace $count$ minutos", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_hoursAgo": { "message": "Hace $count$ horas", "placeholders": { "count": { "content": "$1" } } },
@ -286,5 +280,171 @@
"errorParsingPlexXml": { "message": "Error al analizar el XML de Plex." },
"untitled": { "message": "Sin título" },
"itemCount": { "message": "$count$ elementos", "placeholders": { "count": { "content": "$1" } } },
"noPhotoServers": { "message": "No hay servidores de fotos" }
"noPhotoServers": { "message": "No hay servidores de fotos" },
"jellyfinScanInProgress": { "message": "El escaneo Jellyfin ya está en curso." },
"jellyfinScanning": { "message": "Escaneando Jellyfin..." },
"jellyfinMissingCredentials": { "message": "Por favor, completa la URL y el usuario de Jellyfin." },
"jellyfinConnecting": { "message": "Conectando a Jellyfin en: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinAuthFailed": { "message": "Autenticación Jellyfin fallida: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinAuthSuccess": { "message": "Autenticación Jellyfin exitosa." },
"jellyfinFetchingLibraries": { "message": "Obteniendo bibliotecas..." },
"jellyfinFetchFailed": { "message": "Error al obtener bibliotecas: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinNoMediaLibraries": { "message": "No se encontraron bibliotecas de películas o series en Jellyfin." },
"jellyfinLibrariesFound": { "message": "$count$ biblioteca(s) de medios encontrada(s).", "placeholders": { "count": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Éxito] '$libraryName escaneada, $count$ títulos añadidos.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"jellyfinLibraryScanFailed": { "message": "Error al escanear la biblioteca '$libraryName.", "placeholders": { "libraryName": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Escaneo Jellyfin completado. Añadidas $movies$ películas y $series$ series.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Credenciales de Jellyfin no configuradas." },
"notFoundOnJellyfin": { "message": "No se encontró \"$query$\" en Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
"notFoundOnAnyServer": { "message": "No se encontró \"$query$\" en ningún servidor.", "placeholders": { "query": { "content": "$1" } } },
"localOnPlex": { "message": "En Plex" },
"searchOnPlex": { "message": "Buscar en Plex" },
"jellyfinTitle": { "message": "Contenido de Jellyfin" },
"noJellyfinContent": { "message": "No se encontró contenido de Jellyfin." },
"noJellyfinContentSub": { "message": "Asegúrate de haber escaneado tu servidor Jellyfin en los ajustes." },
"activityViewerTitle": { "message": "Visor de Actividad del Servidor" },
"activitySelectServer": { "message": "Selecciona un servidor" },
"activityCheckBtn": { "message": "Actualizar" },
"activityNoSessions": { "message": "No hay sesiones activas en este servidor." },
"activitySessionUser": { "message": "Usuario" },
"activitySessionDevice": { "message": "Dispositivo" },
"activitySessionContent": { "message": "Contenido" },
"activitySessionState": { "message": "Estado" },
"activitySessionIdentifier": { "message": "Identificador del Cliente" },
"activityCopyID": { "message": "Copiar ID" },
"activityError": { "message": "No se pudo obtener la actividad del servidor." },
"activityCopied": { "message": "¡Identificador copiado al portapapeles!" },
"activityCopyError": { "message": "Error al copiar el identificador." },
"noProvidersFound": { "message": "No se encontraron proveedores." },
"availableOnPlex": { "message": "Disponible en Plex" },
"m3uGeneratorTitle": { "message": "Generador de Listas M3U" },
"selectAServer": { "message": "Selecciona un servidor..." },
"downloadM3u": { "message": "Descargar M3U" },
"m3uGenerator": { "message": "Generador M3U" },
"selectLibraries": { "message": "Seleccionar Bibliotecas" },
"howToUse": { "message": "Cómo Usar" },
"m3uInstruction1": { "message": "Elige un servidor de la lista." },
"m3uInstruction2": { "message": "Selecciona una o más bibliotecas para incluir." },
"m3uInstruction3": { "message": "Haz clic en el botón de descarga." },
"m3uInstruction4": { "message": "Importa el archivo .m3u en tu reproductor compatible." },
"chatOpen": { "message": "Abrir Chat" },
"chatTitle": { "message": "Asistente IA" },
"chatClose": { "message": "X" },
"chatPlaceholder": { "message": "Escribe tu mensaje..." },
"chatSend": { "message": "➤" },
"chatWelcome": { "message": "¡Bienvenido! Soy tu asistente de CinePlex. Pregúntame sobre películas, series o cualquier otra cosa que quieras saber." },
"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." },
"chatApiInvalidResponse": { "message": "La API ha devuelto una respuesta no válida. Por favor, inténtalo de nuevo." },
"chatApiError": { "message": "Error al comunicarse con el asistente de IA" },
"downloadAll": { "message": "Descargar todo" },
"download": { "message": "Descargar" },
"aiToolSearchLibraryDesc": { "message": "Busca en la biblioteca de Plex del usuario películas o series por título." },
"aiToolSearchLibraryQueryParamDesc": { "message": "El título de la película o serie a buscar." },
"aiToolSearchLibraryTypeParamDesc": { "message": "El tipo de contenido a buscar. Puede ser 'movie' para películas o 'series' para series. (Opcional)." },
"aiToolSearchLibraryResolutionParamDesc": { "message": "La resolución del video a buscar (por ejemplo, '4k', '1080p'). (Opcional)." },
"aiToolSearchLibraryContainerParamDesc": { "message": "El formato contenedor del video a buscar (por ejemplo, 'mkv', 'mp4'). (Opcional)." },
"aiToolNavigateToPageDesc": { "message": "Navega al usuario a una página específica de la interfaz de la aplicación." },
"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'." },
"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." },
"aiToolShowItemDetailsDesc": { "message": "Muestra la página de detalles de una película o serie específica por su título y tipo." },
"aiToolShowItemDetailsTitleParamDesc": { "message": "El título exacto de la película o serie." },
"aiToolShowItemDetailsTypeParamDesc": { "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." },
"aiToolAddToPlaylistTitleParamDesc": { "message": "El título de la película o serie a añadir." },
"aiToolAddToPlaylistTypeParamDesc": { "message": "El tipo de contenido. 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." },
"aiToolCheckAndDownloadTitlesParamDesc": { "message": "Una matriz de títulos de películas o series para buscar y descargar." },
"aiToolCheckAndDownloadTypeParamDesc": { "message": "El tipo de contenido de la lista. Debe ser 'movie' o 'series'." },
"aiToolCheckAndDownloadFilenameParamDesc": { "message": "El nombre del archivo M3U a descargar (por ejemplo, 'MiLista.m3u'). Si no se proporciona, se usará un nombre por defecto." },
"aiToolToggleFavoriteDesc": { "message": "Añade o quita una película o serie de la lista de favoritos del usuario." },
"aiToolToggleFavoriteTitleParamDesc": { "message": "El título de la película o serie." },
"aiToolToggleFavoriteTypeParamDesc": { "message": "El tipo de contenido. Debe ser 'movie' o 'series'." },
"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." },
"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." },
"aiToolApplyFiltersTypeParamDesc": { "message": "El tipo de contenido al que aplicar los filtros. Debe ser 'movie' o 'series'." },
"aiToolApplyFiltersGenreParamDesc": { "message": "El nombre del género por el que filtrar (por ejemplo, 'Acción', 'Drama')." },
"aiToolApplyFiltersYearParamDesc": { "message": "El año de lanzamiento por el que filtrar (por ejemplo, '2023')." },
"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)." },
"aiToolPlayMusicByArtistDesc": { "message": "Abre el reproductor de música y comienza a reproducir canciones de un artista específico de la biblioteca del usuario." },
"aiToolPlayMusicByArtistNameParamDesc": { "message": "El nombre exacto del artista cuyas canciones se desean reproducir." },
"aiToolClearChatHistoryDesc": { "message": "Borra todo el historial de mensajes de la conversación actual con el asistente de IA." },
"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." },
"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." },
"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." },
"aiToolAddPlexTokenTokenParamDesc": { "message": "La cadena del token X-Plex que se desea añadir." },
"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." },
"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)." },
"aiToolClearAllFavoritesDesc": { "message": "Elimina todas las películas y series que el usuario ha marcado como favoritas." },
"aiToolClearViewingHistoryDesc": { "message": "Borra el historial de visualización del usuario de la página de historial." },
"aiToolClearRecommendationsViewDesc": { "message": "Limpia la vista de recomendaciones y elimina las recomendaciones almacenadas en caché." },
"aiToolSearchNotFound": { "message": "No se encontró '$query en tu biblioteca.", "placeholders": { "query": { "content": "$1" } } },
"aiToolNavigateSuccess": { "message": "Navegado a la página de $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolNavigateError": { "message": "Error al navegar a la página de $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolStatsError": { "message": "Error al obtener estadísticas." },
"aiToolItemNotFound": { "message": "No se encontró el elemento '$title.", "placeholders": { "title": { "content": "$1" } } },
"aiToolShowItemDetailsSuccess": { "message": "Mostrando detalles de '$title.", "placeholders": { "title": { "content": "$1" } } },
"aiToolAddToPlaylistSuccess": { "message": "Añadido '$title a la lista de reproducción.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoriteAdded": { "message": "Añadido '$title a favoritos.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoriteRemoved": { "message": "Eliminado '$title de favoritos.", "placeholders": { "title": { "content": "$1" } } },
"aiToolRecommendationsSuccess": { "message": "Mostrando recomendaciones." },
"aiToolApplyFiltersGenreNotFound": { "message": "Género '$genre no encontrado.", "placeholders": { "genre": { "content": "$1" } } },
"aiToolApplyFiltersSuccess": { "message": "Filtros aplicados correctamente." },
"aiToolPlayMusicNotReady": { "message": "El reproductor de música no está listo. Asegúrate de que tu biblioteca de música de Plex haya sido escaneada." },
"aiToolPlayMusicArtistNotFound": { "message": "Artista '$artist_name no encontrado.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolPlayMusicNoSongs": { "message": "No se encontraron canciones para '$artist_name.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolPlayMusicSuccess": { "message": "Reproduciendo música de '$artist_name.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolChatHistoryCleared": { "message": "Historial de chat borrado." },
"aiToolConfirmDeleteDatabase": { "message": "¿Estás seguro de que quieres eliminar la base de datos local? Esta acción es irreversible." },
"aiToolDeleteDatabaseCancelled": { "message": "Eliminación de la base de datos cancelada." },
"aiToolExecutionError": { "message": "Error al ejecutar la herramienta '$toolName: $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } },
"aiToolUnknown": { "message": "Herramienta desconocida: '$toolName.", "placeholders": { "toolName": { "content": "$1" } } },
"aiToolFavoritesCleared": { "message": "Favoritos eliminados." },
"aiToolFavoritesClearError": { "message": "Error al eliminar los favoritos: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolRecommendationsCleared": { "message": "Recomendaciones eliminadas." },
"aiToolRecommendationsClearError": { "message": "Error al eliminar las recomendaciones: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolDatabaseDeleted": { "message": "Base de datos eliminada. La página se recargará." },
"aiToolDatabaseDeleteError": { "message": "Error al eliminar la base de datos: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolDatabaseDeleteBlocked": { "message": "La eliminación de la base de datos está bloqueada. Cierra otras pestañas de la aplicación." },
"aiToolUpdateAllTokensSuccess": { "message": "Todos los tokens se han actualizado correctamente." },
"aiToolUpdateAllTokensError": { "message": "Error al actualizar los tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolAddPlexTokenSuccess": { "message": "Token de Plex añadido correctamente." },
"aiToolAddPlexTokenError": { "message": "Error al añadir el token de Plex: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolChangeRegionSuccess": { "message": "Región cambiada a $region$. El contenido se está actualizando.", "placeholders": { "region": { "content": "$1" } } },
"aiToolChangeRegionError": { "message": "Error al cambiar la región: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolViewingHistoryCleared": { "message": "Historial de visualización borrado." },
"aiToolViewingHistoryClearError": { "message": "Error al borrar el historial de visualización: $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." },
"aiToolM3UNoTitlesProvided": { "message": "Por favor, proporciona una lista de títulos para crear la lista de reproducción." },
"aiToolM3UCheckingTitles": { "message": "Comprobando los títulos en tus servidores locales..." },
"aiToolM3UNoLocalMatchesForDownload": { "message": "No he encontrado ninguna de las películas o series de la lista en tus servidores locales." },
"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" } } },
"backToProviders": { "message": "Volver a Proveedores" },
"artistsCounterSingle": { "message": "$total$ Artista", "placeholders": { "total": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Cargando..." },
"downloadingSong": { "message": "Iniciando descarga de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" descargado.", "placeholders": { "title": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Error al descargar \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Generando M3U para \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
"albumM3UGenerated": { "message": "M3U para el álbum \"$artist$\" generado.", "placeholders": { "artist": { "content": "$1" } } },
"retyingSection": { "message": "Reintentando sección \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"retrySuccess": { "message": "[ÉXITO] Reintento de \"$title$\" completado.", "placeholders": { "title": { "content": "$1" } } },
"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": "Bienvenido a CinePlex" },
"page": { "message": "Página" },
"all": { "message": "Todo" },
"userScore": { "message": "Puntuación" },
"duration": { "message": "Duración" },
"min": { "message": "Mín" },
"max": { "message": "Máx" }
}

View File

@ -1,25 +1,27 @@
{
"appName": { "message": "CinePlex" },
"appDescription": { "message": "Scanne les serveurs Plex pour trouver du contenu et l'affiche dans l'interface" },
"appTagline": { "message": "Films, Séries & Musique" },
"appDescription": { "message": "Scanne les serveurs Plex à la recherche de contenu et l'affiche dans l'interface" },
"appTagline": { "message": "Films, Séries et Musique" },
"appLocaleCode": { "message": "fr-FR" },
"toggleNavigation": { "message": "Basculer la Navigation" },
"toggleNavigation": { "message": "Basculer la navigation" },
"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" },
"navMovies": { "message": "Films" },
"navSeries": { "message": "Séries" },
"navProviders": { "message": "Fournisseurs" },
"navPhotos": { "message": "Photos" },
"navStats": { "message": "Statistiques" },
"navFavorites": { "message": "Favoris" },
"navHistory": { "message": "Historique" },
"navRecommendations": { "message": "Recommandations" },
"navMusic": { "message": "Musique" },
"navM3uGenerator": { "message": "Générateur M3U" },
"heroWelcome": { "message": "Bienvenue sur CinePlex" },
"heroSubtitle": { "message": "Explorez des milliers de films et de séries." },
"addStream": { "message": "Ajouter le flux" },
"addStream": { "message": "Ajouter un flux" },
"moreInfo": { "message": "Plus d'infos" },
"popularMovies": { "message": "Films Populaires" },
"popularMovies": { "message": "Films populaires" },
"allGenres": { "message": "Tous les genres" },
"allYears": { "message": "Toutes les années" },
"sortPopular": { "message": "Les plus populaires" },
@ -29,108 +31,118 @@
"photosBreadcrumbHome": { "message": "Albums" },
"selectServer": { "message": "Sélectionnez un serveur" },
"loading": { "message": "Chargement..." },
"loadingLibraries": { "message": "Chargement des bibliothèques..." },
"photosEmptyState": { "message": "Aucun album ou photo trouvé." },
"photosEmptyStateSub": { "message": "Veuillez sélectionner un serveur ou vous assurer d'avoir une bibliothèque de photos dans Plex." },
"statsTitle": { "message": "Statistiques de la Bibliothèque" },
"statsAllTokens": { "message": "Tous les Tokens" },
"photosEmptyStateSub": { "message": "Veuillez sélectionner un serveur ou vous assurer que vous disposez d'une photothèque dans Plex." },
"statsTitle": { "message": "Statistiques de la bibliothèque" },
"statsAllTokens": { "message": "Tous les jetons" },
"statsAnalyzing": { "message": "Analyse de votre bibliothèque..." },
"statsActiveTokens": { "message": "Tokens Actifs" },
"statsServersFound": { "message": "Serveurs Trouvés" },
"statsUniqueMovies": { "message": "Films Uniques" },
"statsUniqueSeries": { "message": "Séries Uniques" },
"statsUniqueArtists": { "message": "Artistes Uniques" },
"statsTokenServers": { "message": "Serveurs du Token" },
"statsChartMoviesByGenre": { "message": "Contenu par Genre (Films)" },
"statsChartSeriesByGenre": { "message": "Contenu par Genre (Séries)" },
"statsChartByDecade": { "message": "Contenu par Décennie" },
"statsActiveTokens": { "message": "Jetons actifs" },
"statsServersFound": { "message": "Serveurs trouvés" },
"statsUniqueMovies": { "message": "Films uniques" },
"statsUniqueSeries": { "message": "Séries uniques" },
"statsUniqueArtists": { "message": "Artistes uniques" },
"statsTokenServers": { "message": "Serveurs de jetons" },
"statsChartMoviesByGenre": { "message": "Contenu par genre (Films)" },
"statsChartSeriesByGenre": { "message": "Contenu par genre (Séries)" },
"statsChartByDecade": { "message": "Contenu par décennie" },
"recommendationsTitle": { "message": "Recommandations pour vous" },
"historyTitle": { "message": "Historique de Visionnage" },
"historyTitle": { "message": "Historique de visionnage" },
"clearHistory": { "message": "Tout effacer" },
"consoleTitle": { "message": "Console de Scan Plex" },
"consoleTitle": { "message": "Console d'analyse Plex" },
"footerCredit": { "message": "Une interface pour votre univers Plex." },
"backButton": { "message": "Retour" },
"closeTrailer": { "message": "Fermer la bande-annonce" },
"close": { "message": "Fermer" },
"photoViewer": { "message": "Visionneuse de photos" },
"previous": { "message": "Précédent" },
"next": { "message": "Suivant" },
"notificationTemplateText": { "message": "Notification" },
"settingsTitleFull": { "message": "Paramètres et Configuration" },
"settingsTitleFull": { "message": "Paramètres et configuration" },
"settingsTabGeneral": { "message": "Général" },
"settingsTabPlex": { "message": "Plex" },
"settingsTabJellyfin": { "message": "Jellyfin" },
"settingsTabPhpGen": { "message": "Générateur PHP" },
"settingsTabData": { "message": "Données" },
"settingsApiServer": { "message": "Configuration API et Serveur" },
"settingsTmdbApiLabel": { "message": "Clé API de TMDB (Optionnel)" },
"settingsTmdbApiPlaceholder": { "message": "Utilisera la clé par défaut si laissé vide" },
"settingsTmdbLangLabel": { "message": "Langue pour TMDB & UI" },
"settingsPhpUrlLabel": { "message": "URL du Serveur pour Ajout de Flux" },
"settingsApiServer": { "message": "Paramètres API et serveur" },
"settingsTmdbApiLabel": { "message": "Clé API TMDB (facultatif)" },
"settingsTmdbApiPlaceholder": { "message": "La clé par défaut sera utilisée si ce champ est laissé vide" },
"settingsGoogleApiLabel": { "message": "Clé API Google Gemini (facultatif)" },
"settingsGoogleApiPlaceholder": { "message": "Requis pour utiliser l'assistant IA" },
"settingsRegionLabel": { "message": "Région pour la découverte de contenu" },
"allRegions": { "message": "Toutes les régions" },
"settingsPhpUrlLabel": { "message": "URL du serveur pour l'ajout de flux" },
"settingsPhpUrlPlaceholder": { "message": "https://votre-serveur.com/chemin/vers/script.php" },
"settingsInterface": { "message": "Interface" },
"settingsLightTheme": { "message": "Mode Clair" },
"settingsShowHero": { "message": "Afficher la section d'accueil \"Hero\"" },
"settingsScanContent": { "message": "Scan de Contenu" },
"settingsScanDesc": { "message": "Sélectionnez ce que vous voulez scanner et appuyez sur le bouton." },
"settingsLightTheme": { "message": "Mode clair" },
"settingsShowHero": { "message": "Afficher la section d'accueil 'Hero'" },
"settingsScanContent": { "message": "Analyse du contenu" },
"settingsScanDesc": { "message": "Sélectionnez les éléments à analyser et appuyez sur le bouton." },
"settingsScanMovies": { "message": "Films" },
"settingsScanShows": { "message": "Séries" },
"settingsScanArtists": { "message": "Musique" },
"settingsScanPhotos": { "message": "Photos" },
"settingsSelectAll": { "message": "Tout sélectionner" },
"settingsStartScan": { "message": "Démarrer le Scan" },
"settingsPlexTokens": { "message": "Tokens Plex" },
"settingsPlexTokensDesc": { "message": "Modifiez la liste des tokens Plex (format JSON)." },
"settingsSaveTokens": { "message": "Sauvegarder les Tokens" },
"settingsPhpGenTitle": { "message": "Générateur de Script PHP pour Serveur" },
"settingsPhpFileOptions": { "message": "Options du Fichier" },
"settingsPhpSavePathLabel": { "message": "Chemin de Sauvegarde sur le Serveur" },
"settingsStartScan": { "message": "Démarrer l'analyse" },
"settingsPlexTokens": { "message": "Jetons Plex" },
"settingsPlexTokensDesc": { "message": "Modifiez la liste des jetons Plex (format JSON)." },
"settingsSaveTokens": { "message": "Enregistrer les jetons" },
"settingsJellyfinTitle": { "message": "Paramètres Jellyfin" },
"settingsJellyfinDesc": { "message": "Ajoutez les détails de votre serveur Jellyfin pour analyser son contenu." },
"jellyfinUrlLabel": { "message": "URL du serveur Jellyfin" },
"jellyfinUserLabel": { "message": "Nom d'utilisateur" },
"jellyfinPassLabel": { "message": "Mot de passe" },
"jellyfinConnectAndScan": { "message": "Connecter et analyser" },
"settingsPhpGenTitle": { "message": "Générateur de script PHP pour serveur" },
"settingsPhpFileOptions": { "message": "Options de fichier" },
"settingsPhpSavePathLabel": { "message": "Chemin d'enregistrement sur le serveur" },
"settingsPhpSavePathPlaceholder": { "message": "Ex: /var/www/html/listes (vide pour le même dossier)" },
"settingsPhpFilenameLabel": { "message": "Nom du Fichier" },
"settingsPhpFileAction": { "message": "Action sur le Fichier" },
"settingsPhpFilenameLabel": { "message": "Nom de fichier" },
"settingsPhpFileAction": { "message": "Action sur le fichier" },
"settingsPhpActionAppend": { "message": "Ajouter à la fin du fichier (cumulatif)" },
"settingsPhpActionOverwrite": { "message": "Écraser le fichier (repartir à zéro)" },
"settingsPhpSecurity": { "message": "Sécurité (Optionnel)" },
"settingsPhpUseSecretKey": { "message": "Utiliser une clé secrète (Recommandé)" },
"settingsPhpSecretKeyPlaceholder": { "message": "Entrez une clé secrète sécurisée" },
"settingsPhpGeneratedCode": { "message": "Code Généré" },
"settingsPhpActionOverwrite": { "message": "Écraser le fichier (repartir de zéro)" },
"settingsPhpSecurity": { "message": "Sécurité (facultatif)" },
"settingsPhpUseSecretKey": { "message": "Utiliser une clé secrète (recommandé)" },
"settingsPhpSecretKeyPlaceholder": { "message": "Saisissez une clé secrète sécurisée" },
"settingsPhpGeneratedCode": { "message": "Code généré" },
"settingsPhpGeneratedPlaceholder": { "message": "Le code PHP généré apparaîtra ici." },
"settingsGenerateScript": { "message": "Générer le Script" },
"settingsCopyScript": { "message": "Copier le Script" },
"settingsDataManagement": { "message": "Gestion de la Base de Données Locale" },
"settingsImportDb": { "message": "Importer BD depuis un Fichier" },
"settingsExportDb": { "message": "Exporter BD vers un Fichier" },
"settingsClearContent": { "message": "Effacer les Données de Contenu Locales" },
"settingsClearContentDesc": { "message": "Cette action supprimera les films, séries et musiques de la base de données locale, mais n'affectera pas vos favoris ou paramètres." },
"settingsGenerateScript": { "message": "Générer le script" },
"settingsCopyScript": { "message": "Copier le script" },
"settingsDataManagement": { "message": "Gestion de la base de données locale" },
"settingsImportDb": { "message": "Importer la base de données depuis un fichier" },
"settingsExportDb": { "message": "Exporter la base de données vers un fichier" },
"settingsClearContent": { "message": "Effacer les données de contenu local" },
"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." },
"settingsClose": { "message": "Fermer" },
"settingsSave": { "message": "Sauvegarder les Paramètres" },
"musicSidenavTitle": { "message": "Plex Musique" },
"musicAllServers": { "message": "Tous les Serveurs" },
"settingsSave": { "message": "Enregistrer les paramètres" },
"musicSidenavTitle": { "message": "Musique Plex" },
"musicAllServers": { "message": "Tous les serveurs" },
"musicSearchArtistPlaceholder": { "message": "Rechercher un artiste..." },
"musicSearchDiscographyPlaceholder": { "message": "Rechercher dans la discographie..." },
"musicNothingPlaying": { "message": "Aucune lecture en cours" },
"musicSelectSong": { "message": "Sélectionnez une chanson" },
"musicToStart": { "message": "pour commencer la lecture" },
"musicToStart": { "message": "pour démarrer la lecture" },
"miniplayerDownloadSong": { "message": "Télécharger la chanson" },
"miniplayerDownloadAlbum": { "message": "Télécharger l'album M3U" },
"miniplayerVolume": { "message": "Volume" },
"miniplayerShuffle": { "message": "Aléatoire" },
"miniplayerEqualizer": { "message": "Égaliseur" },
"miniplayerOpenList": { "message": "Ouvrir la liste" },
"eqTitle": { "message": "Égaliseur Graphique" },
"eqTitle": { "message": "Égaliseur graphique" },
"eqPresetsLabel": { "message": "Préréglages" },
"eqPresetFlat": { "message": "Plat" },
"eqPresetRock": { "message": "Rock" },
"eqPresetPop": { "message": "Pop" },
"eqPresetJazz": { "message": "Jazz" },
"eqPresetClassical": { "message": "Classique" },
"eqPresetBassBoost": { "message": "Renforcement des Basses" },
"eqPreampLabel": { "message": "Pré-Amp" },
"infoModalTitle": { "message": "Information" },
"infoModalFieldTitle": { "message": "Titre:" },
"infoModalFieldArtist": { "message": "Artiste:" },
"infoModalFieldAlbum": { "message": "Album:" },
"infoModalFieldSong": { "message": "Chanson:" },
"infoModalFieldYear": { "message": "Année:" },
"infoModalFieldGenre": { "message": "Genre:" },
"eqPresetBassBoost": { "message": "Amplification des basses" },
"eqPreampLabel": { "message": "Préampli" },
"infoModalTitle": { "message": "Informations" },
"infoModalFieldTitle": { "message": "Titre :" },
"infoModalFieldArtist": { "message": "Artiste :" },
"infoModalFieldAlbum": { "message": "Album :" },
"infoModalFieldSong": { "message": "Chanson :" },
"infoModalFieldYear": { "message": "Année :" },
"infoModalFieldGenre": { "message": "Genre :" },
"lang_en": { "message": "Anglais" },
"lang_es": { "message": "Espagnol" },
"lang_fr": { "message": "Français" },
@ -141,36 +153,36 @@
"dbAccessError": { "message": "Erreur d'accès à la base de données locale." },
"dbUpdateNeeded": { "message": "La base de données doit être mise à jour, veuillez recharger la page." },
"dbBlocked": { "message": "Veuillez fermer les autres onglets de cette application pour continuer." },
"deletingContentData": { "message": "Suppression des données de contenu locales..." },
"deletingContentData": { "message": "Suppression des données de contenu local..." },
"noContentDataToDelete": { "message": "Aucune donnée de contenu à supprimer." },
"contentDataDeleted": { "message": "Données de contenu supprimées de IndexedDB." },
"contentDataDeleted": { "message": "Données de contenu supprimées d'IndexedDB." },
"errorDeletingData": { "message": "Erreur lors de la suppression des données : $message$", "placeholders": { "message": { "content": "$1" } } },
"aceEditorNotAvailable": { "message": "Éditeur de texte non disponible." },
"errorLoadingTokens": { "message": "Erreur lors du chargement des tokens pour l'édition." },
"errorLoadingTokensMessage": { "message": "Erreur de chargement des tokens : $message$", "placeholders": { "message": { "content": "$1" } } },
"aceEditorNotAvailableToSave": { "message": "Éditeur non disponible pour la sauvegarde." },
"invalidJsonFormat": { "message": "Format JSON invalide. Doit être { \"tokens\": [...] }" },
"tokensSaved": { "message": "Tokens sauvegardés avec succès." },
"errorSavingTokens": { "message": "Erreur lors de la sauvegarde des tokens : $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingTokens": { "message": "Erreur lors du chargement des jetons pour modification." },
"errorLoadingTokensMessage": { "message": "Erreur lors du chargement des jetons : $message$", "placeholders": { "message": { "content": "$1" } } },
"aceEditorNotAvailableToSave": { "message": "Éditeur non disponible pour l'enregistrement." },
"invalidJsonFormat": { "message": "Format JSON non valide. Il doit être { \"tokens\": [...] }" },
"tokensSaved": { "message": "Jetons enregistrés avec succès." },
"errorSavingTokens": { "message": "Erreur lors de l'enregistrement des jetons : $message$", "placeholders": { "message": { "content": "$1" } } },
"dbNotAvailable": { "message": "IndexedDB n'est pas disponible." },
"dbExported": { "message": "Base de données exportée avec succès." },
"errorExportingDb": { "message": "Erreur lors de l'exportation de la base de données : $message$", "placeholders": { "message": { "content": "$1" } } },
"invalidJsonFile": { "message": "Le fichier ne contient pas un objet JSON valide." },
"noDataToImport": { "message": "Le fichier ne contient pas de données pour les sections actuelles de la BD." },
"invalidJsonFile": { "message": "Le fichier ne contient pas d'objet JSON valide." },
"noDataToImport": { "message": "Le fichier ne contient aucune donnée pour les sections actuelles de la base de données." },
"dbImported": { "message": "Base de données importée avec succès." },
"errorImportingDb": { "message": "Erreur lors de l'importation de la base de données : $message$", "placeholders": { "message": { "content": "$1" } } },
"updatingView": { "message": "Mise à jour de la vue avec les nouvelles données..." },
"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." },
"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." },
"trailerNotFound": { "message": "Aucune bande-annonce trouvée pour ce titre." },
"confirmClearHistory": { "message": "Êtes-vous sûr de vouloir effacer tout votre historique de visionnage ? Cette action est irréversible." },
"historyCleared": { "message": "Historique de visionnage effacé." },
"historyItemDeleted": { "message": "Élément supprimé de l'historique." },
"errorGeneratingScript": { "message": "Générez d'abord un script avant de pouvoir le copier." },
"errorGeneratingScript": { "message": "Générez d'abord un script pour pouvoir le copier." },
"scriptCopied": { "message": "Script PHP copié dans le presse-papiers." },
"errorCopyingScript": { "message": "Erreur lors de la copie du script." },
"scriptGenerated": { "message": "Script PHP généré." },
"errorLoadingAlbum": { "message": "Erreur lors du chargement de l'album : $message$", "placeholders": { "message": { "content": "$1" } } },
"noPhotoServerSelected": { "message": "Erreur : Aucun serveur de photos n'a été sélectionné." },
"noPhotoServerSelected": { "message": "Erreur : Aucun serveur photo n'a été sélectionné." },
"loadingGenres": { "message": "Chargement des genres..." },
"errorLoadingGenres": { "message": "Erreur de chargement" },
"noContentFound": { "message": "Aucun résultat trouvé." },
@ -178,46 +190,35 @@
"noFavorites": { "message": "Vous n'avez pas encore de favoris." },
"errorLoadingFavorites": { "message": "Erreur lors du chargement des favoris." },
"historyEmpty": { "message": "Votre historique est vide." },
"historyEmptySub": { "message": "Parcourez et regardez du contenu pour qu'il apparaisse ici." },
"historyEmptySub": { "message": "Explorez et regardez du contenu pour qu'il apparaisse ici." },
"errorGeneratingRecommendations": { "message": "Erreur lors de la génération des recommandations." },
"noRecommendations": { "message": "Nous avons besoin de mieux vous connaître pour vous faire des recommandations !" },
"errorGeneratingStats": { "message": "Erreur lors de la génération des statistiques." },
"noServersForToken": { "message": "Aucun serveur associé trouvé pour ce token." },
"noServersForToken": { "message": "Aucun serveur associé trouvé pour ce jeton." },
"searchingActorContent": { "message": "Recherche de contenu de $actorName$", "placeholders": { "actorName": { "content": "$1" } } },
"errorLoadingActorContent": { "message": "Impossible de charger le contenu pour $actorName$.", "placeholders": { "actorName": { "content": "$1" } } },
"errorAddingStream": { "message": "Erreur lors de l'ajout du ou des flux : $message$", "placeholders": { "message": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "L'URL du serveur PHP n'est pas configurée. Veuillez la configurer dans les Paramètres." },
"searchingStreams": { "message": "Recherche de flux pour \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
"errorAddingStream": { "message": "Erreur lors de l'ajout de flux : $message$", "placeholders": { "message": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "L'URL du serveur PHP n'est pas configurée. Veuillez la configurer dans les paramètres." },
"searchingStreams": { "message": "Recherche de flux pour \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"sendingStreams": { "message": "Envoi de $count$ flux au serveur...", "placeholders": { "count": { "content": "$1" } } },
"streamAddedSuccess": { "message": "Flux ajouté(s) avec succès." },
"generatingM3U": { "message": "Génération du M3U pour \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
"m3uDownloaded": { "message": "M3U pour \"$title$\" téléchargé.", "placeholders": { "title": { "content": "$1" } } },
"errorGeneratingM3U": { "message": "Erreur lors de la génération du M3U : $message$", "placeholders": { "message": { "content": "$1" } } },
"settingsSavedSuccess": { "message": "Paramètres sauvegardés avec succès." },
"errorSavingSettings": { "message": "Erreur lors de la sauvegarde des paramètres dans la base de données." },
"generatingM3U": { "message": "Génération de M3U pour \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"m3uDownloaded": { "message": "\"$title$\" téléchargé.", "placeholders": { "title": { "content": "$1" } } },
"errorGeneratingM3U": { "message": "Erreur lors de la génération de M3U : $message$", "placeholders": { "message": { "content": "$1" } } },
"settingsSavedSuccess": { "message": "Paramètres enregistrés avec succès." },
"errorSavingSettings": { "message": "Erreur lors de l'enregistrement des paramètres dans la base de données." },
"languageChangeReload": { "message": "Langue modifiée. L'application va maintenant se recharger." },
"addedToFavorites": { "message": "Ajouté aux favoris." },
"removedFromFavorites": { "message": "Retiré des favoris." },
"plexScanInProgress": { "message": "Le scan Plex est déjà en cours." },
"plexScanStarting": { "message": "Démarrage du scan Plex..." },
"noPlexTokens": { "message": "Aucun token Plex configuré." },
"clearingSections": { "message": "Nettoyage des sections : $sections$", "placeholders": { "sections": { "content": "$1" } } },
"sectionsCleared": { "message": "Sections nettoyées." },
"tokenFoundServers": { "message": "Le token $token$... a trouvé $count$ serveurs.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } },
"errorProcessingToken": { "message": "Erreur lors du traitement du token $token$... : $message$", "placeholders": { "token": { "content": "$1" }, "message": { "content": "$2" } } },
"initialScanPhaseComplete": { "message": "Phase de scan initiale terminée." },
"retryPhaseFinished": { "message": "Phase de relance terminée." },
"plexScanFinished": { "message": "Scan terminé. Mise à jour du contenu..." },
"plexScanFatalError": { "message": "ERREUR FATALE : $message$", "placeholders": { "message": { "content": "$1" } } },
"errorDuringScan": { "message": "Erreur pendant le scan : $message$", "placeholders": { "message": { "content": "$1" } } },
"scanCancelled": { "message": "Scan annulé par l'utilisateur." },
"scanCancelledInfo": { "message": "Scan annulé." },
"retyingSection": { "message": "Nouvelle tentative pour la section \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
"retrySuccess": { "message": "[SUCCÈS] Relance de \"$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" } } },
"noRetriesPending": { "message": "Aucune relance en attente." },
"startingRetryPhase": { "message": "Démarrage de la phase de relance pour $count$ sections...", "placeholders": { "count": { "content": "$1" } } },
"retryPhaseCancelled": { "message": "Phase de relance annulée." },
"removedFromFavorites": { "message": "Supprimé des favoris." },
"plexScanInProgress": { "message": "L'analyse Plex est déjà en cours." },
"plexScanStarting": { "message": "Démarrage de l'analyse Plex..." },
"noPlexTokens": { "message": "Aucun jeton Plex configuré." },
"clearingSections": { "message": "Effacement des sections : $sections$", "placeholders": { "sections": { "content": "$1" } } },
"initialScanPhaseComplete": { "message": "Phase d'analyse initiale terminée." },
"retryPhaseFinished": { "message": "Phase de nouvelle tentative terminée." },
"plexScanFinished": { "message": "Analyse terminée. Mise à jour du contenu..." },
"scanCancelled": { "message": "Analyse annulée par l'utilisateur." },
"scanCancelledInfo": { "message": "Analyse annulée." },
"errorInitializingMusicPlayer": { "message": "Erreur lors de l'initialisation du lecteur de musique." },
"criticalErrorLoadingMusic": { "message": "Erreur critique lors du chargement des données musicales." },
"errorLoadingArtists": { "message": "Erreur lors du chargement des artistes." },
@ -227,23 +228,14 @@
"errorFetchingArtistSongs": { "message": "Erreur lors de la récupération des chansons de l'artiste." },
"errorLoadingSongs": { "message": "Erreur lors du chargement des chansons." },
"noArtistsFound": { "message": "Aucun artiste trouvé." },
"artistsCounter": { "message": "$start$-$end$ sur $total$", "placeholders": { "start": { "content": "$1" }, "end": { "content": "$2" }, "total": { "content": "$3" } } },
"artistsCounterSingle": { "message": "$total$ Artistes", "placeholders": { "total": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Chargement..." },
"noSongsFound": { "message": "Aucune chanson trouvée." },
"shuffleOn": { "message": "Mode aléatoire activé." },
"shuffleOff": { "message": "Mode aléatoire désactivé." },
"downloadingSong": { "message": "Début du téléchargement de \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" téléchargé.", "placeholders": { "title": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Erreur lors du téléchargement de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Génération du M3U pour \"$artist$\"...", "placeholders": { "artist": { "content": "$1" } } },
"albumM3UGenerated": { "message": "M3U de l'album \"$artist$\" généré.", "placeholders": { "artist": { "content": "$1" } } },
"playbackError": { "message": "Erreur de lecture" },
"errorLabel": { "message": "Erreur" },
"reloadingPage": { "message": "Rechargement de la page..." },
"viewed": { "message": "Vu" },
"local": { "message": "Local" },
"topRatedSort": {"message": "Mieux notés"},
"topRatedSort": {"message": "Les mieux notés"},
"recentSort": {"message": "Récents"},
"popularSort": {"message": "Populaires"},
"moviesSectionTitle": {"message": "Films"},
@ -254,37 +246,204 @@
"noGenre": {"message": "Non classé"},
"synopsis": {"message": "Synopsis"},
"noSynopsis": {"message": "Aucun synopsis disponible."},
"director": {"message": "Réalisateur:"},
"writer": {"message": "Scénariste:"},
"director": {"message": "Réalisateur :"},
"writer": {"message": "Scénariste :"},
"viewOnImdb": {"message": "Voir sur IMDb"},
"watchTrailer": {"message": "Voir la bande-annonce"},
"watchTrailer": {"message": "Regarder la bande-annonce"},
"addToFavorites": {"message": "Ajouter aux favoris"},
"removeFromFavorites": {"message": "Retirer des favoris"},
"notAvailable": {"message": "Non disponible"},
"mainCast": {"message": "Distribution Principale"},
"seasonsAndEpisodes": {"message": "Saisons & Épisodes"},
"similarContent": {"message": "Contenu Similaire"},
"episodesCount": {"message": "$count$ Épisodes", "placeholders": {"count": {"content": "$1"}}},
"seasonsCount": {"message": "$count$ Saisons", "placeholders": {"count": {"content": "$1"}}},
"mainCast": {"message": "Distribution principale"},
"seasonsAndEpisodes": {"message": "Saisons et épisodes"},
"similarContent": {"message": "Contenu similaire"},
"filmography": {"message": "Filmographie"},
"availableOn": {"message": "Disponible sur"},
"episodesCount": {"message": "$count$ épisodes", "placeholders": {"count": {"content": "$1"}}},
"seasonsCount": {"message": "$count$ saisons", "placeholders": {"count": {"content": "$1"}}},
"runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}},
"noTrailerFound": {"message": "Aucune bande-annonce n'a été trouvée pour ce titre."},
"fatalInitError": {"message": "Erreur d'Initialisation Fatale"},
"fatalInitErrorSub": {"message": "L'application n'a pas pu être chargée."},
"invalidStreamInfo": {"message": "Information invalide."},
"noTrailerFound": {"message": "Aucune bande-annonce trouvée pour ce titre."},
"fatalInitError": {"message": "Erreur d'initialisation fatale"},
"fatalInitErrorSub": {"message": "Impossible de charger l'application."},
"invalidStreamInfo": {"message": "Informations non valides."},
"dbUnavailableForStreams": {"message": "Base de données locale non disponible."},
"noPlexServersForStreams": {"message": "Pas de serveurs Plex."},
"notFoundOnServers": {"message": "\"$query$\" non trouvé sur les serveurs.", "placeholders": {"query": {"content": "$1"}}},
"noPlexServersForStreams": {"message": "Aucun serveur Plex."},
"notFoundOnServers": {"message": "\"$query$\" introuvable sur les serveurs Plex.", "placeholders": {"query": {"content": "$1"}}},
"relativeTime_justNow": { "message": "À l'instant" },
"relativeTime_minutesAgo": { "message": "Il y a $count$ minutes", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_hoursAgo": { "message": "Il y a $count$ heures", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_yesterday": { "message": "Hier" },
"relativeTime_daysAgo": { "message": "Il y a $count$ jours", "placeholders": { "count": { "content": "$1" } } },
"errorLoadingDetails": { "message": "Erreur de Chargement des Détails" },
"errorLoadingDetails": { "message": "Erreur lors du chargement des détails" },
"errorLoadingLocalContent": { "message": "Erreur lors du chargement du contenu local." },
"errorServerResponse": { "message": "Réponse non réussie du serveur." },
"errorServerResponse": { "message": "Réponse du serveur infructueuse." },
"errorPlexApi": { "message": "Erreur $status$ de l'API Plex.", "placeholders": { "status": { "content": "$1" } } },
"errorParsingPlexXml": { "message": "Erreur d'analyse du XML de Plex." },
"errorParsingPlexXml": { "message": "Erreur lors de l'analyse du XML Plex." },
"untitled": { "message": "Sans titre" },
"itemCount": { "message": "$count$ éléments", "placeholders": { "count": { "content": "$1" } } },
"noPhotoServers": { "message": "Aucun serveur de photos" }
"noPhotoServers": { "message": "Aucun serveur photo" },
"jellyfinScanInProgress": { "message": "L'analyse Jellyfin est déjà en cours." },
"jellyfinScanning": { "message": "Analyse de Jellyfin..." },
"jellyfinMissingCredentials": { "message": "Veuillez compléter l'URL et le nom d'utilisateur de Jellyfin." },
"jellyfinConnecting": { "message": "Connexion à Jellyfin à : $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinAuthFailed": { "message": "Échec de l'authentification Jellyfin : $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinAuthSuccess": { "message": "Authentification Jellyfin réussie." },
"jellyfinFetchingLibraries": { "message": "Récupération des bibliothèques..." },
"jellyfinFetchFailed": { "message": "Erreur lors de la récupération des bibliothèques : $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinNoMediaLibraries": { "message": "Aucune bibliothèque de films ou de séries trouvée dans Jellyfin." },
"jellyfinLibrariesFound": { "message": "$count$ bibliothèque(s) multimédia(s) trouvée(s).", "placeholders": { "count": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Succès] '$libraryName' analysée, $count$ titres ajoutés.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"jellyfinLibraryScanFailed": { "message": "Erreur lors de l'analyse de la bibliothèque '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Analyse Jellyfin terminée. $movies$ films et $series$ séries ajoutés.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Informations d'identification Jellyfin non configurées." },
"notFoundOnJellyfin": { "message": "\"$query$\" introuvable sur Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" introuvable sur aucun serveur.", "placeholders": { "query": { "content": "$1" } } },
"localOnPlex": { "message": "Sur Plex" },
"searchOnPlex": { "message": "Rechercher sur Plex" },
"jellyfinTitle": { "message": "Contenu Jellyfin" },
"noJellyfinContent": { "message": "Aucun contenu Jellyfin trouvé." },
"noJellyfinContentSub": { "message": "Assurez-vous d'avoir analysé votre serveur Jellyfin dans les paramètres." },
"activityViewerTitle": { "message": "Visionneuse d'activité du serveur" },
"activitySelectServer": { "message": "Sélectionnez un serveur" },
"activityCheckBtn": { "message": "Actualiser" },
"activityNoSessions": { "message": "Aucune session active sur ce serveur." },
"activitySessionUser": { "message": "Utilisateur" },
"activitySessionDevice": { "message": "Appareil" },
"activitySessionContent": { "message": "Contenu" },
"activitySessionState": { "message": "État" },
"activitySessionIdentifier": { "message": "Identifiant du client" },
"activityCopyID": { "message": "Copier l'ID" },
"activityError": { "message": "Impossible d'obtenir l'activité du serveur." },
"activityCopied": { "message": "Identifiant copié dans le presse-papiers !" },
"activityCopyError": { "message": "Erreur lors de la copie de l'identifiant." },
"noProvidersFound": { "message": "Aucun fournisseur trouvé." },
"availableOnPlex": { "message": "Disponible sur Plex" },
"m3uGeneratorTitle": { "message": "Générateur de listes M3U" },
"selectAServer": { "message": "Sélectionnez un serveur..." },
"downloadM3u": { "message": "Télécharger M3U" },
"m3uGenerator": { "message": "Générateur M3U" },
"selectLibraries": { "message": "Sélectionner les bibliothèques" },
"howToUse": { "message": "Comment utiliser" },
"m3uInstruction1": { "message": "Choisissez un serveur dans la liste." },
"m3uInstruction2": { "message": "Sélectionnez une ou plusieurs bibliothèques à inclure." },
"m3uInstruction3": { "message": "Cliquez sur le bouton de téléchargement." },
"m3uInstruction4": { "message": "Importez le fichier .m3u dans votre lecteur compatible." },
"chatOpen": { "message": "Ouvrir le chat" },
"chatTitle": { "message": "Assistant IA" },
"chatClose": { "message": "X" },
"chatPlaceholder": { "message": "Saisissez votre message..." },
"chatSend": { "message": "➤" },
"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." },
"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." },
"chatApiInvalidResponse": { "message": "L'API a renvoyé une réponse non valide. Veuillez réessayer." },
"chatApiError": { "message": "Erreur de communication avec l'assistant IA" },
"downloadAll": { "message": "Tout télécharger" },
"download": { "message": "Télécharger" },
"aiToolSearchLibraryDesc": { "message": "Recherche dans la bibliothèque Plex de l'utilisateur des films ou des séries par titre." },
"aiToolSearchLibraryQueryParamDesc": { "message": "Le titre du film ou de la série à rechercher." },
"aiToolSearchLibraryTypeParamDesc": { "message": "Le type de contenu à rechercher. Peut être 'movie' pour les films ou 'series' pour les séries. (Facultatif)." },
"aiToolSearchLibraryResolutionParamDesc": { "message": "La résolution vidéo à rechercher (par exemple, '4k', '1080p'). (Facultatif)." },
"aiToolSearchLibraryContainerParamDesc": { "message": "Le format de conteneur vidéo à rechercher (par exemple, 'mkv', 'mp4'). (Facultatif)." },
"aiToolNavigateToPageDesc": { "message": "Dirige l'utilisateur vers une page spécifique de l'interface de l'application." },
"aiToolNavigateToPagePageParamDesc": { "message": "Le nom de la page vers laquelle naviguer, par exemple : 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers' ou 'm3u-generator'." },
"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." },
"aiToolShowItemDetailsDesc": { "message": "Affiche la page de détails d'un film ou d'une série spécifique par son titre et son type." },
"aiToolShowItemDetailsTitleParamDesc": { "message": "Le titre exact du film ou de la série." },
"aiToolShowItemDetailsTypeParamDesc": { "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 le diffuser sur un serveur PHP configuré." },
"aiToolAddToPlaylistTitleParamDesc": { "message": "Le titre du film ou de la série à ajouter." },
"aiToolAddToPlaylistTypeParamDesc": { "message": "Le type de contenu. 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, si trouvés, génère et télécharge un fichier de liste de lecture M3U avec les flux trouvés." },
"aiToolCheckAndDownloadTitlesParamDesc": { "message": "Un tableau de titres de films ou de séries à rechercher et à télécharger." },
"aiToolCheckAndDownloadTypeParamDesc": { "message": "Le type de contenu de la liste. Doit être 'movie' ou 'series'." },
"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é." },
"aiToolToggleFavoriteDesc": { "message": "Ajoute ou supprime un film ou une série de la liste des favoris de l'utilisateur." },
"aiToolToggleFavoriteTitleParamDesc": { "message": "Le titre du film ou de la série." },
"aiToolToggleFavoriteTypeParamDesc": { "message": "Le type de contenu. Doit être 'movie' ou 'series'." },
"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." },
"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." },
"aiToolApplyFiltersTypeParamDesc": { "message": "Le type de contenu auquel appliquer les filtres. Doit être 'movie' ou 'series'." },
"aiToolApplyFiltersGenreParamDesc": { "message": "Le nom du genre par lequel filtrer (par exemple, 'Action', 'Drame')." },
"aiToolApplyFiltersYearParamDesc": { "message": "L'année de sortie par laquelle filtrer (par exemple, '2023')." },
"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)." },
"aiToolPlayMusicByArtistDesc": { "message": "Ouvre le lecteur de musique et commence à jouer les chansons d'un artiste spécifique de la bibliothèque de l'utilisateur." },
"aiToolPlayMusicByArtistNameParamDesc": { "message": "Le nom exact de l'artiste dont les chansons doivent être jouées." },
"aiToolClearChatHistoryDesc": { "message": "Efface tout l'historique des messages de la conversation en cours avec l'assistant IA." },
"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." },
"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." },
"aiToolAddPlexTokenDesc": { "message": "Ajoute un nouveau jeton X-Plex à la configuration de l'extension, permettant à l'application d'analyser le contenu de nouveaux serveurs Plex." },
"aiToolAddPlexTokenTokenParamDesc": { "message": "La chaîne de jeton X-Plex à ajouter." },
"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." },
"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)." },
"aiToolClearAllFavoritesDesc": { "message": "Supprime tous les films et séries que l'utilisateur a marqués comme favoris." },
"aiToolClearViewingHistoryDesc": { "message": "Efface l'historique de visionnage de l'utilisateur de la page d'historique." },
"aiToolClearRecommendationsViewDesc": { "message": "Efface la vue des recommandations et supprime les recommandations mises en cache." },
"aiToolSearchNotFound": { "message": "'$query' introuvable dans votre bibliothèque.", "placeholders": { "query": { "content": "$1" } } },
"aiToolNavigateSuccess": { "message": "Navigation vers la page $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolNavigateError": { "message": "Erreur lors de la navigation vers la page $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolStatsError": { "message": "Erreur lors de l'obtention des statistiques." },
"aiToolItemNotFound": { "message": "Élément '$title' introuvable.", "placeholders": { "title": { "content": "$1" } } },
"aiToolShowItemDetailsSuccess": { "message": "Affichage des détails de '$title'.", "placeholders": { "title": { "content": "$1" } } },
"aiToolAddToPlaylistSuccess": { "message": "'$title' ajouté à la liste de lecture.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoriteAdded": { "message": "'$title' ajouté aux favoris.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoriteRemoved": { "message": "'$title' supprimé des favoris.", "placeholders": { "title": { "content": "$1" } } },
"aiToolRecommendationsSuccess": { "message": "Affichage des recommandations." },
"aiToolApplyFiltersGenreNotFound": { "message": "Genre '$genre' introuvable.", "placeholders": { "genre": { "content": "$1" } } },
"aiToolApplyFiltersSuccess": { "message": "Filtres appliqués avec succès." },
"aiToolPlayMusicNotReady": { "message": "Le lecteur de musique n'est pas prêt. Assurez-vous que votre bibliothèque musicale Plex a été analysée." },
"aiToolPlayMusicArtistNotFound": { "message": "Artiste '$artist_name' introuvable.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolPlayMusicNoSongs": { "message": "Aucune chanson trouvée pour '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolPlayMusicSuccess": { "message": "Lecture de la musique de '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolChatHistoryCleared": { "message": "Historique du chat effacé." },
"aiToolConfirmDeleteDatabase": { "message": "Êtes-vous sûr de vouloir supprimer la base de données locale ? Cette action est irréversible." },
"aiToolDeleteDatabaseCancelled": { "message": "Suppression de la base de données annulée." },
"aiToolExecutionError": { "message": "Erreur lors de l'exécution de l'outil '$toolName' : $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } },
"aiToolUnknown": { "message": "Outil inconnu : '$toolName'.", "placeholders": { "toolName": { "content": "$1" } } },
"aiToolFavoritesCleared": { "message": "Favoris effacés." },
"aiToolFavoritesClearError": { "message": "Erreur lors de l'effacement des favoris : $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolRecommendationsCleared": { "message": "Recommandations effacées." },
"aiToolRecommendationsClearError": { "message": "Erreur lors de l'effacement des recommandations : $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolDatabaseDeleted": { "message": "Base de données supprimée. La page va être rechargée." },
"aiToolDatabaseDeleteError": { "message": "Erreur lors de la suppression de la base de données : $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolDatabaseDeleteBlocked": { "message": "La suppression de la base de données est bloquée. Fermez les autres onglets de l'application." },
"aiToolUpdateAllTokensSuccess": { "message": "Tous les jetons ont été mis à jour avec succès." },
"aiToolUpdateAllTokensError": { "message": "Erreur lors de la mise à jour des jetons : $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolAddPlexTokenSuccess": { "message": "Jeton Plex ajouté avec succès." },
"aiToolAddPlexTokenError": { "message": "Erreur lors de l'ajout du jeton Plex : $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolChangeRegionSuccess": { "message": "Région modifiée en $region$. Le contenu est en cours de mise à jour.", "placeholders": { "region": { "content": "$1" } } },
"aiToolChangeRegionError": { "message": "Erreur lors du changement de région : $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolViewingHistoryCleared": { "message": "Historique de visionnage effacé." },
"aiToolViewingHistoryClearError": { "message": "Erreur lors de l'effacement de l'historique de visionnage : $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." },
"aiToolM3UNoTitlesProvided": { "message": "Veuillez fournir une liste de titres pour créer la liste de lecture." },
"aiToolM3UCheckingTitles": { "message": "Vérification des titres sur vos serveurs locaux..." },
"aiToolM3UNoLocalMatchesForDownload": { "message": "Je n'ai trouvé aucun des films ou séries de la liste sur vos serveveurs locaux." },
"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" } } },
"backToProviders": { "message": "Retour aux fournisseurs" },
"artistsCounterSingle": { "message": "$total$ artiste", "placeholders": { "total": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Chargement..." },
"downloadingSong": { "message": "Début du téléchargement de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" téléchargé.", "placeholders": { "title": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Erreur lors du téléchargement de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Génération de M3U pour \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
"albumM3UGenerated": { "message": "M3U pour l'album \"$artist$\" généré.", "placeholders": { "artist": { "content": "$1" } } },
"retyingSection": { "message": "Nouvelle tentative de la section \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"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": "Jeton $token$... a trouvé $count$ serveurs.", "placeholders": { "token": { "content": "$1" }, "count": { "content": "$2" } } },
"errorProcessingToken": { "message": "Erreur lors du traitement du jeton $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": "Jeton non valide fourni." },
"tokenAlreadyExists": { "message": "Le jeton existe déjà." },
"tokenAddedSuccessfully": { "message": "Jeton ajouté avec succès." },
"noStreamsFoundForSelection": { "message": "Aucun flux trouvé pour la sélection." },
"autoplayBlocked": { "message": "Lecture automatique bloquée." },
"page": { "message": "Page" },
"all": { "message": "Tous" },
"userScore": { "message": "Score des utilisateurs" },
"duration": { "message": "Durée" },
"min": { "message": "Min" },
"max": { "message": "Max" }
}

View File

@ -1,110 +1,122 @@
{
"appName": { "message": "CinePlex" },
"appDescription": { "message": "Analizza i server Plex per trovare contenuti e li visualizza nell'interfaccia" },
"appTagline": { "message": "Film, Serie TV e Musica" },
"appDescription": { "message": "Scansiona i server Plex alla ricerca di contenuti e li visualizza nell'interfaccia" },
"appTagline": { "message": "Film, Serie e Musica" },
"appLocaleCode": { "message": "it-IT" },
"toggleNavigation": { "message": "Mostra/Nascondi Navigazione" },
"searchPlaceholder": { "message": "Cerca film o serie TV..." },
"openMusicPlayer": { "message": "Apri Lettore Musicale" },
"toggleNavigation": { "message": "Attiva/disattiva la navigazione" },
"searchPlaceholder": { "message": "Cerca film o serie..." },
"openMusicPlayer": { "message": "Apri il lettore musicale" },
"settings": { "message": "Impostazioni" },
"navMovies": { "message": "Film" },
"navSeries": { "message": "Serie TV" },
"navSeries": { "message": "Serie" },
"navProviders": { "message": "Provider" },
"navPhotos": { "message": "Foto" },
"navStats": { "message": "Statistiche" },
"navFavorites": { "message": "Preferiti" },
"navHistory": { "message": "Cronologia" },
"navRecommendations": { "message": "Consigliati" },
"navRecommendations": { "message": "Raccomandazioni" },
"navMusic": { "message": "Musica" },
"heroWelcome": { "message": "Benvenuto su CinePlex" },
"heroSubtitle": { "message": "Esplora migliaia di film e serie TV." },
"addStream": { "message": "Aggiungi Stream" },
"navM3uGenerator": { "message": "Generatore M3U" },
"heroWelcome": { "message": "Benvenuto in CinePlex" },
"heroSubtitle": { "message": "Esplora migliaia di film e serie." },
"addStream": { "message": "Aggiungi streaming" },
"moreInfo": { "message": "Più informazioni" },
"popularMovies": { "message": "Film Popolari" },
"allGenres": { "message": "Tutti i Generi" },
"allYears": { "message": "Tutti gli Anni" },
"sortPopular": { "message": "Più Popolari" },
"sortTopRated": { "message": "Più Votati" },
"sortRecent": { "message": "Più Recenti" },
"loadMore": { "message": "Carica Altri" },
"popularMovies": { "message": "Film popolari" },
"allGenres": { "message": "Tutti i generi" },
"allYears": { "message": "Tutti gli anni" },
"sortPopular": { "message": "I più popolari" },
"sortTopRated": { "message": "I più votati" },
"sortRecent": { "message": "I più recenti" },
"loadMore": { "message": "Carica altro" },
"photosBreadcrumbHome": { "message": "Album" },
"selectServer": { "message": "Seleziona un server" },
"loading": { "message": "Caricamento in corso..." },
"loading": { "message": "Caricamento..." },
"loadingLibraries": { "message": "Caricamento delle librerie..." },
"photosEmptyState": { "message": "Nessun album o foto trovati." },
"photosEmptyStateSub": { "message": "Seleziona un server o assicurati di avere una libreria di foto su Plex." },
"statsTitle": { "message": "Statistiche della Libreria" },
"statsAllTokens": { "message": "Tutti i Token" },
"statsAnalyzing": { "message": "Analisi della tua libreria in corso..." },
"statsActiveTokens": { "message": "Token Attivi" },
"statsServersFound": { "message": "Server Trovati" },
"statsUniqueMovies": { "message": "Film Unici" },
"statsUniqueSeries": { "message": "Serie TV Uniche" },
"statsUniqueArtists": { "message": "Artisti Unici" },
"statsTokenServers": { "message": "Server del Token" },
"statsChartMoviesByGenre": { "message": "Contenuti per Genere (Film)" },
"statsChartSeriesByGenre": { "message": "Contenuti per Genere (Serie TV)" },
"statsChartByDecade": { "message": "Contenuti per Decennio" },
"recommendationsTitle": { "message": "Consigliati per Te" },
"historyTitle": { "message": "Cronologia di Visione" },
"clearHistory": { "message": "Cancella Tutto" },
"consoleTitle": { "message": "Console di Scansione Plex" },
"photosEmptyStateSub": { "message": "Seleziona un server o assicurati di avere una libreria di foto in Plex." },
"statsTitle": { "message": "Statistiche della libreria" },
"statsAllTokens": { "message": "Tutti i token" },
"statsAnalyzing": { "message": "Analisi della tua libreria..." },
"statsActiveTokens": { "message": "Token attivi" },
"statsServersFound": { "message": "Server trovati" },
"statsUniqueMovies": { "message": "Film unici" },
"statsUniqueSeries": { "message": "Serie uniche" },
"statsUniqueArtists": { "message": "Artisti unici" },
"statsTokenServers": { "message": "Server token" },
"statsChartMoviesByGenre": { "message": "Contenuti per genere (Film)" },
"statsChartSeriesByGenre": { "message": "Contenuti per genere (Serie)" },
"statsChartByDecade": { "message": "Contenuti per decennio" },
"recommendationsTitle": { "message": "Raccomandazioni per te" },
"historyTitle": { "message": "Cronologia visualizzazioni" },
"clearHistory": { "message": "Cancella tutto" },
"consoleTitle": { "message": "Console di scansione Plex" },
"footerCredit": { "message": "Un'interfaccia per il tuo universo Plex." },
"backButton": { "message": "Indietro" },
"closeTrailer": { "message": "Chiudi trailer" },
"close": { "message": "Chiudi" },
"photoViewer": { "message": "Visualizzatore Foto" },
"photoViewer": { "message": "Visualizzatore di foto" },
"previous": { "message": "Precedente" },
"next": { "message": "Successivo" },
"notificationTemplateText": { "message": "Notifica" },
"settingsTitleFull": { "message": "Impostazioni e Configurazione" },
"settingsTitleFull": { "message": "Impostazioni e configurazione" },
"settingsTabGeneral": { "message": "Generale" },
"settingsTabPlex": { "message": "Plex" },
"settingsTabJellyfin": { "message": "Jellyfin" },
"settingsTabPhpGen": { "message": "Generatore PHP" },
"settingsTabData": { "message": "Dati" },
"settingsApiServer": { "message": "Configurazione API e Server" },
"settingsTmdbApiLabel": { "message": "Chiave API TMDB (Opzionale)" },
"settingsTmdbApiPlaceholder": { "message": "Verrà usata la chiave predefinita se lasciato vuoto" },
"settingsTmdbLangLabel": { "message": "Lingua per TMDB e Interfaccia" },
"settingsPhpUrlLabel": { "message": "URL del server per aggiungere gli stream" },
"settingsApiServer": { "message": "Impostazioni API e server" },
"settingsTmdbApiLabel": { "message": "Chiave API TMDB (facoltativa)" },
"settingsTmdbApiPlaceholder": { "message": "La chiave predefinita verrà utilizzata se lasciata vuota" },
"settingsGoogleApiLabel": { "message": "Chiave API Google Gemini (facoltativa)" },
"settingsGoogleApiPlaceholder": { "message": "Necessaria per utilizzare l'assistente AI" },
"settingsRegionLabel": { "message": "Regione per la scoperta di contenuti" },
"allRegions": { "message": "Tutte le regioni" },
"settingsPhpUrlLabel": { "message": "URL del server per l'aggiunta di streaming" },
"settingsPhpUrlPlaceholder": { "message": "https://tuo-server.com/percorso/dello/script.php" },
"settingsInterface": { "message": "Interfaccia" },
"settingsLightTheme": { "message": "Modalità Chiara" },
"settingsShowHero": { "message": "Mostra sezione di benvenuto 'Hero'" },
"settingsScanContent": { "message": "Scansione Contenuti" },
"settingsLightTheme": { "message": "Modalità chiara" },
"settingsShowHero": { "message": "Mostra la sezione di benvenuto 'Hero'" },
"settingsScanContent": { "message": "Scansione dei contenuti" },
"settingsScanDesc": { "message": "Seleziona cosa scansionare e premi il pulsante." },
"settingsScanMovies": { "message": "Film" },
"settingsScanShows": { "message": "Serie TV" },
"settingsScanShows": { "message": "Serie" },
"settingsScanArtists": { "message": "Musica" },
"settingsScanPhotos": { "message": "Foto" },
"settingsSelectAll": { "message": "Seleziona Tutto" },
"settingsStartScan": { "message": "Avvia Scansione" },
"settingsSelectAll": { "message": "Seleziona tutto" },
"settingsStartScan": { "message": "Avvia scansione" },
"settingsPlexTokens": { "message": "Token Plex" },
"settingsPlexTokensDesc": { "message": "Modifica la lista dei token Plex (formato JSON)." },
"settingsSaveTokens": { "message": "Salva Token" },
"settingsPhpGenTitle": { "message": "Generatore di Script PHP per Server" },
"settingsPhpFileOptions": { "message": "Opzioni File" },
"settingsPlexTokensDesc": { "message": "Modifica l'elenco dei token Plex (formato JSON)." },
"settingsSaveTokens": { "message": "Salva token" },
"settingsJellyfinTitle": { "message": "Impostazioni Jellyfin" },
"settingsJellyfinDesc": { "message": "Aggiungi i dettagli del tuo server Jellyfin per scansionarne il contenuto." },
"jellyfinUrlLabel": { "message": "URL del server Jellyfin" },
"jellyfinUserLabel": { "message": "Nome utente" },
"jellyfinPassLabel": { "message": "Password" },
"jellyfinConnectAndScan": { "message": "Connetti e scansiona" },
"settingsPhpGenTitle": { "message": "Generatore di script PHP per server" },
"settingsPhpFileOptions": { "message": "Opzioni file" },
"settingsPhpSavePathLabel": { "message": "Percorso di salvataggio sul server" },
"settingsPhpSavePathPlaceholder": { "message": "Es: /var/www/html/lists (vuoto per la stessa cartella)" },
"settingsPhpSavePathPlaceholder": { "message": "Es: /var/www/html/liste (vuoto per la stessa cartella)" },
"settingsPhpFilenameLabel": { "message": "Nome file" },
"settingsPhpFileAction": { "message": "Azione sul file" },
"settingsPhpFileAction": { "message": "Azione file" },
"settingsPhpActionAppend": { "message": "Aggiungi alla fine del file (cumulativo)" },
"settingsPhpActionOverwrite": { "message": "Sovrascrivi il file (ricomincia da capo)" },
"settingsPhpSecurity": { "message": "Sicurezza (Opzionale)" },
"settingsPhpUseSecretKey": { "message": "Usa chiave segreta (Consigliato)" },
"settingsPhpSecurity": { "message": "Sicurezza (facoltativa)" },
"settingsPhpUseSecretKey": { "message": "Usa chiave segreta (consigliato)" },
"settingsPhpSecretKeyPlaceholder": { "message": "Inserisci una chiave segreta sicura" },
"settingsPhpGeneratedCode": { "message": "Codice Generato" },
"settingsPhpGeneratedCode": { "message": "Codice generato" },
"settingsPhpGeneratedPlaceholder": { "message": "Il codice PHP generato apparirà qui." },
"settingsGenerateScript": { "message": "Genera Script" },
"settingsCopyScript": { "message": "Copia Script" },
"settingsDataManagement": { "message": "Gestione Database Locale" },
"settingsImportDb": { "message": "Importa DB da File" },
"settingsExportDb": { "message": "Esporta DB su File" },
"settingsClearContent": { "message": "Cancella Dati Locali dei Contenuti" },
"settingsClearContentDesc": { "message": "Questa azione eliminerà film, serie TV e musica dal database locale, ma non influenzerà i tuoi preferiti o le impostazioni." },
"settingsGenerateScript": { "message": "Genera script" },
"settingsCopyScript": { "message": "Copia script" },
"settingsDataManagement": { "message": "Gestione del database locale" },
"settingsImportDb": { "message": "Importa DB da file" },
"settingsExportDb": { "message": "Esporta DB su file" },
"settingsClearContent": { "message": "Cancella i dati dei contenuti locali" },
"settingsClearContentDesc": { "message": "Questa azione eliminerà film, serie e musica dal database locale, ma non influirà sui preferiti o sulle impostazioni." },
"settingsClose": { "message": "Chiudi" },
"settingsSave": { "message": "Salva Impostazioni" },
"settingsSave": { "message": "Salva impostazioni" },
"musicSidenavTitle": { "message": "Musica Plex" },
"musicAllServers": { "message": "Tutti i Server" },
"musicSearchArtistPlaceholder": { "message": "Cerca artista..." },
"musicAllServers": { "message": "Tutti i server" },
"musicSearchArtistPlaceholder": { "message": "Cerca un artista..." },
"musicSearchDiscographyPlaceholder": { "message": "Cerca nella discografia..." },
"musicNothingPlaying": { "message": "Nessuna riproduzione in corso" },
"musicSelectSong": { "message": "Seleziona un brano" },
@ -112,18 +124,18 @@
"miniplayerDownloadSong": { "message": "Scarica brano" },
"miniplayerDownloadAlbum": { "message": "Scarica album M3U" },
"miniplayerVolume": { "message": "Volume" },
"miniplayerShuffle": { "message": "Riproduzione Casuale" },
"miniplayerShuffle": { "message": "Casuale" },
"miniplayerEqualizer": { "message": "Equalizzatore" },
"miniplayerOpenList": { "message": "Apri lista" },
"eqTitle": { "message": "Equalizzatore Grafico" },
"miniplayerOpenList": { "message": "Apri elenco" },
"eqTitle": { "message": "Equalizzatore grafico" },
"eqPresetsLabel": { "message": "Preimpostazioni" },
"eqPresetFlat": { "message": "Piatto" },
"eqPresetRock": { "message": "Rock" },
"eqPresetPop": { "message": "Pop" },
"eqPresetJazz": { "message": "Jazz" },
"eqPresetClassical": { "message": "Classica" },
"eqPresetBassBoost": { "message": "Aumento Bassi" },
"eqPreampLabel": { "message": "Pre-Amp" },
"eqPresetBassBoost": { "message": "Aumento dei bassi" },
"eqPreampLabel": { "message": "Preamplificatore" },
"infoModalTitle": { "message": "Informazioni" },
"infoModalFieldTitle": { "message": "Titolo:" },
"infoModalFieldArtist": { "message": "Artista:" },
@ -139,152 +151,299 @@
"lang_pt": { "message": "Portoghese" },
"essentialFeaturesNotSupported": { "message": "Il tuo browser non supporta le funzionalità essenziali." },
"dbAccessError": { "message": "Errore di accesso al database locale." },
"dbUpdateNeeded": { "message": "Il database deve essere aggiornato, si prega di ricaricare la pagina." },
"dbBlocked": { "message": "Per continuare, chiudi le altre schede di questa applicazione." },
"deletingContentData": { "message": "Cancellazione dei dati locali dei contenuti in corso..." },
"noContentDataToDelete": { "message": "Nessun dato di contenuto da cancellare." },
"contentDataDeleted": { "message": "Dati dei contenuti cancellati da IndexedDB." },
"errorDeletingData": { "message": "Errore durante la cancellazione dei dati: $message$", "placeholders": { "message": { "content": "$1" } } },
"dbUpdateNeeded": { "message": "Il database deve essere aggiornato, ricarica la pagina." },
"dbBlocked": { "message": "Chiudi le altre schede di questa applicazione per continuare." },
"deletingContentData": { "message": "Eliminazione dei dati dei contenuti locali..." },
"noContentDataToDelete": { "message": "Nessun dato di contenuto da eliminare." },
"contentDataDeleted": { "message": "Dati dei contenuti eliminati da IndexedDB." },
"errorDeletingData": { "message": "Errore durante l'eliminazione dei dati: $message$", "placeholders": { "message": { "content": "$1" } } },
"aceEditorNotAvailable": { "message": "Editor di testo non disponibile." },
"errorLoadingTokens": { "message": "Errore nel caricamento dei token nell'editor." },
"errorLoadingTokensMessage": { "message": "Errore nel caricamento dei token: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorLoadingTokens": { "message": "Errore durante il caricamento dei token per la modifica." },
"errorLoadingTokensMessage": { "message": "Errore durante il caricamento dei token: $message$", "placeholders": { "message": { "content": "$1" } } },
"aceEditorNotAvailableToSave": { "message": "Editor non disponibile per il salvataggio." },
"invalidJsonFormat": { "message": "Formato JSON non valido. Deve essere { \"tokens\": [...] }" },
"tokensSaved": { "message": "Token salvati con successo." },
"errorSavingTokens": { "message": "Errore nel salvataggio dei token: $message$", "placeholders": { "message": { "content": "$1" } } },
"tokensSaved": { "message": "Token salvati correttamente." },
"errorSavingTokens": { "message": "Errore durante il salvataggio dei token: $message$", "placeholders": { "message": { "content": "$1" } } },
"dbNotAvailable": { "message": "IndexedDB non è disponibile." },
"dbExported": { "message": "Database esportato con successo." },
"dbExported": { "message": "Database esportato correttamente." },
"errorExportingDb": { "message": "Errore durante l'esportazione del database: $message$", "placeholders": { "message": { "content": "$1" } } },
"invalidJsonFile": { "message": "Il file non contiene un oggetto JSON valido." },
"noDataToImport": { "message": "Il file non contiene dati per le sezioni correnti del DB." },
"dbImported": { "message": "Database importato con successo." },
"noDataToImport": { "message": "Il file non contiene dati per le sezioni correnti del database." },
"dbImported": { "message": "Database importato correttamente." },
"errorImportingDb": { "message": "Errore durante l'importazione del database: $message$", "placeholders": { "message": { "content": "$1" } } },
"updatingView": { "message": "Aggiornamento della vista con i nuovi dati in corso..." },
"confirmClearContent": { "message": "Sei sicuro di voler cancellare i dati locali dei contenuti (Film, Serie TV, Musica, ecc.)? Preferiti e Impostazioni NON verranno eliminati." },
"updatingView": { "message": "Aggiornamento della vista con i nuovi dati..." },
"confirmClearContent": { "message": "Sei sicuro di voler eliminare i dati dei contenuti locali (film, serie, musica, ecc.)? I preferiti e le impostazioni NON verranno eliminati." },
"trailerNotFound": { "message": "Nessun trailer trovato per questo titolo." },
"confirmClearHistory": { "message": "Sei sicuro di voler cancellare tutta la cronologia di visione? Questa azione non può essere annullata." },
"historyCleared": { "message": "Cronologia di visione cancellata." },
"historyItemDeleted": { "message": "Elemento cancellato dalla cronologia." },
"errorGeneratingScript": { "message": "Prima genera uno script per poterlo copiare." },
"confirmClearHistory": { "message": "Sei sicuro di voler cancellare tutta la cronologia delle visualizzazioni? Questa azione non può essere annullata." },
"historyCleared": { "message": "Cronologia visualizzazioni cancellata." },
"historyItemDeleted": { "message": "Elemento eliminato dalla cronologia." },
"errorGeneratingScript": { "message": "Genera prima uno script per poterlo copiare." },
"scriptCopied": { "message": "Script PHP copiato negli appunti." },
"errorCopyingScript": { "message": "Errore durante la copia dello script." },
"scriptGenerated": { "message": "Script PHP generato." },
"errorLoadingAlbum": { "message": "Errore nel caricamento dell'album: $message$", "placeholders": { "message": { "content": "$1" } } },
"noPhotoServerSelected": { "message": "Errore: Nessun server di foto è stato selezionato." },
"loadingGenres": { "message": "Caricamento generi in corso..." },
"errorLoadingGenres": { "message": "Errore nel caricamento" },
"errorLoadingAlbum": { "message": "Errore durante il caricamento dell'album: $message$", "placeholders": { "message": { "content": "$1" } } },
"noPhotoServerSelected": { "message": "Errore: non è stato selezionato alcun server di foto." },
"loadingGenres": { "message": "Caricamento dei generi..." },
"errorLoadingGenres": { "message": "Errore durante il caricamento" },
"noContentFound": { "message": "Nessun risultato trovato." },
"couldNotLoadContent": { "message": "Impossibile caricare il contenuto." },
"noFavorites": { "message": "Non hai ancora nessun preferito." },
"errorLoadingFavorites": { "message": "Errore nel caricamento dei preferiti." },
"noFavorites": { "message": "Non hai ancora preferiti." },
"errorLoadingFavorites": { "message": "Errore durante il caricamento dei preferiti." },
"historyEmpty": { "message": "La tua cronologia è vuota." },
"historyEmptySub": { "message": "Sfoglia e guarda contenuti perché appaiano qui." },
"errorGeneratingRecommendations": { "message": "Errore nella generazione dei consigliati." },
"noRecommendations": { "message": "Dobbiamo conoscerti meglio per darti dei consigli!" },
"errorGeneratingStats": { "message": "Errore nella generazione delle statistiche." },
"historyEmptySub": { "message": "Esplora e guarda i contenuti perché appaiano qui." },
"errorGeneratingRecommendations": { "message": "Errore durante la generazione delle raccomandazioni." },
"noRecommendations": { "message": "Dobbiamo conoscerti meglio per darti consigli!" },
"errorGeneratingStats": { "message": "Errore durante la generazione delle statistiche." },
"noServersForToken": { "message": "Nessun server associato trovato per questo token." },
"searchingActorContent": { "message": "Ricerca dei contenuti di $actorName$ in corso", "placeholders": { "actorName": { "content": "$1" } } },
"errorLoadingActorContent": { "message": "Impossibile caricare i contenuti di $actorName$.", "placeholders": { "actorName": { "content": "$1" } } },
"errorAddingStream": { "message": "Errore nell'aggiungere stream: $message$", "placeholders": { "message": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "L'URL del server PHP non è configurato. Per favore, impostalo nelle Impostazioni." },
"searchingStreams": { "message": "Ricerca di stream per \"$title$\" in corso...", "placeholders": { "title": { "content": "$1" } } },
"sendingStreams": { "message": "Invio di $count$ stream al server in corso...", "placeholders": { "count": { "content": "$1" } } },
"streamAddedSuccess": { "message": "Stream aggiunti con successo." },
"generatingM3U": { "message": "Generazione M3U per \"$title$\" in corso...", "placeholders": { "title": { "content": "$1" } } },
"m3uDownloaded": { "message": "M3U per \"$title$\" scaricato.", "placeholders": { "title": { "content": "$1" } } },
"errorGeneratingM3U": { "message": "Errore nella generazione dell'M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
"settingsSavedSuccess": { "message": "Impostazioni salvate con successo." },
"errorSavingSettings": { "message": "Errore nel salvataggio delle impostazioni nel database." },
"languageChangeReload": { "message": "Lingua cambiata. L'applicazione verrà ora ricaricata." },
"searchingActorContent": { "message": "Ricerca di contenuti di $actorName$", "placeholders": { "actorName": { "content": "$1" } } },
"errorLoadingActorContent": { "message": "Impossibile caricare i contenuti per $actorName$.", "placeholders": { "actorName": { "content": "$1" } } },
"errorAddingStream": { "message": "Errore durante l'aggiunta di streaming: $message$", "placeholders": { "message": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "L'URL del server PHP non è configurato. Configuralo nelle Impostazioni." },
"searchingStreams": { "message": "Ricerca di streaming per \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"sendingStreams": { "message": "Invio di $count$ streaming al server...", "placeholders": { "count": { "content": "$1" } } },
"streamAddedSuccess": { "message": "Streaming aggiunto/i con successo." },
"generatingM3U": { "message": "Generazione di M3U per \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"m3uDownloaded": { "message": "\"$title$\" scaricato.", "placeholders": { "title": { "content": "$1" } } },
"errorGeneratingM3U": { "message": "Errore durante la generazione di M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
"settingsSavedSuccess": { "message": "Impostazioni salvate correttamente." },
"errorSavingSettings": { "message": "Errore durante il salvataggio delle impostazioni nel database." },
"languageChangeReload": { "message": "Lingua modificata. L'applicazione verrà ora ricaricata." },
"addedToFavorites": { "message": "Aggiunto ai preferiti." },
"removedFromFavorites": { "message": "Rimosso dai preferiti." },
"plexScanInProgress": { "message": "Scansione Plex già in corso." },
"plexScanStarting": { "message": "Avvio scansione Plex in corso..." },
"plexScanInProgress": { "message": "La scansione di Plex è già in corso." },
"plexScanStarting": { "message": "Avvio della scansione di Plex..." },
"noPlexTokens": { "message": "Nessun token Plex configurato." },
"clearingSections": { "message": "Pulizia sezioni: $sections$", "placeholders": { "sections": { "content": "$1" } } },
"sectionsCleared": { "message": "Sezioni pulite." },
"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" } } },
"clearingSections": { "message": "Cancellazione delle sezioni: $sections$", "placeholders": { "sections": { "content": "$1" } } },
"initialScanPhaseComplete": { "message": "Fase di scansione iniziale completata." },
"retryPhaseFinished": { "message": "Fase di nuovo tentativo terminata." },
"plexScanFinished": { "message": "Scansione Plex terminata. Aggiornamento dei contenuti in corso..." },
"plexScanFatalError": { "message": "ERRORE FATALE: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorDuringScan": { "message": "Errore durante la scansione: $message$", "placeholders": { "message": { "content": "$1" } } },
"retryPhaseFinished": { "message": "Fase di tentativi ripetuti terminata." },
"plexScanFinished": { "message": "Scansione terminata. Aggiornamento dei contenuti..." },
"scanCancelled": { "message": "Scansione annullata dall'utente." },
"scanCancelledInfo": { "message": "Scansione annullata." },
"retyingSection": { "message": "Nuovo tentativo per la sezione \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
"retrySuccess": { "message": "[SUCCESSO] Nuovo tentativo per \"$title$\" completato.", "placeholders": { "title": { "content": "$1" } } },
"retryError": { "message": "[ERRORE FINALE] Nuovo tentativo fallito per \"$title$\": $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } },
"noRetriesPending": { "message": "Nessun nuovo tentativo in sospeso." },
"startingRetryPhase": { "message": "Avvio fase di nuovo tentativo per $count$ sezioni...", "placeholders": { "count": { "content": "$1" } } },
"retryPhaseCancelled": { "message": "Fase di nuovo tentativo annullata." },
"errorInitializingMusicPlayer": { "message": "Errore durante l'inizializzazione del lettore musicale." },
"criticalErrorLoadingMusic": { "message": "Errore critico nel caricamento dei dati musicali." },
"errorLoadingArtists": { "message": "Errore nel caricamento degli artisti." },
"dbUnavailableError": { "message": "Errore: Database non disponibile." },
"updatingMusicData": { "message": "Aggiornamento dati musicali in corso..." },
"criticalErrorLoadingMusic": { "message": "Errore critico durante il caricamento dei dati musicali." },
"errorLoadingArtists": { "message": "Errore durante il caricamento degli artisti." },
"dbUnavailableError": { "message": "Errore: database non disponibile." },
"updatingMusicData": { "message": "Aggiornamento dei dati musicali..." },
"musicDataUpdated": { "message": "Dati musicali aggiornati." },
"errorFetchingArtistSongs": { "message": "Errore nel recupero dei brani dell'artista." },
"errorLoadingSongs": { "message": "Errore nel caricamento dei brani." },
"errorFetchingArtistSongs": { "message": "Errore durante il recupero dei brani dell'artista." },
"errorLoadingSongs": { "message": "Errore durante il caricamento dei brani." },
"noArtistsFound": { "message": "Nessun artista trovato." },
"artistsCounter": { "message": "$start$-$end$ di $total$", "placeholders": { "start": { "content": "$1" }, "end": { "content": "$2" }, "total": { "content": "$3" } } },
"artistsCounterSingle": { "message": "$total$ Artisti", "placeholders": { "total": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Caricamento..." },
"noSongsFound": { "message": "Nessun brano trovato." },
"shuffleOn": { "message": "Modalità riproduzione casuale attivata." },
"shuffleOff": { "message": "Modalità riproduzione casuale disattivata." },
"downloadingSong": { "message": "Avvio download di \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" scaricato.", "placeholders": { "title": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Errore durante il download di \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Generazione M3U per \"$artist$\"...", "placeholders": { "artist": { "content": "$1" } } },
"albumM3UGenerated": { "message": "M3U per l'album \"$artist$\" generato.", "placeholders": { "artist": { "content": "$1" } } },
"shuffleOn": { "message": "Modalità casuale attivata." },
"shuffleOff": { "message": "Modalità casuale disattivata." },
"playbackError": { "message": "Errore di riproduzione" },
"errorLabel": { "message": "Errore" },
"reloadingPage": { "message": "Ricaricamento pagina in corso..." },
"reloadingPage": { "message": "Ricaricamento della pagina..." },
"viewed": { "message": "Visto" },
"local": { "message": "Locale" },
"topRatedSort": {"message": "Più Votati"},
"topRatedSort": {"message": "I più votati"},
"recentSort": {"message": "Recenti"},
"popularSort": {"message": "Popolari"},
"moviesSectionTitle": {"message": "Film"},
"seriesSectionTitle": {"message": "Serie TV"},
"seriesSectionTitle": {"message": "Serie"},
"searchResultsFor": {"message": "Risultati per \"$query$\"", "placeholders": {"query": {"content": "$1"}}},
"contentFrom": {"message": "Contenuti di $actor$", "placeholders": {"actor": {"content": "$1"}}},
"explore": {"message": "Esplora"},
"noGenre": {"message": "Senza Categoria"},
"noGenre": {"message": "Senza categoria"},
"synopsis": {"message": "Sinossi"},
"noSynopsis": {"message": "Nessuna sinossi disponibile."},
"director": {"message": "Regista:"},
"writer": {"message": "Sceneggiatore:"},
"viewOnImdb": {"message": "Vedi su IMDb"},
"watchTrailer": {"message": "Guarda Trailer"},
"watchTrailer": {"message": "Guarda il trailer"},
"addToFavorites": {"message": "Aggiungi ai preferiti"},
"removeFromFavorites": {"message": "Rimuovi dai preferiti"},
"notAvailable": {"message": "Non Disponibile"},
"mainCast": {"message": "Cast Principale"},
"seasonsAndEpisodes": {"message": "Stagioni ed Episodi"},
"similarContent": {"message": "Contenuti Simili"},
"episodesCount": {"message": "$count$ Episodi", "placeholders": {"count": {"content": "$1"}}},
"seasonsCount": {"message": "$count$ Stagioni", "placeholders": {"count": {"content": "$1"}}},
"notAvailable": {"message": "Non disponibile"},
"mainCast": {"message": "Cast principale"},
"seasonsAndEpisodes": {"message": "Stagioni ed episodi"},
"similarContent": {"message": "Contenuti simili"},
"filmography": {"message": "Filmografia"},
"availableOn": {"message": "Disponibile su"},
"episodesCount": {"message": "$count$ episodi", "placeholders": {"count": {"content": "$1"}}},
"seasonsCount": {"message": "$count$ stagioni", "placeholders": {"count": {"content": "$1"}}},
"runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}},
"noTrailerFound": {"message": "Nessun trailer trovato per questo titolo."},
"fatalInitError": {"message": "Errore Fatale di Inizializzazione"},
"fatalInitError": {"message": "Errore fatale di inizializzazione"},
"fatalInitErrorSub": {"message": "Impossibile caricare l'applicazione."},
"invalidStreamInfo": {"message": "Informazioni non valide."},
"dbUnavailableForStreams": {"message": "Database locale non disponibile."},
"noPlexServersForStreams": {"message": "Nessun server Plex."},
"notFoundOnServers": {"message": "\"$query$\" non trovato sui server.", "placeholders": {"query": {"content": "$1"}}},
"notFoundOnServers": {"message": "\"$query$\" non trovato sui server Plex.", "placeholders": {"query": {"content": "$1"}}},
"relativeTime_justNow": { "message": "Poco fa" },
"relativeTime_minutesAgo": { "message": "$count$ minuti fa", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_hoursAgo": { "message": "$count$ ore fa", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_yesterday": { "message": "Ieri" },
"relativeTime_daysAgo": { "message": "$count$ giorni fa", "placeholders": { "count": { "content": "$1" } } },
"errorLoadingDetails": { "message": "Errore nel Caricamento dei Dettagli" },
"errorLoadingLocalContent": { "message": "Errore nel caricamento del contenuto locale." },
"errorServerResponse": { "message": "Risposta non riuscita dal server." },
"errorPlexApi": { "message": "Errore $status$ dall'API di Plex.", "placeholders": { "status": { "content": "$1" } } },
"errorParsingPlexXml": { "message": "Errore nell'analisi dell'XML di Plex." },
"errorLoadingDetails": { "message": "Errore durante il caricamento dei dettagli" },
"errorLoadingLocalContent": { "message": "Errore durante il caricamento del contenuto locale." },
"errorServerResponse": { "message": "Risposta del server non riuscita." },
"errorPlexApi": { "message": "Errore $status$ dell'API Plex.", "placeholders": { "status": { "content": "$1" } } },
"errorParsingPlexXml": { "message": "Errore durante l'analisi dell'XML di Plex." },
"untitled": { "message": "Senza titolo" },
"itemCount": { "message": "$count$ elementi", "placeholders": { "count": { "content": "$1" } } },
"noPhotoServers": { "message": "Nessun server di foto" }
"noPhotoServers": { "message": "Nessun server di foto" },
"jellyfinScanInProgress": { "message": "La scansione di Jellyfin è già in corso." },
"jellyfinScanning": { "message": "Scansione di Jellyfin..." },
"jellyfinMissingCredentials": { "message": "Completa l'URL e il nome utente di Jellyfin." },
"jellyfinConnecting": { "message": "Connessione a Jellyfin in corso: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinAuthFailed": { "message": "Autenticazione Jellyfin non riuscita: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinAuthSuccess": { "message": "Autenticazione Jellyfin riuscita." },
"jellyfinFetchingLibraries": { "message": "Recupero delle librerie..." },
"jellyfinFetchFailed": { "message": "Errore durante il recupero delle librerie: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinNoMediaLibraries": { "message": "Nessuna libreria di film o serie trovata in Jellyfin." },
"jellyfinLibrariesFound": { "message": "$count$ libreria/e multimediale/i trovata/e.", "placeholders": { "count": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Successo] '$libraryName' scansionata, $count$ titoli aggiunti.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"jellyfinLibraryScanFailed": { "message": "Errore durante la scansione della libreria '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Scansione Jellyfin completata. Aggiunti $movies$ film e $series$ serie.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Credenziali Jellyfin non configurate." },
"notFoundOnJellyfin": { "message": "\"$query$\" non trovato su Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" non trovato su nessun server.", "placeholders": { "query": { "content": "$1" } } },
"localOnPlex": { "message": "Su Plex" },
"searchOnPlex": { "message": "Cerca su Plex" },
"jellyfinTitle": { "message": "Contenuti Jellyfin" },
"noJellyfinContent": { "message": "Nessun contenuto Jellyfin trovato." },
"noJellyfinContentSub": { "message": "Assicurati di aver scansionato il tuo server Jellyfin nelle impostazioni." },
"activityViewerTitle": { "message": "Visualizzatore attività del server" },
"activitySelectServer": { "message": "Seleziona un server" },
"activityCheckBtn": { "message": "Aggiorna" },
"activityNoSessions": { "message": "Nessuna sessione attiva su questo server." },
"activitySessionUser": { "message": "Utente" },
"activitySessionDevice": { "message": "Dispositivo" },
"activitySessionContent": { "message": "Contenuto" },
"activitySessionState": { "message": "Stato" },
"activitySessionIdentifier": { "message": "Identificatore del client" },
"activityCopyID": { "message": "Copia ID" },
"activityError": { "message": "Impossibile ottenere l'attività del server." },
"activityCopied": { "message": "Identificatore copiato negli appunti!" },
"activityCopyError": { "message": "Errore durante la copia dell'identificatore." },
"noProvidersFound": { "message": "Nessun provider trovato." },
"availableOnPlex": { "message": "Disponibile su Plex" },
"m3uGeneratorTitle": { "message": "Generatore di elenchi M3U" },
"selectAServer": { "message": "Seleziona un server..." },
"downloadM3u": { "message": "Scarica M3U" },
"m3uGenerator": { "message": "Generatore M3U" },
"selectLibraries": { "message": "Seleziona librerie" },
"howToUse": { "message": "Come usare" },
"m3uInstruction1": { "message": "Scegli un server dall'elenco." },
"m3uInstruction2": { "message": "Seleziona una o più librerie da includere." },
"m3uInstruction3": { "message": "Fai clic sul pulsante di download." },
"m3uInstruction4": { "message": "Importa il file .m3u nel tuo lettore compatibile." },
"chatOpen": { "message": "Apri chat" },
"chatTitle": { "message": "Assistente AI" },
"chatClose": { "message": "X" },
"chatPlaceholder": { "message": "Scrivi il tuo messaggio..." },
"chatSend": { "message": "➤" },
"chatWelcome": { "message": "Benvenuto! Sono il tuo assistente CinePlex. Chiedimi di film, serie o qualsiasi altra cosa tu voglia sapere." },
"chatGoogleApiKeyMissing": { "message": "La chiave API di Google Gemini non è configurata. Impostala nelle impostazioni dell'estensione per utilizzare l'assistente AI." },
"chatApiInvalidResponse": { "message": "L'API ha restituito una risposta non valida. Riprova." },
"chatApiError": { "message": "Errore di comunicazione con l'assistente AI" },
"downloadAll": { "message": "Scarica tutto" },
"download": { "message": "Scarica" },
"aiToolSearchLibraryDesc": { "message": "Cerca nella libreria Plex dell'utente film o serie per titolo." },
"aiToolSearchLibraryQueryParamDesc": { "message": "Il titolo del film o della serie da cercare." },
"aiToolSearchLibraryTypeParamDesc": { "message": "Il tipo di contenuto da cercare. Può essere 'movie' per i film o 'series' per le serie. (Facoltativo)." },
"aiToolSearchLibraryResolutionParamDesc": { "message": "La risoluzione video da cercare (ad es. '4k', '1080p'). (Facoltativo)." },
"aiToolSearchLibraryContainerParamDesc": { "message": "Il formato del contenitore video da cercare (ad es. 'mkv', 'mp4'). (Facoltativo)." },
"aiToolNavigateToPageDesc": { "message": "Indirizza l'utente a una pagina specifica dell'interfaccia dell'applicazione." },
"aiToolNavigateToPagePageParamDesc": { "message": "Il nome della pagina a cui navigare, ad es.: 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers' o 'm3u-generator'." },
"aiToolGetUserStatsDesc": { "message": "Recupera e visualizza le statistiche della libreria dell'utente, come il numero totale di film, serie e artisti unici." },
"aiToolShowItemDetailsDesc": { "message": "Visualizza la pagina dei dettagli di un film o di una serie specifica in base al titolo e al tipo." },
"aiToolShowItemDetailsTitleParamDesc": { "message": "Il titolo esatto del film o della serie." },
"aiToolShowItemDetailsTypeParamDesc": { "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." },
"aiToolAddToPlaylistTitleParamDesc": { "message": "Il titolo del film o della serie da aggiungere." },
"aiToolAddToPlaylistTypeParamDesc": { "message": "Il tipo di contenuto. Deve essere 'movie' o 'series'." },
"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." },
"aiToolCheckAndDownloadTitlesParamDesc": { "message": "Un array di titoli di film o serie da cercare e scaricare." },
"aiToolCheckAndDownloadTypeParamDesc": { "message": "Il tipo di contenuto dell'elenco. Deve essere 'movie' o 'series'." },
"aiToolCheckAndDownloadFilenameParamDesc": { "message": "Il nome del file M3U da scaricare (ad es. 'MiaLista.m3u'). Se non fornito, verrà utilizzato un nome predefinito." },
"aiToolToggleFavoriteDesc": { "message": "Aggiunge o rimuove un film o una serie dall'elenco dei preferiti dell'utente." },
"aiToolToggleFavoriteTitleParamDesc": { "message": "Il titolo del film o della serie." },
"aiToolToggleFavoriteTypeParamDesc": { "message": "Il tipo di contenuto. Deve essere 'movie' o 'series'." },
"aiToolGetRecommendationsDesc": { "message": "Genera e visualizza un elenco di consigli di film o serie basati sulla cronologia di visualizzazione e sui preferiti dell'utente." },
"aiToolApplyFiltersDesc": { "message": "Applica filtri alla visualizzazione corrente di film o serie, consentendo di affinare i risultati per tipo, genere, anno e ordine di ordinamento." },
"aiToolApplyFiltersTypeParamDesc": { "message": "Il tipo di contenuto a cui applicare i filtri. Deve essere 'movie' o 'series'." },
"aiToolApplyFiltersGenreParamDesc": { "message": "Il nome del genere per cui filtrare (ad es. 'Azione', 'Drammatico')." },
"aiToolApplyFiltersYearParamDesc": { "message": "L'anno di uscita per cui filtrare (ad es. '2023')." },
"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)." },
"aiToolPlayMusicByArtistDesc": { "message": "Apre il lettore musicale e avvia la riproduzione dei brani di un artista specifico dalla libreria dell'utente." },
"aiToolPlayMusicByArtistNameParamDesc": { "message": "Il nome esatto dell'artista di cui si desidera riprodurre i brani." },
"aiToolClearChatHistoryDesc": { "message": "Cancella tutta la cronologia dei messaggi della conversazione corrente con l'assistente AI." },
"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." },
"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." },
"aiToolAddPlexTokenDesc": { "message": "Aggiunge un nuovo token X-Plex alla configurazione dell'estensione, consentendo all'applicazione di scansionare i contenuti di nuovi server Plex." },
"aiToolAddPlexTokenTokenParamDesc": { "message": "La stringa del token X-Plex da aggiungere." },
"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." },
"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)." },
"aiToolClearAllFavoritesDesc": { "message": "Rimuove tutti i film e le serie che l'utente ha contrassegnato come preferiti." },
"aiToolClearViewingHistoryDesc": { "message": "Cancella la cronologia di visualizzazione dell'utente dalla pagina della cronologia." },
"aiToolClearRecommendationsViewDesc": { "message": "Svuota la vista dei consigli e rimuove i consigli memorizzati nella cache." },
"aiToolSearchNotFound": { "message": "'$query' non trovato nella tua libreria.", "placeholders": { "query": { "content": "$1" } } },
"aiToolNavigateSuccess": { "message": "Navigato alla pagina $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolNavigateError": { "message": "Errore durante la navigazione alla pagina $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolStatsError": { "message": "Errore nel recupero delle statistiche." },
"aiToolItemNotFound": { "message": "Elemento '$title' non trovato.", "placeholders": { "title": { "content": "$1" } } },
"aiToolShowItemDetailsSuccess": { "message": "Visualizzazione dei dettagli di '$title'.", "placeholders": { "title": { "content": "$1" } } },
"aiToolAddToPlaylistSuccess": { "message": "Aggiunto '$title' alla playlist.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoriteAdded": { "message": "Aggiunto '$title' ai preferiti.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoriteRemoved": { "message": "Rimosso '$title' dai preferiti.", "placeholders": { "title": { "content": "$1" } } },
"aiToolRecommendationsSuccess": { "message": "Visualizzazione dei consigli." },
"aiToolApplyFiltersGenreNotFound": { "message": "Genere '$genre' non trovato.", "placeholders": { "genre": { "content": "$1" } } },
"aiToolApplyFiltersSuccess": { "message": "Filtri applicati correttamente." },
"aiToolPlayMusicNotReady": { "message": "Il lettore musicale non è pronto. Assicurati che la tua libreria musicale di Plex sia stata scansionata." },
"aiToolPlayMusicArtistNotFound": { "message": "Artista '$artist_name' non trovato.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolPlayMusicNoSongs": { "message": "Nessun brano trovato per '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolPlayMusicSuccess": { "message": "Riproduzione di musica di '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolChatHistoryCleared": { "message": "Cronologia della chat cancellata." },
"aiToolConfirmDeleteDatabase": { "message": "Sei sicuro di voler eliminare il database locale? Questa azione è irreversibile." },
"aiToolDeleteDatabaseCancelled": { "message": "Eliminazione del database annullata." },
"aiToolExecutionError": { "message": "Errore durante l'esecuzione dello strumento '$toolName$': $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } },
"aiToolUnknown": { "message": "Strumento sconosciuto: '$toolName$'.", "placeholders": { "toolName": { "content": "$1" } } },
"aiToolFavoritesCleared": { "message": "Preferiti cancellati." },
"aiToolFavoritesClearError": { "message": "Errore durante la cancellazione dei preferiti: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolRecommendationsCleared": { "message": "Consigli cancellati." },
"aiToolRecommendationsClearError": { "message": "Errore durante la cancellazione dei consigli: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolDatabaseDeleted": { "message": "Database eliminato. La pagina verrà ricaricata." },
"aiToolDatabaseDeleteError": { "message": "Errore durante l'eliminazione del database: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolDatabaseDeleteBlocked": { "message": "L'eliminazione del database è bloccata. Chiudi le altre schede dell'applicazione." },
"aiToolUpdateAllTokensSuccess": { "message": "Tutti i token sono stati aggiornati correttamente." },
"aiToolUpdateAllTokensError": { "message": "Errore durante l'aggiornamento dei token: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolAddPlexTokenSuccess": { "message": "Token Plex aggiunto correttamente." },
"aiToolAddPlexTokenError": { "message": "Errore durante l'aggiunta del token Plex: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolChangeRegionSuccess": { "message": "Regione modificata in $region$. Il contenuto è in fase di aggiornamento.", "placeholders": { "region": { "content": "$1" } } },
"aiToolChangeRegionError": { "message": "Errore durante la modifica della regione: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolViewingHistoryCleared": { "message": "Cronologia visualizzazioni cancellata." },
"aiToolViewingHistoryClearError": { "message": "Errore durante la cancellazione della cronologia di visualizzazione: $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." },
"aiToolM3UNoTitlesProvided": { "message": "Fornisci un elenco di titoli per creare la playlist." },
"aiToolM3UCheckingTitles": { "message": "Controllo dei titoli sui tuoi server locali..." },
"aiToolM3UNoLocalMatchesForDownload": { "message": "Non ho trovato nessuno dei film o delle serie dell'elenco sui tuoi server locali." },
"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" } } },
"backToProviders": { "message": "Torna ai provider" },
"artistsCounterSingle": { "message": "$total$ artista", "placeholders": { "total": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Caricamento..." },
"downloadingSong": { "message": "Avvio del download di \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" scaricato.", "placeholders": { "title": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Errore durante il download di \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Generazione di M3U per \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
"albumM3UGenerated": { "message": "M3U per l'album \"$artist$\" generato.", "placeholders": { "artist": { "content": "$1" } } },
"retyingSection": { "message": "Nuovo tentativo per la sezione \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"retrySuccess": { "message": "[SUCCESSO] Nuovo tentativo per \"$title$\" completato.", "placeholders": { "title": { "content": "$1" } } },
"retryError": { "message": "[ERRORE FINALE] Tentativo fallito per \"$title$\": $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } },
"startingRetryPhase": { "message": "Avvio della fase di tentativi ripetuti 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 durante l'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": "Arresto della scansione di Plex..." },
"invalidTokenProvided": { "message": "Token non valido fornito." },
"tokenAlreadyExists": { "message": "Il token esiste già." },
"tokenAddedSuccessfully": { "message": "Token aggiunto correttamente." },
"noStreamsFoundForSelection": { "message": "Nessuno streaming trovato per la selezione." },
"autoplayBlocked": { "message": "Riproduzione automatica bloccata." },
"page": { "message": "Pagina" },
"all": { "message": "Tutti" },
"userScore": { "message": "Punteggio degli utenti" },
"duration": { "message": "Durata" },
"min": { "message": "Min" },
"max": { "message": "Max" }
}

View File

@ -1,110 +1,122 @@
{
"appName": { "message": "CinePlex" },
"appDescription": { "message": "Analisa servidores Plex para encontrar conteúdo e o exibe na interface" },
"appDescription": { "message": "Examina servidores Plex em busca de conteúdo e o exibe na interface" },
"appTagline": { "message": "Filmes, Séries e Música" },
"appLocaleCode": { "message": "pt-BR" },
"toggleNavigation": { "message": "Alternar Navegação" },
"toggleNavigation": { "message": "Alternar navegação" },
"searchPlaceholder": { "message": "Pesquisar filmes ou séries..." },
"openMusicPlayer": { "message": "Abrir Player de Música" },
"openMusicPlayer": { "message": "Abrir reprodutor de música" },
"settings": { "message": "Configurações" },
"navMovies": { "message": "Filmes" },
"navSeries": { "message": "Séries" },
"navProviders": { "message": "Provedores" },
"navPhotos": { "message": "Fotos" },
"navStats": { "message": "Estatísticas" },
"navFavorites": { "message": "Favoritos" },
"navHistory": { "message": "Histórico" },
"navRecommendations": { "message": "Recomendações" },
"navMusic": { "message": "Música" },
"navM3uGenerator": { "message": "Gerador de M3U" },
"heroWelcome": { "message": "Bem-vindo ao CinePlex" },
"heroSubtitle": { "message": "Explore milhares de filmes e séries." },
"addStream": { "message": "Adicionar Stream" },
"moreInfo": { "message": "Mais Informações" },
"popularMovies": { "message": "Filmes Populares" },
"allGenres": { "message": "Todos os Gêneros" },
"allYears": { "message": "Todos os Anos" },
"sortPopular": { "message": "Mais Populares" },
"sortTopRated": { "message": "Mais Bem Avaliados" },
"sortRecent": { "message": "Mais Recentes" },
"loadMore": { "message": "Carregar Mais" },
"addStream": { "message": "Adicionar stream" },
"moreInfo": { "message": "Mais informações" },
"popularMovies": { "message": "Filmes populares" },
"allGenres": { "message": "Todos os gêneros" },
"allYears": { "message": "Todos os anos" },
"sortPopular": { "message": "Mais populares" },
"sortTopRated": { "message": "Mais bem avaliados" },
"sortRecent": { "message": "Mais recentes" },
"loadMore": { "message": "Carregar mais" },
"photosBreadcrumbHome": { "message": "Álbuns" },
"selectServer": { "message": "Selecione um servidor" },
"loading": { "message": "Carregando..." },
"loadingLibraries": { "message": "Carregando bibliotecas..." },
"photosEmptyState": { "message": "Nenhum álbum ou foto encontrado." },
"photosEmptyStateSub": { "message": "Por favor, selecione um servidor ou certifique-se de que você tem uma biblioteca de fotos no Plex." },
"statsTitle": { "message": "Estatísticas da Biblioteca" },
"statsAllTokens": { "message": "Todos os Tokens" },
"photosEmptyStateSub": { "message": "Selecione um servidor ou verifique se você tem uma biblioteca de fotos no Plex." },
"statsTitle": { "message": "Estatísticas da biblioteca" },
"statsAllTokens": { "message": "Todos os tokens" },
"statsAnalyzing": { "message": "Analisando sua biblioteca..." },
"statsActiveTokens": { "message": "Tokens Ativos" },
"statsServersFound": { "message": "Servidores Encontrados" },
"statsUniqueMovies": { "message": "Filmes Únicos" },
"statsUniqueSeries": { "message": "Séries Únicas" },
"statsUniqueArtists": { "message": "Artistas Únicos" },
"statsTokenServers": { "message": "Servidores do Token" },
"statsChartMoviesByGenre": { "message": "Conteúdo por Gênero (Filmes)" },
"statsChartSeriesByGenre": { "message": "Conteúdo por Gênero (Séries)" },
"statsChartByDecade": { "message": "Conteúdo por Década" },
"recommendationsTitle": { "message": "Recomendações para Você" },
"historyTitle": { "message": "Histórico de Visualização" },
"clearHistory": { "message": "Limpar Tudo" },
"consoleTitle": { "message": "Console de Análise do Plex" },
"statsActiveTokens": { "message": "Tokens ativos" },
"statsServersFound": { "message": "Servidores encontrados" },
"statsUniqueMovies": { "message": "Filmes únicos" },
"statsUniqueSeries": { "message": "Séries únicas" },
"statsUniqueArtists": { "message": "Artistas únicos" },
"statsTokenServers": { "message": "Servidores de token" },
"statsChartMoviesByGenre": { "message": "Conteúdo por gênero (Filmes)" },
"statsChartSeriesByGenre": { "message": "Conteúdo por gênero (Séries)" },
"statsChartByDecade": { "message": "Conteúdo por década" },
"recommendationsTitle": { "message": "Recomendações para você" },
"historyTitle": { "message": "Histórico de visualização" },
"clearHistory": { "message": "Limpar tudo" },
"consoleTitle": { "message": "Console de verificação do Plex" },
"footerCredit": { "message": "Uma interface para o seu universo Plex." },
"backButton": { "message": "Voltar" },
"closeTrailer": { "message": "Fechar trailer" },
"close": { "message": "Fechar" },
"photoViewer": { "message": "Visualizador de Fotos" },
"photoViewer": { "message": "Visualizador de fotos" },
"previous": { "message": "Anterior" },
"next": { "message": "Próximo" },
"notificationTemplateText": { "message": "Notificação" },
"settingsTitleFull": { "message": "Configurações e Ajustes" },
"settingsTitleFull": { "message": "Configurações e ajustes" },
"settingsTabGeneral": { "message": "Geral" },
"settingsTabPlex": { "message": "Plex" },
"settingsTabJellyfin": { "message": "Jellyfin" },
"settingsTabPhpGen": { "message": "Gerador de PHP" },
"settingsTabData": { "message": "Dados" },
"settingsApiServer": { "message": "Configuração de API e Servidor" },
"settingsTmdbApiLabel": { "message": "Chave da API do TMDB (Opcional)" },
"settingsTmdbApiPlaceholder": { "message": "Usará a chave padrão se deixado em branco" },
"settingsTmdbLangLabel": { "message": "Idioma para TMDB e Interface" },
"settingsPhpUrlLabel": { "message": "URL do Servidor para Adicionar Streams" },
"settingsApiServer": { "message": "Configurações de API e servidor" },
"settingsTmdbApiLabel": { "message": "Chave de API do TMDB (opcional)" },
"settingsTmdbApiPlaceholder": { "message": "A chave padrão será usada se o campo for deixado em branco" },
"settingsGoogleApiLabel": { "message": "Chave de API do Google Gemini (opcional)" },
"settingsGoogleApiPlaceholder": { "message": "Necessária para usar o assistente de IA" },
"settingsRegionLabel": { "message": "Região para descoberta de conteúdo" },
"allRegions": { "message": "Todas as regiões" },
"settingsPhpUrlLabel": { "message": "URL do servidor para adicionar streams" },
"settingsPhpUrlPlaceholder": { "message": "https://seu-servidor.com/caminho/para/script.php" },
"settingsInterface": { "message": "Interface" },
"settingsLightTheme": { "message": "Modo Claro" },
"settingsLightTheme": { "message": "Modo claro" },
"settingsShowHero": { "message": "Mostrar seção de boas-vindas 'Hero'" },
"settingsScanContent": { "message": "Análise de Conteúdo" },
"settingsScanDesc": { "message": "Selecione o que analisar e pressione o botão." },
"settingsScanContent": { "message": "Verificação de conteúdo" },
"settingsScanDesc": { "message": "Selecione o que verificar e pressione o botão." },
"settingsScanMovies": { "message": "Filmes" },
"settingsScanShows": { "message": "Séries" },
"settingsScanArtists": { "message": "Música" },
"settingsScanPhotos": { "message": "Fotos" },
"settingsSelectAll": { "message": "Selecionar Tudo" },
"settingsStartScan": { "message": "Iniciar Análise" },
"settingsSelectAll": { "message": "Selecionar tudo" },
"settingsStartScan": { "message": "Iniciar verificação" },
"settingsPlexTokens": { "message": "Tokens do Plex" },
"settingsPlexTokensDesc": { "message": "Edite a lista de tokens do Plex (formato JSON)." },
"settingsSaveTokens": { "message": "Salvar Tokens" },
"settingsPhpGenTitle": { "message": "Gerador de Script PHP para Servidor" },
"settingsPhpFileOptions": { "message": "Opções de Arquivo" },
"settingsPhpSavePathLabel": { "message": "Caminho para Salvar no Servidor" },
"settingsSaveTokens": { "message": "Salvar tokens" },
"settingsJellyfinTitle": { "message": "Configurações do Jellyfin" },
"settingsJellyfinDesc": { "message": "Adicione os detalhes do seu servidor Jellyfin para verificar o conteúdo dele." },
"jellyfinUrlLabel": { "message": "URL do servidor Jellyfin" },
"jellyfinUserLabel": { "message": "Nome de usuário" },
"jellyfinPassLabel": { "message": "Senha" },
"jellyfinConnectAndScan": { "message": "Conectar e verificar" },
"settingsPhpGenTitle": { "message": "Gerador de script PHP para servidor" },
"settingsPhpFileOptions": { "message": "Opções de arquivo" },
"settingsPhpSavePathLabel": { "message": "Caminho para salvar no servidor" },
"settingsPhpSavePathPlaceholder": { "message": "Ex: /var/www/html/listas (em branco para a mesma pasta)" },
"settingsPhpFilenameLabel": { "message": "Nome do Arquivo" },
"settingsPhpFileAction": { "message": "Ação no Arquivo" },
"settingsPhpFilenameLabel": { "message": "Nome do arquivo" },
"settingsPhpFileAction": { "message": "Ação do arquivo" },
"settingsPhpActionAppend": { "message": "Anexar ao final do arquivo (cumulativo)" },
"settingsPhpActionOverwrite": { "message": "Sobrescrever o arquivo (começar do zero)" },
"settingsPhpSecurity": { "message": "Segurança (Opcional)" },
"settingsPhpUseSecretKey": { "message": "Usar chave secreta (Recomendado)" },
"settingsPhpActionOverwrite": { "message": "Substituir o arquivo (começar do zero)" },
"settingsPhpSecurity": { "message": "Segurança (opcional)" },
"settingsPhpUseSecretKey": { "message": "Usar chave secreta (recomendado)" },
"settingsPhpSecretKeyPlaceholder": { "message": "Digite uma chave secreta segura" },
"settingsPhpGeneratedCode": { "message": "Código Gerado" },
"settingsPhpGeneratedCode": { "message": "Código gerado" },
"settingsPhpGeneratedPlaceholder": { "message": "O código PHP gerado aparecerá aqui." },
"settingsGenerateScript": { "message": "Gerar Script" },
"settingsCopyScript": { "message": "Copiar Script" },
"settingsDataManagement": { "message": "Gerenciamento do Banco de Dados Local" },
"settingsImportDb": { "message": "Importar BD de um Arquivo" },
"settingsExportDb": { "message": "Exportar BD para um Arquivo" },
"settingsClearContent": { "message": "Limpar Dados de Conteúdo Local" },
"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." },
"settingsGenerateScript": { "message": "Gerar script" },
"settingsCopyScript": { "message": "Copiar script" },
"settingsDataManagement": { "message": "Gerenciamento do banco de dados local" },
"settingsImportDb": { "message": "Importar banco de dados de um arquivo" },
"settingsExportDb": { "message": "Exportar banco de dados para um arquivo" },
"settingsClearContent": { "message": "Limpar dados de conteúdo local" },
"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." },
"settingsClose": { "message": "Fechar" },
"settingsSave": { "message": "Salvar Configurações" },
"settingsSave": { "message": "Salvar configurações" },
"musicSidenavTitle": { "message": "Música do Plex" },
"musicAllServers": { "message": "Todos os Servidores" },
"musicSearchArtistPlaceholder": { "message": "Pesquisar artista..." },
"musicAllServers": { "message": "Todos os servidores" },
"musicSearchArtistPlaceholder": { "message": "Pesquisar um artista..." },
"musicSearchDiscographyPlaceholder": { "message": "Pesquisar na discografia..." },
"musicNothingPlaying": { "message": "Nada tocando" },
"musicSelectSong": { "message": "Selecione uma música" },
@ -115,14 +127,14 @@
"miniplayerShuffle": { "message": "Aleatório" },
"miniplayerEqualizer": { "message": "Equalizador" },
"miniplayerOpenList": { "message": "Abrir lista" },
"eqTitle": { "message": "Equalizador Gráfico" },
"eqTitle": { "message": "Equalizador gráfico" },
"eqPresetsLabel": { "message": "Predefinições" },
"eqPresetFlat": { "message": "Plano (Flat)" },
"eqPresetFlat": { "message": "Plano" },
"eqPresetRock": { "message": "Rock" },
"eqPresetPop": { "message": "Pop" },
"eqPresetJazz": { "message": "Jazz" },
"eqPresetClassical": { "message": "Clássica" },
"eqPresetBassBoost": { "message": "Reforço de Graves" },
"eqPresetClassical": { "message": "Clássico" },
"eqPresetBassBoost": { "message": "Reforço de graves" },
"eqPreampLabel": { "message": "Pré-amplificador" },
"infoModalTitle": { "message": "Informações" },
"infoModalFieldTitle": { "message": "Título:" },
@ -139,14 +151,14 @@
"lang_pt": { "message": "Português" },
"essentialFeaturesNotSupported": { "message": "Seu navegador não suporta recursos essenciais." },
"dbAccessError": { "message": "Erro ao acessar o banco de dados local." },
"dbUpdateNeeded": { "message": "O banco de dados precisa ser atualizado, por favor, recarregue a página." },
"dbBlocked": { "message": "Por favor, feche outras abas desta aplicação para continuar." },
"dbUpdateNeeded": { "message": "O banco de dados precisa ser atualizado, recarregue a página." },
"dbBlocked": { "message": "Feche outras abas deste aplicativo para continuar." },
"deletingContentData": { "message": "Excluindo dados de conteúdo local..." },
"noContentDataToDelete": { "message": "Nenhum dado de conteúdo para excluir." },
"contentDataDeleted": { "message": "Dados de conteúdo excluídos do IndexedDB." },
"errorDeletingData": { "message": "Erro ao excluir dados: $message$", "placeholders": { "message": { "content": "$1" } } },
"aceEditorNotAvailable": { "message": "Editor de texto não disponível." },
"errorLoadingTokens": { "message": "Erro ao carregar tokens no editor." },
"errorLoadingTokens": { "message": "Erro ao carregar tokens para edição." },
"errorLoadingTokensMessage": { "message": "Erro ao carregar tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"aceEditorNotAvailableToSave": { "message": "Editor não disponível para salvar." },
"invalidJsonFormat": { "message": "Formato JSON inválido. Deve ser { \"tokens\": [...] }" },
@ -154,15 +166,15 @@
"errorSavingTokens": { "message": "Erro ao salvar tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"dbNotAvailable": { "message": "O IndexedDB não está disponível." },
"dbExported": { "message": "Banco de dados exportado com sucesso." },
"errorExportingDb": { "message": "Erro ao exportar banco de dados: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorExportingDb": { "message": "Erro ao exportar o banco de dados: $message$", "placeholders": { "message": { "content": "$1" } } },
"invalidJsonFile": { "message": "O arquivo não contém um objeto JSON válido." },
"noDataToImport": { "message": "O arquivo não contém dados para as seções atuais do BD." },
"noDataToImport": { "message": "O arquivo não contém dados para as seções atuais do banco de dados." },
"dbImported": { "message": "Banco de dados importado com sucesso." },
"errorImportingDb": { "message": "Erro ao importar banco de dados: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorImportingDb": { "message": "Erro ao importar o banco de dados: $message$", "placeholders": { "message": { "content": "$1" } } },
"updatingView": { "message": "Atualizando a visualização com novos dados..." },
"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." },
"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." },
"trailerNotFound": { "message": "Nenhum trailer encontrado para este título." },
"confirmClearHistory": { "message": "Tem certeza de que deseja excluir todo o seu histórico de visualização? Esta ação não pode ser desfeita." },
"confirmClearHistory": { "message": "Tem certeza de que deseja limpar todo o seu histórico de visualização? Esta ação não pode ser desfeita." },
"historyCleared": { "message": "Histórico de visualização limpo." },
"historyItemDeleted": { "message": "Item excluído do histórico." },
"errorGeneratingScript": { "message": "Primeiro, gere um script para poder copiá-lo." },
@ -170,7 +182,7 @@
"errorCopyingScript": { "message": "Erro ao copiar o script." },
"scriptGenerated": { "message": "Script PHP gerado." },
"errorLoadingAlbum": { "message": "Erro ao carregar o álbum: $message$", "placeholders": { "message": { "content": "$1" } } },
"noPhotoServerSelected": { "message": "Erro: Nenhum servidor de fotos foi selecionado." },
"noPhotoServerSelected": { "message": "Erro: nenhum servidor de fotos foi selecionado." },
"loadingGenres": { "message": "Carregando gêneros..." },
"errorLoadingGenres": { "message": "Erro ao carregar" },
"noContentFound": { "message": "Nenhum resultado encontrado." },
@ -178,72 +190,52 @@
"noFavorites": { "message": "Você ainda não tem favoritos." },
"errorLoadingFavorites": { "message": "Erro ao carregar os favoritos." },
"historyEmpty": { "message": "Seu histórico está vazio." },
"historyEmptySub": { "message": "Navegue e assista a conteúdos para que eles apareçam aqui." },
"historyEmptySub": { "message": "Explore e assista a conteúdo para que ele apareça aqui." },
"errorGeneratingRecommendations": { "message": "Erro ao gerar recomendações." },
"noRecommendations": { "message": "Precisamos te conhecer melhor para dar recomendações!" },
"noRecommendations": { "message": "Precisamos conhecê-lo melhor para dar recomendações!" },
"errorGeneratingStats": { "message": "Erro ao gerar estatísticas." },
"noServersForToken": { "message": "Nenhum servidor associado encontrado para este token." },
"searchingActorContent": { "message": "Buscando conteúdo de $actorName$", "placeholders": { "actorName": { "content": "$1" } } },
"searchingActorContent": { "message": "Pesquisando conteúdo de $actorName$", "placeholders": { "actorName": { "content": "$1" } } },
"errorLoadingActorContent": { "message": "Não foi possível carregar o conteúdo de $actorName$.", "placeholders": { "actorName": { "content": "$1" } } },
"errorAddingStream": { "message": "Erro ao adicionar stream(s): $message$", "placeholders": { "message": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "A URL do servidor PHP não está configurada. Por favor, configure-a nas Configurações." },
"searchingStreams": { "message": "Buscando streams para \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
"phpUrlNotConfigured": { "message": "A URL do servidor PHP não está configurada. Configure-a nas Configurações." },
"searchingStreams": { "message": "Pesquisando streams para \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"sendingStreams": { "message": "Enviando $count$ stream(s) para o servidor...", "placeholders": { "count": { "content": "$1" } } },
"streamAddedSuccess": { "message": "Stream(s) adicionado(s) com sucesso." },
"generatingM3U": { "message": "Gerando M3U para \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
"m3uDownloaded": { "message": "M3U para \"$title$\" baixado.", "placeholders": { "title": { "content": "$1" } } },
"generatingM3U": { "message": "Gerando M3U para \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"m3uDownloaded": { "message": "\"$title$\" baixado.", "placeholders": { "title": { "content": "$1" } } },
"errorGeneratingM3U": { "message": "Erro ao gerar M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
"settingsSavedSuccess": { "message": "Configurações salvas com sucesso." },
"errorSavingSettings": { "message": "Erro ao salvar as configurações no banco de dados." },
"languageChangeReload": { "message": "Idioma alterado. A aplicação será recarregada agora." },
"languageChangeReload": { "message": "Idioma alterado. O aplicativo será recarregado agora." },
"addedToFavorites": { "message": "Adicionado aos favoritos." },
"removedFromFavorites": { "message": "Removido dos favoritos." },
"plexScanInProgress": { "message": "A análise do Plex já está em andamento." },
"plexScanStarting": { "message": "Iniciando análise do Plex..." },
"plexScanInProgress": { "message": "A verificação do Plex já está em andamento." },
"plexScanStarting": { "message": "Iniciando a verificação do Plex..." },
"noPlexTokens": { "message": "Nenhum token do Plex configurado." },
"clearingSections": { "message": "Limpando seções: $sections$", "placeholders": { "sections": { "content": "$1" } } },
"sectionsCleared": { "message": "Seções limpas." },
"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" } } },
"initialScanPhaseComplete": { "message": "Fase de análise inicial concluída." },
"initialScanPhaseComplete": { "message": "Fase de verificação inicial concluída." },
"retryPhaseFinished": { "message": "Fase de nova tentativa concluída." },
"plexScanFinished": { "message": "Análise do Plex concluída. Atualizando conteúdo..." },
"plexScanFatalError": { "message": "ERRO FATAL: $message$", "placeholders": { "message": { "content": "$1" } } },
"errorDuringScan": { "message": "Erro durante a análise: $message$", "placeholders": { "message": { "content": "$1" } } },
"scanCancelled": { "message": "Análise cancelada pelo usuário." },
"scanCancelledInfo": { "message": "Análise cancelada." },
"retyingSection": { "message": "Tentando novamente a seção \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
"retrySuccess": { "message": "[SUCESSO] Nova tentativa de \"$title$\" concluída.", "placeholders": { "title": { "content": "$1" } } },
"retryError": { "message": "[ERRO FINAL] A nova tentativa falhou para \"$title$\": $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } },
"noRetriesPending": { "message": "Nenhuma nova tentativa pendente." },
"startingRetryPhase": { "message": "Iniciando fase de nova tentativa para $count$ seções...", "placeholders": { "count": { "content": "$1" } } },
"retryPhaseCancelled": { "message": "Fase de nova tentativa cancelada." },
"errorInitializingMusicPlayer": { "message": "Erro ao inicializar o player de música." },
"criticalErrorLoadingMusic": { "message": "Erro crítico ao carregar dados de música." },
"errorLoadingArtists": { "message": "Erro ao carregar artistas." },
"dbUnavailableError": { "message": "Erro: Banco de dados não disponível." },
"plexScanFinished": { "message": "Verificação concluída. Atualizando conteúdo..." },
"scanCancelled": { "message": "Verificação cancelada pelo usuário." },
"scanCancelledInfo": { "message": "Verificação cancelada." },
"errorInitializingMusicPlayer": { "message": "Erro ao inicializar o reprodutor de música." },
"criticalErrorLoadingMusic": { "message": "Erro crítico ao carregar os dados de música." },
"errorLoadingArtists": { "message": "Erro ao carregar os artistas." },
"dbUnavailableError": { "message": "Erro: banco de dados indisponível." },
"updatingMusicData": { "message": "Atualizando dados de música..." },
"musicDataUpdated": { "message": "Dados de música atualizados." },
"errorFetchingArtistSongs": { "message": "Erro ao obter as músicas do artista." },
"errorFetchingArtistSongs": { "message": "Erro ao buscar as músicas do artista." },
"errorLoadingSongs": { "message": "Erro ao carregar as músicas." },
"noArtistsFound": { "message": "Nenhum artista encontrado." },
"artistsCounter": { "message": "$start$-$end$ de $total$", "placeholders": { "start": { "content": "$1" }, "end": { "content": "$2" }, "total": { "content": "$3" } } },
"artistsCounterSingle": { "message": "$total$ Artistas", "placeholders": { "total": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Carregando..." },
"noSongsFound": { "message": "Nenhuma música encontrada." },
"shuffleOn": { "message": "Modo aleatório ativado." },
"shuffleOff": { "message": "Modo aleatório desativado." },
"downloadingSong": { "message": "Iniciando download de \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" baixado.", "placeholders": { "title": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Erro ao baixar \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Gerando M3U para \"$artist$\"...", "placeholders": { "artist": { "content": "$1" } } },
"albumM3UGenerated": { "message": "M3U para o álbum \"$artist$\" gerado.", "placeholders": { "artist": { "content": "$1" } } },
"playbackError": { "message": "Erro de reprodução" },
"errorLabel": { "message": "Erro" },
"reloadingPage": { "message": "Recarregando página..." },
"reloadingPage": { "message": "Recarregando a página..." },
"viewed": { "message": "Visto" },
"local": { "message": "Local" },
"topRatedSort": {"message": "Mais Bem Avaliados"},
"topRatedSort": {"message": "Mais bem avaliados"},
"recentSort": {"message": "Recentes"},
"popularSort": {"message": "Populares"},
"moviesSectionTitle": {"message": "Filmes"},
@ -251,40 +243,207 @@
"searchResultsFor": {"message": "Resultados para \"$query$\"", "placeholders": {"query": {"content": "$1"}}},
"contentFrom": {"message": "Conteúdo de $actor$", "placeholders": {"actor": {"content": "$1"}}},
"explore": {"message": "Explorar"},
"noGenre": {"message": "Sem Categoria"},
"noGenre": {"message": "Sem categoria"},
"synopsis": {"message": "Sinopse"},
"noSynopsis": {"message": "Nenhuma sinopse disponível."},
"director": {"message": "Diretor:"},
"writer": {"message": "Roteirista:"},
"viewOnImdb": {"message": "Ver no IMDb"},
"watchTrailer": {"message": "Assistir ao Trailer"},
"addToFavorites": {"message": "Favoritar"},
"removeFromFavorites": {"message": "Desfavoritar"},
"notAvailable": {"message": "Não Disponível"},
"mainCast": {"message": "Elenco Principal"},
"seasonsAndEpisodes": {"message": "Temporadas e Episódios"},
"similarContent": {"message": "Conteúdo Similar"},
"episodesCount": {"message": "$count$ Episódios", "placeholders": {"count": {"content": "$1"}}},
"seasonsCount": {"message": "$count$ Temporadas", "placeholders": {"count": {"content": "$1"}}},
"watchTrailer": {"message": "Assistir ao trailer"},
"addToFavorites": {"message": "Adicionar aos favoritos"},
"removeFromFavorites": {"message": "Remover dos favoritos"},
"notAvailable": {"message": "Não disponível"},
"mainCast": {"message": "Elenco principal"},
"seasonsAndEpisodes": {"message": "Temporadas e episódios"},
"similarContent": {"message": "Conteúdo semelhante"},
"filmography": {"message": "Filmografia"},
"availableOn": {"message": "Disponível em"},
"episodesCount": {"message": "$count$ episódios", "placeholders": {"count": {"content": "$1"}}},
"seasonsCount": {"message": "$count$ temporadas", "placeholders": {"count": {"content": "$1"}}},
"runtimeMinutes": {"message": "$count$ min", "placeholders": {"count": {"content": "$1"}}},
"noTrailerFound": {"message": "Nenhum trailer foi encontrado para este título."},
"fatalInitError": {"message": "Erro Fatal de Inicialização"},
"fatalInitErrorSub": {"message": "A aplicação não pôde ser carregada."},
"invalidStreamInfo": {"message": "Informação inválida."},
"dbUnavailableForStreams": {"message": "Banco de dados local não disponível."},
"noTrailerFound": {"message": "Nenhum trailer encontrado para este título."},
"fatalInitError": {"message": "Erro fatal de inicialização"},
"fatalInitErrorSub": {"message": "Não foi possível carregar o aplicativo."},
"invalidStreamInfo": {"message": "Informações inválidas."},
"dbUnavailableForStreams": {"message": "Banco de dados local indisponível."},
"noPlexServersForStreams": {"message": "Nenhum servidor Plex."},
"notFoundOnServers": {"message": "\"$query$\" não encontrado nos servidores.", "placeholders": {"query": {"content": "$1"}}},
"notFoundOnServers": {"message": "\"$query$\" não encontrado nos servidores Plex.", "placeholders": {"query": {"content": "$1"}}},
"relativeTime_justNow": { "message": "Agora mesmo" },
"relativeTime_minutesAgo": { "message": "há $count$ minutos", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_hoursAgo": { "message": "há $count$ horas", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_minutesAgo": { "message": "Há $count$ minutos", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_hoursAgo": { "message": "Há $count$ horas", "placeholders": { "count": { "content": "$1" } } },
"relativeTime_yesterday": { "message": "Ontem" },
"relativeTime_daysAgo": { "message": "há $count$ dias", "placeholders": { "count": { "content": "$1" } } },
"errorLoadingDetails": { "message": "Erro ao Carregar Detalhes" },
"errorLoadingLocalContent": { "message": "Erro ao carregar conteúdo local." },
"errorServerResponse": { "message": "Resposta sem sucesso do servidor." },
"relativeTime_daysAgo": { "message": "Há $count$ dias", "placeholders": { "count": { "content": "$1" } } },
"errorLoadingDetails": { "message": "Erro ao carregar os detalhes" },
"errorLoadingLocalContent": { "message": "Erro ao carregar o conteúdo local." },
"errorServerResponse": { "message": "Resposta do servidor sem sucesso." },
"errorPlexApi": { "message": "Erro $status$ da API do Plex.", "placeholders": { "status": { "content": "$1" } } },
"errorParsingPlexXml": { "message": "Erro ao analisar o XML do Plex." },
"untitled": { "message": "Sem título" },
"itemCount": { "message": "$count$ itens", "placeholders": { "count": { "content": "$1" } } },
"noPhotoServers": { "message": "Nenhum servidor de fotos" }
"noPhotoServers": { "message": "Nenhum servidor de fotos" },
"jellyfinScanInProgress": { "message": "A verificação do Jellyfin já está em andamento." },
"jellyfinScanning": { "message": "Verificando o Jellyfin..." },
"jellyfinMissingCredentials": { "message": "Preencha a URL e o nome de usuário do Jellyfin." },
"jellyfinConnecting": { "message": "Conectando-se ao Jellyfin em: $url$", "placeholders": { "url": { "content": "$1" } } },
"jellyfinAuthFailed": { "message": "A autenticação do Jellyfin falhou: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinAuthSuccess": { "message": "Autenticação do Jellyfin bem-sucedida." },
"jellyfinFetchingLibraries": { "message": "Buscando bibliotecas..." },
"jellyfinFetchFailed": { "message": "Erro ao buscar bibliotecas: $message$", "placeholders": { "message": { "content": "$1" } } },
"jellyfinNoMediaLibraries": { "message": "Nenhuma biblioteca de filmes ou séries encontrada no Jellyfin." },
"jellyfinLibrariesFound": { "message": "$count$ biblioteca(s) de mídia encontrada(s).", "placeholders": { "count": { "content": "$1" } } },
"jellyfinLibraryScanSuccess": { "message": "[Sucesso] '$libraryName' verificada, $count$ títulos adicionados.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
"jellyfinLibraryScanFailed": { "message": "Erro ao verificar a biblioteca '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } },
"jellyfinScanSuccess": { "message": "Verificação do Jellyfin concluída. Adicionados $movies$ filmes e $series$ séries.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
"noJellyfinCredentials": { "message": "Credenciais do Jellyfin não configuradas." },
"notFoundOnJellyfin": { "message": "\"$query$\" não encontrado no Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
"notFoundOnAnyServer": { "message": "\"$query$\" não encontrado em nenhum servidor.", "placeholders": { "query": { "content": "$1" } } },
"localOnPlex": { "message": "No Plex" },
"searchOnPlex": { "message": "Pesquisar no Plex" },
"jellyfinTitle": { "message": "Conteúdo do Jellyfin" },
"noJellyfinContent": { "message": "Nenhum conteúdo do Jellyfin encontrado." },
"noJellyfinContentSub": { "message": "Verifique se você verificou seu servidor Jellyfin nas configurações." },
"activityViewerTitle": { "message": "Visualizador de atividades do servidor" },
"activitySelectServer": { "message": "Selecione um servidor" },
"activityCheckBtn": { "message": "Atualizar" },
"activityNoSessions": { "message": "Nenhuma sessão ativa neste servidor." },
"activitySessionUser": { "message": "Usuário" },
"activitySessionDevice": { "message": "Dispositivo" },
"activitySessionContent": { "message": "Conteúdo" },
"activitySessionState": { "message": "Estado" },
"activitySessionIdentifier": { "message": "Identificador do cliente" },
"activityCopyID": { "message": "Copiar ID" },
"activityError": { "message": "Não foi possível obter a atividade do servidor." },
"activityCopied": { "message": "Identificador copiado para a área de transferência!" },
"activityCopyError": { "message": "Erro ao copiar o identificador." },
"noProvidersFound": { "message": "Nenhum provedor encontrado." },
"availableOnPlex": { "message": "Disponível no Plex" },
"m3uGeneratorTitle": { "message": "Gerador de listas M3U" },
"selectAServer": { "message": "Selecione um servidor..." },
"downloadM3u": { "message": "Baixar M3U" },
"m3uGenerator": { "message": "Gerador de M3U" },
"selectLibraries": { "message": "Selecionar bibliotecas" },
"howToUse": { "message": "Como usar" },
"m3uInstruction1": { "message": "Escolha um servidor na lista." },
"m3uInstruction2": { "message": "Selecione uma ou mais bibliotecas para incluir." },
"m3uInstruction3": { "message": "Clique no botão de download." },
"m3uInstruction4": { "message": "Importe o arquivo .m3u para o seu reprodutor compatível." },
"chatOpen": { "message": "Abrir chat" },
"chatTitle": { "message": "Assistente de IA" },
"chatClose": { "message": "X" },
"chatPlaceholder": { "message": "Digite sua mensagem..." },
"chatSend": { "message": "➤" },
"chatWelcome": { "message": "Bem-vindo! Eu sou seu assistente CinePlex. Pergunte-me sobre filmes, séries ou qualquer outra coisa que você queira saber." },
"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." },
"chatApiInvalidResponse": { "message": "A API retornou uma resposta inválida. Tente novamente." },
"chatApiError": { "message": "Erro ao se comunicar com o assistente de IA" },
"downloadAll": { "message": "Baixar tudo" },
"download": { "message": "Baixar" },
"aiToolSearchLibraryDesc": { "message": "Pesquisa na biblioteca Plex do usuário por filmes ou séries por título." },
"aiToolSearchLibraryQueryParamDesc": { "message": "O título do filme ou série a ser pesquisado." },
"aiToolSearchLibraryTypeParamDesc": { "message": "O tipo de conteúdo a ser pesquisado. Pode ser 'movie' para filmes ou 'series' para séries. (Opcional)." },
"aiToolSearchLibraryResolutionParamDesc": { "message": "A resolução de vídeo a ser pesquisada (por exemplo, '4k', '1080p'). (Opcional)." },
"aiToolSearchLibraryContainerParamDesc": { "message": "O formato do contêiner de vídeo a ser pesquisado (por exemplo, 'mkv', 'mp4'). (Opcional)." },
"aiToolNavigateToPageDesc": { "message": "Navega o usuário para uma página específica da interface do aplicativo." },
"aiToolNavigateToPagePageParamDesc": { "message": "O nome da página para a qual navegar, por exemplo: 'movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers' ou 'm3u-generator'." },
"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." },
"aiToolShowItemDetailsDesc": { "message": "Exibe a página de detalhes de um filme ou série específica por seu título e tipo." },
"aiToolShowItemDetailsTitleParamDesc": { "message": "O título exato do filme ou série." },
"aiToolShowItemDetailsTypeParamDesc": { "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 transmiti-lo para um servidor PHP configurado." },
"aiToolAddToPlaylistTitleParamDesc": { "message": "O título do filme ou série a ser adicionado." },
"aiToolAddToPlaylistTypeParamDesc": { "message": "O tipo de conteúdo. 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." },
"aiToolCheckAndDownloadTitlesParamDesc": { "message": "Uma matriz de títulos de filmes ou séries para pesquisar e baixar." },
"aiToolCheckAndDownloadTypeParamDesc": { "message": "O tipo de conteúdo da lista. Deve ser 'movie' ou 'series'." },
"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." },
"aiToolToggleFavoriteDesc": { "message": "Adiciona ou remove um filme ou série da lista de favoritos do usuário." },
"aiToolToggleFavoriteTitleParamDesc": { "message": "O título do filme ou série." },
"aiToolToggleFavoriteTypeParamDesc": { "message": "O tipo de conteúdo. Deve ser 'movie' ou 'series'." },
"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." },
"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." },
"aiToolApplyFiltersTypeParamDesc": { "message": "O tipo de conteúdo ao qual aplicar os filtros. Deve ser 'movie' ou 'series'." },
"aiToolApplyFiltersGenreParamDesc": { "message": "O nome do gênero pelo qual filtrar (por exemplo, 'Ação', 'Drama')." },
"aiToolApplyFiltersYearParamDesc": { "message": "O ano de lançamento pelo qual filtrar (por exemplo, '2023')." },
"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)." },
"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." },
"aiToolPlayMusicByArtistNameParamDesc": { "message": "O nome exato do artista cujas músicas devem ser tocadas." },
"aiToolClearChatHistoryDesc": { "message": "Limpa todo o histórico de mensagens da conversa atual com o assistente de IA." },
"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." },
"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." },
"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." },
"aiToolAddPlexTokenTokenParamDesc": { "message": "A string do token X-Plex a ser adicionada." },
"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." },
"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)." },
"aiToolClearAllFavoritesDesc": { "message": "Remove todos os filmes e séries que o usuário marcou como favoritos." },
"aiToolClearViewingHistoryDesc": { "message": "Limpa o histórico de visualização do usuário da página de histórico." },
"aiToolClearRecommendationsViewDesc": { "message": "Limpa a visualização de recomendações e remove as recomendações em cache." },
"aiToolSearchNotFound": { "message": "'$query' não encontrado em sua biblioteca.", "placeholders": { "query": { "content": "$1" } } },
"aiToolNavigateSuccess": { "message": "Navegado para a página $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolNavigateError": { "message": "Erro ao navegar para a página $page$.", "placeholders": { "page": { "content": "$1" } } },
"aiToolStatsError": { "message": "Erro ao obter estatísticas." },
"aiToolItemNotFound": { "message": "Item '$title' não encontrado.", "placeholders": { "title": { "content": "$1" } } },
"aiToolShowItemDetailsSuccess": { "message": "Mostrando detalhes de '$title'.", "placeholders": { "title": { "content": "$1" } } },
"aiToolAddToPlaylistSuccess": { "message": "Adicionado '$title' à lista de reprodução.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoriteAdded": { "message": "Adicionado '$title' aos favoritos.", "placeholders": { "title": { "content": "$1" } } },
"aiToolFavoriteRemoved": { "message": "Removido '$title' dos favoritos.", "placeholders": { "title": { "content": "$1" } } },
"aiToolRecommendationsSuccess": { "message": "Mostrando recomendações." },
"aiToolApplyFiltersGenreNotFound": { "message": "Gênero '$genre' não encontrado.", "placeholders": { "genre": { "content": "$1" } } },
"aiToolApplyFiltersSuccess": { "message": "Filtros aplicados com sucesso." },
"aiToolPlayMusicNotReady": { "message": "O reprodutor de música não está pronto. Verifique se a sua biblioteca de música do Plex foi verificada." },
"aiToolPlayMusicArtistNotFound": { "message": "Artista '$artist_name' não encontrado.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolPlayMusicNoSongs": { "message": "Nenhuma música encontrada para '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolPlayMusicSuccess": { "message": "Tocando música de '$artist_name'.", "placeholders": { "artist_name": { "content": "$1" } } },
"aiToolChatHistoryCleared": { "message": "Histórico do chat limpo." },
"aiToolConfirmDeleteDatabase": { "message": "Tem certeza de que deseja excluir o banco de dados local? Esta ação é irreversível." },
"aiToolDeleteDatabaseCancelled": { "message": "Exclusão do banco de dados cancelada." },
"aiToolExecutionError": { "message": "Erro ao executar a ferramenta '$toolName$': $message$", "placeholders": { "toolName": { "content": "$1" }, "message": { "content": "$2" } } },
"aiToolUnknown": { "message": "Ferramenta desconhecida: '$toolName'.", "placeholders": { "toolName": { "content": "$1" } } },
"aiToolFavoritesCleared": { "message": "Favoritos limpos." },
"aiToolFavoritesClearError": { "message": "Erro ao limpar os favoritos: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolRecommendationsCleared": { "message": "Recomendações limpas." },
"aiToolRecommendationsClearError": { "message": "Erro ao limpar as recomendações: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolDatabaseDeleted": { "message": "Banco de dados excluído. A página será recarregada." },
"aiToolDatabaseDeleteError": { "message": "Erro ao excluir o banco de dados: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolDatabaseDeleteBlocked": { "message": "A exclusão do banco de dados está bloqueada. Feche outras abas do aplicativo." },
"aiToolUpdateAllTokensSuccess": { "message": "Todos os tokens foram atualizados com sucesso." },
"aiToolUpdateAllTokensError": { "message": "Erro ao atualizar os tokens: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolAddPlexTokenSuccess": { "message": "Token do Plex adicionado com sucesso." },
"aiToolAddPlexTokenError": { "message": "Erro ao adicionar o token do Plex: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolChangeRegionSuccess": { "message": "Região alterada para $region$. O conteúdo está sendo atualizado.", "placeholders": { "region": { "content": "$1" } } },
"aiToolChangeRegionError": { "message": "Erro ao alterar a região: $message$", "placeholders": { "message": { "content": "$1" } } },
"aiToolViewingHistoryCleared": { "message": "Histórico de visualização limpo." },
"aiToolViewingHistoryClearError": { "message": "Erro ao limpar o histórico de visualização: $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." },
"aiToolM3UNoTitlesProvided": { "message": "Forneça uma lista de títulos para criar a lista de reprodução." },
"aiToolM3UCheckingTitles": { "message": "Verificando os títulos em seus servidores locais..." },
"aiToolM3UNoLocalMatchesForDownload": { "message": "Não encontrei nenhum dos filmes ou séries da lista em seus servidores locais." },
"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" } } },
"backToProviders": { "message": "Voltar para os provedores" },
"artistsCounterSingle": { "message": "$total$ artista", "placeholders": { "total": { "content": "$1" } } },
"artistsCounterLoading": { "message": "Carregando..." },
"downloadingSong": { "message": "Iniciando o download de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"songDownloaded": { "message": "\"$title$\" baixado.", "placeholders": { "title": { "content": "$1" } } },
"errorDownloadingSong": { "message": "Erro ao baixar \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"generatingAlbumM3U": { "message": "Gerando M3U para \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
"albumM3UGenerated": { "message": "M3U para o álbum \"$artist$\" gerado.", "placeholders": { "artist": { "content": "$1" } } },
"retyingSection": { "message": "Tentando novamente a seção \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
"retrySuccess": { "message": "[SUCESSO] Nova tentativa de \"$title$\" concluída.", "placeholders": { "title": { "content": "$1" } } },
"retryError": { "message": "[ERRO FINAL] A nova tentativa para \"$title$\" falhou: $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } },
"startingRetryPhase": { "message": "Iniciando a fase de nova tentativa para $count$ seções...", "placeholders": { "count": { "content": "$1" } } },
"tokenFoundServers": { "message": "O 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 a verificação: $message$", "placeholders": { "message": { "content": "$1" } } },
"stoppingPlexScan": { "message": "Parando a verificação do Plex..." },
"invalidTokenProvided": { "message": "Token inválido fornecido." },
"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." },
"page": { "message": "Página" },
"all": { "message": "Todos" },
"userScore": { "message": "Pontuação dos usuários" },
"duration": { "message": "Duração" },
"min": { "message": "Mín" },
"max": { "message": "Máx" }
}

View File

@ -1,151 +0,0 @@
:root {
--primary: #0a0a0f;
--secondary: #101116;
--accent: #00e0ff;
--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.7;
min-height: 100vh;
overflow-x: hidden;
position: relative;
padding-top: var(--topbar-height);
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 {
width: 12px;
}
body::-webkit-scrollbar-track {
background: var(--primary);
border-left: 1px solid var(--glass-border);
}
body::-webkit-scrollbar-thumb {
background-color: var(--accent-dark);
border-radius: 10px;
border: 3px solid var(--primary);
}
body::-webkit-scrollbar-thumb:hover {
background-color: var(--accent);
}
body.light-theme::-webkit-scrollbar-track {
background: var(--secondary);
border-left: 1px solid var(--glass-border);
}
body.light-theme::-webkit-scrollbar-thumb {
background-color: #bdc3c7;
border-color: var(--secondary);
}
body.light-theme::-webkit-scrollbar-thumb:hover {
background-color: #a3aab1;
}

View File

@ -1,297 +0,0 @@
.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);
}
.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);
}
.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);
}
@media (max-width: 768px) {
.section-title {
font-size: 1.5rem;
}
}

View File

@ -1,367 +0,0 @@
#main-view {
min-height: calc(100vh - var(--navbar-height) - 100px);
}
.content-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 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: 0;
padding-bottom: 150%;
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-poster img {
display: block;
width: 100%;
height: auto;
}
.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;
}
.history-section {
margin-top: 4rem;
}
#history-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.history-item {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 1rem 1.2rem;
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius-md);
transition: var(--transition);
cursor: pointer;
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: 55px;
height: 82px;
object-fit: cover;
border-radius: var(--border-radius-sm);
flex-shrink: 0;
}
.history-info {
flex: 1;
min-width: 0;
}
.history-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.3rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-primary);
}
.history-meta {
font-size: 0.85rem;
color: var(--text-secondary);
}
.history-remove-btn {
padding: 0.4rem 0.6rem;
font-size: 0.9rem;
line-height: 1;
border-radius: 50%;
opacity: 0.6;
transition: opacity 0.3s ease;
}
.history-item:hover .history-remove-btn {
opacity: 1;
}
.history-remove-btn:hover {
background-color: var(--danger);
color: white !important;
}
@media (max-width: 992px) {
.content-grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 1.5rem;
}
}
@media (max-width: 768px) {
.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;
}
}

103
css/custom-filters.css Normal file
View File

@ -0,0 +1,103 @@
.filter-popover-wrapper {
position: relative;
display: inline-block;
}
.filter-popover {
display: none;
position: absolute;
background-color: var(--secondary);
min-width: 280px;
box-shadow: var(--shadow);
padding: 1rem;
z-index: 100;
border-radius: var(--border-radius-md);
border: 1px solid var(--glass-border);
margin-top: 0.5rem; /* Add some space between button and popover */
}
.range-slider-container {
position: relative;
height: 20px;
display: flex;
align-items: center;
}
.slider-track, .slider-fill {
position: absolute;
width: 100%;
height: 6px;
border-radius: 3px;
left: 0;
}
.slider-track {
background-color: var(--glass);
z-index: 1;
}
.slider-fill {
background-color: var(--accent);
z-index: 2;
}
.filter-popover .range-values {
display: flex;
justify-content: space-between;
margin-top: 0.75rem;
font-size: 0.9rem;
color: var(--text-secondary);
}
.form-range {
-webkit-appearance: none;
appearance: none;
width: 100%;
background: transparent;
position: absolute;
left: 0;
margin: 0;
pointer-events: none;
}
input[id$="-max"].form-range {
z-index: 3;
}
input[id$="-min"].form-range {
z-index: 4;
}
.form-range::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: var(--accent-dark);
background-clip: padding-box;
cursor: pointer;
border-radius: 50%;
border: 3px solid var(--secondary);
pointer-events: auto;
transition: background-color 0.2s;
}
.form-range::-webkit-slider-thumb:hover {
background-color: var(--accent);
}
.form-range::-moz-range-thumb {
width: 20px;
height: 20px;
background: var(--accent-dark);
background-clip: padding-box;
cursor: pointer;
border-radius: 50%;
border: 3px solid var(--secondary);
pointer-events: auto;
transition: background-color 0.2s;
}
.form-range::-moz-range-thumb:hover {
background-color: var(--accent);
}

View File

@ -1,642 +0,0 @@
.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 {
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-accordion .accordion-item {
background: transparent;
border-radius: var(--border-radius-lg);
border: 1px solid var(--glass-border);
margin-bottom: 1.5rem;
overflow: hidden;
}
.seasons-accordion .accordion-button {
background: var(--card-bg);
color: var(--text-primary);
font-size: 1.1rem;
padding: 1.2rem 1.5rem;
border: none;
box-shadow: none !important;
transition: background-color 0.3s ease;
}
.seasons-accordion .accordion-button:not(.collapsed) {
background: var(--glass);
color: var(--accent);
border-bottom: 1px solid var(--glass-border);
}
.seasons-accordion .accordion-button:hover {
background: var(--glass);
}
.seasons-accordion .accordion-button::after {
filter: brightness(0) invert(1) opacity(0.7);
transition: transform 0.3s ease;
}
.seasons-accordion .accordion-button:not(.collapsed)::after {
filter: invert(1) opacity(1) brightness(1.5) contrast(200%);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%2300e0ff'%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");
}
.seasons-accordion .accordion-collapse {
overflow: hidden;
transition: height 0.35s ease;
}
.season-info {
display: flex;
align-items: center;
gap: 1.5rem;
width: 100%;
}
.season-poster {
width: 70px;
height: 105px;
object-fit: cover;
border-radius: var(--border-radius-sm);
flex-shrink: 0;
}
.season-details {
flex-grow: 1;
min-width: 0;
}
.season-title {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 0.4rem;
}
.season-meta {
display: flex;
gap: 1.2rem;
font-size: 0.9rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
.season-meta span {
display: flex;
align-items: center;
gap: 0.5rem;
}
.season-overview {
font-size: 0.9rem;
line-height: 1.5;
color: var(--text-secondary);
margin-top: 0.5rem;
}
.season-episodes {
padding: 1rem 1.5rem;
background: var(--secondary);
border-top: 1px solid var(--glass-border);
max-height: 500px;
overflow-y: auto;
}
.episode-card {
display: flex;
gap: 1rem;
padding: 1.2rem 0.5rem;
border-bottom: 1px solid var(--glass-border);
transition: background-color 0.3s ease;
}
.episode-card:last-child {
border-bottom: none;
padding-bottom: 0.5rem;
}
.episode-card:first-child {
padding-top: 0.5rem;
}
.episode-card:hover {
background: var(--glass);
}
.episode-number {
flex-shrink: 0;
width: 36px;
height: 36px;
background: var(--glass);
border: 1px solid var(--glass-border);
color: var(--text-primary);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-weight: 600;
font-size: 1rem;
margin-top: 5px;
}
.episode-info {
flex: 1;
min-width: 0;
}
.episode-title {
font-size: 1.1rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--text-primary);
}
.episode-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1rem;
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
.episode-meta span {
display: flex;
align-items: center;
gap: 0.5rem;
}
.episode-overview {
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-secondary);
margin-top: 0.7rem;
}
@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 {
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;
}
.season-info {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
text-align: left;
}
.season-poster {
width: 100%;
max-width: 150px;
height: auto;
margin: 0;
}
.episode-card {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.episode-number {
margin-bottom: 0.8rem;
width: 32px;
height: 32px;
font-size: 0.9rem;
}
}
@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 {
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
}
@media (min-width: 992px) {
.back-button {
left: calc(var(--sidebar-width) + 2rem);
}
}
.item-details::-webkit-scrollbar {
width: 12px;
}
.item-details::-webkit-scrollbar-track {
background: var(--primary);
border-left: 1px solid var(--glass-border);
}
.item-details::-webkit-scrollbar-thumb {
background-color: var(--accent-dark);
border-radius: 10px;
border: 3px solid var(--primary);
}
.item-details::-webkit-scrollbar-thumb:hover {
background-color: var(--accent);
}
body.light-theme .item-details::-webkit-scrollbar-track {
background: var(--secondary);
border-left: 1px solid var(--glass-border);
}
body.light-theme .item-details::-webkit-scrollbar-thumb {
background-color: #bdc3c7;
border-color: var(--secondary);
}
body.light-theme .item-details::-webkit-scrollbar-thumb:hover {
background-color: #a3aab1;
}

View File

@ -1,180 +0,0 @@
#equalizer-panel {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(100%);
width: 520px;
background-color: #1c1c22;
border-top: 1px solid #333;
border-left: 1px solid #333;
border-right: 1px solid #333;
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: #25252d;
color: #e0e0e0;
border-bottom: 1px solid #333;
}
.equalizer-header h5 {
margin: 0;
font-weight: 600;
font-family: 'Orbitron', sans-serif;
}
.close-btn {
background: none;
border: none;
color: #999;
font-size: 1.2rem;
cursor: pointer;
transition: color 0.2s;
}
.close-btn:hover { color: #fff; }
.equalizer-top-bar {
display: flex;
justify-content: space-around;
align-items: center;
padding: 15px;
border-bottom: 1px solid #333;
gap: 20px;
}
.control-group {
display: flex;
align-items: center;
gap: 10px;
}
.control-group label {
font-size: 0.8rem;
color: #aaa;
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: #aaa;
font-weight: 500;
}
.eq-slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 6px;
background: #3a3a44;
outline: none;
border-radius: 3px;
cursor: pointer;
transition: opacity 0.2s;
}
.eq-slider::-webkit-slider-runnable-track {
width: 100%;
height: 6px;
cursor: pointer;
background: #3a3a44;
border-radius: 3px;
}
.eq-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
background: #00aaff;
border: 3px solid #1c1c22;
border-radius: 50%;
cursor: pointer;
margin-top: -6px;
transition: background 0.2s, box-shadow 0.2s;
box-shadow: 0 0 5px rgba(0, 170, 255, 0.5);
}
.eq-slider:hover::-webkit-slider-thumb {
background: #00caff;
box-shadow: 0 0 10px rgba(0, 202, 255, 0.8);
}
.eq-slider::-moz-range-track {
width: 100%;
height: 6px;
cursor: pointer;
background: #3a3a44;
border-radius: 3px;
}
.eq-slider::-moz-range-thumb {
width: 18px;
height: 18px;
background: #00aaff;
border: 3px solid #1c1c22;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 5px rgba(0, 170, 255, 0.5);
}
.slider-value {
font-size: 0.7rem;
color: #888;
background: #2a2a33;
padding: 2px 5px;
border-radius: 3px;
min-width: 35px;
text-align: center;
}
.custom-select-sm {
background-color: #2a2a33;
color: #e0e0e0;
border: 1px solid #444;
border-radius: 4px;
padding: 5px 8px;
font-size: 0.8rem;
}
.visualizer-container {
height: 80px;
background-color: #111;
border-top: 1px solid #333;
padding: 0;
margin: 0;
overflow: hidden;
}
#visualizer-canvas {
width: 100%;
height: 100%;
display: block;
}

View File

@ -1,80 +0,0 @@
.footer {
background: var(--secondary);
padding: 1.5rem 2rem;
border-top: 1px solid var(--glass-border);
margin-top: 4rem;
}
.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;
}
}

View File

@ -1,125 +0,0 @@
.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: 3rem;
}
.hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to top, var(--primary) 5%, rgba(10, 10, 15, 0.7) 40%, rgba(10, 10, 15, 0.2) 70%, transparent 100%),
linear-gradient(to right, var(--primary) 10%, transparent 70%);
z-index: 1;
}
.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 center;
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;
}
@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);
}

View File

@ -1,127 +0,0 @@
#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;
gap: 1rem;
}
.history-main-content {
display: flex;
gap: 1rem;
width: 100%;
}
.history-actions {
width: 100%;
justify-content: space-around;
padding-top: 1rem;
border-top: 1px solid var(--glass-border);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,698 +0,0 @@
#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);
}
.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;
/* Removed writing-mode: bt-lr; */
width: 100px; /* Adjusted for horizontal orientation */
height: 8px; /* Adjusted for horizontal orientation */
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; }
}

View File

@ -1,218 +0,0 @@
.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);
}
.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-toggle {
display: none;
}
.sidebar-nav {
transform: translateX(0);
}
#main-container {
padding-left: var(--sidebar-width);
}
}
@media (max-width: 991px) {
#sidebar-toggle {
display: inline-flex;
}
}
@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;
}
#main-view {
padding-top: 5rem;
}
}
@media (max-width: 576px) {
.top-bar {
padding: 0 0.8rem;
}
.logo {
font-size: 1.5rem;
}
.sidebar-nav {
width: 100%;
transform: translateX(-105%);
}
.btn-icon {
width: 36px;
height: 36px;
font-size: 1.1rem;
}
}

View File

@ -1,245 +0,0 @@
.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: var(--secondary);
}
.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(10,10,15,0.7);
}
.light-theme #settingsModal .nav-tabs {
background-color: var(--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 .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;

View File

@ -1,243 +0,0 @@
#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);
}
#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;
}

View File

@ -1,133 +0,0 @@
#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;
}
}

BIN
img/hero-def.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

155
js/activityViewer.js Normal file
View File

@ -0,0 +1,155 @@
import { state } from './state.js';
import { getFromDB } from './db.js';
import { fetchPlexSessions } from './api.js';
import { showNotification, _ } from './utils.js';
export class ActivityViewer {
constructor(modalElement) {
this.modalElement = modalElement;
this.modal = new bootstrap.Modal(this.modalElement);
this.dom = {};
this.isChecking = false;
this.cacheDOM();
this.bindEvents();
}
cacheDOM() {
this.dom.serverSelect = this.modalElement.querySelector('#activity-server-select');
this.dom.checkBtn = this.modalElement.querySelector('#check-activity-btn');
this.dom.loader = this.modalElement.querySelector('#activity-loader');
this.dom.resultsContainer = this.modalElement.querySelector('#activity-results');
}
bindEvents() {
this.modalElement.addEventListener('show.bs.modal', () => this.onModalShow());
this.dom.checkBtn.addEventListener('click', () => this.handleCheckActivity());
this.dom.resultsContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('copy-identifier-btn')) {
const identifier = e.target.dataset.identifier;
this.copyToClipboard(identifier, e.target);
}
});
}
async onModalShow() {
this.dom.resultsContainer.innerHTML = '';
await this.populateServerSelect();
}
async populateServerSelect() {
this.dom.serverSelect.innerHTML = `<option>${_('loading')}</option>`;
try {
const servers = await getFromDB('conexiones_locales');
if (servers.length === 0) {
this.dom.serverSelect.innerHTML = `<option>${_('noServersFound')}</option>`;
this.dom.checkBtn.disabled = true;
return;
}
this.dom.serverSelect.innerHTML = '';
servers.forEach((server, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = server.nombre || server.ip;
this.dom.serverSelect.appendChild(option);
});
this.dom.checkBtn.disabled = false;
} catch (error) {
this.dom.serverSelect.innerHTML = `<option>${_('errorLoadingServers')}</option>`;
this.dom.checkBtn.disabled = true;
}
}
async handleCheckActivity() {
if (this.isChecking) return;
const selectedIndex = this.dom.serverSelect.value;
if (selectedIndex === '') return;
const servers = await getFromDB('conexiones_locales');
const selectedServer = servers[selectedIndex];
if (!selectedServer) return;
this.isChecking = true;
this.dom.checkBtn.disabled = true;
this.dom.loader.style.display = 'block';
this.dom.resultsContainer.innerHTML = '';
try {
const sessions = await fetchPlexSessions(selectedServer);
this.renderSessions(sessions, selectedServer);
} catch (error) {
this.dom.resultsContainer.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('activityError')}</p><p class="text-muted">${error.message}</p></div>`;
} finally {
this.isChecking = false;
this.dom.checkBtn.disabled = false;
this.dom.loader.style.display = 'none';
}
}
renderSessions(sessions, server) {
if (sessions.length === 0) {
this.dom.resultsContainer.innerHTML = `<div class="empty-state"><i class="fas fa-bed"></i><p class="lead">${_('activityNoSessions')}</p></div>`;
return;
}
const fragment = document.createDocumentFragment();
sessions.forEach(session => {
const card = this.createSessionCard(session, server);
fragment.appendChild(card);
});
this.dom.resultsContainer.appendChild(fragment);
}
createSessionCard(session, server) {
const card = document.createElement('div');
card.className = 'session-card';
const posterUrl = session.thumb ? `${server.protocolo}://${server.ip}:${server.puerto}${session.thumb}?X-Plex-Token=${server.token}` : 'img/no-poster.png';
const contentTitle = session.grandparentTitle ? `${session.grandparentTitle} - ${session.title}` : session.title;
const playerStateIcon = session.Player.state === 'playing' ? 'fa-play' : 'fa-pause';
const playerStateColor = session.Player.state === 'playing' ? 'text-success' : 'text-warning';
card.innerHTML = `
<img src="${posterUrl}" class="session-poster" alt="Poster">
<div class="session-info">
<div class="session-details">
<p><strong>${_('activitySessionUser')}:</strong> ${session.User.title}</p>
<p><strong>${_('activitySessionDevice')}:</strong> ${session.Player.product} (${session.Player.title})</p>
<p><strong>${_('activitySessionContent')}:</strong> ${contentTitle}</p>
<p><strong>${_('activitySessionState')}:</strong> <i class="fas ${playerStateIcon} ${playerStateColor}"></i> ${session.Player.state}</p>
</div>
<div class="session-identifier">
<label>${_('activitySessionIdentifier')}:</label>
<div class="input-group">
<input type="text" class="form-control form-control-sm" value="${session.Player.machineIdentifier}" readonly>
<button class="btn btn-sm btn-outline-secondary copy-identifier-btn" data-identifier="${session.Player.machineIdentifier}" title="${_('activityCopyID')}">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
</div>
`;
return card;
}
copyToClipboard(text, button) {
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
const originalIcon = button.innerHTML;
button.innerHTML = '<i class="fas fa-check"></i>';
showNotification(_('activityCopied'), 'success');
setTimeout(() => {
button.innerHTML = originalIcon;
}, 2000);
}).catch(err => {
showNotification(_('activityCopyError'), 'error');
});
}
show() {
this.modal.show();
}
}

521
js/ai-tools.js Normal file
View File

@ -0,0 +1,521 @@
import { state } from './state.js';
import { getFromDB, clearStore, addItemsToStore } from './db.js';
import { showNotification, _ } from './utils.js';
import { switchView, showItemDetails, addStreamToList, downloadM3U, generateStatistics, toggleFavorite, loadRecommendations, applyFilters as applyUIFilters, clearAllFavorites, clearRecommendations, loadInitialContent, loadContent, clearAllHistory } from './ui.js';
import { fetchTMDB } from './api.js';
import { updateAllTokens, addPlexToken } from './plex.js';
import { config } from './config.js';
export class AITools {
constructor(chatInstance) {
this.chat = chatInstance;
this.genreCache = { movie: null, tv: null };
}
get toolDefinitions() {
return [
{
name: 'search_library',
description: _('aiToolSearchLibraryDesc'),
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: _('aiToolSearchLibraryQueryParamDesc') },
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolSearchLibraryTypeParamDesc') },
resolution: { type: 'string', description: _('aiToolSearchLibraryResolutionParamDesc') },
container: { type: 'string', description: _('aiToolSearchLibraryContainerParamDesc') }
},
required: []
}
},
{
name: 'navigate_to_page',
description: _('aiToolNavigateToPageDesc'),
parameters: {
type: 'object',
properties: {
page: {
type: 'string',
enum: ['movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', 'm3u-generator'],
description: _('aiToolNavigateToPagePageParamDesc')
}
},
required: ['page']
}
},
{
name: 'get_user_stats',
description: _('aiToolGetUserStatsDesc'),
parameters: { type: 'object', properties: {} }
},
{
name: 'show_item_details',
description: _('aiToolShowItemDetailsDesc'),
parameters: {
type: 'object',
properties: {
title: { type: 'string', description: _('aiToolShowItemDetailsTitleParamDesc') },
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolShowItemDetailsTypeParamDesc') }
},
required: ['title', 'type']
}
},
{
name: 'add_to_playlist',
description: _('aiToolAddToPlaylistDesc'),
parameters: {
type: 'object',
properties: {
title: { type: 'string', description: _('aiToolAddToPlaylistTitleParamDesc') },
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolAddToPlaylistTypeParamDesc') }
},
required: ['title', 'type']
}
},
{
name: 'check_and_download_titles_list',
description: _('aiToolCheckAndDownloadDesc'),
parameters: {
type: 'object',
properties: {
titles: {
type: 'array',
items: { type: 'string' },
description: _('aiToolCheckAndDownloadTitlesParamDesc')
},
type: {
type: 'string',
enum: ['movie', 'series'],
description: _('aiToolCheckAndDownloadTypeParamDesc')
},
filename: { type: 'string', description: _('aiToolCheckAndDownloadFilenameParamDesc') }
},
required: ['titles', 'type']
}
},
{
name: 'toggle_favorite',
description: _('aiToolToggleFavoriteDesc'),
parameters: {
type: 'object',
properties: {
title: { type: 'string', description: _('aiToolToggleFavoriteTitleParamDesc') },
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolToggleFavoriteTypeParamDesc') }
},
required: ['title', 'type']
}
},
{
name: 'get_recommendations',
description: _('aiToolGetRecommendationsDesc'),
parameters: { type: 'object', properties: {} }
},
{
name: 'apply_filters',
description: _('aiToolApplyFiltersDesc'),
parameters: {
type: 'object',
properties: {
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolApplyFiltersTypeParamDesc') },
genre: { type: 'string', description: _('aiToolApplyFiltersGenreParamDesc') },
year: { type: 'string', description: _('aiToolApplyFiltersYearParamDesc') },
sort: { type: 'string', enum: ['popularity.desc', 'vote_average.desc', 'release_date.desc', 'first_air_date.desc'], description: _('aiToolApplyFiltersSortParamDesc') }
},
required: ['type']
}
},
{
name: 'play_music_by_artist',
description: _('aiToolPlayMusicByArtistDesc'),
parameters: {
type: 'object',
properties: {
artist_name: { type: 'string', description: _('aiToolPlayMusicByArtistNameParamDesc') }
},
required: ['artist_name']
}
},
{
name: 'clear_chat_history',
description: _('aiToolClearChatHistoryDesc'),
parameters: { type: 'object', properties: {} }
},
{
name: 'delete_database',
description: _('aiToolDeleteDatabaseDesc'),
parameters: { type: 'object', properties: {} }
},
{
name: 'update_all_tokens',
description: _('aiToolUpdateAllTokensDesc'),
parameters: { type: 'object', properties: {} }
},
{
name: 'add_plex_token',
description: _('aiToolAddPlexTokenDesc'),
parameters: {
type: 'object',
properties: {
token: { type: 'string', description: _('aiToolAddPlexTokenTokenParamDesc') }
},
required: ['token']
}
},
{
name: 'change_region',
description: _('aiToolChangeRegionDesc'),
parameters: {
type: 'object',
properties: {
region: { type: 'string', description: _('aiToolChangeRegionRegionParamDesc') }
},
required: ['region']
}
},
{
name: 'clear_all_favorites',
description: _('aiToolClearAllFavoritesDesc'),
parameters: { type: 'object', properties: {} }
},
{
name: 'clear_viewing_history',
description: _('aiToolClearViewingHistoryDesc'),
parameters: { type: 'object', properties: {} }
},
{
name: 'clear_recommendations_view',
description: _('aiToolClearRecommendationsViewDesc'),
parameters: { type: 'object', properties: {} }
}
];
}
async "search_library"({ query, type, resolution, container }) {
const movieEntries = await getFromDB('movies');
const seriesEntries = await getFromDB('series');
const allContent = [
...movieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))),
...seriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' })))
];
let results = allContent;
if (query) {
const searchTerm = query.toLowerCase().trim();
results = allContent.filter(item => item.title.toLowerCase().includes(searchTerm));
}
if (type) {
results = results.filter(item => item.type === type);
}
if (resolution) {
results = results.filter(item => item.resolution && item.resolution.toLowerCase() === resolution.toLowerCase());
}
if (container) {
results = results.filter(item => item.container && item.container.toLowerCase() === container.toLowerCase());
}
if (results.length === 0) {
return JSON.stringify({ success: false, message: _('aiToolSearchNotFound', query) });
}
const formattedResults = results.slice(0, 10).map(item => ({ title: item.title, year: item.year, type: item.type, resolution: item.resolution, container: item.container }));
return JSON.stringify({ success: true, count: results.length, results: formattedResults });
}
"navigate_to_page"({ page }) {
try {
switchView(page);
return JSON.stringify({ success: true, message: _('aiToolNavigateSuccess', page) });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolNavigateError', page) });
}
}
async "get_user_stats"() {
try {
const movieItems = (await getFromDB('movies')).flatMap(s => s.titulos);
const seriesItems = (await getFromDB('series')).flatMap(s => s.titulos);
const artistItems = (await getFromDB('artists')).flatMap(s => s.titulos);
const stats = {
totalMovies: new Set(movieItems.map(item => item.title)).size,
totalSeries: new Set(seriesItems.map(item => item.title)).size,
totalArtists: new Set(artistItems.map(item => item.title)).size,
};
switchView('stats');
return JSON.stringify({ success: true, stats });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolStatsError') });
}
}
async "show_item_details"({ title, type }) {
const content = await this.findTmdbContent(title, type);
if (!content) {
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
}
showItemDetails(Number(content.id), content.type);
return JSON.stringify({ success: true, message: _('aiToolShowItemDetailsSuccess', title) });
}
async "add_to_playlist"({ title, type }) {
const content = await this.findLocalContent(title, type);
if (!content) {
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
}
addStreamToList(content.title, content.type);
return JSON.stringify({ success: true, message: _('aiToolAddToPlaylistSuccess', title) });
}
async "check_and_download_titles_list"({ titles, type, filename }) {
try {
if (!titles || titles.length === 0) {
return JSON.stringify({ success: false, message: _('aiToolM3UNoTitlesProvided') });
}
showNotification(_('aiToolM3UCheckingTitles'), 'info');
const plexStore = type === 'movie' ? 'movies' : 'series';
const jellyfinStore = type === 'movie' ? 'jellyfin_movies' : 'jellyfin_series';
const plexContent = await getFromDB(plexStore);
const jellyfinContent = await getFromDB(jellyfinStore);
const allLocalItems = [
...plexContent.flatMap(server => server.titulos || []),
...jellyfinContent.flatMap(lib => lib.titulos || [])
];
const normalize = (str) => str ? str.toLowerCase().trim().replace(/[^a-z0-9]/g, '') : '';
const normalizedTitlesFromAI = new Set(titles.map(normalize));
const matchedItems = [];
const seenTitles = new Set();
for (const localItem of allLocalItems) {
const normalizedLocalTitle = normalize(localItem.title);
if (normalizedTitlesFromAI.has(normalizedLocalTitle) && !seenTitles.has(normalizedLocalTitle)) {
matchedItems.push({ title: localItem.title, type });
seenTitles.add(normalizedLocalTitle);
}
}
if (matchedItems.length === 0) {
return JSON.stringify({ success: true, message: _('aiToolM3UNoLocalMatchesForDownload') });
}
downloadM3U(matchedItems, null, filename);
return JSON.stringify({ success: true, message: _('aiToolM3UDownloadStarted', [String(matchedItems.length), String(titles.length)]) });
} catch (error) {
return JSON.stringify({ success: false, message: `Error creating playlist: ${error.message}` });
}
}
async "toggle_favorite"({ title, type }) {
const item = await this.findTmdbContent(title, type);
if (!item) {
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
}
toggleFavorite(item.id, type);
const isFavorite = state.favorites.some(fav => fav.id === item.id && fav.type === type);
const message = isFavorite ? _('aiToolFavoriteAdded', title) : _('aiToolFavoriteRemoved', title);
return JSON.stringify({ success: true, message });
}
async "get_recommendations"() {
switchView('recommendations');
return JSON.stringify({ success: true, message: _('aiToolRecommendationsSuccess') });
}
async "apply_filters"({ type, genre, year, sort }) {
switchView(type === 'movie' ? 'movies' : 'series');
let genreId = '';
if (genre) {
genreId = await this.getGenreId(genre, type);
if (!genreId) {
return JSON.stringify({ success: false, message: _('aiToolApplyFiltersGenreNotFound', genre) });
}
}
state.currentParams.genre = genreId;
state.currentParams.year = year || '';
state.currentParams.sort = sort || 'popularity.desc';
applyUIFilters();
return JSON.stringify({ success: true, message: _('aiToolApplyFiltersSuccess') });
}
async "play_music_by_artist"({ artist_name }) {
if (!state.musicPlayer || !state.musicPlayer.isReady) {
return JSON.stringify({ success: false, message: _('aiToolPlayMusicNotReady') });
}
const allArtists = state.musicPlayer._generateFullArtistListForToken('all');
const searchTerm = artist_name.toLowerCase().trim();
const artist = allArtists.find(a => a.title.toLowerCase() === searchTerm);
if (!artist) {
return JSON.stringify({ success: false, message: _('aiToolPlayMusicArtistNotFound', artist_name) });
}
state.musicPlayer.showPlayer();
const songs = await state.musicPlayer.getArtistSongs(artist);
if (!songs || songs.length === 0) {
return JSON.stringify({ success: false, message: _('aiToolPlayMusicNoSongs', artist_name) });
}
state.musicPlayer.cancionesActuales = songs;
state.musicPlayer.playSong(0);
return JSON.stringify({ success: true, message: _('aiToolPlayMusicSuccess', artist_name) });
}
async "clear_chat_history"() {
try {
this.chat.clearHistory();
return JSON.stringify({ success: true, message: _('aiToolChatHistoryCleared') });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolChatHistoryClearError', error.message) });
}
}
async "delete_database"() {
try {
if (confirm(_('aiToolConfirmDeleteDatabase'))) {
if (state.db) {
state.db.close();
}
const deleteRequest = indexedDB.deleteDatabase(config.dbName);
return new Promise((resolve) => {
deleteRequest.onsuccess = () => {
setTimeout(() => window.location.reload(), 1500);
resolve(JSON.stringify({ success: true, message: _('aiToolDatabaseDeleted') }));
};
deleteRequest.onerror = (event) => {
resolve(JSON.stringify({ success: false, message: _('aiToolDatabaseDeleteError', event.target.error) }));
};
deleteRequest.onblocked = () => {
resolve(JSON.stringify({ success: false, message: _('aiToolDatabaseDeleteBlocked') }));
};
});
} else {
return JSON.stringify({ success: false, message: _('aiToolDeleteDatabaseCancelled') });
}
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolDatabaseDeleteError', error.message) });
}
}
async "update_all_tokens"() {
try {
await updateAllTokens();
return JSON.stringify({ success: true, message: _('aiToolUpdateAllTokensSuccess') });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolUpdateAllTokensError', error.message) });
}
}
async "add_plex_token"({ token }) {
try {
await addPlexToken(token);
return JSON.stringify({ success: true, message: _('aiToolAddPlexTokenSuccess') });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolAddPlexTokenError', error.message) });
}
}
async "change_region"({ region }) {
try {
state.settings.watchRegion = region;
await addItemsToStore('settings', [{ id: 'user_settings', ...state.settings }]);
await loadInitialContent();
if (['movies', 'series', 'search'].includes(state.currentView)) {
await loadContent();
}
return JSON.stringify({ success: true, message: _('aiToolChangeRegionSuccess', region) });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolChangeRegionError', error.message) });
}
}
async "clear_all_favorites"() {
try {
clearAllFavorites();
return JSON.stringify({ success: true, message: _('aiToolFavoritesCleared') });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolFavoritesClearError', error.message) });
}
}
async "clear_viewing_history"() {
try {
clearAllHistory();
return JSON.stringify({ success: true, message: _('aiToolViewingHistoryCleared') });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolViewingHistoryClearError', error.message) });
}
}
async "clear_recommendations_view"() {
try {
clearRecommendations();
return JSON.stringify({ success: true, message: _('aiToolRecommendationsCleared') });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolRecommendationsClearError', error.message) });
}
}
async findLocalContent(title, type) {
const movieEntries = await getFromDB('movies');
const seriesEntries = await getFromDB('series');
const allContent = [
...movieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))),
...seriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' })))
];
const searchTerm = title.toLowerCase().trim();
return allContent.find(item => item.title.toLowerCase() === searchTerm && (!type || item.type === type));
}
async findTmdbContent(title, type) {
try {
const searchResults = await fetchTMDB(`search/${type}?query=${encodeURIComponent(title)}`);
if (searchResults && searchResults.results.length > 0) {
const item = searchResults.results[0];
return { id: item.id, type };
}
return null;
} catch (error) {
return null;
}
}
async getGenreId(genreName, type) {
if (!this.genreCache[type]) {
try {
const data = await fetchTMDB(`genre/${type}/list`);
this.genreCache[type] = data.genres;
} catch (error) {
return null;
}
}
const genre = this.genreCache[type].find(g => g.name.toLowerCase() === genreName.toLowerCase());
return genre ? genre.id : null;
}
async executeTool(toolCall) {
const functionName = toolCall.function.name;
const args = toolCall.function.arguments;
if (typeof this[functionName] === 'function') {
this.chat.addTypingIndicator();
try {
const result = await this[functionName](args);
return result;
} catch (error) {
const errorMessage = _('aiToolExecutionError', [functionName, error.message]);
this.chat.addMessage(errorMessage, 'tool-result', true, functionName);
return JSON.stringify({ success: false, error: errorMessage });
} finally {
this.chat.removeTypingIndicator();
}
} else {
const errorMessage = _('aiToolUnknown', functionName);
this.chat.addMessage(errorMessage, 'tool-result', true, functionName);
return JSON.stringify({ success: false, error: errorMessage });
}
}
}

271
js/api.js
View File

@ -4,23 +4,31 @@ import { fetchWithTimeout } from './utils.js';
import { getFromDB } from './db.js';
import { _ } from './utils.js';
export async function fetchTMDB(endpoint, signal) {
let tmdbLang = 'en-US';
const langMap = {
'es': 'es-ES',
'en': 'en-US',
'fr': 'fr-FR',
'de': 'de-DE',
'it': 'it-IT',
'pt': 'pt-BR'
};
export async function fetchTMDB(endpoint, params = {}, signal) {
const region = state.settings.watchRegion || 'US';
const lang = state.settings.language || 'en';
if (langMap[state.settings.language]) {
tmdbLang = langMap[state.settings.language];
const [path, existingQuery] = endpoint.split('?');
const finalParams = new URLSearchParams(existingQuery);
finalParams.set('api_key', state.settings.apiKey);
finalParams.set('language', lang);
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.maxScore) finalParams.set('vote_average.lte', params.maxScore);
if (params.minDuration) finalParams.set('with_runtime.gte', params.minDuration);
if (params.maxDuration) finalParams.set('with_runtime.lte', params.maxDuration);
for (const [key, value] of Object.entries(params)) {
if (value) {
finalParams.set(key, value);
}
}
const separator = endpoint.includes('?') ? '&' : '?';
const url = `https://api.themoviedb.org/3/${endpoint}${separator}language=${tmdbLang}&api_key=${state.settings.apiKey}`;
const url = `https://api.themoviedb.org/3/${path}?${finalParams.toString()}`;
const response = await fetch(url, { signal });
if (!response.ok) {
const errorData = await response.json().catch(() => ({ status_message: "Unknown error" }));
@ -29,12 +37,23 @@ export async function fetchTMDB(endpoint, signal) {
return response.json();
}
export async function fetchPlexSessions(server) {
const { protocolo, ip, puerto, token } = server;
const url = `${protocolo}://${ip}:${puerto}/status/sessions?X-Plex-Token=${token}`;
const response = await fetchWithTimeout(url, { headers: { 'Accept': 'application/json' } }, 8000);
if (!response.ok) {
throw new Error(`Error ${response.status}`);
}
const data = await response.json();
return data.MediaContainer.Metadata || [];
}
export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId) {
const url = `${protocolo}://${ip}:${puerto}/library/metadata/${artistaId}/allLeaves?X-Plex-Token=${token}`;
try {
const response = await fetchWithTimeout(url, {}, 15000);
if (!response.ok) throw new Error(`Failed to fetch tracks: ${response.status}`);
const data = await response.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(data, "text/xml");
@ -46,11 +65,11 @@ export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artista
const fileKey = part.getAttribute("key");
const fileUrl = `${protocolo}://${ip}:${puerto}${fileKey}?X-Plex-Token=${token}`;
const thumb = track.getAttribute("thumb");
const parentThumb = track.getAttribute("parentThumb");
const grandparentThumb = track.getAttribute("grandparentThumb");
let coverUrl = 'img/no-poster.png';
if (thumb) {
coverUrl = `${protocolo}://${ip}:${puerto}${thumb}?X-Plex-Token=${token}`;
@ -72,10 +91,11 @@ export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artista
year: track.getAttribute("parentYear") || track.getAttribute("year"),
genre: Array.from(track.querySelectorAll("Genre")).map(g => g.getAttribute('tag')).join(', ') || '',
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)
};
}).filter(track => track !== null);
tracks.sort((a, b) => {
if (a.albumIndex !== b.albumIndex) {
return a.albumIndex - b.albumIndex;
@ -91,7 +111,63 @@ export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artista
}
}
export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
export async function getMusicUrlsFromJellyfin(serverUrl, userId, token, artistId) {
try {
const albumsUrl = `${serverUrl}/Users/${userId}/Items?ParentId=${artistId}&IncludeItemTypes=MusicAlbum&Recursive=true&Fields=ImageTags`;
const albumsResponse = await fetch(albumsUrl, { headers: { 'X-Emby-Token': token } });
if (!albumsResponse.ok) throw new Error(`Failed to fetch albums: ${albumsResponse.status}`);
const albumsData = await albumsResponse.json();
let allTracks = [];
for (const album of albumsData.Items) {
const songsUrl = `${serverUrl}/Users/${userId}/Items?ParentId=${album.Id}&IncludeItemTypes=Audio&Recursive=true&Fields=MediaSources,ImageTags`;
const songsResponse = await fetch(songsUrl, { headers: { 'X-Emby-Token': token } });
if (!songsResponse.ok) continue;
const songsData = await songsResponse.json();
const albumTracks = songsData.Items.map(track => {
const source = track.MediaSources?.[0];
if (!source) return null;
const streamUrl = `${serverUrl}/Audio/${track.Id}/stream?api_key=${token}&static=true`;
const coverUrl = album.ImageTags?.Primary ? `${serverUrl}/Items/${album.Id}/Images/Primary?tag=${album.ImageTags.Primary}` : 'img/no-poster.png';
return {
url: streamUrl,
titulo: track.Name || 'Pista desconocida',
album: album.Name || 'Álbum desconocido',
artista: track.AlbumArtist || 'Artista desconocido',
cover: coverUrl,
extension: source.Container || "mp3",
id: track.Id,
artistId: artistId,
year: album.ProductionYear,
genre: track.Genres?.join(', ') || '',
index: track.IndexNumber || 0,
albumIndex: album.IndexNumber || 0,
trackIndex: track.IndexNumber || 0
};
}).filter(track => track !== null);
allTracks.push(...albumTracks);
}
allTracks.sort((a, b) => {
if (a.albumIndex !== b.albumIndex) {
return a.albumIndex - b.albumIndex;
}
return a.index - b.index;
});
return allTracks;
} catch (error) {
console.error("Error in getMusicUrlsFromJellyfin:", error);
throw error;
}
}
export async function fetchAllStreamsFromPlex(busqueda, tipoContenido, year = null) {
if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') };
if (!state.db) return { success: false, streams: [], message: _('dbUnavailableForStreams') };
@ -117,10 +193,23 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
if (tipoContenido === 'movie') {
const videos = Array.from(xml.querySelectorAll("Video"));
let videosToProcess = videos;
const exactMatch = videos.find(v => v.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase());
if (exactMatch) {
videosToProcess = [exactMatch];
let videosToProcess = [];
if (year) {
const exactMatch = videos.find(v =>
v.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase() &&
v.getAttribute('year') == year
);
if (exactMatch) {
videosToProcess = [exactMatch];
}
} else {
const exactMatch = videos.find(v => v.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase());
if (exactMatch) {
videosToProcess = [exactMatch];
} else {
videosToProcess = videos;
}
}
videosToProcess.forEach(video => {
@ -139,30 +228,42 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
});
}
});
} else {
} else {
const directories = Array.from(xml.querySelectorAll('Directory[type="show"]'));
let directoryToProcess = directories.find(d => d.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase());
if (!directoryToProcess && directories.length > 0) {
directoryToProcess = directories[0];
let directoryToProcess;
if(year){
directoryToProcess = directories.find(d =>
d.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase() &&
d.getAttribute('year') == year
);
}
if (!directoryToProcess) {
directoryToProcess = directories.find(d => d.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase());
}
if (!directoryToProcess && directories.length > 0) {
directoryToProcess = directories[0];
}
if (directoryToProcess && directoryToProcess.getAttribute("ratingKey")) {
const serieKey = directoryToProcess.getAttribute("ratingKey");
const serieTitulo = directoryToProcess.getAttribute("title") || busqueda;
const serieYear = directoryToProcess.getAttribute("year");
const leavesUrl = `${protocolo}://${ip}:${puerto}/library/metadata/${serieKey}/allLeaves?X-Plex-Token=${token}`;
const leavesResponse = await fetchWithTimeout(leavesUrl, { headers: { 'Accept': 'application/xml' } });
if (leavesResponse.ok) {
const leavesData = await leavesResponse.text();
const leavesXml = parser.parseFromString(leavesData, "text/xml");
if (!leavesXml.querySelector('parsererror')) {
const episodes = Array.from(leavesXml.querySelectorAll("Video"));
episodes.sort((a,b) => {
episodes.sort((a, b) => {
const seasonA = parseInt(a.getAttribute("parentIndex") || 0, 10);
const seasonB = parseInt(b.getAttribute("parentIndex") || 0, 10);
if(seasonA !== seasonB) return seasonA - seasonB;
if (seasonA !== seasonB) return seasonA - seasonB;
const episodeA = parseInt(a.getAttribute("index") || 0, 10);
const episodeB = parseInt(b.getAttribute("index") || 0, 10);
return episodeA - episodeB;
@ -212,7 +313,7 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
}
if (tipoContenido === 'movie') {
uniqueStreams.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
uniqueStreams.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
}
if (uniqueStreams.length > 0) {
@ -220,4 +321,108 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
} else {
return { success: false, streams: [], message: _('notFoundOnServers', busqueda) };
}
}
export async function fetchAllStreamsFromJellyfin(busqueda, tipoContenido) {
if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') };
const { url, userId, apiKey } = state.jellyfinSettings;
if (!url || !userId || !apiKey) return { success: false, streams: [], message: _('noJellyfinCredentials') };
const jellyfinSearchType = tipoContenido === 'movie' ? 'Movie' : 'Series';
const searchUrl = `${url}/Users/${userId}/Items?searchTerm=${encodeURIComponent(busqueda)}&IncludeItemTypes=${jellyfinSearchType}&Recursive=true`;
try {
const response = await fetch(searchUrl, { headers: { 'X-Emby-Token': apiKey } });
if (!response.ok) throw new Error(`Error buscando en Jellyfin: ${response.status}`);
const searchData = await response.json();
if (!searchData.Items || searchData.Items.length === 0) {
return { success: false, streams: [], message: _('notFoundOnJellyfin', busqueda) };
}
const item = searchData.Items.find(i => i.Name.toLowerCase() === busqueda.toLowerCase()) || searchData.Items[0];
const itemId = item.Id;
const itemName = item.Name;
const itemYear = item.ProductionYear;
const posterTag = item.ImageTags?.Primary;
const posterUrl = posterTag ? `${url}/Items/${itemId}/Images/Primary?tag=${posterTag}` : '';
let streams = [];
if (item.Type === 'Movie') {
const streamUrl = `${url}/Videos/${itemId}/stream?api_key=${apiKey}`;
const extinfName = `${itemName}${itemYear ? ` (${itemYear})` : ''}`;
const groupTitle = extinfName.replace(/"/g, "'");
streams.push({
url: streamUrl,
title: extinfName,
extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${posterUrl}" group-title="${groupTitle}",${extinfName}`
});
} else if (item.Type === 'Series') {
const episodesUrl = `${url}/Shows/${itemId}/Episodes?userId=${userId}`;
const episodesResponse = await fetch(episodesUrl, { headers: { 'X-Emby-Token': apiKey } });
if (!episodesResponse.ok) throw new Error(`Error obteniendo episodios: ${episodesResponse.status}`);
const episodesData = await episodesResponse.json();
const sortedEpisodes = episodesData.Items.sort((a, b) => {
if (a.ParentIndexNumber !== b.ParentIndexNumber) return (a.ParentIndexNumber || 0) - (b.ParentIndexNumber || 0);
return (a.IndexNumber || 0) - (b.IndexNumber || 0);
});
sortedEpisodes.forEach(ep => {
const streamUrl = `${url}/Videos/${ep.Id}/stream?api_key=${apiKey}`;
const seasonNum = ep.ParentIndexNumber || 'S';
const episodeNum = ep.IndexNumber || 'E';
const episodeTitle = ep.Name || 'Episodio';
const groupTitle = `${itemName} - Temporada ${seasonNum}`.replace(/"/g, "'");
const extinfName = `${itemName} T${seasonNum}E${episodeNum} ${episodeTitle}`;
streams.push({
url: streamUrl,
title: extinfName,
extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${posterUrl}" group-title="${groupTitle}",${extinfName}`
});
});
}
return { success: true, streams };
} catch (error) {
console.error("Error fetching streams from Jellyfin:", error);
return { success: false, streams: [], message: error.message };
}
}
export async function fetchAllAvailableStreams(title, type, year = null) {
const plexPromise = fetchAllStreamsFromPlex(title, type, year);
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]);
let allStreams = [];
const errorMessages = [];
results.forEach((result, index) => {
const sourceName = index === 0 ? 'Plex' : 'Jellyfin';
if (result.status === 'fulfilled' && result.value.success) {
allStreams.push(...result.value.streams);
} else if (result.status === 'fulfilled' && !result.value.success) {
if (result.value.message !== _('noPlexServersForStreams') && result.value.message !== _('noJellyfinCredentials')) {
errorMessages.push(`${sourceName}: ${result.value.message}`);
}
} else if (result.status === 'rejected') {
errorMessages.push(`${sourceName}: ${result.reason.message}`);
}
});
const uniqueStreamsMap = new Map(allStreams.map(stream => [stream.url, stream]));
const uniqueStreams = Array.from(uniqueStreamsMap.values());
if (uniqueStreams.length > 0) {
return { success: true, streams: uniqueStreams, message: `Found ${uniqueStreams.length} streams.` };
} else {
return { success: false, streams: [], message: errorMessages.join('; ') || _('notFoundOnAnyServer', title) };
}
}

307
js/chat.js Normal file
View File

@ -0,0 +1,307 @@
import { state } from './state.js';
import { showNotification, _ } from './utils.js';
import { AITools } from './ai-tools.js';
export class Chat {
constructor() {
this.dom = {
fab: document.getElementById('chat-fab'),
window: document.getElementById('chat-window'),
header: document.querySelector('.chat-header'),
closeBtn: document.getElementById('chat-close-btn'),
messagesContainer: document.getElementById('chat-messages'),
inputForm: document.getElementById('chat-input-form'),
input: document.getElementById('chat-input'),
sendBtn: document.getElementById('chat-send-btn')
};
this.isOpen = false;
this.isDragging = false;
this.offset = { x: 0, y: 0 };
this.conversationHistory = [];
this.aiTools = new AITools(this);
this.bindEvents();
}
bindEvents() {
this.dom.fab.addEventListener('click', () => this.toggle());
this.dom.closeBtn.addEventListener('click', () => this.close());
this.dom.inputForm.addEventListener('submit', (e) => {
e.preventDefault();
this.sendMessage();
});
this.dom.input.addEventListener('input', this.autoResizeTextarea.bind(this));
this.dom.input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
this.dom.header.addEventListener('mousedown', this.startDrag.bind(this));
document.addEventListener('mousemove', this.drag.bind(this));
document.addEventListener('mouseup', this.stopDrag.bind(this));
document.addEventListener('mouseleave', this.stopDrag.bind(this));
}
toggle() {
this.isOpen ? this.close() : this.open();
}
open() {
if (this.isOpen) return;
this.isOpen = true;
this.dom.window.style.top = '';
this.dom.window.style.left = '';
this.dom.window.style.bottom = '95px';
this.dom.window.style.right = '2rem';
this.dom.window.style.display = 'flex';
gsap.fromTo(this.dom.window, { opacity: 0, scale: 0.9, y: 20 }, { opacity: 1, scale: 1, y: 0, duration: 0.3, ease: 'power3.out' });
gsap.to(this.dom.fab, { scale: 0, opacity: 0, duration: 0.2, ease: 'power2.in' });
if (this.conversationHistory.length === 0) {
const welcomeMessage = _('chatWelcome');
this.addMessage(welcomeMessage, 'assistant');
this.conversationHistory.push({ role: 'model', parts: [{ text: welcomeMessage }] });
}
}
close() {
if (!this.isOpen) return;
this.isOpen = false;
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';
}});
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() {
const userInput = this.dom.input.value.trim();
if (!userInput) return;
this.addMessage(userInput, 'user');
this.conversationHistory.push({ role: 'user', parts: [{ text: userInput }] });
this.dom.input.value = '';
this.autoResizeTextarea();
this.dom.sendBtn.disabled = true;
this.addTypingIndicator();
try {
const response = await this.getAIResponseWithTools();
this.removeTypingIndicator();
if (response) {
this.addMessage(response, 'assistant');
this.conversationHistory.push({ role: 'model', parts: [{ text: response }] });
}
} catch (error) {
this.removeTypingIndicator();
this.addMessage(error.message, 'assistant', true);
} finally {
this.dom.sendBtn.disabled = false;
}
}
addMessage(text, sender, isError = false, toolName = null) {
const wrapper = document.createElement('div');
wrapper.className = `message-wrapper ${sender}-wrapper`;
const messageEl = document.createElement('div');
messageEl.classList.add('message', `${sender}-message`);
if(isError) messageEl.style.color = 'var(--danger)';
if (sender === 'assistant' || sender === 'tool-call' || sender === 'tool-result') {
const avatar = document.createElement('div');
avatar.className = 'avatar';
let icon = '';
if (sender === 'assistant') {
icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm-2.5-5h5v2h-5v-2z"/></svg>';
} else {
icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12-.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/></svg>';
}
avatar.innerHTML = icon;
wrapper.appendChild(avatar);
}
const p = document.createElement('p');
if (sender === 'tool-call' || sender === 'tool-result') {
p.innerHTML = `<strong>${toolName}:</strong> ${text}`;
} else {
p.textContent = text;
}
messageEl.appendChild(p);
wrapper.appendChild(messageEl);
this.dom.messagesContainer.appendChild(wrapper);
this.scrollToBottom();
}
addTypingIndicator() {
const wrapper = document.createElement('div');
wrapper.id = 'typing-indicator';
wrapper.className = 'message-wrapper assistant-wrapper';
const avatar = document.createElement('div');
avatar.className = 'avatar';
avatar.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm-2.5-5h5v2h-5v-2z"/></svg>`;
wrapper.appendChild(avatar);
const indicator = document.createElement('div');
indicator.classList.add('message', 'assistant-message', 'typing-indicator-bubble');
indicator.innerHTML = '<span></span><span></span><span></span>';
wrapper.appendChild(indicator);
this.dom.messagesContainer.appendChild(wrapper);
this.scrollToBottom();
}
removeTypingIndicator() {
const indicator = document.getElementById('typing-indicator');
if (indicator) indicator.remove();
}
async getAIResponseWithTools() {
const apiKey = state.settings.googleApiKey;
if (!apiKey) {
return _('chatGoogleApiKeyMissing');
}
const systemPrompt = _('aiSystemPrompt_v3');
const tools = [{
functionDeclarations: this.aiTools.toolDefinitions
}];
const model = "gemini-2.5-flash";
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': apiKey
},
body: JSON.stringify({
contents: this.conversationHistory,
tools: tools,
system_instruction: {
parts: [{ text: systemPrompt }]
}
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = errorData?.error?.message || `Error HTTP ${response.status}`;
throw new Error(errorMessage);
}
const data = await response.json();
if (!data.candidates || !data.candidates.length || !data.candidates[0].content || !data.candidates[0].content.parts) {
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) {
console.error('Fallo en la llamada a la API de Google AI:', error);
const errorMessage = _('chatApiError') + `: ${error.message}`;
showNotification(errorMessage, 'error');
return errorMessage;
}
}
autoResizeTextarea() {
this.dom.input.style.height = 'auto';
const scrollHeight = this.dom.input.scrollHeight;
if (scrollHeight > 200) {
this.dom.input.style.height = '200px';
this.dom.input.style.overflowY = 'auto';
} else {
this.dom.input.style.height = `${scrollHeight}px`;
this.dom.input.style.overflowY = 'hidden';
}
}
scrollToBottom() {
this.dom.messagesContainer.scrollTop = this.dom.messagesContainer.scrollHeight;
}
startDrag(e) {
if (e.target !== this.dom.header && e.target !== this.dom.header.querySelector('.chat-title')) return;
this.isDragging = true;
const rect = this.dom.window.getBoundingClientRect();
this.offset.x = e.clientX - rect.left;
this.offset.y = e.clientY - rect.top;
this.dom.header.style.cursor = 'grabbing';
}
drag(e) {
if (!this.isDragging) return;
e.preventDefault();
let newX = e.clientX - this.offset.x;
let newY = e.clientY - this.offset.y;
const winWidth = this.dom.window.offsetWidth;
const winHeight = this.dom.window.offsetHeight;
const docWidth = document.documentElement.clientWidth;
const docHeight = document.documentElement.clientHeight;
newX = Math.max(0, Math.min(newX, docWidth - winWidth));
newY = Math.max(0, Math.min(newY, docHeight - winHeight));
this.dom.window.style.left = `${newX}px`;
this.dom.window.style.top = `${newY}px`;
this.dom.window.style.bottom = 'auto';
this.dom.window.style.right = 'auto';
}
stopDrag() {
this.isDragging = false;
this.dom.header.style.cursor = 'move';
}
clearHistory() {
this.conversationHistory = [];
this.dom.messagesContainer.innerHTML = '';
const welcomeMessage = _('chatWelcome');
this.addMessage(welcomeMessage, 'assistant');
this.conversationHistory.push({ role: 'model', parts: [{ text: welcomeMessage }] });
}
}

View File

@ -1,5 +1,5 @@
export const config = {
defaultApiKey: '4e44d9029b1270a757cddc766a1bcb63',
dbName: 'PlexDB',
dbVersion: 6,
dbVersion: 9,
};

View File

@ -13,14 +13,17 @@ export function initDB() {
request.onupgradeneeded = e => {
state.db = e.target.result;
const transaction = e.target.transaction;
const storesToCreate = ['movies', 'series', 'artists', 'photos', 'tokens', 'conexiones_locales', 'settings'];
const storesToCreate = ['movies', 'series', 'artists', 'photos', 'tokens', 'conexiones_locales', 'settings', 'jellyfin_settings', 'jellyfin_movies', 'jellyfin_series'];
storesToCreate.forEach(storeName => {
if (!state.db.objectStoreNames.contains(storeName)) {
let storeOptions;
if (storeName === 'settings') {
if (['settings', 'jellyfin_settings'].includes(storeName)) {
storeOptions = { keyPath: 'id' };
} else {
} else if (['jellyfin_movies', 'jellyfin_series'].includes(storeName)) {
storeOptions = { keyPath: 'libraryId' };
}
else {
storeOptions = { keyPath: 'id', autoIncrement: true };
}
const store = state.db.createObjectStore(storeName, storeOptions);
@ -126,7 +129,7 @@ export function addItemsToStore(storeName, items) {
export async function clearContentData() {
showNotification(_("deletingContentData"), "info");
mostrarSpinner();
const storesToDelete = ['movies', 'series', 'artists', 'photos', 'conexiones_locales'];
const storesToDelete = ['movies', 'series', 'artists', 'photos', 'conexiones_locales', 'jellyfin_movies', 'jellyfin_series'];
try {
if (!state.db) throw new Error(_("dbNotAvailable"));
const storesPresent = storesToDelete.filter(name => state.db.objectStoreNames.contains(name));
@ -262,4 +265,30 @@ export async function importDatabase(file) {
}
};
reader.readAsText(file);
}
export async function getServers() {
const plexConnections = await getFromDB('conexiones_locales');
const jellyfinConnections = await getFromDB('jellyfin_settings');
const plexServers = plexConnections.map(conn => ({
id: conn.id,
name: conn.nombre,
type: 'plex',
accessToken: conn.token,
publicUrl: conn.protocolo + '://' + conn.ip + ':' + conn.puerto,
localUrl: conn.protocolo + '://' + conn.ip + ':' + conn.puerto,
}));
const jellyfinServers = jellyfinConnections.map(conn => ({
id: conn.id,
name: conn.username || 'Jellyfin Server',
type: 'jellyfin',
accessToken: conn.apiKey,
userId: conn.userId,
publicUrl: conn.url,
localUrl: conn.url,
}));
return [...plexServers, ...jellyfinServers];
}

View File

@ -1,13 +1,15 @@
import { state } from './state.js';
import { switchView, resetView, showMainView, showItemDetails, 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 } from './ui.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 { loadProviderContent, changeProviderPage, backToProviders } from './providers.js';
import { debounce, showNotification, _ } from './utils.js';
import { clearContentData, loadTokensToEditor, saveTokensFromEditor, exportDatabase, importDatabase } from './db.js';
import { startPlexScan } from './plex.js';
import { startJellyfinScan } from './jellyfin.js';
import { Equalizer } from './equalizer.js';
async function handleDatabaseUpdate() {
showNotification(_('updatingView'), "info", 2000);
await loadLocalContent();
await loadLocalContent();
switch(state.currentView) {
case 'stats':
@ -28,18 +30,30 @@ async function handleDatabaseUpdate() {
}
export function setupEventListeners() {
const savedSidebarState = localStorage.getItem('sidebarCollapsed');
if (savedSidebarState === 'true') {
document.body.classList.add('sidebar-collapsed');
} else {
document.body.classList.remove('sidebar-collapsed');
}
document.getElementById('sidebar-toggle').addEventListener('click', () => {
document.getElementById('sidebar-nav').classList.toggle('open');
document.getElementById('main-container').classList.toggle('sidebar-open');
if (window.innerWidth < 992) {
document.body.classList.toggle('sidebar-open');
} else {
document.body.classList.toggle('sidebar-collapsed');
localStorage.setItem('sidebarCollapsed', document.body.classList.contains('sidebar-collapsed'));
}
});
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-providers').addEventListener('click', (e) => { e.preventDefault(); switchView('providers'); });
document.getElementById('nav-photos').addEventListener('click', (e) => { e.preventDefault(); switchView('photos'); });
document.getElementById('nav-stats').addEventListener('click', (e) => { e.preventDefault(); switchView('stats'); });
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-recommendations').addEventListener('click', (e) => { e.preventDefault(); switchView('recommendations'); });
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('footer-logo-btn').addEventListener('click', (e) => { e.preventDefault(); resetView(); });
@ -48,6 +62,8 @@ export function setupEventListeners() {
document.getElementById('footer-stats').addEventListener('click', (e) => { e.preventDefault(); switchView('stats'); });
document.getElementById('footer-favorites').addEventListener('click', (e) => { e.preventDefault(); switchView('favorites'); });
document.getElementById('activity-viewer-btn').addEventListener('click', () => state.activityViewer.show());
document.getElementById('load-more').addEventListener('click', () => {
if (!state.isLoading) {
state.currentPage++;
@ -58,25 +74,118 @@ export function setupEventListeners() {
document.getElementById('search-input').addEventListener('keyup', debounce(async (e) => {
const query = e.target.value.trim();
if (query === state.currentParams.query && state.currentView === 'search') return;
state.currentView = 'search';
if (!query) {
resetView();
return;
}
if (state.currentView !== 'search') {
switchView('search');
}
state.currentParams.query = query;
state.currentPage = 1;
if (!query) {
switchView(state.currentParams.contentType === 'movie' ? 'movies' : 'series');
} else {
updateSectionTitle();
await loadContent();
}
updateSectionTitle();
await loadContent();
}, 400));
document.getElementById('genre-filter').addEventListener('change', applyFilters);
document.getElementById('year-filter').addEventListener('change', applyFilters);
document.getElementById('sort-filter').addEventListener('change', applyFilters);
// Filter Popover Logic
const durationBtn = document.getElementById('duration-filter-btn');
const scoreBtn = document.getElementById('score-filter-btn');
const durationPopover = document.getElementById('duration-popover');
const scorePopover = document.getElementById('score-popover');
const setupPopover = (button, popover) => {
if (!button || !popover) return;
button.addEventListener('click', (event) => {
event.stopPropagation();
const isVisible = popover.style.display === 'block';
// Close all popovers
document.querySelectorAll('.filter-popover').forEach(p => {
if (p !== popover) p.style.display = 'none';
});
// Toggle current popover
popover.style.display = isVisible ? 'none' : 'block';
});
};
setupPopover(durationBtn, durationPopover);
setupPopover(scoreBtn, scorePopover);
// Close popovers when clicking outside
window.addEventListener('click', (event) => {
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');
}
});
// Range Slider Logic
function setupRangeSlider(minId, maxId, fillId, minValueId, maxValueId) {
const minSlider = document.getElementById(minId);
const maxSlider = document.getElementById(maxId);
const fill = document.getElementById(fillId);
const minValueDisplay = document.getElementById(minValueId);
const maxValueDisplay = document.getElementById(maxValueId);
if (!minSlider || !maxSlider || !fill || !minValueDisplay || !maxValueDisplay) return;
const updateFill = () => {
const min = parseFloat(minSlider.value);
const max = parseFloat(maxSlider.value);
const range = parseFloat(minSlider.max) - parseFloat(minSlider.min);
const left = ((min - parseFloat(minSlider.min)) / range) * 100;
const width = ((max - min) / range) * 100;
fill.style.left = `${left}%`;
fill.style.width = `${width}%`;
minValueDisplay.textContent = minSlider.value;
maxValueDisplay.textContent = maxSlider.value;
};
minSlider.addEventListener('input', () => {
if (parseFloat(minSlider.value) > parseFloat(maxSlider.value)) {
minSlider.value = maxSlider.value;
}
updateFill();
});
maxSlider.addEventListener('input', () => {
if (parseFloat(maxSlider.value) < parseFloat(minSlider.value)) {
maxSlider.value = minSlider.value;
}
updateFill();
});
minSlider.addEventListener('change', applyFilters);
maxSlider.addEventListener('change', applyFilters);
updateFill(); // Initial call
}
setupRangeSlider('duration-min', 'duration-max', 'duration-fill', 'duration-min-value', 'duration-max-value');
setupRangeSlider('score-min', 'score-max', 'score-fill', 'score-min-value', 'score-max-value');
document.getElementById('stats-token-filter').addEventListener('change', generateStatistics);
document.getElementById('photos-token-select').addEventListener('change', handlePhotoTokenChange);
document.getElementById('region-filter').addEventListener('change', saveSettings);
document.getElementById('providers-grid').addEventListener('click', (e) => {
const providerCard = e.target.closest('.provider-card');
if (providerCard) {
const providerId = providerCard.dataset.providerId;
const providerName = providerCard.dataset.providerName;
loadProviderContent(providerId, providerName, 1);
}
});
document.getElementById('back-to-providers').addEventListener('click', backToProviders);
document.getElementById('prev-page').addEventListener('click', () => changeProviderPage(-1));
document.getElementById('next-page').addEventListener('click', () => changeProviderPage(1));
document.querySelector('.back-button').addEventListener('click', showMainView);
document.getElementById('main-view').addEventListener('click', handleMainViewClick);
document.getElementById('item-details-view').addEventListener('click', handleDetailsClick);
@ -109,6 +218,8 @@ export function setupEventListeners() {
}
});
document.getElementById('jellyfinScanBtn').addEventListener('click', startJellyfinScan);
document.getElementById('clearDataBtn').addEventListener('click', () => {
if (confirm(_('confirmClearContent'))) {
clearContentData();
@ -144,6 +255,22 @@ export function setupEventListeners() {
}
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
const heroSection = document.getElementById('hero-section');
const bg1 = document.querySelector('.hero-background-1');
if (heroSection && heroSection.style.display !== 'none' && state.currentView === 'home' && state.heroIntervalId) {
const isBgVisible = (bg1.style.backgroundImage && bg1.style.backgroundImage !== 'none') ||
(document.querySelector('.hero-background-2').style.backgroundImage && document.querySelector('.hero-background-2').style.backgroundImage !== 'none');
if (!isBgVisible || bg1.style.opacity === '0' && document.querySelector('.hero-background-2').style.opacity === '0') {
console.log('Hero background missing on visibility change, re-initializing.');
initializeHeroSection();
}
}
}
});
window.addEventListener('indexedDBUpdated', handleDatabaseUpdate);
const eqBtn = document.getElementById('eqBtn');
@ -235,26 +362,38 @@ function handleMainViewClick(e) {
handlePhotoGridClick(photoCard);
return;
}
const card = e.target.closest('.item-card');
if (!card) return;
state.lastClickedCardElement = card;
const itemCard = e.target.closest('.item-card, .provider-item-card');
if (!itemCard) return;
state.lastClickedCardElement = itemCard;
const actionBtn = e.target.closest('.action-btn');
if (actionBtn) {
e.stopPropagation();
const { id, type } = card.dataset;
const title = card.querySelector('.item-title')?.textContent;
if (actionBtn.classList.contains('info-btn')) showItemDetails(Number(id), type);
const { id, type, year, name } = itemCard.dataset;
const title = itemCard.querySelector('.item-title')?.textContent;
if (actionBtn.classList.contains('info-btn')) {
if (type === 'person') {
searchByActor(id, name);
} else {
showItemDetails(Number(id), type);
}
}
else if (actionBtn.classList.contains('favorites-btn')) toggleFavorite(Number(id), type);
else if (actionBtn.classList.contains('play-btn')) addStreamToList(title, type, actionBtn);
else if (actionBtn.classList.contains('download-btn')) downloadM3U(title, type, actionBtn);
else if (actionBtn.classList.contains('play-btn')) addStreamToList(title, type, year, actionBtn);
else if (actionBtn.classList.contains('download-btn')) downloadM3U([{ title, type, year }], actionBtn);
return;
}
const { id, type } = card.dataset;
if (id && type) showItemDetails(Number(id), type);
const { id, type } = itemCard.dataset;
if (id && type) {
if (type === 'person') {
showActorDetails(Number(id));
} else {
showItemDetails(Number(id), type);
}
}
}
async function handleHistoryClick(e, historyItem) {
@ -305,8 +444,7 @@ async function handleDetailsClick(e) {
const castCard = e.target.closest('.cast-card');
if (castCard) {
const { actorId } = castCard.dataset;
const actorName = castCard.querySelector('.cast-name').textContent;
if (actorId && actorName) await searchByActor(actorId, actorName);
if (actorId) showActorDetails(Number(actorId));
return;
}
@ -330,4 +468,4 @@ async function handleDetailsClick(e) {
downloadM3U(title, type, downloadM3uBtn);
return;
}
}
}

266
js/jellyfin.js Normal file
View File

@ -0,0 +1,266 @@
import { state } from './state.js';
import { addItemsToStore, clearStore, getFromDB } from './db.js';
import { showNotification, _, emitirEventoActualizacion } from './utils.js';
async function authenticateJellyfin(url, username, password) {
const authUrl = `${url}/Users/AuthenticateByName`;
const body = JSON.stringify({
Username: username,
Pw: password
});
try {
const response = await fetch(authUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Emby-Authorization': 'MediaBrowser Client="CinePlex", Device="Chrome", DeviceId="cineplex-jellyfin-integration", Version="1.0"'
},
body: body
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.AuthenticationResult?.ErrorMessage || `Error ${response.status}`);
}
const data = await response.json();
return { success: true, token: data.AccessToken, userId: data.User.Id };
} catch (error) {
return { success: false, message: error.message };
}
}
export async function fetchLibraryViews(url, userId, apiKey) {
const viewsUrl = `${url}/Users/${userId}/Views`;
try {
const response = await fetch(viewsUrl, {
headers: {
'X-Emby-Token': apiKey
}
});
if (!response.ok) throw new Error(`Error ${response.status} fetching library views`);
const data = await response.json();
return { success: true, views: data.Items };
} catch (error) {
return { success: false, message: error.message };
}
}
export async function fetchItemsFromLibrary(url, userId, apiKey, library) {
const isMusic = library.CollectionType === 'music';
const itemTypes = isMusic ? 'MusicArtist' : 'Movie,Series';
const fields = isMusic ? 'ImageTags' : 'ProductionYear,RunTimeTicks,SeriesName,ParentIndexNumber,ImageTags';
const itemsUrl = `${url}/Users/${userId}/Items?ParentId=${library.Id}&recursive=true&fields=${fields}&includeItemTypes=${itemTypes}`;
try {
const response = await fetch(itemsUrl, {
headers: { 'X-Emby-Token': apiKey }
});
if (!response.ok) throw new Error(`Error ${response.status}`);
const data = await response.json();
const items = data.Items.map(item => {
const mediaStream = item.MediaStreams?.find(s => s.Type === 'Video');
const container = item.Container;
let resolution = null;
if (mediaStream && mediaStream.Height) {
if (mediaStream.Height >= 2160) resolution = '4k';
else if (mediaStream.Height >= 1080) resolution = '1080p';
else if (mediaStream.Height >= 720) resolution = '720p';
}
const baseItem = {
id: item.Id,
title: item.Name,
type: item.Type,
thumb: item.ImageTags?.Primary ? `${url}/Items/${item.Id}/Images/Primary?tag=${item.ImageTags.Primary}` : '',
container: container,
resolution: resolution
};
if (!isMusic) {
baseItem.year = item.ProductionYear;
baseItem.duration = item.RunTimeTicks;
baseItem.seriesTitle = item.SeriesName;
baseItem.seasonNumber = item.ParentIndexNumber;
}
return baseItem;
});
return { success: true, items, libraryName: library.Name, libraryId: library.Id };
} catch (error) {
return { success: false, message: error.message, libraryName: library.Name, libraryId: library.Id };
}
}
export async function startJellyfinScan() {
if (state.isScanningJellyfin) {
showNotification(_('jellyfinScanInProgress'), 'warning');
return;
}
state.isScanningJellyfin = true;
const statusDiv = document.getElementById('jellyfinScanStatus');
const scanBtn = document.getElementById('jellyfinScanBtn');
const originalBtnText = scanBtn.innerHTML;
scanBtn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${_('jellyfinScanning')}`;
scanBtn.disabled = true;
const urlInput = document.getElementById('jellyfinServerUrl');
const usernameInput = document.getElementById('jellyfinUsername');
const passwordInput = document.getElementById('jellyfinPassword');
let url = urlInput.value.trim();
const username = usernameInput.value.trim();
const password = passwordInput.value;
if (!url || !username) {
showNotification(_('jellyfinMissingCredentials'), 'error');
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
return;
}
url = url.replace(/\/web\/.*$/, '').replace(/\/$/, '');
statusDiv.innerHTML = `<div class="text-info">${_('jellyfinConnecting', url)}</div>`;
const authResult = await authenticateJellyfin(url, username, password);
if (!authResult.success) {
statusDiv.innerHTML = `<div class="text-danger">${_('jellyfinAuthFailed', authResult.message)}</div>`;
showNotification(_('jellyfinAuthFailed', authResult.message), 'error');
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
return;
}
statusDiv.innerHTML = `<div class="text-success">${_('jellyfinAuthSuccess')}</div><div class="text-info">${_('jellyfinFetchingLibraries')}</div>`;
const viewsResult = await fetchLibraryViews(url, authResult.userId, authResult.token);
if (!viewsResult.success) {
statusDiv.innerHTML += `<div class="text-danger">${_('jellyfinFetchFailed', viewsResult.message)}</div>`;
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
return;
}
const mediaLibraries = viewsResult.views.filter(v => v.CollectionType === 'movies' || v.CollectionType === 'tvshows' || v.CollectionType === 'music');
if (mediaLibraries.length === 0) {
statusDiv.innerHTML += `<div class="text-warning">${_('jellyfinNoMediaLibraries')}</div>`;
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
return;
}
statusDiv.innerHTML += `<div class="text-info">${_('jellyfinLibrariesFound', String(mediaLibraries.length))}</div>`;
await clearStore('jellyfin_movies');
await clearStore('jellyfin_series');
const allArtists = await getFromDB('artists') || [];
const plexArtists = allArtists.filter(a => !a.isJellyfin);
await clearStore('artists');
if (plexArtists.length > 0) {
await addItemsToStore('artists', plexArtists);
}
let totalMovies = 0;
let totalSeries = 0;
let totalMusic = 0;
const scanPromises = mediaLibraries.map(library =>
fetchItemsFromLibrary(url, authResult.userId, authResult.token, library)
);
const results = await Promise.allSettled(scanPromises);
const urlObject = new URL(url);
for (const result of results) {
if (result.status === 'fulfilled' && result.value.success) {
const library = mediaLibraries.find(lib => lib.Id === result.value.libraryId);
if (library) {
const collectionType = library.CollectionType;
let storeName;
if (collectionType === 'movies') storeName = 'jellyfin_movies';
else if (collectionType === 'tvshows') storeName = 'jellyfin_series';
else if (collectionType === 'music') storeName = 'artists';
if (storeName) {
const titulos = result.value.items;
if (storeName === 'artists') {
const jellyfinArtists = {
ip: urlObject.hostname,
puerto: urlObject.port || (urlObject.protocol === 'https:' ? '443' : '80'),
token: authResult.token,
protocolo: urlObject.protocol.replace(':', ''),
serverName: `Jellyfin - ${library.Name}`,
titulos: titulos.map(t => ({...t, isJellyfin: true, userId: authResult.userId, serverUrl: url})),
tokenPrincipal: authResult.token,
isJellyfin: true,
userId: authResult.userId,
serverUrl: url
};
if (titulos.length > 0) {
await addItemsToStore(storeName, [jellyfinArtists]);
}
} else {
const dbEntry = {
serverUrl: url,
libraryId: library.Id,
libraryName: library.Name,
titulos: titulos,
};
if (titulos.length > 0) {
await addItemsToStore(storeName, [dbEntry]);
}
}
if (collectionType === 'movies') totalMovies += titulos.length;
else if (collectionType === 'tvshows') totalSeries += titulos.length;
else if (collectionType === 'music') totalMusic += titulos.length;
statusDiv.innerHTML += `<div class="text-success-secondary">${_('jellyfinLibraryScanSuccess', [library.Name, String(titulos.length)])}</div>`;
}
}
} else {
const libraryName = result.reason?.libraryName || result.value?.libraryName || 'Unknown';
statusDiv.innerHTML += `<div class="text-warning">${_('jellyfinLibraryScanFailed', libraryName)}</div>`;
}
}
const newSettings = {
id: 'jellyfin_credentials',
url: url,
username: username,
password: password,
apiKey: authResult.token,
userId: authResult.userId
};
await addItemsToStore('jellyfin_settings', [newSettings]);
state.jellyfinSettings = newSettings;
const message = `Scan finished. Found ${totalMovies} movies, ${totalSeries} series, and ${totalMusic} music artists.`;
statusDiv.innerHTML += `<div class="text-success mt-2">${message}</div>`;
showNotification(message, 'success');
setTimeout(() => {
const modalInstance = bootstrap.Modal.getInstance(document.getElementById('settingsModal'));
if(modalInstance) modalInstance.hide();
emitirEventoActualizacion();
}, 2000);
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
}

223
js/m3u-generator.js Normal file
View File

@ -0,0 +1,223 @@
import { getServers } from './db.js';
import { fetchLibraries as fetchPlexLibraries, fetchLibraryContents as fetchPlexLibraryContents } from './plex.js';
import { fetchLibraryViews as fetchJellyfinLibraries, fetchItemsFromLibrary as fetchJellyfinLibraryContents } from './jellyfin.js';
import { showNotification, _ } from './utils.js';
document.addEventListener('DOMContentLoaded', () => {
const m3uServerSelect = document.getElementById('m3u-server-select');
const m3uLibrariesContainer = document.getElementById('m3u-libraries-container');
const m3uLibrariesLoader = document.getElementById('m3u-libraries-loader');
const m3uLibrariesStep = document.getElementById('m3u-libraries-step');
const m3uConfigPanel = document.querySelector('.m3u-config-panel');
const m3uInfoPanel = document.querySelector('.m3u-info-panel');
const downloadM3uBtn = document.getElementById('download-m3u-btn');
let isInitialized = false;
// GSAP animations
gsap.set([m3uConfigPanel, m3uInfoPanel], { autoAlpha: 0, y: 50 });
async function initializeM3uGenerator() {
if (isInitialized) return;
isInitialized = true;
gsap.to(m3uConfigPanel, { autoAlpha: 1, y: 0, duration: 0.8, ease: "power3.out" });
gsap.to(m3uInfoPanel, { autoAlpha: 1, y: 0, duration: 0.8, ease: "power3.out", delay: 0.2 });
await loadServers();
}
async function loadServers() {
try {
const servers = await getServers();
m3uServerSelect.innerHTML = `<option value="">${_('selectAServer')}</option>`;
servers.forEach(server => {
const option = document.createElement('option');
option.value = server.id;
option.textContent = server.name;
option.dataset.type = server.type;
m3uServerSelect.appendChild(option);
});
} catch (error) {
console.error('Error loading servers for M3U generator:', error);
}
}
m3uServerSelect.addEventListener('change', async () => {
const selectedOption = m3uServerSelect.options[m3uServerSelect.selectedIndex];
const serverId = selectedOption.value;
const serverType = selectedOption.dataset.type;
m3uLibrariesContainer.innerHTML = '';
downloadM3uBtn.disabled = true;
if (!serverId) {
gsap.to(m3uLibrariesStep, { autoAlpha: 0, height: 0, duration: 0.3, onComplete: () => m3uLibrariesStep.style.display = 'none' });
return;
}
m3uLibrariesStep.style.display = 'block';
gsap.fromTo(m3uLibrariesStep, { autoAlpha: 0, height: 0 }, { autoAlpha: 1, height: 'auto', duration: 0.5, ease: "power3.out" });
m3uLibrariesContainer.innerHTML = ''; // Clear previous libraries
m3uLibrariesLoader.style.display = 'block'; // Show loader
downloadM3uBtn.disabled = true;
try {
const servers = await getServers();
const server = servers.find(s => s.id == serverId);
if (!server) return;
let libraries = [];
if (serverType === 'plex') {
libraries = await fetchPlexLibraries(server.accessToken, server.publicUrl, server.localUrl);
} else if (serverType === 'jellyfin') {
const jellyfinLibraries = await fetchJellyfinLibraries(server.publicUrl, server.userId, server.accessToken);
if(jellyfinLibraries.success) {
libraries = jellyfinLibraries.views.map(lib => ({
key: lib.Id,
title: lib.Name,
type: lib.CollectionType === 'movies' ? 'movie' : (lib.CollectionType === 'tvshows' ? 'show' : 'music')
}));
}
}
const checkboxes = [];
libraries.forEach(library => {
if (library.type === 'movie' || library.type === 'show' || library.type === 'music') {
const checkbox = document.createElement('div');
checkbox.classList.add('form-check');
checkbox.innerHTML = `
<input class="form-check-input" type="checkbox" value="${library.key}" id="library-${library.key}" data-type="${library.type}">
<label class="form-check-label" for="library-${library.key}">
<i class="fas ${library.type === 'movie' ? 'fa-film' : (library.type === 'show' ? 'fa-tv' : 'fa-music')} me-2"></i>
${library.title}
</label>
`;
m3uLibrariesContainer.appendChild(checkbox);
checkboxes.push(checkbox);
// Make the entire form-check div clickable
checkbox.addEventListener('click', () => {
const input = checkbox.querySelector('.form-check-input');
input.checked = !input.checked;
// Update download button state
downloadM3uBtn.disabled = m3uLibrariesContainer.querySelectorAll('input:checked').length === 0;
});
}
});
gsap.from(checkboxes, { opacity: 0, y: 20, stagger: 0.05, duration: 0.3 });
downloadM3uBtn.disabled = m3uLibrariesContainer.querySelectorAll('input:checked').length === 0;
} catch (error) {
console.error('Error fetching libraries:', error);
showNotification('Error fetching libraries.', 'error');
} finally {
m3uLibrariesLoader.style.display = 'none'; // Hide loader
}
});
downloadM3uBtn.addEventListener('click', async () => {
const selectedOption = m3uServerSelect.options[m3uServerSelect.selectedIndex];
const serverId = selectedOption.value;
const serverType = selectedOption.dataset.type;
const selectedLibraries = Array.from(m3uLibrariesContainer.querySelectorAll('input:checked')).map(input => input.value);
if (!serverId || selectedLibraries.length === 0) {
showNotification('Please select a server and at least one library.', 'error');
return;
}
downloadM3uBtn.disabled = true;
downloadM3uBtn.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Generating...`;
try {
const servers = await getServers();
const server = servers.find(s => s.id == serverId);
if (!server) return;
let m3uContent = '#EXTM3U\n';
let hasErrors = false;
for (const libraryKey of selectedLibraries) {
try {
const libraryElement = m3uLibrariesContainer.querySelector(`#library-${libraryKey}`);
const libraryType = libraryElement.getAttribute('data-type');
const libraryTitle = libraryElement ? libraryElement.nextElementSibling.textContent.trim() : '';
let items = [];
if (serverType === 'plex') {
items = await fetchPlexLibraryContents(server.accessToken, server.publicUrl, server.localUrl, libraryKey, libraryType, 60000);
} else if (serverType === 'jellyfin') {
const jellyfinItems = await fetchJellyfinLibraryContents(server.publicUrl, server.userId, server.accessToken, { Id: libraryKey });
if(jellyfinItems.success) {
items = jellyfinItems.items.map(item => ({
title: item.title,
duration: item.duration ? item.duration / 10000 : -1,
seriesTitle: item.seriesTitle,
seasonNumber: item.seasonNumber,
thumb: item.thumb,
url: `${server.publicUrl}/Videos/${item.id}/stream?static=true&api_key=${server.accessToken}`
}));
}
}
items.forEach(item => {
const duration = item.duration ? Math.round(item.duration / 1000) : -1;
const groupTitle = item.seriesTitle && item.seasonNumber
? `group-title="${item.seriesTitle} - Season ${item.seasonNumber}"`
: `group-title="${libraryTitle}"`;
const tvgLogo = item.thumb ? `tvg-logo="${item.thumb}"` : '';
m3uContent += `#EXTINF:${duration} ${tvgLogo} ${groupTitle},${item.title}\n`;
m3uContent += `${item.url}\n`;
});
} catch (error) {
hasErrors = true;
console.error(`Error processing library ${libraryKey}:`, error);
showNotification(`Error processing library ${libraryKey}. Skipping.`, 'warning');
}
}
if (m3uContent.split('\n').length <= 2 && hasErrors) {
throw new Error("All selected libraries failed to process.");
}
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 = 'playlist.m3u';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
if (hasErrors) {
showNotification('M3U generated with some errors. Some libraries may be missing.', 'warning');
} else {
showNotification('M3U playlist downloaded successfully.', 'success');
}
} catch (error) {
console.error('Error generating M3U file:', error);
showNotification('Error generating M3U file.', 'error');
} finally {
downloadM3uBtn.disabled = false;
downloadM3uBtn.innerHTML = `<i class="fas fa-download"></i> __MSG_downloadM3u__`;
}
});
const navLink = document.getElementById('nav-m3u-generator');
if (navLink) {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.attributeName === 'class' && mutation.target.classList.contains('active')) {
initializeM3uGenerator();
}
});
});
observer.observe(navLink, { attributes: true });
// Initial check in case it's already active
if (navLink.classList.contains('active')) {
initializeM3uGenerator();
}
}
document.addEventListener('actualizacion', loadServers);
});

View File

@ -2,7 +2,10 @@ import { state } from './state.js';
import { config } from './config.js';
import { initDB, getFromDB } from './db.js';
import { MusicPlayer } from './musicPlayer.js';
import { ActivityViewer } from './activityViewer.js';
import { Chat } from './chat.js';
import { setupEventListeners } from './eventListeners.js';
import { fetchAllProviders, renderProviders, getRegions } from './providers.js';
import { loadInitialContent, initializeFavorites, initializeUserData, loadLocalContent, applyTheme, applyHeroVisibility } from './ui.js';
import { showNotification, _ } from './utils.js';
@ -11,16 +14,39 @@ async function loadSettings() {
const settingsData = await getFromDB('settings');
if (settingsData && settingsData.length > 0) {
state.settings = { ...state.settings, ...settingsData[0] };
} else {
}
if (!state.settings.watchRegion) {
const langToRegionMap = {
'es': 'ES',
'en': 'US',
'fr': 'FR',
'de': 'DE',
'it': 'IT',
'pt': 'PT'
};
const userLang = chrome.i18n.getUILanguage().split('-')[0];
state.settings.watchRegion = langToRegionMap[userLang] || 'US';
}
if (!state.settings.language) {
state.settings.language = chrome.i18n.getUILanguage().split('-')[0];
}
if (!state.settings.apiKey) {
state.settings.apiKey = config.defaultApiKey;
}
const jellyfinSettingsData = await getFromDB('jellyfin_settings');
if (jellyfinSettingsData && jellyfinSettingsData.length > 0) {
state.jellyfinSettings = { ...state.jellyfinSettings, ...jellyfinSettingsData[0] };
}
} catch (error) {
console.error("Could not load settings from DB, using defaults.", error);
state.settings.language = chrome.i18n.getUILanguage().split('-')[0];
const userLang = chrome.i18n.getUILanguage();
const region = userLang.split('-')[1] || userLang.split('-')[0];
state.settings.watchRegion = region.toUpperCase();
}
}
@ -36,6 +62,8 @@ document.addEventListener('DOMContentLoaded', async () => {
state.musicPlayer = new MusicPlayer();
state.musicPlayer.setDB(state.db);
state.activityViewer = new ActivityViewer(document.getElementById('activityViewerModal'));
state.chat = new Chat();
initializeFavorites();
initializeUserData();

View File

@ -1,13 +1,18 @@
import { getFromDB } from './db.js';
import { debounce, showNotification, _ } from './utils.js';
import { getMusicUrlsFromPlex } from './api.js';
import { getMusicUrlsFromPlex, getMusicUrlsFromJellyfin } from './api.js';
import { state } from './state.js';
export class MusicPlayer {
constructor() {
this.cancionesActuales = [];
this.displayedSongs = [];
this.indiceActual = -1;
this.isPlaying = false;
this.audioPlayer = document.getElementById("audioPlayer");
this.preloaderAudio = document.createElement('audio');
this.currentArtist = "";
this.currentAlbumId = null;
this.currentSongId = null;
@ -23,6 +28,8 @@ export class MusicPlayer {
this.isPlayerVisible = false;
this.isReady = false;
this.isInitializing = false;
this.miniplayerManuallyClosed = false;
this.isPreloading = false;
}
setDB(databaseInstance) {
@ -79,6 +86,8 @@ export class MusicPlayer {
btn.addEventListener('click', () => this.togglePlayerVisibility());
});
document.getElementById('closeSideNavBtn').addEventListener('click', () => this.hidePlayer());
document.getElementById('closeMiniplayerBtn').addEventListener('click', () => this.closeMiniplayer());
document.getElementById('fab-music-player').addEventListener('click', () => this.openMiniplayer());
document.getElementById('searchArtist').addEventListener("input", debounce(() => this.filterArtists(), 300));
document.getElementById('searchSong').addEventListener("input", debounce(() => this.filterSongs(), 300));
document.getElementById('backBtn').addEventListener('click', () => this.showArtistList());
@ -112,9 +121,19 @@ export class MusicPlayer {
}
});
this.audioPlayer.addEventListener("ended", () => this.handleAudioEnded());
this.audioPlayer.addEventListener("timeupdate", () => this.updateProgressBar());
this.audioPlayer.addEventListener("timeupdate", () => this.handleTimeUpdate());
this.audioPlayer.addEventListener("error", () => this.handleAudioErrorEvent());
this.audioPlayer.addEventListener("volumechange", () => {
this.preloaderAudio.volume = this.audioPlayer.volume;
});
this.preloaderAudio.addEventListener("error", (e) => {
this.isPreloading = false;
});
this.preloaderAudio.addEventListener("canplaythrough", () => {
this.isPreloading = true;
});
const progressBarContainer = document.getElementById('progressBarContainer');
progressBarContainer.addEventListener('click', (event) => this.seek(event));
progressBarContainer.addEventListener('mousemove', (event) => this.updateSeekHover(event));
@ -128,8 +147,15 @@ export class MusicPlayer {
const item = event.target.closest('.song-item');
if(item) {
const index = parseInt(item.dataset.index, 10);
if (!isNaN(index) && this.cancionesActuales[index]) {
this.playSong(this.cancionesActuales[index], index);
if (!isNaN(index) && this.displayedSongs[index]) {
this.cancionesActuales = [...this.displayedSongs];
if (this.shuffleMode) {
this.shuffleArray(this.cancionesActuales);
const newIndex = this.cancionesActuales.findIndex(s => s.id === this.displayedSongs[index].id);
this.playSong(newIndex);
} else {
this.playSong(index);
}
}
}
});
@ -175,7 +201,7 @@ export class MusicPlayer {
return document.querySelector("#tokenSelectorContainer .select-selected span").dataset.value;
}
handleAudioEnded() { this.playNext(); }
handleAudioEnded() { this.playNext(true); }
handleAudioErrorEvent() { this.handleAudioError(_('playbackError')); }
togglePlayerVisibility() {
@ -211,6 +237,31 @@ export class MusicPlayer {
this.isPlayerVisible = false;
}
closeMiniplayer() {
this.miniplayerManuallyClosed = true;
const miniplayer = document.getElementById('miniplayer');
gsap.to(miniplayer, { y: '110%', duration: 0.5, ease: 'power3.in', onComplete: () => {
miniplayer.style.display = 'none';
document.body.classList.remove('miniplayer-active');
if (this.indiceActual >= 0) {
document.getElementById('fab-music-player').style.display = 'flex';
gsap.fromTo('#fab-music-player', { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)' });
}
}});
}
openMiniplayer() {
this.miniplayerManuallyClosed = false;
const miniplayer = document.getElementById('miniplayer');
const fab = document.getElementById('fab-music-player');
gsap.to(fab, { scale: 0, opacity: 0, duration: 0.3, ease: 'back.in(1.7)', onComplete: () => {
fab.style.display = 'none';
miniplayer.style.display = 'grid';
document.body.classList.add('miniplayer-active');
gsap.fromTo(miniplayer, { y: '110%' }, { y: '0%', duration: 0.5, ease: 'power3.out' });
}});
}
async handleDatabaseUpdate() {
if (!this.isReady) await this.asyncInitialize();
if (!this.isReady) return;
@ -228,7 +279,13 @@ export class MusicPlayer {
const artistCard = document.querySelector(`.artist-card[data-id='${wasArtistId}']`);
if (artistCard) {
try {
const canciones = await getMusicUrlsFromPlex(artistCard.dataset.token, artistCard.dataset.protocolo, artistCard.dataset.ip, artistCard.dataset.puerto, wasArtistId);
const { token, protocolo, ip, puerto, id: artistaId, isjellyfin, serverurl, userid } = artistCard.dataset;
let canciones;
if (isjellyfin === 'true') {
canciones = await getMusicUrlsFromJellyfin(serverurl, userid, token, artistaId);
} else {
canciones = await getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId);
}
this.handleSongsLoaded(canciones, wasArtistId);
this.markCurrentSong();
@ -297,12 +354,25 @@ export class MusicPlayer {
if (servidor && Array.isArray(servidor.titulos)) {
servidor.titulos.forEach(artista => {
if (artista && typeof artista.id !== 'undefined' && artista.id !== null && !artistMap.has(artista.id)) {
artistMap.set(artista.id, {
id: artista.id, title: artista.title || 'Artista Desconocido',
token: servidor.token, ip: servidor.ip, puerto: servidor.puerto,
protocolo: servidor.protocolo, serverName: servidor.serverName || servidor.ip || 'Servidor Desconocido',
thumb: artista.thumb
});
const artistEntry = {
id: artista.id,
title: artista.title || 'Artista Desconocido',
thumb: artista.thumb,
isJellyfin: servidor.isJellyfin || false,
serverName: servidor.serverName || 'Servidor Desconocido',
};
if (servidor.isJellyfin) {
artistEntry.serverUrl = servidor.serverUrl;
artistEntry.userId = servidor.userId;
artistEntry.token = servidor.token;
} else {
artistEntry.token = servidor.token;
artistEntry.ip = servidor.ip;
artistEntry.puerto = servidor.puerto;
artistEntry.protocolo = servidor.protocolo;
}
artistMap.set(artista.id, artistEntry);
}
});
}
@ -331,9 +401,14 @@ export class MusicPlayer {
const card = document.createElement('div');
card.className = 'artist-card';
const thumbUrl = artista.thumb
? `${artista.protocolo}://${artista.ip}:${artista.puerto}${artista.thumb}?X-Plex-Token=${artista.token}`
: null;
let thumbUrl = null;
if (artista.thumb) {
if (artista.isJellyfin) {
thumbUrl = artista.thumb;
} else {
thumbUrl = `${artista.protocolo}://${artista.ip}:${artista.puerto}${artista.thumb}?X-Plex-Token=${artista.token}`;
}
}
card.innerHTML = `
<div class="artist-thumb-wrapper">
@ -344,12 +419,21 @@ export class MusicPlayer {
<div class="artist-card-title">${artista.title}</div>
`;
card.dataset.id = artista.id;
card.dataset.token = artista.token;
card.dataset.ip = artista.ip;
card.dataset.puerto = artista.puerto;
card.dataset.protocolo = artista.protocolo;
card.dataset.thumb = artista.thumb || '';
card.dataset.isjellyfin = artista.isJellyfin;
card.title = `${artista.title} en ${artista.serverName || 'Servidor Desconocido'}`;
if (artista.isJellyfin) {
card.dataset.serverurl = artista.serverUrl;
card.dataset.userid = artista.userId;
card.dataset.token = artista.token;
} else {
card.dataset.token = artista.token;
card.dataset.ip = artista.ip;
card.dataset.puerto = artista.puerto;
card.dataset.protocolo = artista.protocolo;
}
artistGrid.appendChild(card);
}
});
@ -396,12 +480,19 @@ export class MusicPlayer {
async loadArtistSongs(selectedCard) {
if (!this.isReady) return;
const { token, protocolo, ip, puerto, id: artistaId, thumb } = selectedCard.dataset;
const { id: artistaId, thumb, isjellyfin, serverurl, userid, token, protocolo, ip, puerto } = selectedCard.dataset;
this.currentArtist = selectedCard.querySelector('.artist-card-title').textContent;
this.currentArtistId = artistaId;
if (!artistaId) return;
const thumbUrl = thumb ? `${protocolo}://${ip}:${puerto}${thumb}?X-Plex-Token=${token}` : 'img/no-profile.png';
let thumbUrl = 'img/no-profile.png';
if (thumb) {
if (isjellyfin === 'true') {
thumbUrl = thumb;
} else {
thumbUrl = `${protocolo}://${ip}:${puerto}${thumb}?X-Plex-Token=${token}`;
}
}
document.getElementById('artist-header-thumb').src = thumbUrl;
document.getElementById('artist-header-title').textContent = this.currentArtist;
@ -413,7 +504,12 @@ export class MusicPlayer {
document.getElementById('listaCanciones').innerHTML = '';
try {
let canciones = await getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId);
let canciones;
if (isjellyfin === 'true') {
canciones = await getMusicUrlsFromJellyfin(serverurl, userid, token, artistaId);
} else {
canciones = await getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId);
}
this.handleSongsLoaded(canciones, artistaId);
} catch (error) {
showNotification(_('errorFetchingArtistSongs'), "error");
@ -424,28 +520,40 @@ export class MusicPlayer {
}
}
async getArtistSongs(artist) {
if (!this.isReady) return;
const { id: artistaId, isJellyfin, serverUrl, userId, token, protocolo, ip, puerto } = artist;
try {
let canciones;
if (isJellyfin) {
canciones = await getMusicUrlsFromJellyfin(serverUrl, userId, token, artistaId);
} else {
canciones = await getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId);
}
return canciones;
} catch (error) {
showNotification(_('errorFetchingArtistSongs'), "error");
return [];
}
}
handleSongsLoaded(canciones, artistId) {
if (!this.isReady) return;
if (!Array.isArray(canciones)) canciones = [];
if (canciones.length > 0) {
if (!this.shuffleMode) {
canciones.sort((a, b) => {
const albumCompare = (a.album || '').localeCompare(b.album || '');
if (albumCompare !== 0) return albumCompare;
const trackIndexA = a.trackIndex !== undefined ? a.trackIndex : (a.title || '');
const trackIndexB = b.trackIndex !== undefined ? b.trackIndex : (b.title || '');
return trackIndexA - trackIndexB;
});
} else {
this.shuffleArray(canciones);
}
}
this.cancionesActuales = canciones;
canciones.sort((a, b) => {
const albumCompare = (a.album || '').localeCompare(b.album || '');
if (albumCompare !== 0) return albumCompare;
const trackIndexA = a.trackIndex !== undefined ? a.trackIndex : (a.title || '');
const trackIndexB = b.trackIndex !== undefined ? b.trackIndex : (b.title || '');
return trackIndexA - trackIndexB;
});
this.displayedSongs = canciones;
this.currentAlbumId = artistId;
this.displaySongList(canciones);
this.displaySongList(this.displayedSongs);
this.markCurrentSong();
this.markCurrentArtist(artistId);
}
shuffleArray(array) {
@ -455,16 +563,16 @@ export class MusicPlayer {
}
}
displaySongList(canciones) {
displaySongList(songsToDisplay) {
if (!this.isReady) return;
const lista = document.getElementById("listaCanciones");
lista.innerHTML = '';
if (!Array.isArray(canciones) || canciones.length === 0) {
if (!Array.isArray(songsToDisplay) || songsToDisplay.length === 0) {
lista.innerHTML = `<div class="list-item-empty">${_('noSongsFound')}</div>`;
return;
}
const albums = canciones.reduce((acc, cancion) => {
const albums = songsToDisplay.reduce((acc, cancion) => {
const albumTitle = cancion.album || 'Otras Canciones';
if (!acc[albumTitle]) acc[albumTitle] = [];
acc[albumTitle].push(cancion);
@ -472,6 +580,7 @@ export class MusicPlayer {
}, {});
const fragment = document.createDocumentFragment();
let songCounter = 0;
for (const albumTitle in albums) {
const albumWrapper = document.createElement('div');
albumWrapper.className = 'album-group';
@ -481,22 +590,22 @@ export class MusicPlayer {
albumWrapper.appendChild(albumHeader);
albums[albumTitle].forEach(cancion => {
const originalIndex = this.cancionesActuales.findIndex(c => c.id === cancion.id);
if (cancion && cancion.titulo) {
const item = document.createElement('div');
item.className = 'song-item';
item.innerHTML = `
<span class="song-number">${cancion.index || originalIndex + 1}</span>
<span class="song-number">${cancion.index || songCounter + 1}</span>
<div class="song-details">
<div class="item-title">${cancion.titulo}</div>
</div>
<i class="fas fa-play play-icon"></i>
`;
item.dataset.index = originalIndex;
item.dataset.index = songCounter;
item.dataset.id = cancion.id;
item.dataset.artistId = cancion.artistId;
item.title = `${cancion.titulo} - ${cancion.album}`;
albumWrapper.appendChild(item);
songCounter++;
}
});
fragment.appendChild(albumWrapper);
@ -506,10 +615,59 @@ export class MusicPlayer {
gsap.from(".album-group", { duration: 0.5, opacity: 0, y: 20, stagger: 0.1, ease: "power3.out" });
}
playSong(cancion, index) {
if (!this.isReady || !this.audioPlayer || !cancion || !cancion.url) return;
const songItemElement = document.querySelector(`.song-item[data-index='${index}']`);
_getNextSongIndex() {
if (this.cancionesActuales.length === 0) return -1;
if (this.shuffleMode) {
if (this.cancionesActuales.length <= 1) return 0;
let nextIndex;
do {
nextIndex = Math.floor(Math.random() * this.cancionesActuales.length);
} while (nextIndex === this.indiceActual);
return nextIndex;
}
return (this.indiceActual + 1) % this.cancionesActuales.length;
}
_getPreviousSongIndex() {
if (this.cancionesActuales.length === 0) return -1;
if (this.shuffleMode) {
return this._getNextSongIndex();
}
return (this.indiceActual - 1 + this.cancionesActuales.length) % this.cancionesActuales.length;
}
preloadNextSong() {
this.isPreloading = false;
const nextIndex = this._getNextSongIndex();
if (nextIndex === -1 || nextIndex === this.indiceActual) return;
const nextSong = this.cancionesActuales[nextIndex];
if (nextSong && nextSong.url) {
this.preloaderAudio.innerHTML = '';
const source = document.createElement('source');
source.src = nextSong.url;
source.type = this.getMimeType(nextSong.extension);
this.preloaderAudio.appendChild(source);
this.preloaderAudio.load();
}
}
handleTimeUpdate() {
this.updateProgressBar();
if (this.audioPlayer.duration && this.audioPlayer.currentTime > this.audioPlayer.duration - 15) {
if (!this.isPreloading && this.preloaderAudio.currentSrc === '') {
this.preloadNextSong();
}
}
}
playSong(index, fromNext = false) {
if (!this.isReady || !this.audioPlayer || index < 0 || !this.cancionesActuales[index]) return;
const cancion = this.cancionesActuales[index];
this.indiceActual = index;
const songItemElement = document.querySelector(`.song-item[data-id='${cancion.id}']`);
const playIconElement = songItemElement ? songItemElement.querySelector('.play-icon') : null;
if (playIconElement) {
@ -517,17 +675,11 @@ export class MusicPlayer {
}
const miniplayer = document.getElementById('miniplayer');
if (miniplayer.style.display === 'none') {
if (miniplayer.style.display === 'none' && !this.miniplayerManuallyClosed) {
gsap.fromTo(miniplayer, { y: '100%' }, { display: 'grid', y: '0%', duration: 0.5, ease: 'power3.out' });
}
document.body.classList.add('miniplayer-active');
this.audioPlayer.innerHTML = '';
const source = document.createElement('source');
source.src = cancion.url;
source.type = this.getMimeType(cancion.extension);
this.audioPlayer.appendChild(source);
const tl = gsap.timeline();
tl.to(['#albumCover', '#trackTitle', '#trackArtist'], { opacity: 0, y: -10, duration: 0.2, ease: "power2.in", stagger: 0.05 })
.add(() => {
@ -537,24 +689,48 @@ export class MusicPlayer {
})
.to(['#albumCover', '#trackTitle', '#trackArtist'], { opacity: 1, y: 0, duration: 0.4, ease: "power2.out", stagger: 0.07 });
if (fromNext && this.isPreloading) {
this.audioPlayer.innerHTML = this.preloaderAudio.innerHTML;
this.preloaderAudio.innerHTML = '';
this.isPreloading = false;
} else {
this.audioPlayer.innerHTML = '';
const source = document.createElement('source');
source.src = cancion.url;
source.type = this.getMimeType(cancion.extension);
this.audioPlayer.appendChild(source);
}
this.audioPlayer.load();
this.audioPlayer.play().then(() => {
this.isPlaying = true;
document.getElementById('playPauseBtn').innerHTML = '<i class="fas fa-pause"></i>';
this.indiceActual = index;
this.currentSongId = cancion.id;
this.currentSongArtistId = cancion.artistId;
this.markCurrentSong();
this.markCurrentArtist(cancion.artistId);
if (playIconElement) {
playIconElement.className = 'fas fa-play play-icon';
}
}).catch((error) => {
this.handleAudioError(_('playbackError'));
if (playIconElement) {
playIconElement.className = 'fas fa-play play-icon';
}
});
const playPromise = this.audioPlayer.play();
if (playPromise !== undefined) {
playPromise.then(() => {
this.isPlaying = true;
document.getElementById('playPauseBtn').innerHTML = '<i class="fas fa-pause"></i>';
this.currentSongId = cancion.id;
this.currentSongArtistId = cancion.artistId;
this.markCurrentSong();
this.ensureArtistVisible(cancion.artistId);
if (playIconElement) {
playIconElement.className = 'fas fa-play play-icon';
}
if (!this.miniplayerManuallyClosed) {
document.getElementById('fab-music-player').style.display = 'none';
}
this.preloadNextSong();
}).catch(error => {
if (error.name === 'NotAllowedError') {
this.isPlaying = false;
document.getElementById('playPauseBtn').innerHTML = '<i class="fas fa-play"></i>';
showNotification(_('autoplayBlocked'), 'warning');
} else {
this.handleAudioError(_('playbackError'));
}
if (playIconElement) {
playIconElement.className = 'fas fa-play play-icon';
}
});
}
}
getMimeType(extension) {
@ -573,40 +749,25 @@ export class MusicPlayer {
.catch(err => { this.isPlaying = false; btn.innerHTML = '<i class="fas fa-play"></i>'; });
}
this.isPlaying = !this.isPlaying;
if (this.isPlaying) {
document.getElementById('fab-music-player').style.display = 'none';
}
}
playNext() {
playNext(fromEnded = false) {
if (!this.isReady || this.cancionesActuales.length === 0) return;
let nextIndex;
if (this.shuffleMode) {
if (this.cancionesActuales.length <= 1) {
nextIndex = 0;
} else {
do {
nextIndex = Math.floor(Math.random() * this.cancionesActuales.length);
} while (nextIndex === this.indiceActual);
}
} else {
nextIndex = (this.indiceActual + 1) % this.cancionesActuales.length;
const nextIndex = this._getNextSongIndex();
if (nextIndex !== -1) {
this.playSong(nextIndex, fromEnded);
}
if (this.cancionesActuales[nextIndex]) this.playSong(this.cancionesActuales[nextIndex], nextIndex);
}
playPrevious() {
if (!this.isReady || this.cancionesActuales.length === 0) return;
let prevIndex;
if (this.shuffleMode) {
if (this.cancionesActuales.length <= 1) {
prevIndex = 0;
} else {
do {
prevIndex = Math.floor(Math.random() * this.cancionesActuales.length);
} while (prevIndex === this.indiceActual);
}
} else {
prevIndex = (this.indiceActual - 1 + this.cancionesActuales.length) % this.cancionesActuales.length;
const prevIndex = this._getPreviousSongIndex();
if (prevIndex !== -1) {
this.playSong(prevIndex);
}
if (this.cancionesActuales[prevIndex]) this.playSong(this.cancionesActuales[prevIndex], prevIndex);
}
toggleShuffle() {
@ -614,6 +775,7 @@ export class MusicPlayer {
this.shuffleMode = !this.shuffleMode;
document.getElementById('shuffleBtn').classList.toggle("active", this.shuffleMode);
showNotification(this.shuffleMode ? _('shuffleOn') : _('shuffleOff'), 'info', 1500);
this.preloadNextSong();
}
markCurrentSong() {
@ -629,14 +791,16 @@ export class MusicPlayer {
}
}
markCurrentArtist(artistIdToMark = null) {
markCurrentArtist() {
if (!this.isReady) return;
const targetArtistId = artistIdToMark !== null ? artistIdToMark : this.currentArtistId;
document.querySelectorAll(".artist-card").forEach(card => card.classList.remove("current-artist"));
if (targetArtistId !== null) {
const artistCard = document.querySelector(`.artist-card[data-id='${targetArtistId}']`);
if (artistCard) artistCard.classList.add("current-artist");
}
const targetArtistId = this.currentSongArtistId;
document.querySelectorAll(".artist-card").forEach(card => {
if (targetArtistId != null && card.dataset.id == targetArtistId) {
card.classList.add("current-artist");
} else {
card.classList.remove("current-artist");
}
});
}
updateProgressBar() {
@ -750,7 +914,14 @@ export class MusicPlayer {
filteredArtists.forEach((artista) => {
const card = document.createElement('div');
card.className = 'artist-card';
const thumbUrl = artista.thumb ? `${artista.protocolo}://${artista.ip}:${artista.puerto}${artista.thumb}?X-Plex-Token=${artista.token}` : null;
let thumbUrl = null;
if (artista.thumb) {
if (artista.isJellyfin) {
thumbUrl = artista.thumb;
} else {
thumbUrl = `${artista.protocolo}://${artista.ip}:${artista.puerto}${artista.thumb}?X-Plex-Token=${artista.token}`;
}
}
card.innerHTML = `
<div class="artist-thumb-wrapper">
${thumbUrl ? `<img src="${thumbUrl}" class="artist-thumb" alt="${artista.title}" loading="lazy">` : '<i class="fas fa-user-music artist-thumb-placeholder"></i>'}
@ -758,12 +929,20 @@ export class MusicPlayer {
<div class="artist-card-title">${artista.title}</div>
`;
card.dataset.id = artista.id;
card.dataset.token = artista.token;
card.dataset.ip = artista.ip;
card.dataset.puerto = artista.puerto;
card.dataset.protocolo = artista.protocolo;
card.dataset.thumb = artista.thumb || '';
card.dataset.isjellyfin = artista.isJellyfin;
card.title = `${artista.title} en ${artista.serverName || 'Servidor Desconocido'}`;
if (artista.isJellyfin) {
card.dataset.serverurl = artista.serverUrl;
card.dataset.userid = artista.userId;
card.dataset.token = artista.token;
} else {
card.dataset.token = artista.token;
card.dataset.ip = artista.ip;
card.dataset.puerto = artista.puerto;
card.dataset.protocolo = artista.protocolo;
}
artistGrid.appendChild(card);
});
}
@ -811,4 +990,25 @@ export class MusicPlayer {
const infoModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('infoModal'));
infoModal.show();
}
ensureArtistVisible(artistId) {
if (!this.isReady || artistId === null) return;
const artistCard = document.querySelector(`.artist-card[data-id='${artistId}']`);
if (artistCard) {
this.markCurrentArtist(artistId);
return;
}
const fullList = this._generateFullArtistListForToken(this._getCurrentTokenFilter());
const artistIndex = fullList.findIndex(a => a.id == artistId);
if (artistIndex !== -1) {
const targetPage = Math.floor(artistIndex / this.artistsPageSize);
if (this.currentPage !== targetPage) {
this.currentPage = targetPage;
this.loadArtists(fullList, this.currentPage);
}
}
this.markCurrentArtist(artistId);
}
}

View File

@ -32,17 +32,76 @@ async function fetchSectionContent(url, signal, timeout = 7000) {
return xmlDoc;
}
export async function fetchLibraries(accessToken, publicUrl, localUrl) {
const url = `${localUrl || publicUrl}/library/sections?X-Plex-Token=${accessToken}`;
const xmlDoc = await fetchSectionContent(url, new AbortController().signal);
return Array.from(xmlDoc.querySelectorAll('Directory')).map(dir => ({
key: dir.getAttribute('key'),
title: dir.getAttribute('title'),
type: dir.getAttribute('type'),
}));
}
export async function fetchLibraryContents(accessToken, publicUrl, localUrl, libraryKey, libraryType, timeout = 7000) {
const endpoint = libraryType === 'show' ? 'allLeaves' : 'all';
const url = `${localUrl || publicUrl}/library/sections/${libraryKey}/${endpoint}?X-Plex-Token=${accessToken}`;
const contentXml = await fetchSectionContent(url, new AbortController().signal, timeout);
const items = [];
const baseUrl = localUrl || publicUrl;
const processItems = (selector, type) => {
contentXml.querySelectorAll(selector).forEach(el => {
const media = el.querySelector('Media Part');
if (media) {
const mediaUrl = `${baseUrl}${media.getAttribute('key')}?X-Plex-Token=${accessToken}`;
let thumbUrl = el.getAttribute('thumb') || el.getAttribute('parentThumb') || el.getAttribute('grandparentThumb');
if (thumbUrl) {
thumbUrl = `${baseUrl}/photo/:/transcode?width=400&height=600&minSize=1&upscale=1&url=${encodeURIComponent(thumbUrl)}&X-Plex-Token=${accessToken}`;
}
const finalTitle = el.getAttribute('title');
items.push({
title: finalTitle,
duration: el.getAttribute('duration'),
url: mediaUrl,
thumb: thumbUrl,
seriesTitle: el.getAttribute('type') === 'episode' ? el.getAttribute('grandparentTitle') : null,
seasonNumber: el.getAttribute('type') === 'episode' ? el.getAttribute('parentIndex') : null,
grandparentTitle: el.getAttribute('grandparentTitle') || (type === 'music' ? el.getAttribute('parentTitle') : null),
});
}
});
};
processItems('Video', 'video'); // Handles both movies and episodes
processItems('Track', 'music'); // Handles music
return items;
}
async function parseAndStoreSectionItems(contentXml, storeName, serverData) {
const type = storeName === 'movies' ? 'movie' : (storeName === 'series' ? 'show' : (storeName === 'artists' ? 'artist' : 'photo'));
let items;
if (type === 'movie' || type === 'show') {
const itemSelector = type === 'movie' ? 'Video' : 'Directory';
items = Array.from(contentXml.querySelectorAll(itemSelector)).map(el => ({
title: el.getAttribute('title'),
year: el.getAttribute('year'),
genre: el.querySelector('Genre')?.getAttribute('tag') || _('noGenre')
}));
items = Array.from(contentXml.querySelectorAll(itemSelector)).map(el => {
const media = el.querySelector('Media');
const part = media?.querySelector('Part');
const container = part?.getAttribute('container');
const videoResolution = media?.getAttribute('videoResolution');
return {
id: el.getAttribute('ratingKey'),
title: el.getAttribute('title'),
year: el.getAttribute('year'),
genre: el.querySelector('Genre')?.getAttribute('tag') || _('noGenre'),
type: type,
container: container,
resolution: videoResolution
};
});
} else if (type === 'artist' || type === 'photo') {
items = Array.from(contentXml.querySelectorAll('Directory')).map(el => ({
id: el.getAttribute('ratingKey'),
@ -215,4 +274,35 @@ export async function startPlexScan(tipos) {
state.plexScanAbortController = null;
ocultarSpinner();
}
}
}
export function stopPlexScan() {
if (state.plexScanAbortController) {
state.plexScanAbortController.abort();
logToConsole(_('stoppingPlexScan'));
}
}
export async function updateAllTokens() {
const tipos = ['movies', 'series', 'artists', 'photos'];
await startPlexScan(tipos);
}
export async function addPlexToken(token) {
if (!token || typeof token !== 'string') {
showNotification(_('invalidTokenProvided'), 'error');
return;
}
try {
const existingTokens = await getFromDB('tokens');
if (existingTokens.some(t => t.token === token)) {
showNotification(_('tokenAlreadyExists'), 'warning');
return;
}
await addItemsToStore('tokens', [{ token: token }]);
showNotification(_('tokenAddedSuccessfully'), 'success');
emitirEventoActualizacion();
} catch (error) {
showNotification(_('errorAddingToken', error.message), 'error');
}
}

201
js/providers.js Normal file
View File

@ -0,0 +1,201 @@
import { fetchTMDB } from './api.js';
import { state } from './state.js';
import { _, showNotification } from './utils.js';
import { renderGrid } from './ui.js';
let currentProviderId = null;
let currentPage = 1;
let totalPages = 1;
export async function getAvailableProviders(type, region) {
try {
const url = `watch/providers/${type}`;
const providers = await fetchTMDB(url, { watch_region: region });
return providers.results || [];
} catch (error) {
console.error(`Error fetching ${type} providers for region ${region}:`, error);
return [];
}
}
export async function fetchAllProviders(region) {
const [movieProviders, tvProviders] = await Promise.all([
getAvailableProviders('movie', region),
getAvailableProviders('tv', region)
]);
const allProvidersMap = new Map();
[...movieProviders, ...tvProviders].forEach(provider => {
allProvidersMap.set(provider.provider_id, provider);
});
return Array.from(allProvidersMap.values()).sort((a, b) => a.provider_name.localeCompare(b.provider_name));
}
export async function getRegions() {
try {
const regions = await fetchTMDB('watch/providers/regions');
return regions.results || [];
} catch (error) {
console.error('Error fetching regions:', error);
return [];
}
}
export function resetProvidersView() {
const providersSection = document.getElementById('providers-section');
if (!providersSection) return;
const grid = document.getElementById('providers-grid');
const itemsContainer = document.getElementById('provider-items-container');
const titleElement = document.querySelector('#providers-section .section-title');
const backButton = document.getElementById('back-to-providers');
if (grid) grid.style.display = 'grid';
if (itemsContainer) itemsContainer.style.display = 'none';
if (titleElement) titleElement.textContent = _('navProviders');
if (backButton) backButton.style.display = 'none';
}
export async function backToProviders() {
resetProvidersView();
const providers = await fetchAllProviders(state.settings.watchRegion);
renderProviders(providers);
}
export function renderProviders(providers) {
const grid = document.getElementById('providers-grid');
if (!grid) return;
grid.style.display = 'grid';
if (providers.length === 0) {
grid.innerHTML = `<div class="empty-state"><i class="fas fa-tv"></i><p>${_('noProvidersFound')}</p></div>`;
return;
}
const availableProviderIds = new Set(state.localSeries.flatMap(s => s.titulos).map(t => t.provider_id));
const providersHtml = providers.map(provider => {
const isAvailable = availableProviderIds.has(provider.provider_id);
return `
<div class="provider-card ${isAvailable ? 'available' : ''}" data-provider-id="${provider.provider_id}" data-provider-name="${provider.provider_name}">
<img src="https://image.tmdb.org/t/p/w200${provider.logo_path}" alt="${provider.provider_name}" class="provider-logo">
<div class="provider-tooltip">${provider.provider_name}</div>
</div>
`;
}).join('');
grid.innerHTML = providersHtml;
gsap.fromTo(".provider-card",
{ scale: 0.5, opacity: 0 },
{
duration: 0.5,
scale: 1,
opacity: 1,
delay: 0.2,
stagger: 0.08,
ease: "back.out(1.7)",
force3D: true
}
);
}
export async function getProviderItems(providerId, page = 1, mediaType = 'all') {
try {
const watchRegion = state.settings.watchRegion || 'US';
const params = {
with_watch_providers: providerId,
watch_region: watchRegion,
page: page,
};
let items = [];
let total_pages = 0;
if (mediaType === 'movie' || mediaType === 'all') {
const moviesResponse = await fetchTMDB('discover/movie', params);
items.push(...moviesResponse.results.map(item => ({ ...item, media_type: 'movie' })));
total_pages = Math.max(total_pages, moviesResponse.total_pages);
}
if (mediaType === 'tv' || mediaType === 'all') {
const seriesResponse = await fetchTMDB('discover/tv', params);
items.push(...seriesResponse.results.map(item => ({ ...item, media_type: 'tv' })));
total_pages = Math.max(total_pages, seriesResponse.total_pages);
}
items.sort((a, b) => b.popularity - a.popularity);
return { success: true, items, total_pages };
} catch (error) {
showNotification(`${_('couldNotLoadContent')}: ${error.message}`, 'error');
return { success: false, items: [], total_pages: 0 };
}
}
let currentMediaType = 'all';
export async function loadProviderContent(providerId, providerName, page = 1, isFilterChange = false) {
currentProviderId = providerId;
currentPage = page;
const itemsContainer = document.getElementById('provider-items-container');
const providersGrid = document.getElementById('providers-grid');
const itemsGrid = document.getElementById('provider-items');
const paginationControls = document.getElementById('pagination-controls');
const pageNumberSpan = document.getElementById('page-number');
const prevButton = document.getElementById('prev-page');
const nextButton = document.getElementById('next-page');
const sectionTitle = document.querySelector('#providers-section .section-title');
const backButton = document.getElementById('back-to-providers');
if (!isFilterChange) {
const filterButtons = document.querySelectorAll('#provider-content-type-filter button');
filterButtons.forEach(button => {
button.replaceWith(button.cloneNode(true));
});
document.querySelectorAll('#provider-content-type-filter button').forEach(button => {
button.addEventListener('click', () => {
document.querySelectorAll('#provider-content-type-filter button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
currentMediaType = button.dataset.type;
loadProviderContent(currentProviderId, providerName, 1, true);
});
});
}
providersGrid.style.display = 'none';
itemsContainer.style.display = 'block';
backButton.style.display = 'block';
if (sectionTitle) sectionTitle.textContent = providerName;
itemsGrid.innerHTML = `<div class="col-12 text-center mt-5"><div class="spinner" style="position: static; margin: auto; display: block;"></div></div>`;
paginationControls.style.display = 'none';
const { success, items, total_pages } = await getProviderItems(providerId, page, currentMediaType);
if (success) {
totalPages = total_pages;
renderGrid(items, false, 'provider-items', !isFilterChange);
if (totalPages > 1) {
paginationControls.style.display = 'flex';
pageNumberSpan.textContent = `${_('page')} ${currentPage} / ${totalPages}`;
prevButton.disabled = currentPage === 1;
nextButton.disabled = currentPage >= totalPages;
}
} else {
itemsGrid.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('couldNotLoadContent')}</p></div>`;
}
}
export function changeProviderPage(direction) {
const newPage = currentPage + direction;
if (newPage > 0 && newPage <= totalPages && currentProviderId) {
const providerName = document.querySelector('#providers-section .section-title').textContent;
loadProviderContent(currentProviderId, providerName, newPage);
}
}

View File

@ -1,3 +1,5 @@
import { config } from './config.js';
export const state = {
currentPage: 1,
currentView: 'movies',
@ -5,6 +7,7 @@ export const state = {
settings: {
id: 'user_settings',
apiKey: '',
googleApiKey: '',
theme: 'dark',
showHero: true,
language: 'es',
@ -15,10 +18,21 @@ export const state = {
phpFilename: 'CinePlex_Playlist.m3u',
phpFileAction: 'append',
},
jellyfinSettings: {
id: 'jellyfin_credentials',
url: '',
username: '',
password: '',
apiKey: '',
userId: '',
},
jellyfinMovies: [],
jellyfinSeries: [],
localMovies: [],
localSeries: [],
localArtists: [],
localPhotos: [],
localContentLookup: new Set(),
db: null,
lastScrollPosition: 0,
currentItemId: null,
@ -32,7 +46,10 @@ export const state = {
isAddingStream: false,
isDownloadingM3U: false,
isScanningPlex: false,
isScanningJellyfin: false,
musicPlayer: null,
activityViewer: null,
chat: null,
currentContentFetchController: null,
plexScanAbortController: null,
aceEditor: null,
@ -44,4 +61,6 @@ export const state = {
currentPhotoToken: null,
currentPhotoItems: [],
currentPhotoLightboxIndex: 0,
providerContentPage: 1,
providerContentTotalPages: 1,
};

1102
js/ui.js

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,8 @@
],
"host_permissions": [
"https://*.plex.tv/*",
"*://*:*/*"
"http://*/*",
"https://*/*"
],
"background": {
"service_worker": "js/background.js",

246
plex.html
View File

@ -4,6 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="__MSG_appDescription__">
<title>__MSG_appName__ - __MSG_appTagline__</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
@ -11,8 +12,7 @@
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Orbitron:wght@500;600;700&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/equalizer.css">
<link rel="stylesheet" href="css/photos.css">
<link rel="stylesheet" href="css/custom-filters.css">
</head>
<body class="unlocalized">
@ -21,7 +21,7 @@
<header class="top-bar">
<div class="top-bar-left">
<button id="sidebar-toggle" class="btn-icon" aria-label="__MSG_toggleNavigation__"><i class="fas fa-bars"></i></button>
<a class="navbar-brand logo" href="#" id="reset-view-btn">__MSG_appName__</a>
<a class="navbar-brand logo" href="#" id="reset-view-btn" role="button">__MSG_appName__</a>
</div>
<div class="top-bar-center">
<div class="search-bar">
@ -30,6 +30,9 @@
</div>
</div>
<div class="top-bar-right">
<button id="activity-viewer-btn" class="btn-icon" title="__MSG_activityViewerTitle__">
<i class="fas fa-desktop"></i>
</button>
<button id="openMusicPlayerDesktop" class="btn-icon" title="__MSG_openMusicPlayer__">
<i class="fas fa-music"></i>
</button>
@ -39,20 +42,22 @@
</div>
</header>
<nav class="sidebar-nav" id="sidebar-nav">
<nav class="sidebar-nav" id="sidebar-nav" role="navigation">
<ul class="sidebar-menu">
<li><a class="nav-link active" href="#" id="nav-movies"><i class="fas fa-film"></i><span>__MSG_navMovies__</span></a></li>
<li><a class="nav-link" href="#" id="nav-series"><i class="fas fa-tv"></i><span>__MSG_navSeries__</span></a></li>
<li><a class="nav-link" href="#" id="nav-photos"><i class="fas fa-images"></i><span>__MSG_navPhotos__</span></a></li>
<li><a class="nav-link" href="#" id="nav-stats"><i class="fas fa-chart-pie"></i><span>__MSG_navStats__</span></a></li>
<li><a class="nav-link" href="#" id="nav-favorites"><i class="fas fa-heart"></i><span>__MSG_navFavorites__</span></a></li>
<li><a class="nav-link" href="#" id="nav-history"><i class="fas fa-history"></i><span>__MSG_navHistory__</span></a></li>
<li><a class="nav-link" href="#" id="nav-recommendations"><i class="fas fa-magic"></i><span>__MSG_navRecommendations__</span></a></li>
<li><a class="nav-link d-lg-none" href="#" id="openMusicPlayerMobile"><i class="fas fa-music"></i><span>__MSG_navMusic__</span></a></li>
<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" 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-providers" role="button"><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-stats" role="button"><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-history" role="button"><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 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" href="#" id="nav-m3u-generator" role="button"><i class="fas fa-list-ul"></i><span>__MSG_navM3uGenerator__</span></a></li>
</ul>
</nav>
<div id="main-container">
<div id="main-container" role="main">
<div id="main-view">
<section class="hero" id="hero-section">
<div class="hero-background-container">
@ -92,6 +97,34 @@
<option value="vote_average.desc">__MSG_sortTopRated__</option>
<option id="sort-release-date" value="release_date.desc">__MSG_sortRecent__</option>
</select>
<div class="filter-popover-wrapper">
<button type="button" class="btn btn-secondary" id="duration-filter-btn" data-i18n="duration_min">Duración (Min)</button>
<div id="duration-popover" class="filter-popover">
<div class="range-slider-container">
<div class="slider-track"></div>
<div class="slider-fill" id="duration-fill"></div>
<input type="range" class="form-range" id="duration-min" min="0" max="300" value="0">
<input type="range" class="form-range" id="duration-max" min="0" max="300" value="300">
</div>
<div class="range-values">
<span id="duration-min-value">0</span> - <span id="duration-max-value">300</span>
</div>
</div>
</div>
<div class="filter-popover-wrapper">
<button type="button" class="btn btn-secondary" id="score-filter-btn" data-i18n="score">Puntuación</button>
<div id="score-popover" class="filter-popover">
<div class="range-slider-container">
<div class="slider-track"></div>
<div class="slider-fill" id="score-fill"></div>
<input type="range" class="form-range" id="score-min" min="0" max="10" step="0.1" value="0">
<input type="range" class="form-range" id="score-max" min="0" max="10" step="0.1" value="10">
</div>
<div class="range-values">
<span id="score-min-value">0</span> - <span id="score-max-value">10</span>
</div>
</div>
</div>
</div>
<div id="content-grid" class="content-grid">
<div class="col-12 text-center mt-5">
@ -101,6 +134,29 @@
<div class="text-center mt-5"><button id="load-more" class="btn btn-primary" style="display: none;">__MSG_loadMore__</button></div>
</section>
<section id="providers-section" style="display: none;">
<div class="section-header">
<h2 class="section-title">__MSG_navProviders__</h2>
</div>
<div id="providers-grid" class="providers-grid"></div>
<div id="provider-items-container" style="display: none;">
<div class="d-flex justify-content-between align-items-center mb-3">
<button id="back-to-providers" class="btn btn-secondary"><i class="fas fa-arrow-left me-2"></i>__MSG_backToProviders__</button>
<div id="provider-content-type-filter" class="btn-group">
<button type="button" class="btn btn-outline-primary active" data-type="all">__MSG_all__</button>
<button type="button" class="btn btn-outline-primary" data-type="movie">__MSG_navMovies__</button>
<button type="button" class="btn btn-outline-primary" data-type="tv">__MSG_navSeries__</button>
</div>
</div>
<div id="provider-items" class="content-grid"></div>
<div id="pagination-controls" class="text-center mt-4" style="display: none;">
<button id="prev-page" class="btn btn-primary">&larr; __MSG_previous__</button>
<span id="page-number" class="mx-3 align-middle"></span>
<button id="next-page" class="btn btn-primary">__MSG_next__ &rarr;</button>
</div>
</div>
</section>
<section id="photos-section" style="display: none;">
<div class="photos-header">
<nav aria-label="breadcrumb">
@ -210,6 +266,49 @@
<div id="history-list"></div>
</section>
<section id="m3u-generator-section" class="content-section">
<div class="section-header">
<h2 class="section-title">__MSG_m3uGenerator__</h2>
</div>
<div class="m3u-container">
<div class="m3u-config-panel m3u-animated-item">
<div class="m3u-step">
<div class="m3u-step-header">
<span class="m3u-step-number">1</span>
<h3 class="m3u-step-title">__MSG_selectServer__</h3>
</div>
<select id="m3u-server-select" class="filter-select"></select>
</div>
<div id="m3u-libraries-step" class="m3u-step" style="display: none;">
<div class="m3u-step-header">
<span class="m3u-step-number">2</span>
<h3 class="m3u-step-title">__MSG_selectLibraries__</h3>
</div>
<div id="m3u-libraries-container"></div>
<div id="m3u-libraries-loader" class="text-center py-4" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">__MSG_loading__</span>
</div>
<p class="mt-3 lead">__MSG_loadingLibraries__</p>
</div>
</div>
</div>
<div class="m3u-info-panel m3u-animated-item">
<h3 class="m3u-info-title">__MSG_howToUse__</h3>
<ol class="m3u-instructions">
<li>__MSG_m3uInstruction1__</li>
<li>__MSG_m3uInstruction2__</li>
<li>__MSG_m3uInstruction3__</li>
<li>__MSG_m3uInstruction4__</li>
</ol>
<button id="download-m3u-btn" class="btn btn-primary" disabled>
<i class="fas fa-download"></i>
<span>__MSG_downloadM3u__</span>
</button>
</div>
</div>
</section>
<div id="consoleOutputContainer" class="mt-5" style="display: none;">
<h3 class="section-subtitle mt-4">__MSG_consoleTitle__</h3>
<div id="consoleOutput"></div>
@ -222,14 +321,15 @@
<footer class="footer">
<div class="container">
<div class="footer-content">
<a class="footer-logo-link" href="#" id="footer-logo-btn">
<a class="footer-logo-link" href="#" id="footer-logo-btn" role="button">
<span class="footer-logo-text">__MSG_appName__</span>
</a>
<div class="footer-links">
<a href="#" class="footer-link" id="footer-movies">__MSG_navMovies__</a>
<a href="#" class="footer-link" id="footer-series">__MSG_navSeries__</a>
<a href="#" class="footer-link" id="footer-stats">__MSG_navStats__</a>
<a href="#" class="footer-link" id="footer-favorites">__MSG_navFavorites__</a>
<a href="#" class="footer-link" id="footer-movies" role="button">__MSG_navMovies__</a>
<a href="#" class="footer-link" id="footer-series" role="button">__MSG_navSeries__</a>
<a href="#" class="footer-link" id="footer-providers" role="button">__MSG_navProviders__</a>
<a href="#" class="footer-link" id="footer-stats" role="button">__MSG_navStats__</a>
<a href="#" class="footer-link" id="footer-favorites" role="button">__MSG_navFavorites__</a>
</div>
<p class="footer-credit">__MSG_footerCredit__</p>
</div>
@ -242,7 +342,7 @@
<div id="item-details-content"></div>
</section>
<div id="video-lightbox" class="lightbox">
<div id="video-lightbox" class="lightbox" role="dialog" aria-modal="true">
<div class="lightbox-content">
<button class="lightbox-close" aria-label="__MSG_closeTrailer__"><i class="fas fa-times"></i></button>
<div class="video-container"><iframe id="video-iframe" frameborder="0" allow="autoplay; encrypted-media"
@ -250,7 +350,7 @@
</div>
</div>
<div id="photo-lightbox" style="display: none;">
<div id="photo-lightbox" style="display: none;" role="dialog" aria-modal="true">
<div class="photo-lightbox-container">
<button class="photo-lightbox-btn" id="photo-lightbox-close" aria-label="__MSG_close__"><i class="fas fa-times"></i></button>
<button class="photo-lightbox-btn" id="photo-lightbox-prev" aria-label="__MSG_previous__"><i class="fas fa-chevron-left"></i></button>
@ -269,7 +369,30 @@
<div class="spinner" id="spinner"></div>
<div class="modal fade" id="settingsModal" tabindex="-1" aria-labelledby="settingsModalLabel" aria-hidden="true">
<div class="modal fade" id="activityViewerModal" tabindex="-1" aria-labelledby="activityViewerModalLabel" 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="activityViewerModalLabel"><i class="fas fa-desktop me-2"></i>__MSG_activityViewerTitle__</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="__MSG_close__"></button>
</div>
<div class="modal-body">
<div class="d-flex gap-3 mb-4">
<select class="form-control filter-select flex-grow-1" id="activity-server-select"></select>
<button class="btn btn-primary" id="check-activity-btn"><i class="fas fa-sync-alt"></i></button>
</div>
<div id="activity-loader" style="display: none;" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">__MSG_loading__</span>
</div>
</div>
<div id="activity-results"></div>
</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">
@ -288,6 +411,11 @@
<i class="fas fa-server me-2"></i>__MSG_settingsTabPlex__
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="jellyfin-tab" data-bs-toggle="tab" data-bs-target="#jellyfin" type="button" role="tab" aria-controls="jellyfin" aria-selected="false">
<i class="fas fa-database me-2"></i>__MSG_settingsTabJellyfin__
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="php-gen-tab" data-bs-toggle="tab" data-bs-target="#php-gen" type="button" role="tab" aria-controls="php-gen" aria-selected="false">
<i class="fab fa-php me-2"></i>__MSG_settingsTabPhpGen__
@ -307,14 +435,13 @@
<input type="password" class="form-control" id="tmdbApiKey" placeholder="__MSG_settingsTmdbApiPlaceholder__">
</div>
<div class="mb-3">
<label for="appLanguage" class="form-label">__MSG_settingsTmdbLangLabel__</label>
<select class="form-control filter-select" id="appLanguage">
<option value="es">__MSG_lang_es__</option>
<option value="en">__MSG_lang_en__</option>
<option value="fr">__MSG_lang_fr__</option>
<option value="de">__MSG_lang_de__</option>
<option value="it">__MSG_lang_it__</option>
<option value="pt">__MSG_lang_pt__</option>
<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">
@ -359,6 +486,28 @@
</div>
</div>
</div>
<div class="tab-pane fade" id="jellyfin" role="tabpanel" aria-labelledby="jellyfin-tab">
<h5 class="mb-3">__MSG_settingsJellyfinTitle__</h5>
<p class="small text-muted mb-3">__MSG_settingsJellyfinDesc__</p>
<div class="mb-3">
<label for="jellyfinServerUrl" class="form-label">__MSG_jellyfinUrlLabel__</label>
<input type="url" class="form-control" id="jellyfinServerUrl" placeholder="http://192.168.1.10:8096">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="jellyfinUsername" class="form-label">__MSG_jellyfinUserLabel__</label>
<input type="text" class="form-control" id="jellyfinUsername">
</div>
<div class="col-md-6 mb-3">
<label for="jellyfinPassword" class="form-label">__MSG_jellyfinPassLabel__</label>
<input type="password" class="form-control" id="jellyfinPassword">
</div>
</div>
<div class="d-grid">
<button type="button" class="btn btn-primary" id="jellyfinScanBtn"><i class="fas fa-search-plus me-2"></i>__MSG_jellyfinConnectAndScan__</button>
</div>
<div id="jellyfinScanStatus" class="mt-3"></div>
</div>
<div class="tab-pane fade" id="php-gen" role="tabpanel" aria-labelledby="php-gen-tab">
<h5 class="mb-3">__MSG_settingsPhpGenTitle__</h5>
<div class="row">
@ -526,6 +675,7 @@
<button id="shuffleBtn" class="control-btn" title="__MSG_miniplayerShuffle__"><i class="fas fa-random"></i></button>
<button id="eqBtn" class="control-btn" title="__MSG_miniplayerEqualizer__"><i class="fas fa-sliders-h"></i></button>
<button id="openMusicPlayerFromMiniplayer" class="control-btn" title="__MSG_miniplayerOpenList__"><i class="fas fa-list"></i></button>
<button id="closeMiniplayerBtn" class="control-btn" title="__MSG_close__"><i class="fas fa-times"></i></button>
</div>
<audio id="audioPlayer"></audio>
@ -611,7 +761,7 @@
</div>
</div>
<div class="modal fade" id="infoModal" tabindex="-1" aria-labelledby="infoModalLabel" aria-hidden="true">
<div class="modal fade" id="infoModal" tabindex="-1" aria-labelledby="infoModalLabel" aria-hidden="true" role="dialog" aria-modal="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
@ -630,6 +780,37 @@
</div>
</div>
<div id="fab-container">
<button id="fab-music-player" class="fab-btn" style="display: none;" title="__MSG_openMusicPlayer__"><i class="fas fa-music"></i></button>
<div id="chat-fab" class="chat-fab" data-i18n-title="chatOpen" title="Open Chat">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="28" height="28">
<path d="M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2ZM20 16H5.2L4 17.2V4H20V16Z"/>
<path d="M7.5 8.5h9v2h-9zM7.5 11.5h6v2h-6z"/>
</svg>
</div>
</div>
<div id="chat-window" class="chat-window" style="display: none;">
<div class="chat-header">
<h3 class="chat-title" data-i18n="chatTitle">AI Assistant</h3>
<button id="chat-close-btn" class="chat-close-btn" data-i18n-title="chatClose" title="Close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>
</div>
<div id="chat-messages" class="chat-messages">
</div>
<form id="chat-input-form" class="chat-input-container">
<textarea id="chat-input" class="chat-input" data-i18n-placeholder="chatPlaceholder" placeholder="Type your message..." rows="1"></textarea>
<button id="chat-send-btn" type="submit" class="chat-send-btn" data-i18n-title="chatSend" title="Send">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</form>
</div>
<script src="lib/bootstrap.bundle.min.js"></script>
<script src="lib/gsap.min.js"></script>
<script src="lib/ScrollTrigger.min.js"></script>
@ -638,6 +819,11 @@
<script src="lib/chart.umd.min.js"></script>
<script src="js/i18n.js"></script>
<script type="module" src="js/main.js"></script>
<script type="module" src="js/activityViewer.js"></script>
<script type="module" src="js/m3u-generator.js"></script>
<script type="module" src="js/ai-tools.js"></script>
<script type="module" src="js/chat.js"></script>
</body>
</html>