add provider
This commit is contained in:
parent
98de6ec451
commit
50b9d82f0f
51
README.md
51
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 & 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!
|
||||
* **📡 Psychic Plex Scanner:** You give it your Plex tokens, and CinePlex goes into full detective mode. It scans your servers, figures out what you *actually* have, and jots it down in its secret notebook (a local IndexedDB database in your browser). It's like having a personal librarian for your media!
|
||||
* **✅ "Got It" Badge of Honor:** See a movie you want to watch? CinePlex will let you know if you already have it on your server with a neat "Local" badge. No more blind searching!
|
||||
* **🎶 Music Jukebox 2077:** It's not all about movies. We've built a full-fledged music player that connects directly to your Plex music library. Browse artists, listen to albums, and rock out with a **graphic equalizer and audio visualizer**! Your personal party, guaranteed!
|
||||
* **📊 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,7 +33,6 @@ 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!
|
||||
|
||||
---
|
||||
@ -42,10 +41,6 @@ Get ready to be amazed by everything CinePlex has under the hood. This isn't jus
|
||||
|
||||
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.
|
||||
|
||||
## 🛠️ Installation and First Steps: Liftoff in 3, 2, 1...!
|
||||
|
||||
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!
|
||||
|
||||
Since we're not yet on the Chrome Web Store (but we will be, oh yes!), you'll have to load it as an "unpacked" extension. Think of it as an exclusive beta launch just for you.
|
||||
@ -61,27 +56,31 @@ Since we're not yet on the Chrome Web Store (but we will be, oh yes!), you'll ha
|
||||
|
||||
When you first open CinePlex, it's like a newly built spaceship: impressive, but it needs fuel and coordinates. Let's bring it to life!
|
||||
|
||||
1. **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!
|
||||
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!
|
||||
|
||||
2. **Connect to Your Servers:** CinePlex supports both Plex and Jellyfin. You can use either or both!
|
||||
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!
|
||||
|
||||
* **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"**.
|
||||
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 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.
|
||||
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!
|
||||
|
||||
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!
|
||||
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!
|
||||
|
||||
---
|
||||
|
||||
@ -98,10 +97,10 @@ Want to take your experience to the next level and use the "Add Stream" button?
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Permissions: What Does CinePlex Need and Why?
|
||||
## 🔒 Permissions: What Does CinePlex Need?
|
||||
|
||||
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, 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!
|
||||
* **`storage`**: This allows CinePlex to store your Plex tokens, scanned library data, and your personalized settings directly in your browser's local storage. All your data stays on your machine, safe and sound!
|
||||
* **`notifications`**: Used to send you helpful notifications, for example, when a scan is complete or if there's an important update.
|
||||
* **`host_permissions` for `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.
|
||||
* **`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.
|
@ -9,6 +9,7 @@
|
||||
"settings": { "message": "Einstellungen" },
|
||||
"navMovies": { "message": "Filme" },
|
||||
"navSeries": { "message": "Serien" },
|
||||
"navProviders": { "message": "Anbieter" },
|
||||
"navPhotos": { "message": "Fotos" },
|
||||
"navStats": { "message": "Statistiken" },
|
||||
"navFavorites": { "message": "Favoriten" },
|
||||
@ -65,6 +66,10 @@
|
||||
"settingsTmdbApiLabel": { "message": "TMDB API-Schlüssel (Optional)" },
|
||||
"settingsTmdbApiPlaceholder": { "message": "Verwendet den Standardschlüssel, wenn leer gelassen" },
|
||||
"settingsTmdbLangLabel": { "message": "Sprache für TMDB & UI" },
|
||||
"settingsRegionLabel": { "message": "Region für Anbieter" },
|
||||
"allRegions": { "message": "Alle Regionen" },
|
||||
"loadingRegions": { "message": "Regionen werden geladen..." },
|
||||
"errorLoadingRegions": { "message": "Fehler beim Laden der Regionen" },
|
||||
"settingsPhpUrlLabel": { "message": "Server-URL zum Hinzufügen von Streams" },
|
||||
"settingsPhpUrlPlaceholder": { "message": "https://dein-server.com/pfad/zum/script.php" },
|
||||
"settingsInterface": { "message": "Oberfläche" },
|
||||
@ -241,7 +246,7 @@
|
||||
"shuffleOn": { "message": "Zufallsmodus aktiviert." },
|
||||
"shuffleOff": { "message": "Zufallsmodus deaktiviert." },
|
||||
"downloadingSong": { "message": "Starte Download von \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"songDownloaded": { "message": "\"$title$\" heruntergeladen.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"songDownloaded": { "message": "\"\"$title$\" heruntergeladen.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"errorDownloadingSong": { "message": "Fehler beim Herunterladen von \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"generatingAlbumM3U": { "message": "Generiere M3U für \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
|
||||
"albumM3UGenerated": { "message": "M3U für Album \"$artist$\" generiert.", "placeholders": { "artist": { "content": "$1" } } },
|
||||
@ -274,13 +279,12 @@
|
||||
"episodesCount": {"message": "$count$ Episoden", "placeholders": {"count": {"content": "$1"}}},
|
||||
"seasonsCount": {"message": "$count$ Staffeln", "placeholders": {"count": {"content": "$1"}}},
|
||||
"runtimeMinutes": {"message": "$count$ Min.", "placeholders": {"count": {"content": "$1"}}},
|
||||
"noTrailerFound": {"message": "Für diesen Titel wurde kein Trailer gefunden."},
|
||||
"fatalInitError": {"message": "Fataler Initialisierungsfehler"},
|
||||
"noTrailerFound": {"message": "Für diesen Titel wurde kein Trailer gefunden."}, "fatalInitError": {"message": "Fataler Initialisierungsfehler"},
|
||||
"fatalInitErrorSub": {"message": "Die Anwendung konnte nicht geladen werden."},
|
||||
"invalidStreamInfo": {"message": "Ungültige Information."},
|
||||
"dbUnavailableForStreams": {"message": "Lokale Datenbank nicht verfügbar."},
|
||||
"noPlexServersForStreams": {"message": "Keine Plex-Server."},
|
||||
"notFoundOnServers": {"message": "\"$query$\" auf Servern nicht gefunden.", "placeholders": {"query": {"content": "$1"}}},
|
||||
"notFoundOnServers": {"message": "\"query$\" auf Servern nicht gefunden.", "placeholders": {"query": {"content": "$1"}}},
|
||||
"relativeTime_justNow": { "message": "Gerade eben" },
|
||||
"relativeTime_minutesAgo": { "message": "Vor $count$ Minuten", "placeholders": { "count": { "content": "$1" } } },
|
||||
"relativeTime_hoursAgo": { "message": "Vor $count$ Stunden", "placeholders": { "count": { "content": "$1" } } },
|
||||
@ -308,8 +312,8 @@
|
||||
"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" } } },
|
||||
"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" },
|
||||
@ -327,5 +331,7 @@
|
||||
"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." }
|
||||
"activityCopyError": { "message": "Fehler beim Kopieren der Kennung." },
|
||||
"noProvidersFound": { "message": "Keine Anbieter gefunden." },
|
||||
"availableOnPlex": { "message": "Auf Plex verfügbar" }
|
||||
}
|
@ -9,6 +9,7 @@
|
||||
"settings": { "message": "Settings" },
|
||||
"navMovies": { "message": "Movies" },
|
||||
"navSeries": { "message": "Series" },
|
||||
"navProviders": { "message": "Providers" },
|
||||
"navPhotos": { "message": "Photos" },
|
||||
"navStats": { "message": "Statistics" },
|
||||
"navFavorites": { "message": "Favorites" },
|
||||
@ -198,7 +199,7 @@
|
||||
"sendingStreams": { "message": "Sending $count$ stream(s) to the server...", "placeholders": { "count": { "content": "$1" } } },
|
||||
"streamAddedSuccess": { "message": "Stream(s) added successfully." },
|
||||
"generatingM3U": { "message": "Generating M3U for \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
|
||||
"m3uDownloaded": { "message": "M3U for \"$title$\" downloaded.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"m3uDownloaded": { "message": "\"$title$\" downloaded.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"errorGeneratingM3U": { "message": "Error generating M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"settingsSavedSuccess": { "message": "Settings saved successfully." },
|
||||
"errorSavingSettings": { "message": "Error saving settings to the database." },
|
||||
@ -305,7 +306,7 @@
|
||||
"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" } } },
|
||||
"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" } } },
|
||||
@ -327,5 +328,7 @@
|
||||
"activityCopyID": { "message": "Copy ID" },
|
||||
"activityError": { "message": "Could not fetch server activity." },
|
||||
"activityCopied": { "message": "Identifier copied to clipboard!" },
|
||||
"activityCopyError": { "message": "Failed to copy identifier." }
|
||||
"activityCopyError": { "message": "Failed to copy identifier." },
|
||||
"noProvidersFound": { "message": "No providers found." },
|
||||
"availableOnPlex": { "message": "Available on Plex" }
|
||||
}
|
@ -9,6 +9,7 @@
|
||||
"settings": { "message": "Ajustes" },
|
||||
"navMovies": { "message": "Películas" },
|
||||
"navSeries": { "message": "Series" },
|
||||
"navProviders": { "message": "Proveedores" },
|
||||
"navPhotos": { "message": "Fotos" },
|
||||
"navStats": { "message": "Estadísticas" },
|
||||
"navFavorites": { "message": "Favoritos" },
|
||||
@ -198,7 +199,7 @@
|
||||
"sendingStreams": { "message": "Enviando $count$ stream(s) al servidor...", "placeholders": { "count": { "content": "$1" } } },
|
||||
"streamAddedSuccess": { "message": "Stream(s) añadido(s) con éxito." },
|
||||
"generatingM3U": { "message": "Generando M3U para \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"m3uDownloaded": { "message": "M3U para \"$title$\" descargado.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"m3uDownloaded": { "message": "\"\"$title$\" descargado.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"errorGeneratingM3U": { "message": "Error al generar M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"settingsSavedSuccess": { "message": "Ajustes guardados correctamente." },
|
||||
"errorSavingSettings": { "message": "Error al guardar los ajustes en la base de datos." },
|
||||
@ -327,5 +328,7 @@
|
||||
"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." }
|
||||
"activityCopyError": { "message": "Error al copiar el identificador." },
|
||||
"noProvidersFound": { "message": "No se encontraron proveedores." },
|
||||
"availableOnPlex": { "message": "Disponible en Plex" }
|
||||
}
|
@ -9,6 +9,7 @@
|
||||
"settings": { "message": "Paramètres" },
|
||||
"navMovies": { "message": "Films" },
|
||||
"navSeries": { "message": "Séries" },
|
||||
"navProviders": { "message": "Fournisseurs" },
|
||||
"navPhotos": { "message": "Photos" },
|
||||
"navStats": { "message": "Statistiques" },
|
||||
"navFavorites": { "message": "Favoris" },
|
||||
@ -194,10 +195,10 @@
|
||||
"errorLoadingActorContent": { "message": "Impossible de charger le contenu pour $actorName$.", "placeholders": { "actorName": { "content": "$1" } } },
|
||||
"errorAddingStream": { "message": "Erreur lors de l'ajout du ou des flux : $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"phpUrlNotConfigured": { "message": "L'URL du serveur PHP n'est pas configurée. Veuillez la configurer dans les Paramètres." },
|
||||
"searchingStreams": { "message": "Recherche de flux pour \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
|
||||
"searchingStreams": { "message": "Recherche de flux pour \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"sendingStreams": { "message": "Envoi de $count$ flux au serveur...", "placeholders": { "count": { "content": "$1" } } },
|
||||
"streamAddedSuccess": { "message": "Flux ajouté(s) avec succès." },
|
||||
"generatingM3U": { "message": "Génération du M3U pour \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
|
||||
"generatingM3U": { "message": "Génération du M3U pour \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"m3uDownloaded": { "message": "M3U pour \"$title$\" téléchargé.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"errorGeneratingM3U": { "message": "Erreur lors de la génération du M3U : $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"settingsSavedSuccess": { "message": "Paramètres sauvegardés avec succès." },
|
||||
@ -219,7 +220,7 @@
|
||||
"errorDuringScan": { "message": "Erreur pendant le scan : $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"scanCancelled": { "message": "Scan annulé par l'utilisateur." },
|
||||
"scanCancelledInfo": { "message": "Scan annulé." },
|
||||
"retyingSection": { "message": "Nouvelle tentative pour la section \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
|
||||
"retyingSection": { "message": "Nouvelle tentative pour la section \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"retrySuccess": { "message": "[SUCCÈS] Relance de \"$title$\" terminée.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"retryError": { "message": "[ERREUR FINALE] Échec de la nouvelle tentative pour \"$title$\" : $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } },
|
||||
"noRetriesPending": { "message": "Aucune relance en attente." },
|
||||
@ -240,10 +241,10 @@
|
||||
"noSongsFound": { "message": "Aucune chanson trouvée." },
|
||||
"shuffleOn": { "message": "Mode aléatoire activé." },
|
||||
"shuffleOff": { "message": "Mode aléatoire désactivé." },
|
||||
"downloadingSong": { "message": "Début du téléchargement de \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
|
||||
"songDownloaded": { "message": "\"$title$\" téléchargé.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"downloadingSong": { "message": "Début du téléchargement de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"songDownloaded": { "message": "\"\"$title$\" téléchargé.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"errorDownloadingSong": { "message": "Erreur lors du téléchargement de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"generatingAlbumM3U": { "message": "Génération du M3U pour \"$artist$\"...", "placeholders": { "artist": { "content": "$1" } } },
|
||||
"generatingAlbumM3U": { "message": "Génération du M3U pour \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
|
||||
"albumM3UGenerated": { "message": "M3U de l'album \"$artist$\" généré.", "placeholders": { "artist": { "content": "$1" } } },
|
||||
"playbackError": { "message": "Erreur de lecture" },
|
||||
"errorLabel": { "message": "Erreur" },
|
||||
@ -280,7 +281,7 @@
|
||||
"invalidStreamInfo": {"message": "Information invalide."},
|
||||
"dbUnavailableForStreams": {"message": "Base de données locale non disponible."},
|
||||
"noPlexServersForStreams": {"message": "Pas de serveurs Plex."},
|
||||
"notFoundOnServers": {"message": "\"$query$\" non trouvé sur les serveurs.", "placeholders": {"query": {"content": "$1"}}},
|
||||
"notFoundOnServers": {"message": "\"query$\" non trouvé sur les serveurs.", "placeholders": {"query": {"content": "$1"}}},
|
||||
"relativeTime_justNow": { "message": "À l'instant" },
|
||||
"relativeTime_minutesAgo": { "message": "Il y a $count$ minutes", "placeholders": { "count": { "content": "$1" } } },
|
||||
"relativeTime_hoursAgo": { "message": "Il y a $count$ heures", "placeholders": { "count": { "content": "$1" } } },
|
||||
@ -308,8 +309,8 @@
|
||||
"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" } } },
|
||||
"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" },
|
||||
@ -327,5 +328,7 @@
|
||||
"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." }
|
||||
"activityCopyError": { "message": "Échec de la copie de l'identifiant." },
|
||||
"noProvidersFound": { "message": "Aucun fournisseur trouvé." },
|
||||
"availableOnPlex": { "message": "Disponible sur Plex" }
|
||||
}
|
@ -9,6 +9,7 @@
|
||||
"settings": { "message": "Impostazioni" },
|
||||
"navMovies": { "message": "Film" },
|
||||
"navSeries": { "message": "Serie TV" },
|
||||
"navProviders": { "message": "Fornitori" },
|
||||
"navPhotos": { "message": "Foto" },
|
||||
"navStats": { "message": "Statistiche" },
|
||||
"navFavorites": { "message": "Preferiti" },
|
||||
@ -296,7 +297,7 @@
|
||||
"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." },
|
||||
"jellyfinMissingCredentials": { "message": "Per favor, 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." },
|
||||
@ -327,5 +328,7 @@
|
||||
"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." }
|
||||
"activityCopyError": { "message": "Copia dell'identificatore non riuscita." },
|
||||
"noProvidersFound": { "message": "Nessun fornitore trovato." },
|
||||
"availableOnPlex": { "message": "Disponibile su Plex" }
|
||||
}
|
@ -9,6 +9,7 @@
|
||||
"settings": { "message": "Configurações" },
|
||||
"navMovies": { "message": "Filmes" },
|
||||
"navSeries": { "message": "Séries" },
|
||||
"navProviders": { "message": "Provedores" },
|
||||
"navPhotos": { "message": "Fotos" },
|
||||
"navStats": { "message": "Estatísticas" },
|
||||
"navFavorites": { "message": "Favoritos" },
|
||||
@ -194,10 +195,10 @@
|
||||
"errorLoadingActorContent": { "message": "Não foi possível carregar o conteúdo de $actorName$.", "placeholders": { "actorName": { "content": "$1" } } },
|
||||
"errorAddingStream": { "message": "Erro ao adicionar stream(s): $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"phpUrlNotConfigured": { "message": "A URL do servidor PHP não está configurada. Por favor, configure-a nas Configurações." },
|
||||
"searchingStreams": { "message": "Buscando streams para \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
|
||||
"searchingStreams": { "message": "Buscando streams para \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"sendingStreams": { "message": "Enviando $count$ stream(s) para o servidor...", "placeholders": { "count": { "content": "$1" } } },
|
||||
"streamAddedSuccess": { "message": "Stream(s) adicionado(s) com sucesso." },
|
||||
"generatingM3U": { "message": "Gerando M3U para \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
|
||||
"generatingM3U": { "message": "Gerando M3U para \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"m3uDownloaded": { "message": "M3U para \"$title$\" baixado.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"errorGeneratingM3U": { "message": "Erro ao gerar M3U: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"settingsSavedSuccess": { "message": "Configurações salvas com sucesso." },
|
||||
@ -219,7 +220,7 @@
|
||||
"errorDuringScan": { "message": "Erro durante a análise: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"scanCancelled": { "message": "Análise cancelada pelo usuário." },
|
||||
"scanCancelledInfo": { "message": "Análise cancelada." },
|
||||
"retyingSection": { "message": "Tentando novamente a seção \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
|
||||
"retyingSection": { "message": "Tentando novamente a seção \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"retrySuccess": { "message": "[SUCESSO] Nova tentativa de \"$title$\" concluída.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"retryError": { "message": "[ERRO FINAL] A nova tentativa falhou para \"$title$\": $message$", "placeholders": { "title": { "content": "$1" }, "message": { "content": "$2" } } },
|
||||
"noRetriesPending": { "message": "Nenhuma nova tentativa pendente." },
|
||||
@ -240,10 +241,10 @@
|
||||
"noSongsFound": { "message": "Nenhuma música encontrada." },
|
||||
"shuffleOn": { "message": "Modo aleatório ativado." },
|
||||
"shuffleOff": { "message": "Modo aleatório desativado." },
|
||||
"downloadingSong": { "message": "Iniciando download de \"$title$\"...", "placeholders": { "title": { "content": "$1" } } },
|
||||
"downloadingSong": { "message": "Iniciando download de \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"songDownloaded": { "message": "\"$title$\" baixado.", "placeholders": { "title": { "content": "$1" } } },
|
||||
"errorDownloadingSong": { "message": "Erro ao baixar \"$title$\"", "placeholders": { "title": { "content": "$1" } } },
|
||||
"generatingAlbumM3U": { "message": "Gerando M3U para \"$artist$\"...", "placeholders": { "artist": { "content": "$1" } } },
|
||||
"generatingAlbumM3U": { "message": "Gerando M3U para \"$artist$\"", "placeholders": { "artist": { "content": "$1" } } },
|
||||
"albumM3UGenerated": { "message": "M3U para o álbum \"$artist$\" gerado.", "placeholders": { "artist": { "content": "$1" } } },
|
||||
"playbackError": { "message": "Erro de reprodução" },
|
||||
"errorLabel": { "message": "Erro" },
|
||||
@ -327,5 +328,7 @@
|
||||
"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." }
|
||||
"activityCopyError": { "message": "Falha ao copiar o identificador." },
|
||||
"noProvidersFound": { "message": "Nenhum provedor encontrado." },
|
||||
"availableOnPlex": { "message": "Disponível no Plex" }
|
||||
}
|
90
css/providers.css
Normal file
90
css/providers.css
Normal file
@ -0,0 +1,90 @@
|
||||
.providers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 30px;
|
||||
padding: 30px;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.provider-card {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
background-color: transparent;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.4s ease, box-shadow 0.4s ease;
|
||||
box-shadow: 0 0 0px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.provider-card:hover {
|
||||
transform: scale(1.1) translateY(-5px);
|
||||
box-shadow: 0 0 25px rgba(0, 224, 255, 0.7);
|
||||
}
|
||||
|
||||
.provider-logo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 0;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.provider-card:hover .provider-logo {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.provider-card.available {
|
||||
box-shadow: 0 0 15px #00e0ff;
|
||||
}
|
||||
|
||||
.provider-card.available::after {
|
||||
content: '\f00c';
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
background-color: #00e0ff;
|
||||
color: #121212;
|
||||
border-radius: 50%;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 0 10px rgba(0, 224, 255, 0.5);
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.provider-tooltip {
|
||||
position: absolute;
|
||||
bottom: -35px;
|
||||
background-color: #00e0ff;
|
||||
color: #121212;
|
||||
padding: 5px 12px;
|
||||
border-radius: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.provider-card:hover .provider-tooltip {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
24
js/api.js
24
js/api.js
@ -4,8 +4,7 @@ import { fetchWithTimeout } from './utils.js';
|
||||
import { getFromDB } from './db.js';
|
||||
import { _ } from './utils.js';
|
||||
|
||||
export async function fetchTMDB(endpoint, signal) {
|
||||
let tmdbLang = 'en-US';
|
||||
export async function fetchTMDB(endpoint, params = {}, signal) {
|
||||
const langMap = {
|
||||
'es': 'es-ES',
|
||||
'en': 'en-US',
|
||||
@ -14,13 +13,26 @@ export async function fetchTMDB(endpoint, signal) {
|
||||
'it': 'it-IT',
|
||||
'pt': 'pt-BR'
|
||||
};
|
||||
const tmdbLang = langMap[state.settings.language] || 'en-US';
|
||||
|
||||
if (langMap[state.settings.language]) {
|
||||
tmdbLang = langMap[state.settings.language];
|
||||
const [path, existingQuery] = endpoint.split('?');
|
||||
const finalParams = new URLSearchParams(existingQuery);
|
||||
|
||||
finalParams.set('api_key', state.settings.apiKey);
|
||||
finalParams.set('language', tmdbLang);
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value) {
|
||||
finalParams.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const separator = endpoint.includes('?') ? '&' : '?';
|
||||
const url = `https://api.themoviedb.org/3/${endpoint}${separator}language=${tmdbLang}&api_key=${state.settings.apiKey}`;
|
||||
if (state.settings.watchRegion && !finalParams.has('watch_region')) {
|
||||
finalParams.set('watch_region', state.settings.watchRegion);
|
||||
}
|
||||
|
||||
const url = `https://api.themoviedb.org/3/${path}?${finalParams.toString()}`;
|
||||
|
||||
const response = await fetch(url, { signal });
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ status_message: "Unknown error" }));
|
||||
|
@ -1,5 +1,6 @@
|
||||
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, initializeHeroSection } from './ui.js';
|
||||
import { loadProviderContent, changeProviderPage, backToProviders } from './providers.js';
|
||||
import { debounce, showNotification, _ } from './utils.js';
|
||||
import { clearContentData, loadTokensToEditor, saveTokensFromEditor, exportDatabase, importDatabase } from './db.js';
|
||||
import { startPlexScan } from './plex.js';
|
||||
@ -46,6 +47,7 @@ export function setupEventListeners() {
|
||||
|
||||
document.getElementById('nav-movies').addEventListener('click', (e) => { e.preventDefault(); switchView('movies'); });
|
||||
document.getElementById('nav-series').addEventListener('click', (e) => { e.preventDefault(); switchView('series'); });
|
||||
document.getElementById('nav-providers').addEventListener('click', (e) => { e.preventDefault(); switchView('providers'); });
|
||||
document.getElementById('nav-photos').addEventListener('click', (e) => { e.preventDefault(); switchView('photos'); });
|
||||
document.getElementById('nav-stats').addEventListener('click', (e) => { e.preventDefault(); switchView('stats'); });
|
||||
document.getElementById('nav-favorites').addEventListener('click', (e) => { e.preventDefault(); switchView('favorites'); });
|
||||
@ -87,9 +89,23 @@ export function setupEventListeners() {
|
||||
document.getElementById('sort-filter').addEventListener('change', applyFilters);
|
||||
document.getElementById('stats-token-filter').addEventListener('change', generateStatistics);
|
||||
document.getElementById('photos-token-select').addEventListener('change', handlePhotoTokenChange);
|
||||
document.getElementById('region-filter').addEventListener('change', saveSettings);
|
||||
|
||||
document.getElementById('providers-grid').addEventListener('click', (e) => {
|
||||
const providerCard = e.target.closest('.provider-card');
|
||||
if (providerCard) {
|
||||
const providerId = providerCard.dataset.providerId;
|
||||
const providerName = providerCard.dataset.providerName;
|
||||
loadProviderContent(providerId, providerName, 1);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('back-to-providers').addEventListener('click', backToProviders);
|
||||
document.getElementById('prev-page').addEventListener('click', () => changeProviderPage(-1));
|
||||
document.getElementById('next-page').addEventListener('click', () => changeProviderPage(1));
|
||||
|
||||
document.querySelector('.back-button').addEventListener('click', showMainView);
|
||||
|
||||
|
||||
document.getElementById('main-view').addEventListener('click', handleMainViewClick);
|
||||
document.getElementById('item-details-view').addEventListener('click', handleDetailsClick);
|
||||
|
||||
@ -161,7 +177,6 @@ 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) {
|
||||
@ -178,7 +193,7 @@ export function setupEventListeners() {
|
||||
|
||||
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');
|
||||
|
||||
@ -268,16 +283,16 @@ function handleMainViewClick(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const card = e.target.closest('.item-card');
|
||||
if (!card) return;
|
||||
const itemCard = e.target.closest('.item-card, .provider-item-card');
|
||||
if (!itemCard) return;
|
||||
|
||||
state.lastClickedCardElement = card;
|
||||
state.lastClickedCardElement = itemCard;
|
||||
const actionBtn = e.target.closest('.action-btn');
|
||||
|
||||
if (actionBtn) {
|
||||
e.stopPropagation();
|
||||
const { id, type } = card.dataset;
|
||||
const title = card.querySelector('.item-title')?.textContent;
|
||||
const { id, type } = itemCard.dataset;
|
||||
const title = itemCard.querySelector('.item-title')?.textContent;
|
||||
if (actionBtn.classList.contains('info-btn')) showItemDetails(Number(id), type);
|
||||
else if (actionBtn.classList.contains('favorites-btn')) toggleFavorite(Number(id), type);
|
||||
else if (actionBtn.classList.contains('play-btn')) addStreamToList(title, type, actionBtn);
|
||||
@ -285,7 +300,7 @@ function handleMainViewClick(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, type } = card.dataset;
|
||||
const { id, type } = itemCard.dataset;
|
||||
if (id && type) showItemDetails(Number(id), type);
|
||||
}
|
||||
|
||||
|
14
js/main.js
14
js/main.js
@ -4,6 +4,7 @@ import { initDB, getFromDB } from './db.js';
|
||||
import { MusicPlayer } from './musicPlayer.js';
|
||||
import { ActivityViewer } from './activityViewer.js';
|
||||
import { setupEventListeners } from './eventListeners.js';
|
||||
import { fetchAllProviders, renderProviders, getRegions } from './providers.js';
|
||||
import { loadInitialContent, initializeFavorites, initializeUserData, loadLocalContent, applyTheme, applyHeroVisibility } from './ui.js';
|
||||
import { showNotification, _ } from './utils.js';
|
||||
|
||||
@ -20,6 +21,19 @@ async function loadSettings() {
|
||||
state.settings.apiKey = config.defaultApiKey;
|
||||
}
|
||||
|
||||
if (!state.settings.watchRegion) {
|
||||
const tmdbLangMap = {
|
||||
'es': 'es-ES',
|
||||
'en': 'en-US',
|
||||
'fr': 'fr-FR',
|
||||
'de': 'de-DE',
|
||||
'it': 'it-IT',
|
||||
'pt': 'pt-BR'
|
||||
};
|
||||
const fullLangCode = tmdbLangMap[state.settings.language] || 'en-US';
|
||||
state.settings.watchRegion = fullLangCode.split('-')[1] || 'US';
|
||||
}
|
||||
|
||||
const jellyfinSettingsData = await getFromDB('jellyfin_settings');
|
||||
if (jellyfinSettingsData && jellyfinSettingsData.length > 0) {
|
||||
state.jellyfinSettings = { ...state.jellyfinSettings, ...jellyfinSettingsData[0] };
|
||||
|
178
js/providers.js
Normal file
178
js/providers.js
Normal file
@ -0,0 +1,178 @@
|
||||
import { fetchTMDB } from './api.js';
|
||||
import { state } from './state.js';
|
||||
import { _, showNotification } from './utils.js';
|
||||
import { renderGrid } from './ui.js';
|
||||
|
||||
let currentProviderId = null;
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
|
||||
export async function getAvailableProviders(type, region) {
|
||||
try {
|
||||
const url = `watch/providers/${type}`;
|
||||
const providers = await fetchTMDB(url, { watch_region: region });
|
||||
return providers.results || [];
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${type} providers for region ${region}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAllProviders(region) {
|
||||
const [movieProviders, tvProviders] = await Promise.all([
|
||||
getAvailableProviders('movie', region),
|
||||
getAvailableProviders('tv', region)
|
||||
]);
|
||||
|
||||
const allProvidersMap = new Map();
|
||||
[...movieProviders, ...tvProviders].forEach(provider => {
|
||||
allProvidersMap.set(provider.provider_id, provider);
|
||||
});
|
||||
|
||||
return Array.from(allProvidersMap.values()).sort((a, b) => a.provider_name.localeCompare(b.provider_name));
|
||||
}
|
||||
|
||||
export async function getRegions() {
|
||||
try {
|
||||
const regions = await fetchTMDB('watch/providers/regions');
|
||||
return regions.results || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching regions:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function resetProvidersView() {
|
||||
const providersSection = document.getElementById('providers-section');
|
||||
if (!providersSection) return;
|
||||
|
||||
const grid = document.getElementById('providers-grid');
|
||||
const itemsContainer = document.getElementById('provider-items-container');
|
||||
const titleElement = document.querySelector('#providers-section .section-title');
|
||||
const backButton = document.getElementById('back-to-providers');
|
||||
|
||||
if (grid) grid.style.display = 'grid';
|
||||
if (itemsContainer) itemsContainer.style.display = 'none';
|
||||
if (titleElement) titleElement.textContent = _('navProviders');
|
||||
if (backButton) backButton.style.display = 'none';
|
||||
}
|
||||
|
||||
export async function backToProviders() {
|
||||
resetProvidersView();
|
||||
const providers = await fetchAllProviders(state.settings.watchRegion);
|
||||
renderProviders(providers);
|
||||
}
|
||||
|
||||
export function renderProviders(providers) {
|
||||
const grid = document.getElementById('providers-grid');
|
||||
if (!grid) return;
|
||||
|
||||
grid.style.display = 'grid';
|
||||
if (providers.length === 0) {
|
||||
grid.innerHTML = `<div class="empty-state"><i class="fas fa-tv"></i><p>${_('noProvidersFound')}</p></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const availableProviderIds = new Set(state.localSeries.flatMap(s => s.titulos).map(t => t.provider_id));
|
||||
|
||||
const providersHtml = providers.map(provider => {
|
||||
const isAvailable = availableProviderIds.has(provider.provider_id);
|
||||
return `
|
||||
<div class="provider-card ${isAvailable ? 'available' : ''}" data-provider-id="${provider.provider_id}" data-provider-name="${provider.provider_name}">
|
||||
<img src="https://image.tmdb.org/t/p/w200${provider.logo_path}" alt="${provider.provider_name}" class="provider-logo">
|
||||
<div class="provider-tooltip">${provider.provider_name}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
grid.innerHTML = providersHtml;
|
||||
|
||||
gsap.fromTo(".provider-card",
|
||||
{ scale: 0.5, opacity: 0 },
|
||||
{
|
||||
duration: 0.5,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
delay: 0.2,
|
||||
stagger: 0.08,
|
||||
ease: "back.out(1.7)",
|
||||
force3D: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getProviderItems(providerId, page = 1) {
|
||||
try {
|
||||
const watchRegion = state.settings.watchRegion || 'US';
|
||||
const language = state.settings.language || 'en-US';
|
||||
|
||||
const params = {
|
||||
with_watch_providers: providerId,
|
||||
watch_region: watchRegion,
|
||||
page: page,
|
||||
language: language
|
||||
};
|
||||
|
||||
const moviesResponse = await fetchTMDB('discover/movie', params);
|
||||
const seriesResponse = await fetchTMDB('discover/tv', params);
|
||||
|
||||
const movies = moviesResponse.results.map(item => ({ ...item, media_type: 'movie' }));
|
||||
const series = seriesResponse.results.map(item => ({ ...item, media_type: 'tv' }));
|
||||
|
||||
const items = [...movies, ...series].sort((a, b) => b.popularity - a.popularity);
|
||||
const total_pages = Math.max(moviesResponse.total_pages, seriesResponse.total_pages);
|
||||
|
||||
return { success: true, items, total_pages };
|
||||
} catch (error) {
|
||||
showNotification(`${_('couldNotLoadContent')}: ${error.message}`, 'error');
|
||||
return { success: false, items: [], total_pages: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadProviderContent(providerId, providerName, page = 1) {
|
||||
currentProviderId = providerId;
|
||||
currentPage = page;
|
||||
|
||||
const itemsContainer = document.getElementById('provider-items-container');
|
||||
const providersGrid = document.getElementById('providers-grid');
|
||||
const itemsGrid = document.getElementById('provider-items');
|
||||
const paginationControls = document.getElementById('pagination-controls');
|
||||
const pageNumberSpan = document.getElementById('page-number');
|
||||
const prevButton = document.getElementById('prev-page');
|
||||
const nextButton = document.getElementById('next-page');
|
||||
const sectionTitle = document.querySelector('#providers-section .section-title');
|
||||
const backButton = document.getElementById('back-to-providers');
|
||||
|
||||
providersGrid.style.display = 'none';
|
||||
itemsContainer.style.display = 'block';
|
||||
backButton.style.display = 'block';
|
||||
if (sectionTitle) sectionTitle.textContent = providerName;
|
||||
|
||||
itemsGrid.innerHTML = `<div class="col-12 text-center mt-5"><div class="spinner" style="position: static; margin: auto; display: block;"></div></div>`;
|
||||
paginationControls.style.display = 'none';
|
||||
|
||||
const { success, items, total_pages } = await getProviderItems(providerId, page);
|
||||
|
||||
if (success) {
|
||||
totalPages = total_pages;
|
||||
renderGrid(items, false, 'provider-items');
|
||||
|
||||
if (totalPages > 1) {
|
||||
paginationControls.style.display = 'flex';
|
||||
pageNumberSpan.textContent = `${_('page')} ${currentPage} / ${totalPages}`;
|
||||
prevButton.disabled = currentPage === 1;
|
||||
nextButton.disabled = currentPage >= totalPages;
|
||||
}
|
||||
} else {
|
||||
itemsGrid.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('couldNotLoadContent')}</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export function changeProviderPage(direction) {
|
||||
const newPage = currentPage + direction;
|
||||
if (newPage > 0 && newPage <= totalPages && currentProviderId) {
|
||||
const providerName = document.querySelector('#providers-section .section-title').textContent;
|
||||
loadProviderContent(currentProviderId, providerName, newPage);
|
||||
}
|
||||
}
|
||||
|
@ -56,4 +56,6 @@ export const state = {
|
||||
currentPhotoToken: null,
|
||||
currentPhotoItems: [],
|
||||
currentPhotoLightboxIndex: 0,
|
||||
providerContentPage: 1,
|
||||
providerContentTotalPages: 1,
|
||||
};
|
226
js/ui.js
226
js/ui.js
@ -2,15 +2,34 @@ import { state } from './state.js';
|
||||
import { fetchTMDB, fetchAllAvailableStreams } from './api.js';
|
||||
import { showNotification, getRelativeTime, fetchWithTimeout, _ } from './utils.js';
|
||||
import { getFromDB, addItemsToStore } from './db.js';
|
||||
import { getAvailableProviders, renderProviders, getRegions, fetchAllProviders, resetProvidersView } from './providers.js';
|
||||
|
||||
let charts = {};
|
||||
|
||||
export async function loadInitialContent() {
|
||||
await Promise.all([loadGenres(), loadYears()]);
|
||||
await Promise.all([loadGenres(), loadYears(), loadRegions()]);
|
||||
resetView();
|
||||
setupScrollEffects();
|
||||
}
|
||||
|
||||
async function loadRegions() {
|
||||
const select = document.getElementById('region-filter');
|
||||
select.innerHTML = `<option value="">${_('loadingRegions')}</option>`;
|
||||
try {
|
||||
const data = await getRegions();
|
||||
select.innerHTML = `<option value="">${_('allRegions')}</option>`;
|
||||
data.forEach(region => {
|
||||
const option = document.createElement('option');
|
||||
option.value = region.iso_3166_1;
|
||||
option.textContent = region.english_name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.value = state.settings.watchRegion || "";
|
||||
} catch (error) {
|
||||
select.innerHTML = `<option value="">${_('errorLoadingRegions')}</option>`;
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeUserData() {
|
||||
try {
|
||||
const savedHistory = localStorage.getItem('cineplex_userHistory');
|
||||
@ -71,6 +90,7 @@ export function resetView() {
|
||||
document.getElementById('history-section').style.display = 'none';
|
||||
document.getElementById('recommendations-section').style.display = 'none';
|
||||
document.getElementById('photos-section').style.display = 'none';
|
||||
document.getElementById('providers-section').style.display = 'none';
|
||||
|
||||
// Show hero if enabled
|
||||
if (heroSection) {
|
||||
@ -109,9 +129,12 @@ export function resetView() {
|
||||
}
|
||||
|
||||
export function switchView(viewType) {
|
||||
console.log(`switchView called with viewType: ${viewType}`);
|
||||
if (state.isLoading) return;
|
||||
|
||||
resetProvidersView();
|
||||
|
||||
resetProvidersView();
|
||||
|
||||
const heroSection = document.getElementById('hero-section');
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
|
||||
@ -152,22 +175,38 @@ export function switchView(viewType) {
|
||||
document.getElementById('search-input').value = '';
|
||||
state.lastScrollPosition = 0;
|
||||
|
||||
const allSections = ['content-section', 'stats-section', 'history-section', 'recommendations-section', 'photos-section'];
|
||||
const allSections = ['content-section', 'stats-section', 'history-section', 'recommendations-section', 'photos-section', 'providers-section'];
|
||||
allSections.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = 'none';
|
||||
});
|
||||
|
||||
// Explicitly reset the Providers section to its initial state before view switch
|
||||
const providersSection = document.getElementById('providers-section');
|
||||
if (providersSection) {
|
||||
const detailsContainer = document.getElementById('provider-details-container');
|
||||
if (detailsContainer) detailsContainer.style.display = 'none';
|
||||
|
||||
const gridContainer = document.getElementById('provider-grid-container');
|
||||
if (gridContainer) gridContainer.style.display = 'grid';
|
||||
|
||||
const sectionTitle = providersSection.querySelector('.section-title');
|
||||
if (sectionTitle) sectionTitle.textContent = _('navProviders');
|
||||
|
||||
const backArrow = providersSection.querySelector('.back-arrow');
|
||||
if (backArrow) backArrow.style.display = 'none';
|
||||
}
|
||||
|
||||
const backToProvidersBtn = document.getElementById('back-to-providers-btn');
|
||||
if (backToProvidersBtn) backToProvidersBtn.style.display = 'none';
|
||||
|
||||
const filters = document.querySelector('.filters');
|
||||
if (filters) filters.style.display = 'none';
|
||||
|
||||
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') {
|
||||
@ -178,27 +217,24 @@ 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;
|
||||
case 'providers':
|
||||
document.getElementById('providers-section').style.display = 'block';
|
||||
break;
|
||||
}
|
||||
|
||||
updateActiveNav(viewType);
|
||||
@ -227,6 +263,9 @@ export function switchView(viewType) {
|
||||
case 'photos':
|
||||
initPhotosView();
|
||||
break;
|
||||
case 'providers':
|
||||
loadProviders();
|
||||
break;
|
||||
}
|
||||
|
||||
if (document.getElementById('item-details-view').classList.contains('active')) {
|
||||
@ -267,6 +306,7 @@ export function updateSectionTitle() {
|
||||
case 'history': title = _('historyTitle'); break;
|
||||
case 'recommendations': title = _('recommendationsTitle'); break;
|
||||
case 'photos': title = _('navPhotos'); break;
|
||||
case 'providers': title = _('navProviders'); break;
|
||||
case 'search':
|
||||
title = state.currentParams.query.startsWith('actor:')
|
||||
? _('contentFrom', state.currentParams.query.split(':')[1])
|
||||
@ -276,7 +316,7 @@ export function updateSectionTitle() {
|
||||
}
|
||||
|
||||
let targetTitleElement;
|
||||
if (['stats', 'history', 'recommendations'].includes(state.currentView)) {
|
||||
if (['stats', 'history', 'recommendations', 'providers'].includes(state.currentView)) {
|
||||
targetTitleElement = document.querySelector(`#${state.currentView}-section .section-title`);
|
||||
} else {
|
||||
targetTitleElement = mainTitleElement;
|
||||
@ -284,6 +324,10 @@ export function updateSectionTitle() {
|
||||
|
||||
if(targetTitleElement && state.currentView !== 'photos') {
|
||||
targetTitleElement.textContent = title;
|
||||
const backArrow = targetTitleElement.parentElement.querySelector('.back-arrow');
|
||||
if (backArrow) {
|
||||
backArrow.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -349,7 +393,6 @@ 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;
|
||||
@ -381,13 +424,11 @@ 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>`;
|
||||
}
|
||||
@ -400,35 +441,38 @@ export async function loadContent(append = false) {
|
||||
}
|
||||
}
|
||||
|
||||
function isContentAvailableLocally(title, type) {
|
||||
function isContentAvailableLocally(title, type, year) {
|
||||
if (!title || !type) return false;
|
||||
const normalizedTitle = title.toLowerCase().trim();
|
||||
|
||||
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)
|
||||
const normalize = (str) => str.toLowerCase().trim().replace(/\s+/g, ' ');
|
||||
const normalizedTitle = normalize(title);
|
||||
|
||||
const checkSource = (source, itemType) => {
|
||||
if (!Array.isArray(source)) return false;
|
||||
return source.some(server =>
|
||||
server && Array.isArray(server.titulos) &&
|
||||
server.titulos.some(t => {
|
||||
if (!t || typeof t.title !== 'string') return false;
|
||||
const localTitle = normalize(t.title);
|
||||
if (localTitle !== normalizedTitle) return false;
|
||||
if (year && t.year && Math.abs(parseInt(t.year) - parseInt(year)) > 1) return false;
|
||||
if (itemType && t.type && t.type.toLowerCase() !== itemType.toLowerCase()) return false;
|
||||
return true;
|
||||
})
|
||||
);
|
||||
if (foundInJellyfin) return true;
|
||||
}
|
||||
};
|
||||
|
||||
if (checkSource(state.localMovies, 'movie')) return true;
|
||||
if (checkSource(state.localSeries, 'series')) return true;
|
||||
if (checkSource(state.jellyfinMovies, 'movie')) return true;
|
||||
if (checkSource(state.jellyfinSeries, 'series')) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function renderGrid(items, append = false) {
|
||||
const grid = document.getElementById('content-grid');
|
||||
export function renderGrid(items, append = false, gridId = 'content-grid') {
|
||||
const grid = document.getElementById(gridId);
|
||||
if (!append) grid.innerHTML = '';
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
@ -452,7 +496,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 = isContentAvailableLocally(title, itemType);
|
||||
const isAvailable = isContentAvailableLocally(title, itemType, year);
|
||||
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');
|
||||
@ -818,12 +862,10 @@ 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;
|
||||
}
|
||||
@ -831,7 +873,6 @@ 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>`;
|
||||
@ -1541,7 +1582,8 @@ export async function saveSettings() {
|
||||
phpSecretKey: document.getElementById('phpSecretKey').value.trim(),
|
||||
phpSavePath: document.getElementById('phpSavePath').value.trim(),
|
||||
phpFilename: document.getElementById('phpFilename').value.trim(),
|
||||
phpFileAction: document.getElementById('phpFileActionAppend').checked ? 'append' : 'overwrite'
|
||||
phpFileAction: document.getElementById('phpFileActionAppend').checked ? 'append' : 'overwrite',
|
||||
watchRegion: document.getElementById('region-filter').value
|
||||
};
|
||||
|
||||
state.settings = { ...state.settings, ...newSettings };
|
||||
@ -1616,94 +1658,11 @@ export const phpScriptGenerator = (() => {
|
||||
const filename = dom.filename.value.trim() || 'CinePlex_Playlist.m3u';
|
||||
const appendToFile = dom.fileActionAppendRadio.checked;
|
||||
|
||||
let script = `<?php
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, Origin, X-Secret-Key');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
define('SAVE_DIRECTORY', '${savePath.replace(/'/g, "\\'")}');
|
||||
define('FILENAME', '${filename.replace(/'/g, "\\'")}');
|
||||
define('FILE_ACTION_APPEND', ${appendToFile ? 'true' : 'false'});
|
||||
${useSecretKey ? `define('SECRET_KEY', '${secretKey.replace(/'/g, "\\'")}');` : ''}
|
||||
|
||||
function sendResponse($success, $message, $filename = '', $http_code = 200) {
|
||||
if (!$success && $http_code === 200) {
|
||||
$http_code = 400;
|
||||
}
|
||||
http_response_code($http_code);
|
||||
echo json_encode(['success' => $success, 'message' => $message, 'filename' => $filename]);
|
||||
exit;
|
||||
}
|
||||
`;
|
||||
let script = `<?php\nheader('Content-Type: application/json');\nheader('Access-Control-Allow-Origin: *');\nheader('Access-Control-Allow-Methods: POST, OPTIONS');\nheader('Access-Control-Allow-Headers: Content-Type, Origin, X-Secret-Key');\n\nif ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {\n http_response_code(200);\n exit(0);\n}\n\ndefine('SAVE_DIRECTORY', '${savePath.replace(/'/g, "\\'")}');\ndefine('FILENAME', '${filename.replace(/'/g, "\\'")}');\ndefine('FILE_ACTION_APPEND', ${appendToFile ? 'true' : 'false'});\n${useSecretKey ? `define('SECRET_KEY', '${secretKey.replace(/'/g, "\\'")}');` : ''}\n\nfunction sendResponse($success, $message, $filename = '', $http_code = 200) {\n if (!$success && $http_code === 200) {\n $http_code = 400;\n }\n http_response_code($http_code);\n echo json_encode(['success' => $success, 'message' => $message, 'filename' => $filename]);\n exit;\n}\n`;
|
||||
if (useSecretKey) {
|
||||
script += `
|
||||
$auth_key = isset($_SERVER['HTTP_X_SECRET_KEY']) ? $_SERVER['HTTP_X_SECRET_KEY'] : '';
|
||||
if (!defined('SECRET_KEY') || SECRET_KEY === '' || $auth_key !== SECRET_KEY) {
|
||||
sendResponse(false, 'Acceso no autorizado. Clave secreta inválida o no proporcionada.', '', 403);
|
||||
}
|
||||
`;
|
||||
script += `\n$auth_key = isset($_SERVER['HTTP_X_SECRET_KEY']) ? $_SERVER['HTTP_X_SECRET_KEY'] : '';\nif (!defined('SECRET_KEY') || SECRET_KEY === '' || $auth_key !== SECRET_KEY) {\n sendResponse(false, 'Acceso no autorizado. Clave secreta inválida o no proporcionada.', '', 403);\n}\n`;
|
||||
}
|
||||
script += `
|
||||
$json_data = file_get_contents('php://input');
|
||||
$data = json_decode($json_data, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
sendResponse(false, 'Error: Datos JSON inválidos.');
|
||||
}
|
||||
|
||||
if (!isset($data['streams']) || !is_array($data['streams']) || empty($data['streams'])) {
|
||||
sendResponse(false, 'Error: El JSON debe contener un array "streams" no vacío.');
|
||||
}
|
||||
|
||||
$save_dir = SAVE_DIRECTORY !== '' ? rtrim(SAVE_DIRECTORY, '/\\\\') : __DIR__;
|
||||
|
||||
if (!is_dir($save_dir) || !is_writable($save_dir)) {
|
||||
sendResponse(false, 'Error del servidor: El directorio de destino no existe o no tiene permisos de escritura.', '', 500);
|
||||
}
|
||||
|
||||
$safe_filename = preg_replace('/[^\\w\\s._-]/', '', basename(FILENAME));
|
||||
$safe_filename = preg_replace('/\\s+/', '_', $safe_filename);
|
||||
$target_path = $save_dir . DIRECTORY_SEPARATOR . $safe_filename;
|
||||
|
||||
$content_to_write = "";
|
||||
|
||||
if (FILE_ACTION_APPEND) {
|
||||
$file_exists = file_exists($target_path);
|
||||
if (!$file_exists) {
|
||||
$content_to_write .= "#EXTM3U\\n";
|
||||
}
|
||||
foreach ($data['streams'] as $stream) {
|
||||
if (isset($stream['extinf'], $stream['url'])) {
|
||||
$content_to_write .= trim($stream['extinf']) . "\\n";
|
||||
$content_to_write .= trim($stream['url']) . "\\n";
|
||||
}
|
||||
}
|
||||
if (file_put_contents($target_path, $content_to_write, FILE_APPEND | LOCK_EX) !== false) {
|
||||
sendResponse(true, 'Streams añadidos correctamente al archivo.', $safe_filename, 200);
|
||||
} else {
|
||||
sendResponse(false, 'Error del servidor: No se pudo añadir contenido al archivo.', '', 500);
|
||||
}
|
||||
} else { // Overwrite mode
|
||||
$content_to_write = "#EXTM3U\\n";
|
||||
foreach ($data['streams'] as $stream) {
|
||||
if (isset($stream['extinf'], $stream['url'])) {
|
||||
$content_to_write .= trim($stream['extinf']) . "\\n";
|
||||
$content_to_write .= trim($stream['url']) . "\\n";
|
||||
}
|
||||
}
|
||||
if (file_put_contents($target_path, $content_to_write, LOCK_EX) !== false) {
|
||||
sendResponse(true, 'Archivo de streams sobrescrito correctamente.', $safe_filename, 201);
|
||||
} else {
|
||||
sendResponse(false, 'Error del servidor: No se pudo escribir el archivo.', '', 500);
|
||||
}
|
||||
}
|
||||
?>`;
|
||||
script += `\n$json_data = file_get_contents('php://input');\n$data = json_decode($json_data, true);\n\nif (json_last_error() !== JSON_ERROR_NONE) {\n sendResponse(false, 'Error: Datos JSON inválidos.');\n}\n\nif (!isset($data['streams']) || !is_array($data['streams']) || empty($data['streams'])) {\n sendResponse(false, 'Error: El JSON debe contener un array "streams" no vacío.');\n}\n\n$save_dir = SAVE_DIRECTORY !== '' ? rtrim(SAVE_DIRECTORY, '/\\') : __DIR__;\n\nif (!is_dir($save_dir) || !is_writable($save_dir)) {\n sendResponse(false, 'Error del servidor: El directorio de destino no existe o no tiene permisos de escritura.', '', 500);\n}\n\n$safe_filename = preg_replace('/[^\\w\\s._-]/', '', basename(FILENAME));\n$safe_filename = preg_replace('/\\s+/', '_', $safe_filename);\n$target_path = $save_dir . DIRECTORY_SEPARATOR . $safe_filename;\n\n$content_to_write = "";\n\nif (FILE_ACTION_APPEND) {\n $file_exists = file_exists($target_path);\n if (!$file_exists) {\n $content_to_write .= "#EXTM3U\\n";\n }\n foreach ($data['streams'] as $stream) {\n if (isset($stream['extinf'], $stream['url'])) {\n $content_to_write .= trim($stream['extinf']) . "\\n";\n $content_to_write .= trim($stream['url']) . "\\n";\n }\n }\n if (file_put_contents($target_path, $content_to_write, FILE_APPEND | LOCK_EX) !== false) {\n sendResponse(true, 'Streams añadidos correctamente al archivo.', $safe_filename, 200);\n } else {\n sendResponse(false, 'Error del servidor: No se pudo añadir contenido al archivo.', '', 500);\n }\n} else { // Overwrite mode\n $content_to_write = "#EXTM3U\\n";\n foreach ($data['streams'] as $stream) {\n if (isset($stream['extinf'], $stream['url'])) {\n $content_to_write .= trim($stream['extinf']) . "\\n";\n $content_to_write .= trim($stream['url']) . "\\n";\n }\n }\n if (file_put_contents($target_path, $content_to_write, LOCK_EX) !== false) {\n sendResponse(true, 'Archivo de streams sobrescrito correctamente.', $safe_filename, 201);\n } else {\n sendResponse(false, 'Error del servidor: No se pudo escribir el archivo.', '', 500);\n }\n}\n?>`;
|
||||
dom.generatedCode.value = script;
|
||||
showNotification(_("scriptGenerated"), "success");
|
||||
}
|
||||
@ -1764,7 +1723,7 @@ export function handlePhotoTokenChange() {
|
||||
}
|
||||
|
||||
state.currentPhotoToken = selectedToken;
|
||||
state.currentPhotoServer = state.localPhotos.find(s => s.tokenPrincipal === state.currentPhotoToken);
|
||||
state.currentPhotoServer = state.localPhotos.find(s => s.tokenPrincipal === selectedToken);
|
||||
|
||||
state.photoStack = [];
|
||||
renderPhotoBreadcrumb();
|
||||
@ -1991,4 +1950,11 @@ export function showNextPhoto() {
|
||||
export function showPrevPhoto() {
|
||||
state.currentPhotoLightboxIndex = (state.currentPhotoLightboxIndex - 1 + state.currentPhotoItems.length) % state.currentPhotoItems.length;
|
||||
updatePhotoLightbox();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function loadProviders() {
|
||||
const providers = await fetchAllProviders(state.settings.watchRegion);
|
||||
renderProviders(providers);
|
||||
}
|
||||
|
25
plex.html
25
plex.html
@ -13,6 +13,7 @@
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<link rel="stylesheet" href="css/equalizer.css">
|
||||
<link rel="stylesheet" href="css/photos.css">
|
||||
<link rel="stylesheet" href="css/providers.css">
|
||||
</head>
|
||||
|
||||
<body class="unlocalized">
|
||||
@ -46,6 +47,7 @@
|
||||
<ul class="sidebar-menu">
|
||||
<li><a class="nav-link active" href="#" id="nav-movies"><i class="fas fa-film"></i><span>__MSG_navMovies__</span></a></li>
|
||||
<li><a class="nav-link" href="#" id="nav-series"><i class="fas fa-tv"></i><span>__MSG_navSeries__</span></a></li>
|
||||
<li><a class="nav-link" href="#" id="nav-providers"><i class="fas fa-broadcast-tower"></i><span>__MSG_navProviders__</span></a></li>
|
||||
<li><a class="nav-link" href="#" id="nav-photos"><i class="fas fa-images"></i><span>__MSG_navPhotos__</span></a></li>
|
||||
<li><a class="nav-link" href="#" id="nav-stats"><i class="fas fa-chart-pie"></i><span>__MSG_navStats__</span></a></li>
|
||||
<li><a class="nav-link" href="#" id="nav-favorites"><i class="fas fa-heart"></i><span>__MSG_navFavorites__</span></a></li>
|
||||
@ -104,6 +106,22 @@
|
||||
<div class="text-center mt-5"><button id="load-more" class="btn btn-primary" style="display: none;">__MSG_loadMore__</button></div>
|
||||
</section>
|
||||
|
||||
<section id="providers-section" style="display: none;">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">__MSG_navProviders__</h2>
|
||||
</div>
|
||||
<div id="providers-grid" class="providers-grid"></div>
|
||||
<div id="provider-items-container" style="display: none;">
|
||||
<button id="back-to-providers" class="btn btn-secondary mb-3"><i class="fas fa-arrow-left me-2"></i>__MSG_backToProviders__</button>
|
||||
<div id="provider-items" class="content-grid"></div>
|
||||
<div id="pagination-controls" class="text-center mt-4" style="display: none;">
|
||||
<button id="prev-page" class="btn btn-primary">← __MSG_previous__</button>
|
||||
<span id="page-number" class="mx-3 align-middle"></span>
|
||||
<button id="next-page" class="btn btn-primary">__MSG_next__ →</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="photos-section" style="display: none;">
|
||||
<div class="photos-header">
|
||||
<nav aria-label="breadcrumb">
|
||||
@ -231,6 +249,7 @@
|
||||
<div class="footer-links">
|
||||
<a href="#" class="footer-link" id="footer-movies">__MSG_navMovies__</a>
|
||||
<a href="#" class="footer-link" id="footer-series">__MSG_navSeries__</a>
|
||||
<a href="#" class="footer-link" id="footer-providers">__MSG_navProviders__</a>
|
||||
<a href="#" class="footer-link" id="footer-stats">__MSG_navStats__</a>
|
||||
<a href="#" class="footer-link" id="footer-favorites">__MSG_navFavorites__</a>
|
||||
</div>
|
||||
@ -349,6 +368,12 @@
|
||||
<option value="it">__MSG_lang_it__</option>
|
||||
<option value="pt">__MSG_lang_pt__</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="region-filter" class="form-label">__MSG_settingsRegionLabel__</label>
|
||||
<select class="form-control filter-select" id="region-filter">
|
||||
<option value="">__MSG_allRegions__</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="phpScriptUrl" class="form-label">__MSG_settingsPhpUrlLabel__</label>
|
||||
|
Loading…
x
Reference in New Issue
Block a user