Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
d7f483ab2c | |||
87a095179b | |||
98de6ec451 | |||
09261a2978 | |||
2345183a6d | |||
6d8f3b2ec5 | |||
e6106c149f | |||
419bfe0ab5 | |||
104d669ac9 | |||
e988ff15c8 |
49
README.md
49
README.md
@ -24,7 +24,7 @@ Get ready to be amazed by everything CinePlex has under the hood. This isn't jus
|
||||
|
||||
* **🎬 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!
|
||||
* **📡 Psychic Plex & Jellyfin Scanner:** You give it your Plex tokens or Jellyfin server details, 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!
|
||||
* **📊 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!
|
||||
@ -33,13 +33,14 @@ Get ready to be amazed by everything CinePlex has under the hood. This isn't jus
|
||||
* **🔥 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.
|
||||
* **🔭 Server Activity Viewer:** Curious about who's watching what on your Plex server? The Activity Viewer gives you a real-time look at active sessions. It's like having your own mission control!
|
||||
* **🔧 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!
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 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.
|
||||
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 universe like a starship captain.
|
||||
|
||||
### 1. Installing the Extension: The First Quantum Leap!
|
||||
|
||||
@ -56,31 +57,27 @@ 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. **Open CinePlex Settings:** 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!
|
||||
|
||||
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!
|
||||
2. **Connect to Your Servers:** CinePlex supports both Plex and Jellyfin. You can use either or both!
|
||||
|
||||
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:
|
||||
```json
|
||||
{
|
||||
"tokens": [
|
||||
"YourPlexTokenGoesHere_abc123",
|
||||
"AnotherTokenIfYouHaveOne_def456"
|
||||
]
|
||||
}
|
||||
```
|
||||
* Click the **"Save Tokens"** button. You've secured the connection!
|
||||
* **For Plex:**
|
||||
* **Find Your Plex Token:** 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!
|
||||
* In CinePlex settings, 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, separate them with commas.
|
||||
* Click **"Save Tokens"**.
|
||||
|
||||
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!
|
||||
* **For Jellyfin:**
|
||||
* In CinePlex settings, go to the **Jellyfin** tab.
|
||||
* Enter your Jellyfin server's URL, username, and password.
|
||||
* Click **"Connect and Scan"** to test the connection and perform an initial scan.
|
||||
|
||||
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!
|
||||
3. **Start Your First Scan:**
|
||||
* For Plex, go to the **Plex** tab, check the boxes for the content you want to scan (e.g., Movies, Series, Music, Photos), and click **"Start Scan"**.
|
||||
* For Jellyfin, the initial scan is done when you connect. You can re-scan at any time by clicking the button again.
|
||||
* 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.
|
||||
|
||||
4. **Enjoy!** Once the scan is complete, the app will automatically refresh. Go back to the main view and start exploring your newly supercharged media interface! The galaxy of your content awaits!
|
||||
|
||||
---
|
||||
|
||||
@ -97,10 +94,10 @@ Want to take your experience to the next level and use the "Add Stream" button?
|
||||
|
||||
---
|
||||
|
||||
## 🔒 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!
|
||||
* **`storage`**: This allows CinePlex to store your Plex tokens, Jellyfin server details, 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.
|
||||
* **`host_permissions` for `http://*/*` and `https://*/*`**: This is crucial! It allows CinePlex to communicate directly with your Plex and Jellyfin servers, which could be on any address on your local network (like `http://192.168.1.100:8096`) or on the web. Without this, CinePlex wouldn't be able to see your awesome media collection.
|
@ -15,7 +15,7 @@
|
||||
"navHistory": { "message": "Verlauf" },
|
||||
"navRecommendations": { "message": "Empfehlungen" },
|
||||
"navMusic": { "message": "Musik" },
|
||||
"heroWelcome": { "message": "Willkommen bei CinePlex" },
|
||||
"heroWelcome": { "message": "" },
|
||||
"heroSubtitle": { "message": "Entdecke Tausende von Filmen und Serien." },
|
||||
"addStream": { "message": "Stream hinzufügen" },
|
||||
"moreInfo": { "message": "Mehr Infos" },
|
||||
@ -58,6 +58,7 @@
|
||||
"settingsTitleFull": { "message": "Einstellungen und Konfiguration" },
|
||||
"settingsTabGeneral": { "message": "Allgemein" },
|
||||
"settingsTabPlex": { "message": "Plex" },
|
||||
"settingsTabJellyfin": { "message": "Jellyfin" },
|
||||
"settingsTabPhpGen": { "message": "PHP-Generator" },
|
||||
"settingsTabData": { "message": "Daten" },
|
||||
"settingsApiServer": { "message": "API- und Serverkonfiguration" },
|
||||
@ -80,6 +81,12 @@
|
||||
"settingsPlexTokens": { "message": "Plex-Tokens" },
|
||||
"settingsPlexTokensDesc": { "message": "Bearbeite die Liste der Plex-Tokens (JSON-Format)." },
|
||||
"settingsSaveTokens": { "message": "Tokens speichern" },
|
||||
"settingsJellyfinTitle": { "message": "Jellyfin-Einstellungen" },
|
||||
"settingsJellyfinDesc": { "message": "Füge die Daten deines Jellyfin-Servers hinzu, um dessen Inhalt zu scannen." },
|
||||
"jellyfinUrlLabel": { "message": "Jellyfin Server-URL" },
|
||||
"jellyfinUserLabel": { "message": "Benutzername" },
|
||||
"jellyfinPassLabel": { "message": "Passwort" },
|
||||
"jellyfinConnectAndScan": { "message": "Verbinden und Scannen" },
|
||||
"settingsPhpGenTitle": { "message": "PHP-Server-Skript-Generator" },
|
||||
"settingsPhpFileOptions": { "message": "Dateioptionen" },
|
||||
"settingsPhpSavePathLabel": { "message": "Speicherpfad auf dem Server" },
|
||||
@ -286,5 +293,39 @@
|
||||
"errorParsingPlexXml": { "message": "Fehler beim Parsen von Plex-XML." },
|
||||
"untitled": { "message": "Ohne Titel" },
|
||||
"itemCount": { "message": "$count$ Elemente", "placeholders": { "count": { "content": "$1" } } },
|
||||
"noPhotoServers": { "message": "Keine Foto-Server" }
|
||||
"noPhotoServers": { "message": "Keine Foto-Server" },
|
||||
"jellyfinScanInProgress": { "message": "Jellyfin-Scan läuft bereits." },
|
||||
"jellyfinScanning": { "message": "Scanne Jellyfin..." },
|
||||
"jellyfinMissingCredentials": { "message": "Bitte vervollständige die Jellyfin-URL und den Benutzernamen." },
|
||||
"jellyfinConnecting": { "message": "Verbinde mit Jellyfin unter: $url$", "placeholders": { "url": { "content": "$1" } } },
|
||||
"jellyfinAuthFailed": { "message": "Jellyfin-Authentifizierung fehlgeschlagen: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"jellyfinAuthSuccess": { "message": "Jellyfin-Authentifizierung erfolgreich." },
|
||||
"jellyfinFetchingLibraries": { "message": "Bibliotheken werden abgerufen..." },
|
||||
"jellyfinFetchFailed": { "message": "Fehler beim Abrufen der Bibliotheken: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"jellyfinNoMediaLibraries": { "message": "Keine Film- oder Serienbibliotheken auf Jellyfin gefunden." },
|
||||
"jellyfinLibrariesFound": { "message": "$count$ Medienbibliothek(en) gefunden.", "placeholders": { "count": { "content": "$1" } } },
|
||||
"jellyfinLibraryScanSuccess": { "message": "[Erfolg] '$libraryName gescannt, $count$ Titel hinzugefügt.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
|
||||
"jellyfinLibraryScanFailed": { "message": "Fehler beim Scannen der Bibliothek '$libraryName.", "placeholders": { "libraryName": { "content": "$1" } } },
|
||||
"jellyfinScanSuccess": { "message": "Jellyfin-Scan abgeschlossen. $movies$ Filme und $series$ Serien hinzugefügt.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
|
||||
"noJellyfinCredentials": { "message": "Jellyfin-Anmeldeinformationen nicht konfiguriert." },
|
||||
"notFoundOnJellyfin": { "message": "\"$query$\" auf Jellyfin nicht gefunden.", "placeholders": { "query": { "content": "$1" } } },
|
||||
"notFoundOnAnyServer": { "message": "\"$query$\" auf keinem Server gefunden.", "placeholders": { "query": { "content": "$1" } } },
|
||||
"localOnPlex": { "message": "Auf Plex" },
|
||||
"searchOnPlex": { "message": "Auf Plex suchen" },
|
||||
"jellyfinTitle": { "message": "Jellyfin-Inhalt" },
|
||||
"noJellyfinContent": { "message": "Kein Jellyfin-Inhalt gefunden." },
|
||||
"noJellyfinContentSub": { "message": "Stelle sicher, dass du deinen Jellyfin-Server in den Einstellungen gescannt hast." },
|
||||
"activityViewerTitle": { "message": "Server-Aktivitätsanzeige" },
|
||||
"activitySelectServer": { "message": "Wählen Sie einen Server aus" },
|
||||
"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-Kennung" },
|
||||
"activityCopyID": { "message": "ID kopieren" },
|
||||
"activityError": { "message": "Serveraktivität konnte nicht abgerufen werden." },
|
||||
"activityCopied": { "message": "Kennung in die Zwischenablage kopiert!" },
|
||||
"activityCopyError": { "message": "Fehler beim Kopieren der Kennung." }
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
"navHistory": { "message": "History" },
|
||||
"navRecommendations": { "message": "Recommendations" },
|
||||
"navMusic": { "message": "Music" },
|
||||
"heroWelcome": { "message": "Welcome to CinePlex" },
|
||||
"heroWelcome": { "message": "" },
|
||||
"heroSubtitle": { "message": "Explore thousands of movies and series." },
|
||||
"addStream": { "message": "Add Stream" },
|
||||
"moreInfo": { "message": "More Info" },
|
||||
@ -58,6 +58,7 @@
|
||||
"settingsTitleFull": { "message": "Settings and Configuration" },
|
||||
"settingsTabGeneral": { "message": "General" },
|
||||
"settingsTabPlex": { "message": "Plex" },
|
||||
"settingsTabJellyfin": { "message": "Jellyfin" },
|
||||
"settingsTabPhpGen": { "message": "PHP Generator" },
|
||||
"settingsTabData": { "message": "Data" },
|
||||
"settingsApiServer": { "message": "API and Server Configuration" },
|
||||
@ -80,6 +81,12 @@
|
||||
"settingsPlexTokens": { "message": "Plex Tokens" },
|
||||
"settingsPlexTokensDesc": { "message": "Edit the list of Plex tokens (JSON format)." },
|
||||
"settingsSaveTokens": { "message": "Save Tokens" },
|
||||
"settingsJellyfinTitle": { "message": "Jellyfin Settings" },
|
||||
"settingsJellyfinDesc": { "message": "Add your Jellyfin server details to scan its content." },
|
||||
"jellyfinUrlLabel": { "message": "Jellyfin Server URL" },
|
||||
"jellyfinUserLabel": { "message": "Username" },
|
||||
"jellyfinPassLabel": { "message": "Password" },
|
||||
"jellyfinConnectAndScan": { "message": "Connect and Scan" },
|
||||
"settingsPhpGenTitle": { "message": "PHP Server Script Generator" },
|
||||
"settingsPhpFileOptions": { "message": "File Options" },
|
||||
"settingsPhpSavePathLabel": { "message": "Save Path on Server" },
|
||||
@ -286,5 +293,39 @@
|
||||
"errorParsingPlexXml": { "message": "Error parsing Plex XML." },
|
||||
"untitled": { "message": "Untitled" },
|
||||
"itemCount": { "message": "$count$ items", "placeholders": { "count": { "content": "$1" } } },
|
||||
"noPhotoServers": { "message": "No photo servers" }
|
||||
"noPhotoServers": { "message": "No photo servers" },
|
||||
"jellyfinScanInProgress": { "message": "Jellyfin scan is already in progress." },
|
||||
"jellyfinScanning": { "message": "Scanning Jellyfin..." },
|
||||
"jellyfinMissingCredentials": { "message": "Please complete the Jellyfin URL and username." },
|
||||
"jellyfinConnecting": { "message": "Connecting to Jellyfin at: $url$", "placeholders": { "url": { "content": "$1" } } },
|
||||
"jellyfinAuthFailed": { "message": "Jellyfin authentication failed: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"jellyfinAuthSuccess": { "message": "Jellyfin authentication successful." },
|
||||
"jellyfinFetchingLibraries": { "message": "Fetching libraries..." },
|
||||
"jellyfinFetchFailed": { "message": "Failed to fetch libraries: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"jellyfinNoMediaLibraries": { "message": "No movie or series libraries found on Jellyfin." },
|
||||
"jellyfinLibrariesFound": { "message": "$count$ media library(s) found.", "placeholders": { "count": { "content": "$1" } } },
|
||||
"jellyfinLibraryScanSuccess": { "message": "[Success] Scanned '$libraryName, added $count$ titles.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
|
||||
"jellyfinLibraryScanFailed": { "message": "Failed to scan library '$libraryName." , "placeholders": { "libraryName": { "content": "$1" } } },
|
||||
"jellyfinScanSuccess": { "message": "Jellyfin scan completed. Added $movies$ movies and $series$ series.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
|
||||
"noJellyfinCredentials": { "message": "Jellyfin credentials not configured." },
|
||||
"notFoundOnJellyfin": { "message": "\"$query$\" not found on Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
|
||||
"notFoundOnAnyServer": { "message": "\"$query$\" not found on any server.", "placeholders": { "query": { "content": "$1" } } },
|
||||
"localOnPlex": { "message": "On Plex" },
|
||||
"searchOnPlex": { "message": "Search on Plex" },
|
||||
"jellyfinTitle": { "message": "Jellyfin Content" },
|
||||
"noJellyfinContent": { "message": "No Jellyfin content found." },
|
||||
"noJellyfinContentSub": { "message": "Make sure you have scanned your Jellyfin server in the settings." },
|
||||
"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 fetch server activity." },
|
||||
"activityCopied": { "message": "Identifier copied to clipboard!" },
|
||||
"activityCopyError": { "message": "Failed to copy identifier." }
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
"navHistory": { "message": "Historial" },
|
||||
"navRecommendations": { "message": "Recomendaciones" },
|
||||
"navMusic": { "message": "Música" },
|
||||
"heroWelcome": { "message": "Bienvenido a CinePlex" },
|
||||
"heroWelcome": { "message": "" },
|
||||
"heroSubtitle": { "message": "Explora miles de películas y series." },
|
||||
"addStream": { "message": "Añadir Stream" },
|
||||
"moreInfo": { "message": "Más información" },
|
||||
@ -58,6 +58,7 @@
|
||||
"settingsTitleFull": { "message": "Ajustes y Configuración" },
|
||||
"settingsTabGeneral": { "message": "General" },
|
||||
"settingsTabPlex": { "message": "Plex" },
|
||||
"settingsTabJellyfin": { "message": "Jellyfin" },
|
||||
"settingsTabPhpGen": { "message": "Generador PHP" },
|
||||
"settingsTabData": { "message": "Datos" },
|
||||
"settingsApiServer": { "message": "Configuración de API y Servidor" },
|
||||
@ -80,6 +81,12 @@
|
||||
"settingsPlexTokens": { "message": "Tokens de Plex" },
|
||||
"settingsPlexTokensDesc": { "message": "Edita la lista de tokens de Plex (formato JSON)." },
|
||||
"settingsSaveTokens": { "message": "Guardar Tokens" },
|
||||
"settingsJellyfinTitle": { "message": "Configuración de Jellyfin" },
|
||||
"settingsJellyfinDesc": { "message": "Añade los datos de tu servidor Jellyfin para escanear su contenido." },
|
||||
"jellyfinUrlLabel": { "message": "URL del Servidor Jellyfin" },
|
||||
"jellyfinUserLabel": { "message": "Nombre de Usuario" },
|
||||
"jellyfinPassLabel": { "message": "Contraseña" },
|
||||
"jellyfinConnectAndScan": { "message": "Conectar y Escanear" },
|
||||
"settingsPhpGenTitle": { "message": "Generador de Script PHP para el Servidor" },
|
||||
"settingsPhpFileOptions": { "message": "Opciones del Archivo" },
|
||||
"settingsPhpSavePathLabel": { "message": "Ruta de Guardado en el Servidor" },
|
||||
@ -162,7 +169,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." },
|
||||
@ -273,7 +280,7 @@
|
||||
"invalidStreamInfo": {"message": "Información inválida."},
|
||||
"dbUnavailableForStreams": {"message": "Base de datos local no disponible."},
|
||||
"noPlexServersForStreams": {"message": "No hay servidores Plex."},
|
||||
"notFoundOnServers": {"message": "No se encontró \"$query$\" en los servidores.", "placeholders": {"query": {"content": "$1"}}},
|
||||
"notFoundOnServers": {"message": "No se encontró \"$query$\" en los servidores de Plex.", "placeholders": {"query": {"content": "$1"}}},
|
||||
"relativeTime_justNow": { "message": "Ahora mismo" },
|
||||
"relativeTime_minutesAgo": { "message": "Hace $count$ minutos", "placeholders": { "count": { "content": "$1" } } },
|
||||
"relativeTime_hoursAgo": { "message": "Hace $count$ horas", "placeholders": { "count": { "content": "$1" } } },
|
||||
@ -286,5 +293,39 @@
|
||||
"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." }
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
"navHistory": { "message": "Historique" },
|
||||
"navRecommendations": { "message": "Recommandations" },
|
||||
"navMusic": { "message": "Musique" },
|
||||
"heroWelcome": { "message": "Bienvenue sur CinePlex" },
|
||||
"heroWelcome": { "message": "" },
|
||||
"heroSubtitle": { "message": "Explorez des milliers de films et de séries." },
|
||||
"addStream": { "message": "Ajouter le flux" },
|
||||
"moreInfo": { "message": "Plus d'infos" },
|
||||
@ -58,6 +58,7 @@
|
||||
"settingsTitleFull": { "message": "Paramètres et Configuration" },
|
||||
"settingsTabGeneral": { "message": "Général" },
|
||||
"settingsTabPlex": { "message": "Plex" },
|
||||
"settingsTabJellyfin": { "message": "Jellyfin" },
|
||||
"settingsTabPhpGen": { "message": "Générateur PHP" },
|
||||
"settingsTabData": { "message": "Données" },
|
||||
"settingsApiServer": { "message": "Configuration API et Serveur" },
|
||||
@ -80,6 +81,12 @@
|
||||
"settingsPlexTokens": { "message": "Tokens Plex" },
|
||||
"settingsPlexTokensDesc": { "message": "Modifiez la liste des tokens Plex (format JSON)." },
|
||||
"settingsSaveTokens": { "message": "Sauvegarder les Tokens" },
|
||||
"settingsJellyfinTitle": { "message": "Paramètres Jellyfin" },
|
||||
"settingsJellyfinDesc": { "message": "Ajoutez les informations de votre serveur Jellyfin pour scanner son contenu." },
|
||||
"jellyfinUrlLabel": { "message": "URL du serveur Jellyfin" },
|
||||
"jellyfinUserLabel": { "message": "Nom d'utilisateur" },
|
||||
"jellyfinPassLabel": { "message": "Mot de passe" },
|
||||
"jellyfinConnectAndScan": { "message": "Connecter et Scanner" },
|
||||
"settingsPhpGenTitle": { "message": "Générateur de Script PHP pour Serveur" },
|
||||
"settingsPhpFileOptions": { "message": "Options du Fichier" },
|
||||
"settingsPhpSavePathLabel": { "message": "Chemin de Sauvegarde sur le Serveur" },
|
||||
@ -286,5 +293,39 @@
|
||||
"errorParsingPlexXml": { "message": "Erreur d'analyse du XML de Plex." },
|
||||
"untitled": { "message": "Sans titre" },
|
||||
"itemCount": { "message": "$count$ éléments", "placeholders": { "count": { "content": "$1" } } },
|
||||
"noPhotoServers": { "message": "Aucun serveur de photos" }
|
||||
"noPhotoServers": { "message": "Aucun serveur de photos" },
|
||||
"jellyfinScanInProgress": { "message": "Le scan Jellyfin est déjà en cours." },
|
||||
"jellyfinScanning": { "message": "Scan de Jellyfin en cours..." },
|
||||
"jellyfinMissingCredentials": { "message": "Veuillez compléter l'URL et le nom d'utilisateur de Jellyfin." },
|
||||
"jellyfinConnecting": { "message": "Connexion à Jellyfin à : $url$", "placeholders": { "url": { "content": "$1" } } },
|
||||
"jellyfinAuthFailed": { "message": "Échec de l'authentification Jellyfin : $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"jellyfinAuthSuccess": { "message": "Authentification Jellyfin réussie." },
|
||||
"jellyfinFetchingLibraries": { "message": "Récupération des bibliothèques..." },
|
||||
"jellyfinFetchFailed": { "message": "Échec de la récupération des bibliothèques : $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"jellyfinNoMediaLibraries": { "message": "Aucune bibliothèque de films ou de séries trouvée sur Jellyfin." },
|
||||
"jellyfinLibrariesFound": { "message": "$count$ bibliothèque(s) multimédia(s) trouvée(s).", "placeholders": { "count": { "content": "$1" } } },
|
||||
"jellyfinLibraryScanSuccess": { "message": "[Succès] '$libraryName scannée, $count$ titres ajoutés.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
|
||||
"jellyfinLibraryScanFailed": { "message": "Échec du scan de la bibliothèque '$libraryName.", "placeholders": { "libraryName": { "content": "$1" } } },
|
||||
"jellyfinScanSuccess": { "message": "Scan Jellyfin terminé. $movies$ films et $series$ séries ajoutés.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
|
||||
"noJellyfinCredentials": { "message": "Identifiants Jellyfin non configurés." },
|
||||
"notFoundOnJellyfin": { "message": "\"$query$\" non trouvé sur Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
|
||||
"notFoundOnAnyServer": { "message": "\"$query$\" non trouvé sur aucun serveur.", "placeholders": { "query": { "content": "$1" } } },
|
||||
"localOnPlex": { "message": "Sur Plex" },
|
||||
"searchOnPlex": { "message": "Rechercher sur Plex" },
|
||||
"jellyfinTitle": { "message": "Contenu Jellyfin" },
|
||||
"noJellyfinContent": { "message": "Aucun contenu Jellyfin trouvé." },
|
||||
"noJellyfinContentSub": { "message": "Assurez-vous d'avoir scanné votre serveur Jellyfin dans les paramètres." },
|
||||
"activityViewerTitle": { "message": "Visualiseur 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 de récupérer l'activité du serveur." },
|
||||
"activityCopied": { "message": "Identifiant copié dans le presse-papiers !" },
|
||||
"activityCopyError": { "message": "Échec de la copie de l'identifiant." }
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
"navHistory": { "message": "Cronologia" },
|
||||
"navRecommendations": { "message": "Consigliati" },
|
||||
"navMusic": { "message": "Musica" },
|
||||
"heroWelcome": { "message": "Benvenuto su CinePlex" },
|
||||
"heroWelcome": { "message": "" },
|
||||
"heroSubtitle": { "message": "Esplora migliaia di film e serie TV." },
|
||||
"addStream": { "message": "Aggiungi Stream" },
|
||||
"moreInfo": { "message": "Più informazioni" },
|
||||
@ -58,6 +58,7 @@
|
||||
"settingsTitleFull": { "message": "Impostazioni e Configurazione" },
|
||||
"settingsTabGeneral": { "message": "Generale" },
|
||||
"settingsTabPlex": { "message": "Plex" },
|
||||
"settingsTabJellyfin": { "message": "Jellyfin" },
|
||||
"settingsTabPhpGen": { "message": "Generatore PHP" },
|
||||
"settingsTabData": { "message": "Dati" },
|
||||
"settingsApiServer": { "message": "Configurazione API e Server" },
|
||||
@ -80,6 +81,12 @@
|
||||
"settingsPlexTokens": { "message": "Token Plex" },
|
||||
"settingsPlexTokensDesc": { "message": "Modifica la lista dei token Plex (formato JSON)." },
|
||||
"settingsSaveTokens": { "message": "Salva Token" },
|
||||
"settingsJellyfinTitle": { "message": "Impostazioni Jellyfin" },
|
||||
"settingsJellyfinDesc": { "message": "Aggiungi i dati del tuo server Jellyfin per scansionarne il contenuto." },
|
||||
"jellyfinUrlLabel": { "message": "URL Server Jellyfin" },
|
||||
"jellyfinUserLabel": { "message": "Nome utente" },
|
||||
"jellyfinPassLabel": { "message": "Password" },
|
||||
"jellyfinConnectAndScan": { "message": "Connetti e Scansiona" },
|
||||
"settingsPhpGenTitle": { "message": "Generatore di Script PHP per Server" },
|
||||
"settingsPhpFileOptions": { "message": "Opzioni File" },
|
||||
"settingsPhpSavePathLabel": { "message": "Percorso di salvataggio sul server" },
|
||||
@ -286,5 +293,39 @@
|
||||
"errorParsingPlexXml": { "message": "Errore nell'analisi dell'XML di Plex." },
|
||||
"untitled": { "message": "Senza titolo" },
|
||||
"itemCount": { "message": "$count$ elementi", "placeholders": { "count": { "content": "$1" } } },
|
||||
"noPhotoServers": { "message": "Nessun server di foto" }
|
||||
"noPhotoServers": { "message": "Nessun server di foto" },
|
||||
"jellyfinScanInProgress": { "message": "Scansione Jellyfin già in corso." },
|
||||
"jellyfinScanning": { "message": "Scansione di Jellyfin in corso..." },
|
||||
"jellyfinMissingCredentials": { "message": "Per favore, completa l'URL e il nome utente di Jellyfin." },
|
||||
"jellyfinConnecting": { "message": "Connessione a Jellyfin a: $url$", "placeholders": { "url": { "content": "$1" } } },
|
||||
"jellyfinAuthFailed": { "message": "Autenticazione Jellyfin fallita: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"jellyfinAuthSuccess": { "message": "Autenticazione Jellyfin riuscita." },
|
||||
"jellyfinFetchingLibraries": { "message": "Recupero delle librerie..." },
|
||||
"jellyfinFetchFailed": { "message": "Recupero delle librerie fallito: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"jellyfinNoMediaLibraries": { "message": "Nessuna libreria di film o serie trovata su Jellyfin." },
|
||||
"jellyfinLibrariesFound": { "message": "$count$ libreria(e) multimediale(i) trovata(e).", "placeholders": { "count": { "content": "$1" } } },
|
||||
"jellyfinLibraryScanSuccess": { "message": "[Successo] Scansionata '$libraryName, aggiunti $count$ titoli.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
|
||||
"jellyfinLibraryScanFailed": { "message": "Scansione della libreria '$libraryName fallita.", "placeholders": { "libraryName": { "content": "$1" } } },
|
||||
"jellyfinScanSuccess": { "message": "Scansione Jellyfin completata. Aggiunti $movies$ film e $series$ serie.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
|
||||
"noJellyfinCredentials": { "message": "Credenziali di Jellyfin non configurate." },
|
||||
"notFoundOnJellyfin": { "message": "\"$query$\" non trovato su Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
|
||||
"notFoundOnAnyServer": { "message": "\"$query$\" non trovato su nessun server.", "placeholders": { "query": { "content": "$1" } } },
|
||||
"localOnPlex": { "message": "Su Plex" },
|
||||
"searchOnPlex": { "message": "Cerca su Plex" },
|
||||
"jellyfinTitle": { "message": "Contenuto Jellyfin" },
|
||||
"noJellyfinContent": { "message": "Nessun contenuto Jellyfin trovato." },
|
||||
"noJellyfinContentSub": { "message": "Assicurati di aver scansionato il tuo server Jellyfin nelle impostazioni." },
|
||||
"activityViewerTitle": { "message": "Visualizzatore Attività 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 Client" },
|
||||
"activityCopyID": { "message": "Copia ID" },
|
||||
"activityError": { "message": "Impossibile recuperare l'attività del server." },
|
||||
"activityCopied": { "message": "Identificatore copiato negli appunti!" },
|
||||
"activityCopyError": { "message": "Copia dell'identificatore non riuscita." }
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
"navHistory": { "message": "Histórico" },
|
||||
"navRecommendations": { "message": "Recomendações" },
|
||||
"navMusic": { "message": "Música" },
|
||||
"heroWelcome": { "message": "Bem-vindo ao CinePlex" },
|
||||
"heroWelcome": { "message": "" },
|
||||
"heroSubtitle": { "message": "Explore milhares de filmes e séries." },
|
||||
"addStream": { "message": "Adicionar Stream" },
|
||||
"moreInfo": { "message": "Mais Informações" },
|
||||
@ -58,6 +58,7 @@
|
||||
"settingsTitleFull": { "message": "Configurações e Ajustes" },
|
||||
"settingsTabGeneral": { "message": "Geral" },
|
||||
"settingsTabPlex": { "message": "Plex" },
|
||||
"settingsTabJellyfin": { "message": "Jellyfin" },
|
||||
"settingsTabPhpGen": { "message": "Gerador de PHP" },
|
||||
"settingsTabData": { "message": "Dados" },
|
||||
"settingsApiServer": { "message": "Configuração de API e Servidor" },
|
||||
@ -80,6 +81,12 @@
|
||||
"settingsPlexTokens": { "message": "Tokens do Plex" },
|
||||
"settingsPlexTokensDesc": { "message": "Edite a lista de tokens do Plex (formato JSON)." },
|
||||
"settingsSaveTokens": { "message": "Salvar Tokens" },
|
||||
"settingsJellyfinTitle": { "message": "Configurações do Jellyfin" },
|
||||
"settingsJellyfinDesc": { "message": "Adicione os detalhes do seu servidor Jellyfin para analisar seu conteúdo." },
|
||||
"jellyfinUrlLabel": { "message": "URL do Servidor Jellyfin" },
|
||||
"jellyfinUserLabel": { "message": "Nome de usuário" },
|
||||
"jellyfinPassLabel": { "message": "Senha" },
|
||||
"jellyfinConnectAndScan": { "message": "Conectar e Analisar" },
|
||||
"settingsPhpGenTitle": { "message": "Gerador de Script PHP para Servidor" },
|
||||
"settingsPhpFileOptions": { "message": "Opções de Arquivo" },
|
||||
"settingsPhpSavePathLabel": { "message": "Caminho para Salvar no Servidor" },
|
||||
@ -286,5 +293,39 @@
|
||||
"errorParsingPlexXml": { "message": "Erro ao analisar o XML do Plex." },
|
||||
"untitled": { "message": "Sem título" },
|
||||
"itemCount": { "message": "$count$ itens", "placeholders": { "count": { "content": "$1" } } },
|
||||
"noPhotoServers": { "message": "Nenhum servidor de fotos" }
|
||||
"noPhotoServers": { "message": "Nenhum servidor de fotos" },
|
||||
"jellyfinScanInProgress": { "message": "A análise do Jellyfin já está em andamento." },
|
||||
"jellyfinScanning": { "message": "Analisando Jellyfin..." },
|
||||
"jellyfinMissingCredentials": { "message": "Por favor, complete a URL e o nome de usuário do Jellyfin." },
|
||||
"jellyfinConnecting": { "message": "Conectando ao Jellyfin em: $url$", "placeholders": { "url": { "content": "$1" } } },
|
||||
"jellyfinAuthFailed": { "message": "A autenticação do Jellyfin falhou: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"jellyfinAuthSuccess": { "message": "Autenticação do Jellyfin bem-sucedida." },
|
||||
"jellyfinFetchingLibraries": { "message": "Buscando bibliotecas..." },
|
||||
"jellyfinFetchFailed": { "message": "Falha ao buscar bibliotecas: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"jellyfinNoMediaLibraries": { "message": "Nenhuma biblioteca de filmes ou séries encontrada no Jellyfin." },
|
||||
"jellyfinLibrariesFound": { "message": "$count$ biblioteca(s) de mídia encontrada(s).", "placeholders": { "count": { "content": "$1" } } },
|
||||
"jellyfinLibraryScanSuccess": { "message": "[Sucesso] Análise de '$libraryName' concluída, $count$ títulos adicionados.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
|
||||
"jellyfinLibraryScanFailed": { "message": "Falha ao analisar a biblioteca '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } },
|
||||
"jellyfinScanSuccess": { "message": "Análise do Jellyfin concluída. Adicionados $movies$ filmes e $series$ séries.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
|
||||
"noJellyfinCredentials": { "message": "Credenciais do Jellyfin não configuradas." },
|
||||
"notFoundOnJellyfin": { "message": "\"$query$\" não encontrado no Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
|
||||
"notFoundOnAnyServer": { "message": "\"$query$\" não encontrado em nenhum servidor.", "placeholders": { "query": { "content": "$1" } } },
|
||||
"localOnPlex": { "message": "No Plex" },
|
||||
"searchOnPlex": { "message": "Pesquisar no Plex" },
|
||||
"jellyfinTitle": { "message": "Conteúdo do Jellyfin" },
|
||||
"noJellyfinContent": { "message": "Nenhum conteúdo do Jellyfin encontrado." },
|
||||
"noJellyfinContentSub": { "message": "Certifique-se de que você analisou seu servidor Jellyfin nas configurações." },
|
||||
"activityViewerTitle": { "message": "Visualizador de Atividade 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 buscar a atividade do servidor." },
|
||||
"activityCopied": { "message": "Identificador copiado para a área de transferência!" },
|
||||
"activityCopyError": { "message": "Falha ao copiar o identificador." }
|
||||
}
|
68
css/activity-viewer.css
Normal file
68
css/activity-viewer.css
Normal file
@ -0,0 +1,68 @@
|
||||
#activityViewerModal .modal-body {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.session-card {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
padding: 1.2rem;
|
||||
background-color: var(--glass);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
margin-bottom: 1rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.session-card:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.session-poster {
|
||||
width: 80px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--border-radius-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.session-details p {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.session-details strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.session-identifier {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.session-identifier label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.session-identifier .input-group .form-control {
|
||||
background-color: var(--primary) !important;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
#activity-results .empty-state {
|
||||
padding: 2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
background: var(--secondary);
|
||||
padding: 1.5rem 2rem;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
margin-top: 4rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.footer .container {
|
||||
|
@ -7,7 +7,7 @@
|
||||
max-height: 800px;
|
||||
overflow: hidden;
|
||||
background-color: var(--primary);
|
||||
margin-bottom: 3rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.hero::before {
|
||||
@ -17,11 +17,15 @@
|
||||
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%),
|
||||
background: linear-gradient(to top, var(--primary) 5%, rgba(0, 0, 0, 0.8) 40%, rgba(0, 0, 0, 0.4) 70%, transparent 100%),
|
||||
linear-gradient(to right, var(--primary) 10%, transparent 70%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero.no-overlay::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero-background-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
13
css/main.css
13
css/main.css
@ -9,4 +9,15 @@
|
||||
@import url('footer.css');
|
||||
@import url('overlays.css');
|
||||
@import url('music-player.css');
|
||||
@import url('photos.css');
|
||||
@import url('photos.css');
|
||||
@import url('activity-viewer.css');
|
||||
|
||||
/* Styles to manage hero loading state and content section visibility */
|
||||
|
||||
.hero.loading .hero-content {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hero:not(.loading) .hero-content {
|
||||
opacity: 1;
|
||||
}
|
@ -574,6 +574,39 @@ body.miniplayer-active #musicPlayerContainer {
|
||||
box-shadow: 0 0 15px rgba(0, 224, 255, 0.4);
|
||||
}
|
||||
|
||||
#closeMiniplayerBtn {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
#closeMiniplayerBtn:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.fab-btn {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--accent);
|
||||
color: var(--primary);
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
z-index: 1030;
|
||||
}
|
||||
|
||||
.fab-btn:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.time-and-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -155,9 +155,9 @@ body.light-theme .sidebar-nav {
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
#sidebar-toggle {
|
||||
/* #sidebar-toggle {
|
||||
display: none;
|
||||
}
|
||||
} */
|
||||
.sidebar-nav {
|
||||
transform: translateX(0);
|
||||
}
|
||||
@ -215,4 +215,16 @@ body.light-theme .sidebar-nav {
|
||||
height: 36px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.sidebar-open .sidebar-nav {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
body.sidebar-collapsed .sidebar-nav {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
body.sidebar-collapsed #main-container {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
BIN
img/hero-def.png
Normal file
BIN
img/hero-def.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
155
js/activityViewer.js
Normal file
155
js/activityViewer.js
Normal 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();
|
||||
}
|
||||
}
|
141
js/api.js
141
js/api.js
@ -12,13 +12,13 @@ export async function fetchTMDB(endpoint, signal) {
|
||||
'fr': 'fr-FR',
|
||||
'de': 'de-DE',
|
||||
'it': 'it-IT',
|
||||
'pt': 'pt-BR'
|
||||
'pt': 'pt-BR'
|
||||
};
|
||||
|
||||
if (langMap[state.settings.language]) {
|
||||
tmdbLang = langMap[state.settings.language];
|
||||
}
|
||||
|
||||
|
||||
const separator = endpoint.includes('?') ? '&' : '?';
|
||||
const url = `https://api.themoviedb.org/3/${endpoint}${separator}language=${tmdbLang}&api_key=${state.settings.apiKey}`;
|
||||
const response = await fetch(url, { signal });
|
||||
@ -29,12 +29,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 +57,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}`;
|
||||
@ -75,7 +86,7 @@ export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artista
|
||||
albumIndex: parseInt(track.getAttribute("parentIndex") || 0, 10)
|
||||
};
|
||||
}).filter(track => track !== null);
|
||||
|
||||
|
||||
tracks.sort((a, b) => {
|
||||
if (a.albumIndex !== b.albumIndex) {
|
||||
return a.albumIndex - b.albumIndex;
|
||||
@ -143,26 +154,26 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
|
||||
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];
|
||||
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 +223,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 +231,108 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
|
||||
} else {
|
||||
return { success: false, streams: [], message: _('notFoundOnServers', busqueda) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAllStreamsFromJellyfin(busqueda, tipoContenido) {
|
||||
if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') };
|
||||
|
||||
const { url, userId, apiKey } = state.jellyfinSettings;
|
||||
if (!url || !userId || !apiKey) return { success: false, streams: [], message: _('noJellyfinCredentials') };
|
||||
|
||||
const jellyfinSearchType = tipoContenido === 'movie' ? 'Movie' : 'Series';
|
||||
const searchUrl = `${url}/Users/${userId}/Items?searchTerm=${encodeURIComponent(busqueda)}&IncludeItemTypes=${jellyfinSearchType}&Recursive=true`;
|
||||
|
||||
try {
|
||||
const response = await fetch(searchUrl, { headers: { 'X-Emby-Token': apiKey } });
|
||||
if (!response.ok) throw new Error(`Error buscando en Jellyfin: ${response.status}`);
|
||||
const searchData = await response.json();
|
||||
|
||||
if (!searchData.Items || searchData.Items.length === 0) {
|
||||
return { success: false, streams: [], message: _('notFoundOnJellyfin', busqueda) };
|
||||
}
|
||||
|
||||
const item = searchData.Items.find(i => i.Name.toLowerCase() === busqueda.toLowerCase()) || searchData.Items[0];
|
||||
const itemId = item.Id;
|
||||
const itemName = item.Name;
|
||||
const itemYear = item.ProductionYear;
|
||||
const posterTag = item.ImageTags?.Primary;
|
||||
const posterUrl = posterTag ? `${url}/Items/${itemId}/Images/Primary?tag=${posterTag}` : '';
|
||||
|
||||
let streams = [];
|
||||
|
||||
if (item.Type === 'Movie') {
|
||||
const streamUrl = `${url}/Videos/${itemId}/stream?api_key=${apiKey}`;
|
||||
const extinfName = `${itemName}${itemYear ? ` (${itemYear})` : ''}`;
|
||||
const groupTitle = extinfName.replace(/"/g, "'");
|
||||
|
||||
streams.push({
|
||||
url: streamUrl,
|
||||
title: extinfName,
|
||||
extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${posterUrl}" group-title="${groupTitle}",${extinfName}`
|
||||
});
|
||||
} else if (item.Type === 'Series') {
|
||||
const episodesUrl = `${url}/Shows/${itemId}/Episodes?userId=${userId}`;
|
||||
const episodesResponse = await fetch(episodesUrl, { headers: { 'X-Emby-Token': apiKey } });
|
||||
if (!episodesResponse.ok) throw new Error(`Error obteniendo episodios: ${episodesResponse.status}`);
|
||||
const episodesData = await episodesResponse.json();
|
||||
|
||||
const sortedEpisodes = episodesData.Items.sort((a, b) => {
|
||||
if (a.ParentIndexNumber !== b.ParentIndexNumber) return (a.ParentIndexNumber || 0) - (b.ParentIndexNumber || 0);
|
||||
return (a.IndexNumber || 0) - (b.IndexNumber || 0);
|
||||
});
|
||||
|
||||
sortedEpisodes.forEach(ep => {
|
||||
const streamUrl = `${url}/Videos/${ep.Id}/stream?api_key=${apiKey}`;
|
||||
const seasonNum = ep.ParentIndexNumber || 'S';
|
||||
const episodeNum = ep.IndexNumber || 'E';
|
||||
const episodeTitle = ep.Name || 'Episodio';
|
||||
const groupTitle = `${itemName} - Temporada ${seasonNum}`.replace(/"/g, "'");
|
||||
const extinfName = `${itemName} T${seasonNum}E${episodeNum} ${episodeTitle}`;
|
||||
|
||||
streams.push({
|
||||
url: streamUrl,
|
||||
title: extinfName,
|
||||
extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${posterUrl}" group-title="${groupTitle}",${extinfName}`
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true, streams };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching streams from Jellyfin:", error);
|
||||
return { success: false, streams: [], message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAllAvailableStreams(title, type) {
|
||||
const plexPromise = fetchAllStreamsFromPlex(title, type);
|
||||
const jellyfinPromise = fetchAllStreamsFromJellyfin(title, type);
|
||||
|
||||
const results = await Promise.allSettled([plexPromise, jellyfinPromise]);
|
||||
|
||||
let allStreams = [];
|
||||
const errorMessages = [];
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const sourceName = index === 0 ? 'Plex' : 'Jellyfin';
|
||||
if (result.status === 'fulfilled' && result.value.success) {
|
||||
allStreams.push(...result.value.streams);
|
||||
} else if (result.status === 'fulfilled' && !result.value.success) {
|
||||
if (result.value.message !== _('noPlexServersForStreams') && result.value.message !== _('noJellyfinCredentials')) {
|
||||
errorMessages.push(`${sourceName}: ${result.value.message}`);
|
||||
}
|
||||
} else if (result.status === 'rejected') {
|
||||
errorMessages.push(`${sourceName}: ${result.reason.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
const uniqueStreamsMap = new Map(allStreams.map(stream => [stream.url, stream]));
|
||||
const uniqueStreams = Array.from(uniqueStreamsMap.values());
|
||||
|
||||
if (uniqueStreams.length > 0) {
|
||||
return { success: true, streams: uniqueStreams, message: `Found ${uniqueStreams.length} streams.` };
|
||||
} else {
|
||||
return { success: false, streams: [], message: errorMessages.join('; ') || _('notFoundOnAnyServer', title) };
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
export const config = {
|
||||
defaultApiKey: '4e44d9029b1270a757cddc766a1bcb63',
|
||||
dbName: 'PlexDB',
|
||||
dbVersion: 6,
|
||||
dbVersion: 7,
|
||||
};
|
11
js/db.js
11
js/db.js
@ -13,14 +13,17 @@ export function initDB() {
|
||||
request.onupgradeneeded = e => {
|
||||
state.db = e.target.result;
|
||||
const transaction = e.target.transaction;
|
||||
const storesToCreate = ['movies', 'series', 'artists', 'photos', 'tokens', 'conexiones_locales', 'settings'];
|
||||
const storesToCreate = ['movies', 'series', 'artists', 'photos', 'tokens', 'conexiones_locales', 'settings', 'jellyfin_settings', 'jellyfin_movies', 'jellyfin_series'];
|
||||
|
||||
storesToCreate.forEach(storeName => {
|
||||
if (!state.db.objectStoreNames.contains(storeName)) {
|
||||
let storeOptions;
|
||||
if (storeName === 'settings') {
|
||||
if (['settings', 'jellyfin_settings'].includes(storeName)) {
|
||||
storeOptions = { keyPath: 'id' };
|
||||
} else {
|
||||
} else if (['jellyfin_movies', 'jellyfin_series'].includes(storeName)) {
|
||||
storeOptions = { keyPath: 'libraryId' };
|
||||
}
|
||||
else {
|
||||
storeOptions = { keyPath: 'id', autoIncrement: true };
|
||||
}
|
||||
const store = state.db.createObjectStore(storeName, storeOptions);
|
||||
@ -126,7 +129,7 @@ export function addItemsToStore(storeName, items) {
|
||||
export async function clearContentData() {
|
||||
showNotification(_("deletingContentData"), "info");
|
||||
mostrarSpinner();
|
||||
const storesToDelete = ['movies', 'series', 'artists', 'photos', 'conexiones_locales'];
|
||||
const storesToDelete = ['movies', 'series', 'artists', 'photos', 'conexiones_locales', 'jellyfin_movies', 'jellyfin_series'];
|
||||
try {
|
||||
if (!state.db) throw new Error(_("dbNotAvailable"));
|
||||
const storesPresent = storesToDelete.filter(name => state.db.objectStoreNames.contains(name));
|
||||
|
@ -1,13 +1,14 @@
|
||||
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, 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 { 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,9 +29,19 @@ 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'); });
|
||||
@ -48,6 +59,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++;
|
||||
@ -109,6 +122,8 @@ export function setupEventListeners() {
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('jellyfinScanBtn').addEventListener('click', startJellyfinScan);
|
||||
|
||||
document.getElementById('clearDataBtn').addEventListener('click', () => {
|
||||
if (confirm(_('confirmClearContent'))) {
|
||||
clearContentData();
|
||||
@ -144,9 +159,26 @@ export function setupEventListeners() {
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
// Check if hero section is active but background is gone
|
||||
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');
|
||||
const eqBtn = document.getElementById('eqBtn');""
|
||||
const closeEqBtn = document.getElementById('closeEqBtn');
|
||||
const equalizerPanel = document.getElementById('equalizer-panel');
|
||||
|
||||
@ -235,7 +267,7 @@ function handleMainViewClick(e) {
|
||||
handlePhotoGridClick(photoCard);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const card = e.target.closest('.item-card');
|
||||
if (!card) return;
|
||||
|
||||
|
209
js/jellyfin.js
Normal file
209
js/jellyfin.js
Normal file
@ -0,0 +1,209 @@
|
||||
import { state } from './state.js';
|
||||
import { addItemsToStore, clearStore } from './db.js';
|
||||
import { showNotification, _, emitirEventoActualizacion } from './utils.js';
|
||||
|
||||
async function authenticateJellyfin(url, username, password) {
|
||||
const authUrl = `${url}/Users/AuthenticateByName`;
|
||||
const body = JSON.stringify({
|
||||
Username: username,
|
||||
Pw: password
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(authUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Emby-Authorization': 'MediaBrowser Client="CinePlex", Device="Chrome", DeviceId="cineplex-jellyfin-integration", Version="1.0"'
|
||||
},
|
||||
body: body
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.AuthenticationResult?.ErrorMessage || `Error ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { success: true, token: data.AccessToken, userId: data.User.Id };
|
||||
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchLibraryViews(url, userId, apiKey) {
|
||||
const viewsUrl = `${url}/Users/${userId}/Views`;
|
||||
try {
|
||||
const response = await fetch(viewsUrl, {
|
||||
headers: {
|
||||
'X-Emby-Token': apiKey
|
||||
}
|
||||
});
|
||||
if (!response.ok) throw new Error(`Error ${response.status} fetching library views`);
|
||||
const data = await response.json();
|
||||
return { success: true, views: data.Items };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function fetchItemsFromLibrary(url, userId, apiKey, library) {
|
||||
const itemsUrl = `${url}/Users/${userId}/Items?ParentId=${library.Id}&recursive=true&fields=ProductionYear&includeItemTypes=Movie,Series`;
|
||||
|
||||
try {
|
||||
const response = await fetch(itemsUrl, {
|
||||
headers: {
|
||||
'X-Emby-Token': apiKey
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`Error ${response.status}`);
|
||||
|
||||
const data = await response.json();
|
||||
const items = data.Items.map(item => ({
|
||||
id: item.Id,
|
||||
title: item.Name,
|
||||
year: item.ProductionYear,
|
||||
type: item.Type,
|
||||
posterTag: item.ImageTags?.Primary,
|
||||
}));
|
||||
|
||||
return { success: true, items, libraryName: library.Name, libraryId: library.Id };
|
||||
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message, libraryName: library.Name, libraryId: library.Id };
|
||||
}
|
||||
}
|
||||
|
||||
export async function startJellyfinScan() {
|
||||
if (state.isScanningJellyfin) {
|
||||
showNotification(_('jellyfinScanInProgress'), 'warning');
|
||||
return;
|
||||
}
|
||||
state.isScanningJellyfin = true;
|
||||
|
||||
const statusDiv = document.getElementById('jellyfinScanStatus');
|
||||
const scanBtn = document.getElementById('jellyfinScanBtn');
|
||||
const originalBtnText = scanBtn.innerHTML;
|
||||
scanBtn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${_('jellyfinScanning')}`;
|
||||
scanBtn.disabled = true;
|
||||
|
||||
const urlInput = document.getElementById('jellyfinServerUrl');
|
||||
const usernameInput = document.getElementById('jellyfinUsername');
|
||||
const passwordInput = document.getElementById('jellyfinPassword');
|
||||
|
||||
let url = urlInput.value.trim();
|
||||
const username = usernameInput.value.trim();
|
||||
const password = passwordInput.value;
|
||||
|
||||
if (!url || !username) {
|
||||
showNotification(_('jellyfinMissingCredentials'), 'error');
|
||||
state.isScanningJellyfin = false;
|
||||
scanBtn.innerHTML = originalBtnText;
|
||||
scanBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
url = url.replace(/\/web\/.*$/, '').replace(/\/$/, '');
|
||||
statusDiv.innerHTML = `<div class="text-info">${_('jellyfinConnecting', url)}</div>`;
|
||||
|
||||
const authResult = await authenticateJellyfin(url, username, password);
|
||||
|
||||
if (!authResult.success) {
|
||||
statusDiv.innerHTML = `<div class="text-danger">${_('jellyfinAuthFailed', authResult.message)}</div>`;
|
||||
showNotification(_('jellyfinAuthFailed', authResult.message), 'error');
|
||||
state.isScanningJellyfin = false;
|
||||
scanBtn.innerHTML = originalBtnText;
|
||||
scanBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
statusDiv.innerHTML = `<div class="text-success">${_('jellyfinAuthSuccess')}</div><div class="text-info">${_('jellyfinFetchingLibraries')}</div>`;
|
||||
|
||||
const viewsResult = await fetchLibraryViews(url, authResult.userId, authResult.token);
|
||||
|
||||
if (!viewsResult.success) {
|
||||
statusDiv.innerHTML += `<div class="text-danger">${_('jellyfinFetchFailed', viewsResult.message)}</div>`;
|
||||
state.isScanningJellyfin = false;
|
||||
scanBtn.innerHTML = originalBtnText;
|
||||
scanBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaLibraries = viewsResult.views.filter(v => v.CollectionType === 'movies' || v.CollectionType === 'tvshows');
|
||||
|
||||
if (mediaLibraries.length === 0) {
|
||||
statusDiv.innerHTML += `<div class="text-warning">${_('jellyfinNoMediaLibraries')}</div>`;
|
||||
state.isScanningJellyfin = false;
|
||||
scanBtn.innerHTML = originalBtnText;
|
||||
scanBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
statusDiv.innerHTML += `<div class="text-info">${_('jellyfinLibrariesFound', String(mediaLibraries.length))}</div>`;
|
||||
|
||||
await clearStore('jellyfin_movies');
|
||||
await clearStore('jellyfin_series');
|
||||
|
||||
let totalMovies = 0;
|
||||
let totalSeries = 0;
|
||||
|
||||
const scanPromises = mediaLibraries.map(library =>
|
||||
fetchItemsFromLibrary(url, authResult.userId, authResult.token, library)
|
||||
);
|
||||
|
||||
const results = await Promise.allSettled(scanPromises);
|
||||
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled' && result.value.success) {
|
||||
const library = mediaLibraries.find(lib => lib.Id === result.value.libraryId);
|
||||
if (library) {
|
||||
const storeName = library.CollectionType === 'movies' ? 'jellyfin_movies' : 'jellyfin_series';
|
||||
const dbEntry = {
|
||||
serverUrl: url,
|
||||
libraryId: library.Id,
|
||||
libraryName: library.Name,
|
||||
titulos: result.value.items,
|
||||
};
|
||||
await addItemsToStore(storeName, [dbEntry]);
|
||||
if (storeName === 'jellyfin_movies') {
|
||||
totalMovies += result.value.items.length;
|
||||
} else {
|
||||
totalSeries += result.value.items.length;
|
||||
}
|
||||
statusDiv.innerHTML += `<div class="text-success-secondary">${_('jellyfinLibraryScanSuccess', [library.Name, String(result.value.items.length)])}</div>`;
|
||||
}
|
||||
} else {
|
||||
const libraryName = result.reason?.libraryName || result.value?.libraryName || 'Unknown';
|
||||
statusDiv.innerHTML += `<div class="text-warning">${_('jellyfinLibraryScanFailed', libraryName)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
const newSettings = {
|
||||
id: 'jellyfin_credentials',
|
||||
url: url,
|
||||
username: username,
|
||||
password: password,
|
||||
apiKey: authResult.token,
|
||||
userId: authResult.userId
|
||||
};
|
||||
|
||||
await addItemsToStore('jellyfin_settings', [newSettings]);
|
||||
state.jellyfinSettings = newSettings;
|
||||
|
||||
const message = _('jellyfinScanSuccess', [String(totalMovies), String(totalSeries)]);
|
||||
statusDiv.innerHTML += `<div class="text-success mt-2">${message}</div>`;
|
||||
showNotification(message, 'success');
|
||||
|
||||
setTimeout(() => {
|
||||
const modalInstance = bootstrap.Modal.getInstance(document.getElementById('settingsModal'));
|
||||
if(modalInstance) modalInstance.hide();
|
||||
emitirEventoActualizacion();
|
||||
}, 2000);
|
||||
|
||||
state.isScanningJellyfin = false;
|
||||
scanBtn.innerHTML = originalBtnText;
|
||||
scanBtn.disabled = false;
|
||||
}
|
@ -2,6 +2,7 @@ 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 { setupEventListeners } from './eventListeners.js';
|
||||
import { loadInitialContent, initializeFavorites, initializeUserData, loadLocalContent, applyTheme, applyHeroVisibility } from './ui.js';
|
||||
import { showNotification, _ } from './utils.js';
|
||||
@ -18,6 +19,12 @@ async function loadSettings() {
|
||||
if (!state.settings.apiKey) {
|
||||
state.settings.apiKey = config.defaultApiKey;
|
||||
}
|
||||
|
||||
const jellyfinSettingsData = await getFromDB('jellyfin_settings');
|
||||
if (jellyfinSettingsData && jellyfinSettingsData.length > 0) {
|
||||
state.jellyfinSettings = { ...state.jellyfinSettings, ...jellyfinSettingsData[0] };
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Could not load settings from DB, using defaults.", error);
|
||||
state.settings.language = chrome.i18n.getUILanguage().split('-')[0];
|
||||
@ -36,6 +43,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
state.musicPlayer = new MusicPlayer();
|
||||
state.musicPlayer.setDB(state.db);
|
||||
state.activityViewer = new ActivityViewer(document.getElementById('activityViewerModal'));
|
||||
|
||||
|
||||
initializeFavorites();
|
||||
initializeUserData();
|
||||
|
@ -5,6 +5,7 @@ import { getMusicUrlsFromPlex } from './api.js';
|
||||
export class MusicPlayer {
|
||||
constructor() {
|
||||
this.cancionesActuales = [];
|
||||
this.displayedSongs = [];
|
||||
this.indiceActual = -1;
|
||||
this.isPlaying = false;
|
||||
this.audioPlayer = document.getElementById("audioPlayer");
|
||||
@ -23,6 +24,7 @@ export class MusicPlayer {
|
||||
this.isPlayerVisible = false;
|
||||
this.isReady = false;
|
||||
this.isInitializing = false;
|
||||
this.miniplayerManuallyClosed = false;
|
||||
}
|
||||
|
||||
setDB(databaseInstance) {
|
||||
@ -79,6 +81,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());
|
||||
@ -128,8 +132,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(this.cancionesActuales[newIndex], newIndex);
|
||||
} else {
|
||||
this.playSong(this.cancionesActuales[index], index);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -211,6 +222,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;
|
||||
@ -428,24 +464,18 @@ export class MusicPlayer {
|
||||
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 +485,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 +502,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 +512,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);
|
||||
@ -517,7 +548,7 @@ 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');
|
||||
@ -545,10 +576,13 @@ export class MusicPlayer {
|
||||
this.currentSongId = cancion.id;
|
||||
this.currentSongArtistId = cancion.artistId;
|
||||
this.markCurrentSong();
|
||||
this.markCurrentArtist(cancion.artistId);
|
||||
this.ensureArtistVisible(cancion.artistId);
|
||||
if (playIconElement) {
|
||||
playIconElement.className = 'fas fa-play play-icon';
|
||||
}
|
||||
if (!this.miniplayerManuallyClosed) {
|
||||
document.getElementById('fab-music-player').style.display = 'none';
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.handleAudioError(_('playbackError'));
|
||||
if (playIconElement) {
|
||||
@ -573,6 +607,9 @@ 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() {
|
||||
@ -629,14 +666,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() {
|
||||
@ -811,4 +850,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);
|
||||
}
|
||||
}
|
12
js/state.js
12
js/state.js
@ -15,6 +15,16 @@ export const state = {
|
||||
phpFilename: 'CinePlex_Playlist.m3u',
|
||||
phpFileAction: 'append',
|
||||
},
|
||||
jellyfinSettings: {
|
||||
id: 'jellyfin_credentials',
|
||||
url: '',
|
||||
username: '',
|
||||
password: '',
|
||||
apiKey: '',
|
||||
userId: '',
|
||||
},
|
||||
jellyfinMovies: [],
|
||||
jellyfinSeries: [],
|
||||
localMovies: [],
|
||||
localSeries: [],
|
||||
localArtists: [],
|
||||
@ -32,7 +42,9 @@ export const state = {
|
||||
isAddingStream: false,
|
||||
isDownloadingM3U: false,
|
||||
isScanningPlex: false,
|
||||
isScanningJellyfin: false,
|
||||
musicPlayer: null,
|
||||
activityViewer: null,
|
||||
currentContentFetchController: null,
|
||||
plexScanAbortController: null,
|
||||
aceEditor: null,
|
||||
|
345
js/ui.js
345
js/ui.js
@ -1,5 +1,5 @@
|
||||
import { state } from './state.js';
|
||||
import { fetchTMDB, fetchAllStreamsFromPlex } from './api.js';
|
||||
import { fetchTMDB, fetchAllAvailableStreams } from './api.js';
|
||||
import { showNotification, getRelativeTime, fetchWithTimeout, _ } from './utils.js';
|
||||
import { getFromDB, addItemsToStore } from './db.js';
|
||||
|
||||
@ -7,7 +7,7 @@ let charts = {};
|
||||
|
||||
export async function loadInitialContent() {
|
||||
await Promise.all([loadGenres(), loadYears()]);
|
||||
resetView(); // Show hero-only view first
|
||||
resetView();
|
||||
setupScrollEffects();
|
||||
}
|
||||
|
||||
@ -30,11 +30,21 @@ export function initializeUserData() {
|
||||
export async function loadLocalContent() {
|
||||
if (!state.db) return;
|
||||
try {
|
||||
const [movies, series, artists, photos] = await Promise.all([getFromDB('movies'), getFromDB('series'), getFromDB('artists'), getFromDB('photos')]);
|
||||
const [movies, series, artists, photos, jfMovies, jfSeries] = await Promise.all([
|
||||
getFromDB('movies'),
|
||||
getFromDB('series'),
|
||||
getFromDB('artists'),
|
||||
getFromDB('photos'),
|
||||
getFromDB('jellyfin_movies'),
|
||||
getFromDB('jellyfin_series')
|
||||
]);
|
||||
state.localMovies = movies;
|
||||
state.localSeries = series;
|
||||
state.localArtists = artists;
|
||||
state.localPhotos = photos;
|
||||
state.jellyfinMovies = jfMovies;
|
||||
state.jellyfinSeries = jfSeries;
|
||||
|
||||
} catch (error) {
|
||||
showNotification(_("errorLoadingLocalContent"), "error");
|
||||
}
|
||||
@ -44,17 +54,54 @@ export function resetView() {
|
||||
if (state.isLoading) return;
|
||||
|
||||
const heroSection = document.getElementById('hero-section');
|
||||
if (heroSection) {
|
||||
heroSection.style.display = 'flex';
|
||||
if (state.settings.showHero) {
|
||||
initializeHeroSection();
|
||||
}
|
||||
}
|
||||
|
||||
const mainContent = document.getElementById('main-content');
|
||||
const contentSection = document.getElementById('content-section');
|
||||
const heroContent = document.querySelector('.hero-content');
|
||||
const heroBg1 = document.querySelector('.hero-background-1');
|
||||
const heroBg2 = document.querySelector('.hero-background-2');
|
||||
|
||||
// Hide all main content sections
|
||||
if (mainContent) {
|
||||
mainContent.style.display = 'none';
|
||||
}
|
||||
if (contentSection) {
|
||||
contentSection.style.display = 'none';
|
||||
}
|
||||
document.getElementById('stats-section').style.display = 'none';
|
||||
document.getElementById('history-section').style.display = 'none';
|
||||
document.getElementById('recommendations-section').style.display = 'none';
|
||||
document.getElementById('photos-section').style.display = 'none';
|
||||
|
||||
// Show hero if enabled
|
||||
if (heroSection) {
|
||||
if (state.settings.showHero) {
|
||||
heroSection.style.display = 'flex';
|
||||
|
||||
// Clear dynamic hero content and reset to default
|
||||
if (state.heroIntervalId) {
|
||||
clearInterval(state.heroIntervalId);
|
||||
state.heroIntervalId = null;
|
||||
}
|
||||
|
||||
if (heroContent) {
|
||||
heroContent.querySelector('.hero-title').textContent = _('welcomeToCinePlex');
|
||||
heroContent.querySelector('.hero-subtitle').textContent = _('welcomeSubtitle');
|
||||
heroContent.querySelector('#hero-rating').innerHTML = '';
|
||||
heroContent.querySelector('#hero-year').innerHTML = '';
|
||||
heroContent.querySelector('#hero-extra').innerHTML = '';
|
||||
heroContent.querySelector('.hero-buttons').style.display = 'none';
|
||||
}
|
||||
|
||||
gsap.set(heroBg1, { backgroundImage: 'url(img/hero-def.png)', autoAlpha: 1, scale: 1 });
|
||||
gsap.set(heroBg2, { autoAlpha: 0 });
|
||||
heroSection.classList.add('no-overlay');
|
||||
|
||||
initializeHeroSection();
|
||||
|
||||
} else {
|
||||
heroSection.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
state.currentView = 'home';
|
||||
updateActiveNav('home');
|
||||
@ -62,11 +109,22 @@ export function resetView() {
|
||||
}
|
||||
|
||||
export function switchView(viewType) {
|
||||
console.log(`switchView called with viewType: ${viewType}`);
|
||||
if (state.isLoading) return;
|
||||
|
||||
const heroSection = document.getElementById('hero-section');
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
|
||||
if (heroSection) {
|
||||
heroSection.style.display = 'none';
|
||||
if (state.heroIntervalId) {
|
||||
clearInterval(state.heroIntervalId);
|
||||
state.heroIntervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (mainContent) {
|
||||
mainContent.style.display = 'block'; // Ensure main content is visible when switching views
|
||||
}
|
||||
|
||||
const sidebar = document.getElementById('sidebar-nav');
|
||||
@ -75,7 +133,6 @@ export function switchView(viewType) {
|
||||
document.getElementById('main-container').classList.remove('sidebar-open');
|
||||
}
|
||||
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
const topBarHeight = document.querySelector('.top-bar')?.offsetHeight || 60;
|
||||
const targetScrollTop = mainContent ? mainContent.offsetTop - topBarHeight : 0;
|
||||
|
||||
@ -106,8 +163,11 @@ export function switchView(viewType) {
|
||||
|
||||
switch(viewType) {
|
||||
case 'movies':
|
||||
console.log('switchView: case movies');
|
||||
case 'series':
|
||||
console.log('switchView: case series');
|
||||
case 'search':
|
||||
console.log('switchView: case search');
|
||||
document.getElementById('content-section').style.display = 'block';
|
||||
filters.style.display = 'flex';
|
||||
if (viewType !== 'search') {
|
||||
@ -118,19 +178,25 @@ export function switchView(viewType) {
|
||||
}
|
||||
break;
|
||||
case 'favorites':
|
||||
console.log('switchView: case favorites');
|
||||
document.getElementById('content-section').style.display = 'block';
|
||||
break;
|
||||
case 'history':
|
||||
console.log('switchView: case history');
|
||||
document.getElementById('history-section').style.display = 'block';
|
||||
break;
|
||||
case 'recommendations':
|
||||
console.log('switchView: case recommendations');
|
||||
document.getElementById('recommendations-section').style.display = 'block';
|
||||
break;
|
||||
case 'stats':
|
||||
console.log('switchView: case stats');
|
||||
document.getElementById('stats-section').style.display = 'block';
|
||||
console.log('switchView: Showing stats-section');
|
||||
document.getElementById('stats-filters').style.display = 'flex';
|
||||
break;
|
||||
case 'photos':
|
||||
|
||||
document.getElementById('photos-section').style.display = 'block';
|
||||
break;
|
||||
}
|
||||
@ -283,6 +349,7 @@ function loadYears() {
|
||||
}
|
||||
|
||||
export async function loadContent(append = false) {
|
||||
console.log(`loadContent called with append: ${append}, currentView: ${state.currentView}, contentType: ${state.currentParams.contentType}`);
|
||||
if (state.currentContentFetchController) state.currentContentFetchController.abort();
|
||||
state.currentContentFetchController = new AbortController();
|
||||
const signal = state.currentContentFetchController.signal;
|
||||
@ -314,11 +381,13 @@ export async function loadContent(append = false) {
|
||||
}
|
||||
|
||||
const data = await fetchTMDB(endpoint, signal);
|
||||
console.log('loadContent: Data fetched successfully', data);
|
||||
renderGrid(data.results, append);
|
||||
loadMoreButton.style.display = (data.page < data.total_pages) ? 'block' : 'none';
|
||||
if (!append) setupScrollEffects();
|
||||
|
||||
} catch (error) {
|
||||
console.error('loadContent: Error fetching content', error);
|
||||
if (error.name !== 'AbortError') {
|
||||
if (!append) grid.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('couldNotLoadContent')}</p></div>`;
|
||||
}
|
||||
@ -331,16 +400,30 @@ export async function loadContent(append = false) {
|
||||
}
|
||||
}
|
||||
|
||||
function buscarContenidoLocal(title, type) {
|
||||
if (!title || !type) return null;
|
||||
function isContentAvailableLocally(title, type) {
|
||||
if (!title || !type) return false;
|
||||
const normalizedTitle = title.toLowerCase().trim();
|
||||
const source = type === 'movie' ? state.localMovies : state.localSeries;
|
||||
if (!Array.isArray(source)) return null;
|
||||
|
||||
return source.find(server =>
|
||||
server && Array.isArray(server.titulos) &&
|
||||
server.titulos.some(t => t && typeof t.title === 'string' && t.title.toLowerCase().trim() === normalizedTitle)
|
||||
) || null;
|
||||
const plexSource = type === 'movie' ? state.localMovies : state.localSeries;
|
||||
if (Array.isArray(plexSource)) {
|
||||
const foundInPlex = plexSource.some(server =>
|
||||
server && Array.isArray(server.titulos) &&
|
||||
server.titulos.some(t => t && typeof t.title === 'string' && t.title.toLowerCase().trim() === normalizedTitle)
|
||||
);
|
||||
if (foundInPlex) return true;
|
||||
}
|
||||
|
||||
const jellyfinType = type === 'movie' ? 'Movie' : 'Series';
|
||||
const jellyfinSource = type === 'movie' ? state.jellyfinMovies : state.jellyfinSeries;
|
||||
if (Array.isArray(jellyfinSource)) {
|
||||
const foundInJellyfin = jellyfinSource.some(library =>
|
||||
library && Array.isArray(library.titulos) &&
|
||||
library.titulos.some(t => t && typeof t.title === 'string' && t.title.toLowerCase().trim() === normalizedTitle && t.type === jellyfinType)
|
||||
);
|
||||
if (foundInJellyfin) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -369,7 +452,7 @@ function renderGrid(items, append = false) {
|
||||
const releaseDate = isMovie ? item.release_date : item.first_air_date;
|
||||
const year = releaseDate ? releaseDate.slice(0, 4) : 'N/A';
|
||||
const posterPath = item.poster_path ? `https://image.tmdb.org/t/p/w500${item.poster_path}` : 'img/no-poster.png';
|
||||
const isAvailable = !!buscarContenidoLocal(title, itemType);
|
||||
const isAvailable = isContentAvailableLocally(title, itemType);
|
||||
const isFavorite = state.favorites.some(fav => fav.id === item.id && fav.type === itemType);
|
||||
const voteAvg = item.vote_average ? item.vote_average.toFixed(1) : 'N/A';
|
||||
const ratingClass = voteAvg >= 7.5 ? 'rating-good' : (voteAvg >= 5.0 ? 'rating-ok' : 'rating-bad');
|
||||
@ -553,7 +636,7 @@ async function renderItemDetails(item) {
|
||||
const voteAverage = item.vote_average ? item.vote_average.toFixed(1) : 'N/A';
|
||||
const genres = item.genres || [];
|
||||
const trailer = item.videos?.results?.find(v => v.site === 'YouTube' && v.type === 'Trailer');
|
||||
const isAvailable = !!buscarContenidoLocal(title, state.currentItemType);
|
||||
const isAvailable = isContentAvailableLocally(title, state.currentItemType);
|
||||
const isFavorite = state.favorites.some(fav => fav.id === item.id && fav.type === state.currentItemType);
|
||||
|
||||
const imdbId = item.external_ids?.imdb_id;
|
||||
@ -735,10 +818,12 @@ function updateFavoriteButtonVisuals(itemId, itemType, isFavorite) {
|
||||
}
|
||||
|
||||
export async function loadFavorites() {
|
||||
console.log('loadFavorites called');
|
||||
const grid = document.getElementById('content-grid');
|
||||
grid.innerHTML = '<div class="col-12 text-center mt-5"><div class="spinner" style="position: static; margin: auto; display: block;"></div></div>';
|
||||
|
||||
if (state.favorites.length === 0) {
|
||||
console.log('loadFavorites: No favorites found.');
|
||||
grid.innerHTML = `<div class="empty-state"><i class="far fa-heart fa-3x mb-3"></i><p class="lead">${_('noFavorites')}</p></div>`;
|
||||
return;
|
||||
}
|
||||
@ -746,6 +831,7 @@ export async function loadFavorites() {
|
||||
try {
|
||||
const favoritePromises = state.favorites.map(fav => fetchTMDB(`${fav.type}/${fav.id}`).catch(()=>null));
|
||||
const favoriteItems = (await Promise.all(favoritePromises)).filter(item => item !== null);
|
||||
console.log('loadFavorites: Data received for rendering', favoriteItems);
|
||||
renderGrid(favoriteItems, false);
|
||||
} catch (error) {
|
||||
grid.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('errorLoadingFavorites')}</p></div>`;
|
||||
@ -765,7 +851,7 @@ export function displayHistory() {
|
||||
const fragment = document.createDocumentFragment();
|
||||
[...state.userHistory].sort((a,b) => b.timestamp - a.timestamp).forEach(item => {
|
||||
const posterUrl = item.poster ? `https://image.tmdb.org/t/p/w92${item.poster}` : 'img/no-poster.png';
|
||||
const isAvailable = !!buscarContenidoLocal(item.title, item.type);
|
||||
const isAvailable = isContentAvailableLocally(item.title, item.type);
|
||||
const historyItem = document.createElement('div');
|
||||
historyItem.className = 'history-item';
|
||||
historyItem.dataset.id = item.id;
|
||||
@ -921,22 +1007,24 @@ export async function generateStatistics() {
|
||||
try {
|
||||
const selectedToken = document.getElementById('stats-token-filter').value;
|
||||
|
||||
const filterByToken = (data) => {
|
||||
if (selectedToken === 'all') return data;
|
||||
return data.filter(server => server.tokenPrincipal === selectedToken);
|
||||
};
|
||||
|
||||
const filteredMovies = filterByToken(state.localMovies);
|
||||
const filteredSeries = filterByToken(state.localSeries);
|
||||
const filteredArtists = filterByToken(state.localArtists);
|
||||
const filteredPlexMovies = selectedToken === 'all' ? state.localMovies : state.localMovies.filter(server => server.tokenPrincipal === selectedToken);
|
||||
const filteredPlexSeries = selectedToken === 'all' ? state.localSeries : state.localSeries.filter(server => server.tokenPrincipal === selectedToken);
|
||||
const filteredPlexArtists = selectedToken === 'all' ? state.localArtists : state.localArtists.filter(server => server.tokenPrincipal === selectedToken);
|
||||
|
||||
const plexMovieItems = filteredPlexMovies.flatMap(s => s.titulos);
|
||||
const plexSeriesItems = filteredPlexSeries.flatMap(s => s.titulos);
|
||||
const plexArtistItems = filteredPlexArtists.flatMap(s => s.titulos);
|
||||
|
||||
const jellyfinMovieItems = state.jellyfinMovies.flatMap(lib => lib.titulos);
|
||||
const jellyfinSeriesItems = state.jellyfinSeries.flatMap(lib => lib.titulos);
|
||||
|
||||
const allMovieItems = [...plexMovieItems, ...jellyfinMovieItems];
|
||||
const allSeriesItems = [...plexSeriesItems, ...jellyfinSeriesItems];
|
||||
const allArtistItems = [...plexArtistItems];
|
||||
|
||||
const allMovieItems = filteredMovies.flatMap(s => s.titulos);
|
||||
const allSeriesItems = filteredSeries.flatMap(s => s.titulos);
|
||||
|
||||
const uniqueMovieTitles = new Set(allMovieItems.map(item => item.title));
|
||||
const uniqueSeriesTitles = new Set(allSeriesItems.map(item => item.title));
|
||||
const uniqueArtists = new Set(filteredArtists.flatMap(s => s.titulos.map(t => t.title)));
|
||||
|
||||
const uniqueArtists = new Set(allArtistItems.map(item => item.title));
|
||||
animateValue('total-movies', 0, uniqueMovieTitles.size, 1000);
|
||||
animateValue('total-series', 0, uniqueSeriesTitles.size, 1000);
|
||||
animateValue('total-artists', 0, uniqueArtists.size, 1000);
|
||||
@ -1133,107 +1221,119 @@ export async function searchByActor(actorId, actorName) {
|
||||
|
||||
export async function initializeHeroSection() {
|
||||
const heroSection = document.getElementById('hero-section');
|
||||
if (heroSection.style.display === 'none') return;
|
||||
if (heroSection.style.display === 'none' || !state.settings.showHero) return;
|
||||
|
||||
try {
|
||||
const type = Math.random() > 0.5 ? 'movie' : 'tv';
|
||||
const data = await fetchTMDB(`${type}/popular?page=1`);
|
||||
const popularItems = data.results.filter(i => i.backdrop_path && i.overview).slice(0, 8);
|
||||
// Clear existing timers for slide changes and initial load
|
||||
if (state.heroIntervalId) {
|
||||
clearInterval(state.heroIntervalId);
|
||||
state.heroIntervalId = null;
|
||||
}
|
||||
if (state.heroLoadTimeoutId) {
|
||||
clearTimeout(state.heroLoadTimeoutId);
|
||||
state.heroLoadTimeoutId = null;
|
||||
}
|
||||
|
||||
if (popularItems.length === 0) {
|
||||
heroSection.style.display = 'none';
|
||||
return;
|
||||
const bg1 = document.querySelector('.hero-background-1');
|
||||
const bg2 = document.querySelector('.hero-background-2');
|
||||
const content = document.querySelector('.hero-content');
|
||||
const heroButtons = content.querySelector('.hero-buttons');
|
||||
|
||||
// Set static background and show default content
|
||||
content.querySelector('.hero-title').textContent = _('heroWelcome');
|
||||
content.querySelector('.hero-subtitle').textContent = _('heroSubtitle');
|
||||
content.querySelector('#hero-rating').innerHTML = '';
|
||||
content.querySelector('#hero-year').innerHTML = '';
|
||||
content.querySelector('#hero-extra').innerHTML = '';
|
||||
heroButtons.style.display = 'none';
|
||||
|
||||
heroSection.classList.add('no-overlay');
|
||||
gsap.set(bg1, { backgroundImage: `url(img/hero-def.png)`, autoAlpha: 1, scale: 1 });
|
||||
gsap.set(bg2, { autoAlpha: 0 });
|
||||
gsap.set(content, { autoAlpha: 1 });
|
||||
heroSection.classList.remove('loading');
|
||||
|
||||
// After 5 seconds, load the dynamic content if we are still on the home view
|
||||
state.heroLoadTimeoutId = setTimeout(() => {
|
||||
if (state.currentView === 'home') {
|
||||
loadTmdbHeroContent();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
const bg1 = document.querySelector('.hero-background-1');
|
||||
const bg2 = document.querySelector('.hero-background-2');
|
||||
const content = document.querySelector('.hero-content');
|
||||
let currentBg = bg1;
|
||||
let nextBg = bg2;
|
||||
let currentIndex = -1;
|
||||
async function loadTmdbHeroContent() {
|
||||
heroSection.classList.remove('no-overlay');
|
||||
try {
|
||||
const type = Math.random() > 0.5 ? 'movie' : 'tv';
|
||||
const data = await fetchTMDB(`${type}/popular?page=1`);
|
||||
const popularItems = data.results.filter(i => i.backdrop_path && i.overview).slice(0, 8);
|
||||
|
||||
function changeHeroSlide(isFirst = false) {
|
||||
currentIndex = (currentIndex + 1) % popularItems.length;
|
||||
const item = popularItems[currentIndex];
|
||||
const nextImage = new Image();
|
||||
nextImage.src = `https://image.tmdb.org/t/p/original${item.backdrop_path}`;
|
||||
if (popularItems.length === 0) {
|
||||
return; // Keep static image if no items
|
||||
}
|
||||
|
||||
nextImage.onload = () => {
|
||||
updateHeroContent(item);
|
||||
let currentBg = bg1;
|
||||
let nextBg = bg2;
|
||||
let currentIndex = -1;
|
||||
|
||||
const heroElements = [
|
||||
content.querySelector('.hero-title'),
|
||||
content.querySelector('.hero-subtitle'),
|
||||
...content.querySelectorAll('.hero-meta-item'),
|
||||
content.querySelector('.hero-buttons')
|
||||
];
|
||||
function changeHeroSlide() {
|
||||
currentIndex = (currentIndex + 1) % popularItems.length;
|
||||
const item = popularItems[currentIndex];
|
||||
const nextImage = new Image();
|
||||
nextImage.src = `https://image.tmdb.org/t/p/original${item.backdrop_path}`;
|
||||
|
||||
nextImage.onload = () => {
|
||||
updateHeroContent(item);
|
||||
|
||||
const heroElements = [
|
||||
content.querySelector('.hero-title'),
|
||||
content.querySelector('.hero-subtitle'),
|
||||
...content.querySelectorAll('.hero-meta-item'),
|
||||
content.querySelector('.hero-buttons')
|
||||
];
|
||||
|
||||
if (isFirst) {
|
||||
gsap.set(currentBg, { backgroundImage: `url(${nextImage.src})` });
|
||||
gsap.to(currentBg, { autoAlpha: 1, duration: 1.5, ease: 'power2.out' });
|
||||
gsap.to(content, { autoAlpha: 1, duration: 1, delay: 0.5 });
|
||||
gsap.fromTo(currentBg, { scale: 1.15, transformOrigin: 'center center' }, { scale: 1, duration: 12, ease: 'none' });
|
||||
} else {
|
||||
const tl = gsap.timeline({
|
||||
onComplete: () => {
|
||||
gsap.set(currentBg, { autoAlpha: 0 });
|
||||
const temp = currentBg;
|
||||
currentBg = nextBg;
|
||||
nextBg = temp;
|
||||
gsap.set(nextBg, { autoAlpha: 0 });
|
||||
}
|
||||
});
|
||||
|
||||
tl.to(heroElements, {
|
||||
autoAlpha: 0,
|
||||
y: 30,
|
||||
stagger: 0.08,
|
||||
duration: 0.6,
|
||||
ease: 'power3.in'
|
||||
});
|
||||
|
||||
gsap.set(nextBg, { backgroundImage: `url(${nextImage.src})` });
|
||||
tl.to(nextBg, { autoAlpha: 1, duration: 1.5, ease: 'power2.inOut' }, '-=0.5');
|
||||
|
||||
tl.to(heroElements, { autoAlpha: 0, y: 30, stagger: 0.08, duration: 0.6, ease: 'power3.in' }, 0);
|
||||
gsap.set(nextBg, { backgroundImage: `url(${nextImage.src})`, autoAlpha: 0 });
|
||||
tl.to(currentBg, { autoAlpha: 0, duration: 2.5, ease: 'power2.inOut' }, 0);
|
||||
tl.to(nextBg, { autoAlpha: 1, duration: 2.5, ease: 'power2.inOut' }, 0);
|
||||
gsap.fromTo(nextBg, { scale: 1.15, transformOrigin: 'center center' }, { scale: 1, duration: 12, ease: 'none' });
|
||||
tl.fromTo(heroElements, { y: -30, autoAlpha: 0 }, { y: 0, autoAlpha: 1, stagger: 0.1, duration: 1.2, ease: 'power3.out' }, '>-0.8');
|
||||
};
|
||||
}
|
||||
|
||||
tl.fromTo(heroElements, {
|
||||
y: -30,
|
||||
autoAlpha: 0
|
||||
}, {
|
||||
y: 0,
|
||||
autoAlpha: 1,
|
||||
stagger: 0.1,
|
||||
duration: 0.8,
|
||||
ease: 'power3.out'
|
||||
}, '>-1');
|
||||
}
|
||||
};
|
||||
if (state.heroIntervalId) {
|
||||
clearInterval(state.heroIntervalId);
|
||||
}
|
||||
changeHeroSlide();
|
||||
state.heroIntervalId = setInterval(changeHeroSlide, 12000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error initializing hero section from TMDB:", error);
|
||||
}
|
||||
|
||||
gsap.set(content, { autoAlpha: 0 });
|
||||
gsap.set([bg1, bg2], { autoAlpha: 0 });
|
||||
|
||||
changeHeroSlide(true);
|
||||
setInterval(() => changeHeroSlide(false), 12000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error initializing hero section:", error);
|
||||
heroSection.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function updateHeroContent(item) {
|
||||
const heroTitle = document.querySelector('.hero-title');
|
||||
const heroSubtitle = document.querySelector('.hero-subtitle');
|
||||
const heroRating = document.querySelector('#hero-rating');
|
||||
const heroYear = document.querySelector('#hero-year');
|
||||
const heroExtra = document.querySelector('#hero-extra');
|
||||
const heroPlayBtn = document.getElementById('hero-play-btn');
|
||||
const heroInfoBtn = document.getElementById('hero-info-btn');
|
||||
const heroContent = document.querySelector('.hero-content');
|
||||
const heroTitle = heroContent.querySelector('.hero-title');
|
||||
const heroSubtitle = heroContent.querySelector('.hero-subtitle');
|
||||
const heroRating = heroContent.querySelector('#hero-rating');
|
||||
const heroYear = heroContent.querySelector('#hero-year');
|
||||
const heroExtra = heroContent.querySelector('#hero-extra');
|
||||
const heroButtons = heroContent.querySelector('.hero-buttons');
|
||||
const heroPlayBtn = heroButtons.querySelector('#hero-play-btn');
|
||||
const heroInfoBtn = heroButtons.querySelector('#hero-info-btn');
|
||||
|
||||
const type = item.title ? 'movie' : 'tv';
|
||||
const title = item.title || item.name;
|
||||
const isAvailable = !!buscarContenidoLocal(title, type);
|
||||
const isAvailable = isContentAvailableLocally(title, type);
|
||||
|
||||
if (heroTitle) heroTitle.textContent = title;
|
||||
if (heroSubtitle) heroSubtitle.textContent = item.overview.substring(0, 200) + (item.overview.length > 200 ? '...' : '');
|
||||
@ -1241,6 +1341,8 @@ function updateHeroContent(item) {
|
||||
if (heroYear) heroYear.innerHTML = `<i class="fas fa-calendar-alt"></i> ${(item.release_date || item.first_air_date).slice(0, 4)}`;
|
||||
if (heroExtra) heroExtra.innerHTML = `<i class="fas ${type === 'movie' ? 'fa-film' : 'fa-tv'}"></i> ${type === 'movie' ? _('moviesSectionTitle') : _('seriesSectionTitle')}`;
|
||||
|
||||
heroButtons.style.display = 'block';
|
||||
|
||||
if (heroPlayBtn) {
|
||||
heroPlayBtn.onclick = () => addStreamToList(title, type, heroPlayBtn);
|
||||
heroPlayBtn.disabled = !isAvailable;
|
||||
@ -1277,7 +1379,7 @@ export async function addStreamToList(title, type, buttonElement = null) {
|
||||
showNotification(_('searchingStreams', title), 'info');
|
||||
|
||||
try {
|
||||
const streamData = await fetchAllStreamsFromPlex(title, type);
|
||||
const streamData = await fetchAllAvailableStreams(title, type);
|
||||
if (!streamData.success || streamData.streams.length === 0) throw new Error(streamData.message);
|
||||
|
||||
showNotification(_('sendingStreams', String(streamData.streams.length)), 'info');
|
||||
@ -1316,7 +1418,8 @@ export async function downloadM3U(title, type, buttonElement = null) {
|
||||
showNotification(_('generatingM3U', title), "info");
|
||||
|
||||
try {
|
||||
const streamData = await fetchAllStreamsFromPlex(title, type);
|
||||
const streamData = await fetchAllAvailableStreams(title, type);
|
||||
|
||||
if (!streamData.success || streamData.streams.length === 0) throw new Error(streamData.message);
|
||||
|
||||
let m3uContent = "#EXTM3U\n";
|
||||
@ -1408,6 +1511,10 @@ export function openSettingsModal() {
|
||||
document.getElementById('lightModeToggle').checked = state.settings.theme === 'light';
|
||||
document.getElementById('showHeroToggle').checked = state.settings.showHero;
|
||||
|
||||
document.getElementById('jellyfinServerUrl').value = state.jellyfinSettings.url || '';
|
||||
document.getElementById('jellyfinUsername').value = state.jellyfinSettings.username || '';
|
||||
document.getElementById('jellyfinPassword').value = state.jellyfinSettings.password || '';
|
||||
|
||||
document.getElementById('phpSecretKeyCheck').checked = state.settings.phpUseSecretKey;
|
||||
document.getElementById('phpSecretKey').value = state.settings.phpSecretKey || '';
|
||||
document.getElementById('phpSavePath').value = state.settings.phpSavePath || '';
|
||||
@ -1438,9 +1545,21 @@ export async function saveSettings() {
|
||||
};
|
||||
|
||||
state.settings = { ...state.settings, ...newSettings };
|
||||
|
||||
const newJellyfinSettings = {
|
||||
id: 'jellyfin_credentials',
|
||||
url: document.getElementById('jellyfinServerUrl').value.trim(),
|
||||
username: document.getElementById('jellyfinUsername').value.trim(),
|
||||
password: document.getElementById('jellyfinPassword').value
|
||||
};
|
||||
state.jellyfinSettings = { ...state.jellyfinSettings, ...newJellyfinSettings };
|
||||
|
||||
try {
|
||||
await addItemsToStore('settings', [state.settings]);
|
||||
await Promise.all([
|
||||
addItemsToStore('settings', [state.settings]),
|
||||
addItemsToStore('jellyfin_settings', [state.jellyfinSettings])
|
||||
]);
|
||||
|
||||
showNotification(_('settingsSavedSuccess'), 'success');
|
||||
applyTheme(state.settings.theme);
|
||||
applyHeroVisibility(state.settings.showHero);
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_appName__",
|
||||
"version": "1.0",
|
||||
"version": "1.0.1",
|
||||
"description": "__MSG_appDescription__",
|
||||
"default_locale": "en",
|
||||
|
||||
@ -15,7 +15,8 @@
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://*.plex.tv/*",
|
||||
"*://*:*/*"
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "js/background.js",
|
||||
|
58
plex.html
58
plex.html
@ -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>
|
||||
@ -267,8 +270,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="fab-music-player" class="fab-btn" style="display: none;" title="__MSG_openMusicPlayer__"><i class="fas fa-music"></i></button>
|
||||
|
||||
<div class="spinner" id="spinner"></div>
|
||||
|
||||
<div class="modal fade" id="activityViewerModal" tabindex="-1" aria-labelledby="activityViewerModalLabel" aria-hidden="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">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
@ -288,6 +316,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__
|
||||
@ -359,6 +392,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 +581,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>
|
||||
@ -638,6 +694,8 @@
|
||||
<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>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user