bug fix audio musicplayer
This commit is contained in:
parent
3e176a5762
commit
ebabbe8b2d
109
README.md
109
README.md
@ -1,70 +1,67 @@
|
|||||||
# CinePlex: Your Plex on Steroids 🚀
|
# CinePlex: Your Plex on Steroids 🚀
|
||||||
|
|
||||||
## 🤔 What the Heck Is This? Prepare for Takeoff!
|
## 🤔 So, What the Heck Is This?
|
||||||
|
|
||||||
Let's be real. You love your Plex server. It's your own little corner of the digital universe, packed with your movies, shows, music, and photos. But... don't you ever feel like the interface could be... *more*? A little more ✨ **magic and sparkle** ✨?
|
Let's be real. You love your Plex server. It's your own little corner of the digital universe, packed with your movies, shows, and music. But... don't you ever feel like the interface could be... *more*? A little more ✨ **pizzazz** ✨?
|
||||||
|
|
||||||
Well, **CinePlex** is the answer to your digital prayers! It's like you bought your Plex a superhero cape and sent it to a futuristic spa. It's a modern, fast, and stunningly designed interface that sits on top of your Plex servers to give you a visually dazzling experience.
|
Well, **CinePlex** is the answer. It's like you bought your Plex a superhero cape and sent it to a futuristic spa. It's a modern, fast, and slick-looking interface that sits on top of your Plex servers to give you a visually stunning experience.
|
||||||
|
|
||||||
**In short:** CinePlex is not a streaming service. It's a *radical transformation* for the content **you already own**. It's your personal media universe, but with steroids and a lot more style!
|
**In short:** CinePlex is not a streaming service. It's a radical new *look* for the content **you already own**. It's your personal media universe, but cooler.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🌐 We Speak All Languages! (Well, Almost)
|
## 🚨 A Quick Heads-Up: ¡Hablamos Español!
|
||||||
|
|
||||||
Goodbye, language barrier! 👋 We've been working hard in the lab, and now CinePlex is a polyglot. Thanks to the magic of Chrome's `i18n` API, the extension is fully translated and ready to speak your language. Yes, you heard that right! No more excuses not to dive into your favorite content.
|
> Right now, the entire user interface is in **Spanish**.
|
||||||
|
|
||||||
**We currently support:** Spanish, English, German, and French. And we're adding more!
|
I'm currently in the lab, tinkering away to integrate Chrome's `i18n` API to bring a full **English** translation to the party. It's a top priority! So please bear with the Spanish for now, an English version is coming soon.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ The Feature Loot Drop (What Makes It Awesome!)
|
## ✨ The Feature Loot Drop (What Makes It Awesome)
|
||||||
|
|
||||||
Get ready to be amazed by everything CinePlex has under the hood. This isn't just an extension; it's an experience!
|
* **🎬 Pimped-Out Interface:** Forget boring UIs. We use TheMovieDB's API to bring you high-res posters, backdrops, synopses, ratings, cast info... all the juicy movie gossip you crave.
|
||||||
|
* **📡 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).
|
||||||
* **🎬 Pimped-Out Interface:** Forget boring UIs. We use TheMovieDB's API to bring you high-res posters, spectacular backdrops, gripping synopses, ratings, cast info... all the juicy movie gossip you crave!
|
|
||||||
* **🗣️ Multilingual Maestro:** Hola! Bonjour! Hallo! CinePlex now speaks your language with full i18n support. No more getting lost in translation – your media, your language!
|
|
||||||
* **📡 Psychic Plex Scanner:** You give it your Plex tokens, and CinePlex goes into full detective mode. It scans your servers, figures out what you *actually* have, and jots it down in its secret notebook (a local IndexedDB database in your browser). It's like having a personal librarian for your media!
|
|
||||||
* **✅ "Got It" Badge of Honor:** See a movie you want to watch? CinePlex will let you know if you already have it on your server with a neat "Local" badge. No more blind searching!
|
* **✅ "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!
|
* **🎶 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**!
|
||||||
* **📊 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!
|
* **📊 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 fancy charts.
|
||||||
* **🖼️ Your Personal Photo Gallery:** Connect to your Plex photo libraries and browse your albums and pictures with a beautiful, integrated lightbox viewer. Relive your memories in style!
|
* **🖼️ Your Personal Photo Gallery:** Connect to your Plex photo libraries and browse your albums and pictures with a beautiful, integrated lightbox viewer.
|
||||||
* **📜 The Scroll of Power (M3U):** For the power users. Found that series you wanted to watch? Generate an `.m3u` file with all the episode links with a single click. Power in your hands!
|
* **📜 The Scroll of Power (M3U):** For the power users. Found that series you wanted to watch? Generate an `.m3u` file with all the episode links with a single click.
|
||||||
* **🔥 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!
|
* **🔥 Stream Straight to Your Server:** This is where it gets crazy. Configure a simple PHP script on your server, and you can send streams from CinePlex directly to your M3U playlist file with one 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!
|
* **❤️ 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.
|
||||||
* **🧠 AI-Powered Recommendations:** Based on your viewing history and favorites, CinePlex will suggest new content you might love. It's like having a personal movie critic living in your browser.
|
* **🔧 Customization Tuning Shop:** Don't like the dark theme? Switch to light mode! Don't want the giant hero banner? Hide it! Add your own TMDB API key. You're the boss.
|
||||||
* **🔧 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!
|
* **💾 Data Hoarder's Dream:** Import and export your entire local CinePlex database. Move your settings and scanned data between computers with ease.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Installation and First Steps: Liftoff in 3, 2, 1...!
|
## 🛠️ Installation & First-Time Setup
|
||||||
|
|
||||||
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 easy. Just follow these steps.
|
||||||
|
|
||||||
### 1. Installing the Extension: The First Quantum Leap!
|
### 1. Installing the Extension
|
||||||
|
|
||||||
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.
|
Since this isn't on the Chrome Web Store, you'll have to load it as an "unpacked" extension.
|
||||||
|
|
||||||
1. **Download this repository:** Click the green "Code" button and select "Download ZIP". Unzip the file somewhere you'll remember (perhaps your "Documents" folder or a secret place where you keep your digital treasures!).
|
1. **Download this repository:** Click the green "Code" button and select "Download ZIP". Unzip the file somewhere you'll remember (like your `Documents` folder).
|
||||||
2. **Open Chrome Extensions:** In a new tab, type `chrome://extensions` and press Enter. Welcome to the control center of your browser superpowers!
|
2. **Open Chrome Extensions:** Open a new tab and go to `chrome://extensions`.
|
||||||
3. **Enable Developer Mode:** Find the "Developer mode" toggle in the top-right corner and switch it **ON**. It's like turning on your browser's turbo mode!
|
3. **Enable Developer Mode:** Find the "Developer mode" toggle in the top-right corner and switch it **ON**.
|
||||||
4. **Load the Extension:** You'll see some new buttons appear. Click on **"Load unpacked"**.
|
4. **Load the Extension:** You'll see some new buttons appear. Click on **"Load unpacked"**.
|
||||||
5. **Select the Folder:** A file browser will open. Navigate to the folder where you unzipped the repository and select it. Don't pick the wrong folder, or the universe might collapse! (Well, maybe not, but it won't work).
|
5. **Select the Folder:** A file browser will open. Navigate to the folder where you unzipped the repository and select it.
|
||||||
6. **Mission Ready!** You should now see the CinePlex card in your extensions list. Congratulations, you've installed your first piece of space technology!
|
6. **Done!** You should now see the CinePlex card in your extensions list.
|
||||||
|
|
||||||
### 2. Initial Setup: Your Personal Operations Base!
|
### 2. First-Time Setup (The Fun Part!)
|
||||||
|
|
||||||
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!
|
When you first open CinePlex, it's a blank slate. It's like a cool new apartment, but you need to bring your furniture. Let's do that.
|
||||||
|
|
||||||
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. **Find Your Plex Token:** This is the most important step. You need to get your `X-Plex-Token` to let CinePlex talk to your 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/).
|
||||||
|
|
||||||
2. **Open CinePlex Settings: The Control Panel!**
|
2. **Open CinePlex Settings:**
|
||||||
* Click the CinePlex icon in your browser's toolbar to open the application in a new tab.
|
* 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!
|
* Click the **cogwheel icon (⚙️)** in the top-right corner to open the Settings modal.
|
||||||
|
|
||||||
3. **Add Your Token: Injecting the Fuel!**
|
3. **Add Your Token:**
|
||||||
* Go to the **Plex** tab.
|
* 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:
|
* 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. It should look like this:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"tokens": [
|
"tokens": [
|
||||||
@ -73,34 +70,24 @@ When you first open CinePlex, it's like a newly built spaceship: impressive, but
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
* Click the **"Save Tokens"** button. You've secured the connection!
|
* Click the **"Guardar Tokens"** (Save Tokens) button.
|
||||||
|
|
||||||
4. **Start Your First Scan: The Great Exploration!**
|
4. **Run Your First Scan:**
|
||||||
* Still in the Plex tab, check the boxes for the content you want to scan (e.g., Movies, Series, Music, Photos).
|
* Still in the Plex tab, check the boxes for the content you want to scan (e.g., Películas, Series).
|
||||||
* Click the big blue **"Start Scan"** button. It's time for CinePlex to discover all your treasures!
|
* Click the big blue **"Iniciar Escaneo"** (Start Scan) button.
|
||||||
* 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!
|
* 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 large library.
|
||||||
|
|
||||||
5. **Enjoy!** Once the scan is complete, the app will automatically refresh. Go back to the main view and start exploring your newly supercharged Plex interface! The galaxy of your content awaits!
|
5. **Enjoy!** Once the scan is complete, the app will automatically refresh. Go back to the main view and start exploring your newly supercharged Plex interface!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 💡 Pro-Tip: Setting Up the "Add Stream" Feature - The Magic Button!
|
## 💡 Pro-Tip: Setting Up the "Add Stream" Feature
|
||||||
|
|
||||||
Want to take your experience to the next level and use the "Add Stream" button? It's like having a magic wand for your content!
|
Want to use the "Add Stream" button? It's awesome.
|
||||||
|
|
||||||
1. In the CinePlex settings, go to the **"PHP Generator"** tab.
|
1. In the CinePlex settings, go to the **"Generador PHP"** tab.
|
||||||
2. Configure the options (like the filename, save path, and security key) and click **"Generate Script"**. Watch as the magic of code materializes!
|
2. Configure the options (like the filename) and click **"Generar Script"**.
|
||||||
3. Copy the generated PHP code. It's your secret recipe!
|
3. Copy the generated PHP code.
|
||||||
4. Save that code as a `.php` file (e.g., `playlist.php`) and upload it to a web server you control. Your own corner on the web for your streams!
|
4. Save that code as a `.php` file (e.g., `playlist.php`) and upload it to a web server you control.
|
||||||
5. Go to the **"General"** tab in settings and paste the public URL to your new script in the **"Stream Server URL"** field. Connecting the dots!
|
5. Go to the **"General"** tab in settings and paste the public URL to your new script in the **"URL del Servidor para Añadir Streams"** field.
|
||||||
6. Save, and you're ready! Now you can add streams with a single click. It's like teleporting your content wherever you need it!
|
6. Save, and you're ready to add streams with a single click!
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔒 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, 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.
|
|
@ -256,8 +256,8 @@
|
|||||||
"writer": {"message": "Autor:"},
|
"writer": {"message": "Autor:"},
|
||||||
"viewOnImdb": {"message": "Auf IMDb ansehen"},
|
"viewOnImdb": {"message": "Auf IMDb ansehen"},
|
||||||
"watchTrailer": {"message": "Trailer ansehen"},
|
"watchTrailer": {"message": "Trailer ansehen"},
|
||||||
"addToFavorites": {"message": "Zu Favoriten hinzufügen"},
|
"addToFavorites": {"message": "Favorit"},
|
||||||
"removeFromFavorites": {"message": "Aus Favoriten entfernen"},
|
"removeFromFavorites": {"message": "Entfernen"},
|
||||||
"notAvailable": {"message": "Nicht verfügbar"},
|
"notAvailable": {"message": "Nicht verfügbar"},
|
||||||
"mainCast": {"message": "Hauptbesetzung"},
|
"mainCast": {"message": "Hauptbesetzung"},
|
||||||
"seasonsAndEpisodes": {"message": "Staffeln & Episoden"},
|
"seasonsAndEpisodes": {"message": "Staffeln & Episoden"},
|
||||||
|
@ -256,8 +256,8 @@
|
|||||||
"writer": {"message": "Writer:"},
|
"writer": {"message": "Writer:"},
|
||||||
"viewOnImdb": {"message": "View on IMDb"},
|
"viewOnImdb": {"message": "View on IMDb"},
|
||||||
"watchTrailer": {"message": "Watch Trailer"},
|
"watchTrailer": {"message": "Watch Trailer"},
|
||||||
"addToFavorites": {"message": "Add to Favorites"},
|
"addToFavorites": {"message": "Favorite"},
|
||||||
"removeFromFavorites": {"message": "Remove from Favorites"},
|
"removeFromFavorites": {"message": "Unfavorite"},
|
||||||
"notAvailable": {"message": "Not Available"},
|
"notAvailable": {"message": "Not Available"},
|
||||||
"mainCast": {"message": "Main Cast"},
|
"mainCast": {"message": "Main Cast"},
|
||||||
"seasonsAndEpisodes": {"message": "Seasons & Episodes"},
|
"seasonsAndEpisodes": {"message": "Seasons & Episodes"},
|
||||||
|
@ -117,7 +117,7 @@
|
|||||||
"miniplayerOpenList": { "message": "Abrir lista" },
|
"miniplayerOpenList": { "message": "Abrir lista" },
|
||||||
"eqTitle": { "message": "Ecualizador Gráfico" },
|
"eqTitle": { "message": "Ecualizador Gráfico" },
|
||||||
"eqPresetsLabel": { "message": "Presets" },
|
"eqPresetsLabel": { "message": "Presets" },
|
||||||
"eqPresetFlat": { "message": "Plano" },
|
"eqPresetFlat": { "message": "Plano (Flat)" },
|
||||||
"eqPresetRock": { "message": "Rock" },
|
"eqPresetRock": { "message": "Rock" },
|
||||||
"eqPresetPop": { "message": "Pop" },
|
"eqPresetPop": { "message": "Pop" },
|
||||||
"eqPresetJazz": { "message": "Jazz" },
|
"eqPresetJazz": { "message": "Jazz" },
|
||||||
@ -256,8 +256,8 @@
|
|||||||
"writer": {"message": "Escritor:"},
|
"writer": {"message": "Escritor:"},
|
||||||
"viewOnImdb": {"message": "Ver en IMDb"},
|
"viewOnImdb": {"message": "Ver en IMDb"},
|
||||||
"watchTrailer": {"message": "Ver Tráiler"},
|
"watchTrailer": {"message": "Ver Tráiler"},
|
||||||
"addToFavorites": {"message": "Añadir a favoritos"},
|
"addToFavorites": {"message": "Favorito"},
|
||||||
"removeFromFavorites": {"message": "Quitar de favoritos"},
|
"removeFromFavorites": {"message": "Quitar Fav."},
|
||||||
"notAvailable": {"message": "No disponible"},
|
"notAvailable": {"message": "No disponible"},
|
||||||
"mainCast": {"message": "Reparto Principal"},
|
"mainCast": {"message": "Reparto Principal"},
|
||||||
"seasonsAndEpisodes": {"message": "Temporadas y Episodios"},
|
"seasonsAndEpisodes": {"message": "Temporadas y Episodios"},
|
||||||
|
@ -257,7 +257,7 @@
|
|||||||
"viewOnImdb": {"message": "Voir sur IMDb"},
|
"viewOnImdb": {"message": "Voir sur IMDb"},
|
||||||
"watchTrailer": {"message": "Voir la bande-annonce"},
|
"watchTrailer": {"message": "Voir la bande-annonce"},
|
||||||
"addToFavorites": {"message": "Ajouter aux favoris"},
|
"addToFavorites": {"message": "Ajouter aux favoris"},
|
||||||
"removeFromFavorites": {"message": "Supprimer des favoris"},
|
"removeFromFavorites": {"message": "Retirer des favoris"},
|
||||||
"notAvailable": {"message": "Non disponible"},
|
"notAvailable": {"message": "Non disponible"},
|
||||||
"mainCast": {"message": "Distribution Principale"},
|
"mainCast": {"message": "Distribution Principale"},
|
||||||
"seasonsAndEpisodes": {"message": "Saisons & Épisodes"},
|
"seasonsAndEpisodes": {"message": "Saisons & Épisodes"},
|
||||||
|
@ -149,7 +149,7 @@
|
|||||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%231f2937'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%231f2937'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-select option, .form-control option {
|
.filter-select option {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border: none;
|
border: none;
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 466 KiB After Width: | Height: | Size: 477 KiB |
Binary file not shown.
Before Width: | Height: | Size: 469 KiB After Width: | Height: | Size: 477 KiB |
91
js/api.js
91
js/api.js
@ -84,17 +84,25 @@ export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artista
|
|||||||
return tracks;
|
return tracks;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Error in getMusicUrlsFromPlex:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
|
||||||
|
if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') };
|
||||||
|
if (!state.db) return { success: false, streams: [], message: _('dbUnavailableForStreams') };
|
||||||
|
|
||||||
async function searchAndProcessServer(server, busqueda, tipoContenido) {
|
const plexSearchType = tipoContenido === 'movie' ? '1' : '2';
|
||||||
|
const servers = await getFromDB('conexiones_locales');
|
||||||
|
if (!servers || servers.length === 0) return { success: false, streams: [], message: _('noPlexServersForStreams') };
|
||||||
|
|
||||||
|
const searchTasks = servers.map(async (server) => {
|
||||||
const { ip, puerto, token, protocolo = 'http', nombre: serverName = 'Servidor Desconocido' } = server;
|
const { ip, puerto, token, protocolo = 'http', nombre: serverName = 'Servidor Desconocido' } = server;
|
||||||
if (!ip || !puerto || !token) return [];
|
if (!ip || !puerto || !token) return [];
|
||||||
|
|
||||||
const plexSearchType = tipoContenido === 'movie' ? '1' : '2';
|
|
||||||
const searchUrl = `${protocolo}://${ip}:${puerto}/search?type=${plexSearchType}&query=${encodeURIComponent(busqueda)}&X-Plex-Token=${token}`;
|
const searchUrl = `${protocolo}://${ip}:${puerto}/search?type=${plexSearchType}&query=${encodeURIComponent(busqueda)}&X-Plex-Token=${token}`;
|
||||||
|
let serverStreams = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetchWithTimeout(searchUrl, { headers: { 'Accept': 'application/xml' } });
|
const response = await fetchWithTimeout(searchUrl, { headers: { 'Accept': 'application/xml' } });
|
||||||
@ -106,16 +114,6 @@ async function searchAndProcessServer(server, busqueda, tipoContenido) {
|
|||||||
if (xml.querySelector('parsererror')) return [];
|
if (xml.querySelector('parsererror')) return [];
|
||||||
|
|
||||||
if (tipoContenido === 'movie') {
|
if (tipoContenido === 'movie') {
|
||||||
return processMovieResults(xml, busqueda, protocolo, ip, puerto, token);
|
|
||||||
} else {
|
|
||||||
return await processShowResults(xml, busqueda, protocolo, ip, puerto, token);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function processMovieResults(xml, busqueda, protocolo, ip, puerto, token) {
|
|
||||||
const videos = Array.from(xml.querySelectorAll("Video"));
|
const videos = Array.from(xml.querySelectorAll("Video"));
|
||||||
let videosToProcess = videos;
|
let videosToProcess = videos;
|
||||||
const exactMatch = videos.find(v => v.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase());
|
const exactMatch = videos.find(v => v.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase());
|
||||||
@ -123,26 +121,23 @@ function processMovieResults(xml, busqueda, protocolo, ip, puerto, token) {
|
|||||||
videosToProcess = [exactMatch];
|
videosToProcess = [exactMatch];
|
||||||
}
|
}
|
||||||
|
|
||||||
return videosToProcess.map(video => {
|
videosToProcess.forEach(video => {
|
||||||
const part = video.querySelector("Part");
|
const part = video.querySelector("Part");
|
||||||
if (!part || !part.getAttribute("key")) return null;
|
if (part && part.getAttribute("key")) {
|
||||||
|
|
||||||
const movieTitle = video.getAttribute("title") || busqueda;
|
const movieTitle = video.getAttribute("title") || busqueda;
|
||||||
const movieYear = video.getAttribute("year");
|
const movieYear = video.getAttribute("year");
|
||||||
const streamUrl = `${protocolo}://${ip}:${puerto}${part.getAttribute("key")}?X-Plex-Token=${token}`;
|
const streamUrl = `${protocolo}://${ip}:${puerto}${part.getAttribute("key")}?X-Plex-Token=${token}`;
|
||||||
const extinfName = `${movieTitle}${movieYear ? ` (${movieYear})` : ''}`;
|
const extinfName = `${movieTitle}${movieYear ? ` (${movieYear})` : ''}`;
|
||||||
const logoUrl = video.getAttribute("thumb") ? `${protocolo}://${ip}:${puerto}${video.getAttribute("thumb")}?X-Plex-Token=${token}` : '';
|
const logoUrl = video.getAttribute("thumb") ? `${protocolo}://${ip}:${puerto}${video.getAttribute("thumb")}?X-Plex-Token=${token}` : '';
|
||||||
const groupTitle = extinfName.replace(/"/g, "'");
|
const groupTitle = extinfName.replace(/"/g, "'");
|
||||||
|
serverStreams.push({
|
||||||
return {
|
|
||||||
url: streamUrl,
|
url: streamUrl,
|
||||||
title: extinfName,
|
title: extinfName,
|
||||||
extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${logoUrl}" group-title="${groupTitle}",${extinfName}`
|
extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${logoUrl}" group-title="${groupTitle}",${extinfName}`
|
||||||
};
|
});
|
||||||
}).filter(Boolean);
|
}
|
||||||
}
|
});
|
||||||
|
} else {
|
||||||
async function processShowResults(xml, busqueda, protocolo, ip, puerto, token) {
|
|
||||||
const directories = Array.from(xml.querySelectorAll('Directory[type="show"]'));
|
const directories = Array.from(xml.querySelectorAll('Directory[type="show"]'));
|
||||||
let directoryToProcess = directories.find(d => d.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase());
|
let directoryToProcess = directories.find(d => d.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase());
|
||||||
if (!directoryToProcess && directories.length > 0) {
|
if (!directoryToProcess && directories.length > 0) {
|
||||||
@ -151,37 +146,29 @@ async function processShowResults(xml, busqueda, protocolo, ip, puerto, token) {
|
|||||||
|
|
||||||
if (directoryToProcess && directoryToProcess.getAttribute("ratingKey")) {
|
if (directoryToProcess && directoryToProcess.getAttribute("ratingKey")) {
|
||||||
const serieKey = directoryToProcess.getAttribute("ratingKey");
|
const serieKey = directoryToProcess.getAttribute("ratingKey");
|
||||||
|
const serieTitulo = directoryToProcess.getAttribute("title") || busqueda;
|
||||||
|
const serieYear = directoryToProcess.getAttribute("year");
|
||||||
const leavesUrl = `${protocolo}://${ip}:${puerto}/library/metadata/${serieKey}/allLeaves?X-Plex-Token=${token}`;
|
const leavesUrl = `${protocolo}://${ip}:${puerto}/library/metadata/${serieKey}/allLeaves?X-Plex-Token=${token}`;
|
||||||
|
|
||||||
const leavesResponse = await fetchWithTimeout(leavesUrl, { headers: { 'Accept': 'application/xml' } });
|
const leavesResponse = await fetchWithTimeout(leavesUrl, { headers: { 'Accept': 'application/xml' } });
|
||||||
if (leavesResponse.ok) {
|
if (leavesResponse.ok) {
|
||||||
const leavesData = await leavesResponse.text();
|
const leavesData = await leavesResponse.text();
|
||||||
const leavesXml = new DOMParser().parseFromString(leavesData, "text/xml");
|
const leavesXml = parser.parseFromString(leavesData, "text/xml");
|
||||||
if (!leavesXml.querySelector('parsererror')) {
|
if (!leavesXml.querySelector('parsererror')) {
|
||||||
return processShowEpisodes(leavesXml, busqueda, protocolo, ip, puerto, token);
|
const episodes = Array.from(leavesXml.querySelectorAll("Video"));
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function processShowEpisodes(xml, busqueda, protocolo, ip, puerto, token) {
|
episodes.sort((a,b) => {
|
||||||
const episodes = Array.from(xml.querySelectorAll("Video"));
|
|
||||||
episodes.sort((a, b) => {
|
|
||||||
const seasonA = parseInt(a.getAttribute("parentIndex") || 0, 10);
|
const seasonA = parseInt(a.getAttribute("parentIndex") || 0, 10);
|
||||||
const seasonB = parseInt(b.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 episodeA = parseInt(a.getAttribute("index") || 0, 10);
|
||||||
const episodeB = parseInt(b.getAttribute("index") || 0, 10);
|
const episodeB = parseInt(b.getAttribute("index") || 0, 10);
|
||||||
return episodeA - episodeB;
|
return episodeA - episodeB;
|
||||||
});
|
});
|
||||||
|
|
||||||
return episodes.map(episode => {
|
episodes.forEach(episode => {
|
||||||
const part = episode.querySelector("Part");
|
const part = episode.querySelector("Part");
|
||||||
if (!part || !part.getAttribute("key")) return null;
|
if (part && part.getAttribute("key")) {
|
||||||
|
|
||||||
const serieTitulo = episode.getAttribute("grandparentTitle") || busqueda;
|
|
||||||
const serieYear = episode.getAttribute("parentYear");
|
|
||||||
const seasonNum = episode.getAttribute("parentIndex") || 'S';
|
const seasonNum = episode.getAttribute("parentIndex") || 'S';
|
||||||
const episodeNum = episode.getAttribute("index") || 'E';
|
const episodeNum = episode.getAttribute("index") || 'E';
|
||||||
const episodeTitle = episode.getAttribute("title") || 'Episodio';
|
const episodeTitle = episode.getAttribute("title") || 'Episodio';
|
||||||
@ -190,23 +177,23 @@ function processShowEpisodes(xml, busqueda, protocolo, ip, puerto, token) {
|
|||||||
const extinfName = `${serieTitulo} T${seasonNum}E${episodeNum} ${episodeTitle}`;
|
const extinfName = `${serieTitulo} T${seasonNum}E${episodeNum} ${episodeTitle}`;
|
||||||
const logoUrl = episode.getAttribute("grandparentThumb") || episode.getAttribute("parentThumb") || episode.getAttribute("thumb");
|
const logoUrl = episode.getAttribute("grandparentThumb") || episode.getAttribute("parentThumb") || episode.getAttribute("thumb");
|
||||||
const fullLogoUrl = logoUrl ? `${protocolo}://${ip}:${puerto}${logoUrl}?X-Plex-Token=${token}` : '';
|
const fullLogoUrl = logoUrl ? `${protocolo}://${ip}:${puerto}${logoUrl}?X-Plex-Token=${token}` : '';
|
||||||
|
serverStreams.push({
|
||||||
return {
|
|
||||||
url: streamUrl,
|
url: streamUrl,
|
||||||
title: extinfName,
|
title: extinfName,
|
||||||
extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${fullLogoUrl}" group-title="${groupTitle}",${extinfName}`
|
extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${fullLogoUrl}" group-title="${groupTitle}",${extinfName}`
|
||||||
};
|
});
|
||||||
}).filter(Boolean);
|
}
|
||||||
}
|
});
|
||||||
|
}
|
||||||
export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
|
}
|
||||||
if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') };
|
}
|
||||||
if (!state.db) return { success: false, streams: [], message: _('dbUnavailableForStreams') };
|
}
|
||||||
|
return serverStreams;
|
||||||
const servers = await getFromDB('conexiones_locales');
|
} catch (error) {
|
||||||
if (!servers || servers.length === 0) return { success: false, streams: [], message: _('noPlexServersForStreams') };
|
console.warn(`Error buscando streams en ${serverName}:`, error.message);
|
||||||
|
return [];
|
||||||
const searchTasks = servers.map(server => searchAndProcessServer(server, busqueda, tipoContenido));
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const results = await Promise.allSettled(searchTasks);
|
const results = await Promise.allSettled(searchTasks);
|
||||||
const allFoundStreams = results
|
const allFoundStreams = results
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
export const API_URLS = {
|
|
||||||
TMDB_BASE: 'https://api.themoviedb.org/3',
|
|
||||||
TMDB_IMAGE_BASE: 'https://image.tmdb.org/t/p',
|
|
||||||
PLEX_TV: 'https://plex.tv/api/resources',
|
|
||||||
YOUTUBE_EMBED: 'https://www.youtube.com/embed/',
|
|
||||||
IMDB_TITLE: 'https://www.imdb.com/title/'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CONFIG = {
|
|
||||||
DEFAULT_API_KEY: '4e44d9029b1270a757cddc766a1bcb63',
|
|
||||||
DB_NAME: 'PlexDB',
|
|
||||||
DB_VERSION: 6
|
|
||||||
};
|
|
||||||
|
|
||||||
export const STORAGE_KEYS = {
|
|
||||||
USER_HISTORY: 'cineplex_userHistory',
|
|
||||||
USER_PREFERENCES: 'cineplex_userPreferences',
|
|
||||||
FAVORITES: 'cineplex_favorites',
|
|
||||||
RECOMMENDATIONS_CACHE: 'cineplex_recommendations'
|
|
||||||
};
|
|
3
js/db.js
3
js/db.js
@ -55,6 +55,7 @@ export function initDB() {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
state.db.onerror = event => {
|
state.db.onerror = event => {
|
||||||
|
console.error(`Database error: ${event.target.errorCode}`);
|
||||||
reject(event.target.error);
|
reject(event.target.error);
|
||||||
};
|
};
|
||||||
resolve();
|
resolve();
|
||||||
@ -113,7 +114,7 @@ export function addItemsToStore(storeName, items) {
|
|||||||
if (item !== undefined && item !== null) {
|
if (item !== undefined && item !== null) {
|
||||||
const request = store.put(item);
|
const request = store.put(item);
|
||||||
request.onsuccess = () => successCount++;
|
request.onsuccess = () => successCount++;
|
||||||
request.onerror = (e) => {};
|
request.onerror = (e) => console.warn(`Error adding/updating item in ${storeName}:`, e.target.error, item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,6 +54,7 @@ export class Equalizer {
|
|||||||
this.drawVisualizer();
|
this.drawVisualizer();
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error("Error initializing Web Audio API:", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ function localizeHtmlPage() {
|
|||||||
try {
|
try {
|
||||||
return p1 ? chrome.i18n.getMessage(p1) : match;
|
return p1 ? chrome.i18n.getMessage(p1) : match;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.warn(`Could not find message for: ${p1}`);
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,7 +34,6 @@ function localizeHtmlPage() {
|
|||||||
|
|
||||||
document.documentElement.lang = chrome.i18n.getUILanguage().split('-')[0];
|
document.documentElement.lang = chrome.i18n.getUILanguage().split('-')[0];
|
||||||
document.title = document.title.replace(i18nRegex, replaceMsg);
|
document.title = document.title.replace(i18nRegex, replaceMsg);
|
||||||
document.body.classList.add('loaded');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', localizeHtmlPage);
|
document.addEventListener('DOMContentLoaded', localizeHtmlPage);
|
@ -19,6 +19,7 @@ async function loadSettings() {
|
|||||||
state.settings.apiKey = config.defaultApiKey;
|
state.settings.apiKey = config.defaultApiKey;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Could not load settings from DB, using defaults.", error);
|
||||||
state.settings.language = chrome.i18n.getUILanguage().split('-')[0];
|
state.settings.language = chrome.i18n.getUILanguage().split('-')[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,6 +47,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
initializeThirdPartyLibs();
|
initializeThirdPartyLibs();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Fatal Initialization failed:", error);
|
||||||
showNotification(_("fatalInitError"), "error");
|
showNotification(_("fatalInitError"), "error");
|
||||||
document.getElementById('main-container').innerHTML = `<div class="container text-center mt-5 pt-5"><h1 class="text-danger">${_("fatalInitError")}</h1><p>${_("fatalInitErrorSub")}</p></div>`;
|
document.getElementById('main-container').innerHTML = `<div class="container text-center mt-5 pt-5"><h1 class="text-danger">${_("fatalInitError")}</h1><p>${_("fatalInitErrorSub")}</p></div>`;
|
||||||
}
|
}
|
||||||
|
203
js/ui.js
203
js/ui.js
@ -5,27 +5,6 @@ import { getFromDB, addItemsToStore } from './db.js';
|
|||||||
|
|
||||||
let charts = {};
|
let charts = {};
|
||||||
|
|
||||||
function createElement(tag, options = {}, children = []) {
|
|
||||||
const el = document.createElement(tag);
|
|
||||||
Object.entries(options).forEach(([key, value]) => {
|
|
||||||
if (key === 'dataset') {
|
|
||||||
Object.entries(value).forEach(([dataKey, dataValue]) => {
|
|
||||||
el.dataset[dataKey] = dataValue;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
el[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
children.forEach(child => {
|
|
||||||
if (typeof child === 'string') {
|
|
||||||
el.appendChild(document.createTextNode(child));
|
|
||||||
} else if (child) {
|
|
||||||
el.appendChild(child);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadInitialContent() {
|
export async function loadInitialContent() {
|
||||||
await Promise.all([loadGenres(), loadYears()]);
|
await Promise.all([loadGenres(), loadYears()]);
|
||||||
await Promise.all([loadContent(), initializeHeroSection()]);
|
await Promise.all([loadContent(), initializeHeroSection()]);
|
||||||
@ -248,29 +227,31 @@ function updateSortOptions() {
|
|||||||
async function loadGenres() {
|
async function loadGenres() {
|
||||||
const type = state.currentParams.contentType;
|
const type = state.currentParams.contentType;
|
||||||
const select = document.getElementById('genre-filter');
|
const select = document.getElementById('genre-filter');
|
||||||
select.innerHTML = '';
|
select.innerHTML = `<option value="">${_('loadingGenres')}</option>`;
|
||||||
select.appendChild(createElement('option', { value: '', textContent: _('loadingGenres') }));
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchTMDB(`genre/${type}/list`);
|
const data = await fetchTMDB(`genre/${type}/list`);
|
||||||
select.innerHTML = '';
|
select.innerHTML = `<option value="">${_('allGenres')}</option>`;
|
||||||
select.appendChild(createElement('option', { value: '', textContent: _('allGenres') }));
|
|
||||||
data.genres.forEach(genre => {
|
data.genres.forEach(genre => {
|
||||||
select.appendChild(createElement('option', { value: genre.id, textContent: genre.name }));
|
const option = document.createElement('option');
|
||||||
|
option.value = genre.id;
|
||||||
|
option.textContent = genre.name;
|
||||||
|
select.appendChild(option);
|
||||||
});
|
});
|
||||||
select.value = state.currentParams.genre || "";
|
select.value = state.currentParams.genre || "";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
select.innerHTML = '';
|
select.innerHTML = `<option value="">${_('errorLoadingGenres')}</option>`;
|
||||||
select.appendChild(createElement('option', { value: '', textContent: _('errorLoadingGenres') }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadYears() {
|
function loadYears() {
|
||||||
const select = document.getElementById('year-filter');
|
const select = document.getElementById('year-filter');
|
||||||
select.innerHTML = '';
|
select.innerHTML = `<option value="">${_('allYears')}</option>`;
|
||||||
select.appendChild(createElement('option', { value: '', textContent: _('allYears') }));
|
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
for (let year = currentYear; year >= 1900; year--) {
|
for (let year = currentYear; year >= 1900; year--) {
|
||||||
select.appendChild(createElement('option', { value: year, textContent: year }));
|
const option = document.createElement('option');
|
||||||
|
option.value = year;
|
||||||
|
option.textContent = year;
|
||||||
|
select.appendChild(option);
|
||||||
}
|
}
|
||||||
select.value = state.currentParams.year || "";
|
select.value = state.currentParams.year || "";
|
||||||
}
|
}
|
||||||
@ -287,14 +268,11 @@ export async function loadContent(append = false) {
|
|||||||
const loadMoreButton = document.getElementById('load-more');
|
const loadMoreButton = document.getElementById('load-more');
|
||||||
|
|
||||||
if (!append) {
|
if (!append) {
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '<div class="col-12 text-center mt-5"><div class="spinner" style="position: static; margin: auto; display: block;"></div></div>';
|
||||||
grid.appendChild(createElement('div', { className: 'col-12 text-center mt-5' }, [createElement('div', { className: 'spinner', style: 'position: static; margin: auto; display: block;' })]));
|
|
||||||
loadMoreButton.style.display = 'none';
|
loadMoreButton.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
loadMoreButton.disabled = true;
|
loadMoreButton.disabled = true;
|
||||||
loadMoreButton.innerHTML = '';
|
loadMoreButton.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ${_('loading')}`;
|
||||||
loadMoreButton.appendChild(createElement('span', { className: 'spinner-border spinner-border-sm', role: 'status', "aria-hidden": 'true' }));
|
|
||||||
loadMoreButton.appendChild(document.createTextNode(` ${_('loading')}`));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -316,13 +294,7 @@ export async function loadContent(append = false) {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name !== 'AbortError') {
|
if (error.name !== 'AbortError') {
|
||||||
if (!append) {
|
if (!append) grid.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('couldNotLoadContent')}</p></div>`;
|
||||||
grid.innerHTML = '';
|
|
||||||
grid.appendChild(createElement('div', { className: 'empty-state' }, [
|
|
||||||
createElement('i', { className: 'fas fa-exclamation-triangle' }),
|
|
||||||
createElement('p', { textContent: _('couldNotLoadContent') })
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
@ -355,11 +327,7 @@ function renderGrid(items, append = false) {
|
|||||||
const emptyStateTarget = document.getElementById('recommendations-grid') || grid;
|
const emptyStateTarget = document.getElementById('recommendations-grid') || grid;
|
||||||
const emptyIcon = state.currentView === 'recommendations' ? 'fa-user-astronaut' : 'fa-film';
|
const emptyIcon = state.currentView === 'recommendations' ? 'fa-user-astronaut' : 'fa-film';
|
||||||
const emptyText = state.currentView === 'recommendations' ? _('noRecommendations') : _('noContentFound');
|
const emptyText = state.currentView === 'recommendations' ? _('noRecommendations') : _('noContentFound');
|
||||||
emptyStateTarget.innerHTML = '';
|
emptyStateTarget.innerHTML = `<div class="empty-state"><i class="fas ${emptyIcon} fa-3x mb-3"></i><p class="lead">${emptyText}</p></div>`;
|
||||||
emptyStateTarget.appendChild(createElement('div', { className: 'empty-state' }, [
|
|
||||||
createElement('i', { className: `fas ${emptyIcon} fa-3x mb-3` }),
|
|
||||||
createElement('p', { className: 'lead', textContent: emptyText })
|
|
||||||
]));
|
|
||||||
}
|
}
|
||||||
document.getElementById('load-more').style.display = 'none';
|
document.getElementById('load-more').style.display = 'none';
|
||||||
return;
|
return;
|
||||||
@ -380,7 +348,10 @@ function renderGrid(items, append = false) {
|
|||||||
const voteAvg = item.vote_average ? item.vote_average.toFixed(1) : 'N/A';
|
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');
|
const ratingClass = voteAvg >= 7.5 ? 'rating-good' : (voteAvg >= 5.0 ? 'rating-ok' : 'rating-bad');
|
||||||
|
|
||||||
const card = createElement('div', { className: 'item-card', dataset: { id: item.id, type: itemType } });
|
const card = document.createElement('div');
|
||||||
|
card.className = `item-card`;
|
||||||
|
card.dataset.id = item.id;
|
||||||
|
card.dataset.type = itemType;
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="item-poster" style="background-image: url('${posterPath}')"></div>
|
<div class="item-poster" style="background-image: url('${posterPath}')"></div>
|
||||||
${voteAvg >= 7.8 ? '<span class="badge top-badge">TOP</span>' : ''}
|
${voteAvg >= 7.8 ? '<span class="badge top-badge">TOP</span>' : ''}
|
||||||
@ -470,8 +441,7 @@ export async function showItemDetails(itemId, contentType) {
|
|||||||
if (mainView.style.display !== 'none') state.lastScrollPosition = window.scrollY;
|
if (mainView.style.display !== 'none') state.lastScrollPosition = window.scrollY;
|
||||||
|
|
||||||
document.body.classList.add('details-view-active');
|
document.body.classList.add('details-view-active');
|
||||||
detailsContent.innerHTML = '';
|
detailsContent.innerHTML = '<div class="text-center py-5"><div class="spinner" style="display: block; margin: auto; position: static;"></div></div>';
|
||||||
detailsContent.appendChild(createElement('div', { className: 'text-center py-5' }, [createElement('div', { className: 'spinner', style: 'display: block; margin: auto; position: static;' })]));
|
|
||||||
|
|
||||||
detailsView.classList.add('active');
|
detailsView.classList.add('active');
|
||||||
|
|
||||||
@ -529,7 +499,7 @@ export async function showItemDetails(itemId, contentType) {
|
|||||||
if (!isMovie && item.seasons && item.seasons.length > 0) {
|
if (!isMovie && item.seasons && item.seasons.length > 0) {
|
||||||
const seasonPromises = item.seasons
|
const seasonPromises = item.seasons
|
||||||
.filter(s => s.season_number > 0)
|
.filter(s => s.season_number > 0)
|
||||||
.map(s => fetchTMDB(`${contentType}/${itemId}/season/${s.season_number}`).catch(()=>null));
|
.map(s => fetchTMDB(`${contentType}/${itemId}/season/${s.season_number}`).catch(() => null));
|
||||||
const seasonsData = await Promise.all(seasonPromises);
|
const seasonsData = await Promise.all(seasonPromises);
|
||||||
item.seasons_with_episodes = seasonsData.filter(s => s !== null);
|
item.seasons_with_episodes = seasonsData.filter(s => s !== null);
|
||||||
}
|
}
|
||||||
@ -538,11 +508,7 @@ export async function showItemDetails(itemId, contentType) {
|
|||||||
await renderItemDetails(item);
|
await renderItemDetails(item);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
detailsContent.innerHTML = '';
|
detailsContent.innerHTML = `<div class="alert alert-danger mx-3 my-5 text-center"><h4>${_('errorLoadingDetails')}</h4><p>${error.message}</p></div>`;
|
||||||
detailsContent.appendChild(createElement('div', { className: 'alert alert-danger mx-3 my-5 text-center' }, [
|
|
||||||
createElement('h4', { textContent: _('errorLoadingDetails') }),
|
|
||||||
createElement('p', { textContent: error.message })
|
|
||||||
]));
|
|
||||||
} finally {
|
} finally {
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
}
|
}
|
||||||
@ -569,7 +535,6 @@ async function renderItemDetails(item) {
|
|||||||
const director = crew.find(c => c.job === 'Director');
|
const director = crew.find(c => c.job === 'Director');
|
||||||
const writer = crew.find(c => c.job === 'Screenplay' || c.job === 'Writer' || c.job === 'Story');
|
const writer = crew.find(c => c.job === 'Screenplay' || c.job === 'Writer' || c.job === 'Story');
|
||||||
|
|
||||||
detailsContent.innerHTML = '';
|
|
||||||
detailsContent.innerHTML = `
|
detailsContent.innerHTML = `
|
||||||
${backdropPath ? `<div class="details-backdrop-container"><img src="${backdropPath}" class="details-backdrop-img"><div class="details-backdrop-overlay"></div></div>` : ''}
|
${backdropPath ? `<div class="details-backdrop-container"><img src="${backdropPath}" class="details-backdrop-img"><div class="details-backdrop-overlay"></div></div>` : ''}
|
||||||
<div class="item-details-container">
|
<div class="item-details-container">
|
||||||
@ -745,15 +710,10 @@ function updateFavoriteButtonVisuals(itemId, itemType, isFavorite) {
|
|||||||
|
|
||||||
export async function loadFavorites() {
|
export async function loadFavorites() {
|
||||||
const grid = document.getElementById('content-grid');
|
const grid = document.getElementById('content-grid');
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '<div class="col-12 text-center mt-5"><div class="spinner" style="position: static; margin: auto; display: block;"></div></div>';
|
||||||
grid.appendChild(createElement('div', { className: 'col-12 text-center mt-5' }, [createElement('div', { className: 'spinner', style: 'position: static; margin: auto; display: block;' })]));
|
|
||||||
|
|
||||||
if (state.favorites.length === 0) {
|
if (state.favorites.length === 0) {
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = `<div class="empty-state"><i class="far fa-heart fa-3x mb-3"></i><p class="lead">${_('noFavorites')}</p></div>`;
|
||||||
grid.appendChild(createElement('div', { className: 'empty-state' }, [
|
|
||||||
createElement('i', { className: 'far fa-heart fa-3x mb-3' }),
|
|
||||||
createElement('p', { className: 'lead', textContent: _('noFavorites') })
|
|
||||||
]));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -762,11 +722,7 @@ export async function loadFavorites() {
|
|||||||
const favoriteItems = (await Promise.all(favoritePromises)).filter(item => item !== null);
|
const favoriteItems = (await Promise.all(favoritePromises)).filter(item => item !== null);
|
||||||
renderGrid(favoriteItems, false);
|
renderGrid(favoriteItems, false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('errorLoadingFavorites')}</p></div>`;
|
||||||
grid.appendChild(createElement('div', { className: 'empty-state' }, [
|
|
||||||
createElement('i', { className: 'fas fa-exclamation-triangle' }),
|
|
||||||
createElement('p', { textContent: _('errorLoadingFavorites') })
|
|
||||||
]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -774,11 +730,7 @@ export function displayHistory() {
|
|||||||
const listContainer = document.getElementById('history-list');
|
const listContainer = document.getElementById('history-list');
|
||||||
listContainer.innerHTML = "";
|
listContainer.innerHTML = "";
|
||||||
if (state.userHistory.length === 0) {
|
if (state.userHistory.length === 0) {
|
||||||
listContainer.appendChild(createElement('div', { className: 'empty-state' }, [
|
listContainer.innerHTML = `<div class="empty-state"><i class="fas fa-history fa-3x mb-3"></i><p class="lead">${_('historyEmpty')}</p><p class="text-muted">${_('historyEmptySub')}</p></div>`;
|
||||||
createElement('i', { className: 'fas fa-history fa-3x mb-3' }),
|
|
||||||
createElement('p', { className: 'lead', textContent: _('historyEmpty') }),
|
|
||||||
createElement('p', { className: 'text-muted', textContent: _('historyEmptySub') })
|
|
||||||
]));
|
|
||||||
document.getElementById('clear-history-btn').style.display = 'none';
|
document.getElementById('clear-history-btn').style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -788,7 +740,11 @@ export function displayHistory() {
|
|||||||
[...state.userHistory].sort((a,b) => b.timestamp - a.timestamp).forEach(item => {
|
[...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 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 = !!buscarContenidoLocal(item.title, item.type);
|
||||||
const historyItem = createElement('div', { className: 'history-item', dataset: { id: item.id, type: item.type, title: item.title } });
|
const historyItem = document.createElement('div');
|
||||||
|
historyItem.className = 'history-item';
|
||||||
|
historyItem.dataset.id = item.id;
|
||||||
|
historyItem.dataset.type = item.type;
|
||||||
|
historyItem.dataset.title = item.title;
|
||||||
historyItem.innerHTML = `
|
historyItem.innerHTML = `
|
||||||
<div class="history-main-content">
|
<div class="history-main-content">
|
||||||
<img src="${posterUrl}" class="history-poster info-btn">
|
<img src="${posterUrl}" class="history-poster info-btn">
|
||||||
@ -856,8 +812,7 @@ export async function getTrailerKey(id, type) {
|
|||||||
|
|
||||||
export async function loadRecommendations() {
|
export async function loadRecommendations() {
|
||||||
const grid = document.getElementById('recommendations-grid');
|
const grid = document.getElementById('recommendations-grid');
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '<div class="col-12 text-center mt-5"><div class="spinner" style="position: static; margin: auto; display: block;"></div></div>';
|
||||||
grid.appendChild(createElement('div', { className: 'col-12 text-center mt-5' }, [createElement('div', { className: 'spinner', style: 'position: static; margin: auto; display: block;' })]));
|
|
||||||
|
|
||||||
const cachedRecs = sessionStorage.getItem('cineplex_recommendations');
|
const cachedRecs = sessionStorage.getItem('cineplex_recommendations');
|
||||||
if (cachedRecs) {
|
if (cachedRecs) {
|
||||||
@ -889,19 +844,14 @@ export async function loadRecommendations() {
|
|||||||
renderGrid([]);
|
renderGrid([]);
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('errorGeneratingRecommendations')}</p></div>`;
|
||||||
grid.appendChild(createElement('div', { className: 'empty-state' }, [
|
|
||||||
createElement('i', { className: 'fas fa-exclamation-triangle' }),
|
|
||||||
createElement('p', { textContent: _('errorGeneratingRecommendations') })
|
|
||||||
]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function populateStatsTokenFilter() {
|
async function populateStatsTokenFilter() {
|
||||||
const select = document.getElementById('stats-token-filter');
|
const select = document.getElementById('stats-token-filter');
|
||||||
const currentValue = select.value;
|
const currentValue = select.value;
|
||||||
select.innerHTML = '';
|
select.innerHTML = `<option value="all">${_('statsAllTokens')}</option>`;
|
||||||
select.appendChild(createElement('option', { value: 'all', textContent: _('statsAllTokens') }));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokensData = await getFromDB('tokens');
|
const tokensData = await getFromDB('tokens');
|
||||||
@ -916,7 +866,10 @@ async function populateStatsTokenFilter() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
primaryTokens.forEach(token => {
|
primaryTokens.forEach(token => {
|
||||||
select.appendChild(createElement('option', { value: token, textContent: tokenNames[token] || `Token...${token.slice(-4)}` }));
|
const option = document.createElement('option');
|
||||||
|
option.value = token;
|
||||||
|
option.textContent = tokenNames[token] || `Token...${token.slice(-4)}`;
|
||||||
|
select.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (currentValue) {
|
if (currentValue) {
|
||||||
@ -924,8 +877,7 @@ async function populateStatsTokenFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
select.innerHTML = '';
|
select.innerHTML = `<option value="all">${_('errorLoadingTokens')}</option>`;
|
||||||
select.appendChild(createElement('option', { value: 'all', textContent: _('errorLoadingTokens') }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -998,11 +950,7 @@ export async function generateStatistics() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
loader.innerHTML = '';
|
loader.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('errorGeneratingStats')}</p></div>`;
|
||||||
loader.appendChild(createElement('div', { className: 'empty-state' }, [
|
|
||||||
createElement('i', { className: 'fas fa-exclamation-triangle' }),
|
|
||||||
createElement('p', { textContent: _('errorGeneratingStats') })
|
|
||||||
]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1017,15 +965,12 @@ function updateTokenDetailsCard(selectedToken, allConnections) {
|
|||||||
|
|
||||||
const associatedServers = allConnections.filter(c => c.tokenPrincipal === selectedToken);
|
const associatedServers = allConnections.filter(c => c.tokenPrincipal === selectedToken);
|
||||||
|
|
||||||
serverList.innerHTML = '';
|
|
||||||
if (associatedServers.length === 0) {
|
if (associatedServers.length === 0) {
|
||||||
serverList.appendChild(createElement('li', { textContent: _('noServersForToken') }));
|
serverList.innerHTML = `<li>${_('noServersForToken')}</li>`;
|
||||||
} else {
|
} else {
|
||||||
associatedServers.forEach(s => {
|
serverList.innerHTML = associatedServers.map(s =>
|
||||||
const li = createElement('li');
|
`<li><strong>${s.nombre}:</strong> <code>${s.token.slice(0, 5)}...${s.token.slice(-5)}</code></li>`
|
||||||
li.innerHTML = `<strong>${s.nombre}:</strong> <code>${s.token.slice(0, 5)}...${s.token.slice(-5)}</code>`;
|
).join('');
|
||||||
serverList.appendChild(li);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
card.style.display = 'block';
|
card.style.display = 'block';
|
||||||
}
|
}
|
||||||
@ -1147,8 +1092,7 @@ export async function searchByActor(actorId, actorName) {
|
|||||||
updateActiveNav(state.currentParams.contentType);
|
updateActiveNav(state.currentParams.contentType);
|
||||||
|
|
||||||
const grid = document.getElementById('content-grid');
|
const grid = document.getElementById('content-grid');
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '<div class="col-12 text-center mt-5"><div class="spinner" style="display: block; margin: auto; position: static;"></div></div>';
|
||||||
grid.appendChild(createElement('div', { className: 'col-12 text-center mt-5' }, [createElement('div', { className: 'spinner', style: 'display: block; margin: auto; position: static;' })]));
|
|
||||||
document.querySelector('.filters').style.display = 'none';
|
document.querySelector('.filters').style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -1157,11 +1101,7 @@ export async function searchByActor(actorId, actorName) {
|
|||||||
renderGrid(data.results, false);
|
renderGrid(data.results, false);
|
||||||
document.getElementById('load-more').style.display = (data.page < data.total_pages) ? 'block' : 'none';
|
document.getElementById('load-more').style.display = (data.page < data.total_pages) ? 'block' : 'none';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('errorLoadingActorContent', actorName)}</p></div>`;
|
||||||
grid.appendChild(createElement('div', { className: 'empty-state' }, [
|
|
||||||
createElement('i', { className: 'fas fa-exclamation-triangle' }),
|
|
||||||
createElement('p', { textContent: _('errorLoadingActorContent', actorName) })
|
|
||||||
]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1251,6 +1191,7 @@ export async function initializeHeroSection() {
|
|||||||
setInterval(() => changeHeroSlide(false), 12000);
|
setInterval(() => changeHeroSlide(false), 12000);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Error initializing hero section:", error);
|
||||||
heroSection.style.display = 'none';
|
heroSection.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1572,13 +1513,13 @@ if (!isset($data['streams']) || !is_array($data['streams']) || empty($data['stre
|
|||||||
sendResponse(false, 'Error: El JSON debe contener un array "streams" no vacío.');
|
sendResponse(false, 'Error: El JSON debe contener un array "streams" no vacío.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$save_dir = SAVE_DIRECTORY !== '' ? rtrim(SAVE_DIRECTORY, '/\\') : __DIR__;
|
$save_dir = SAVE_DIRECTORY !== '' ? rtrim(SAVE_DIRECTORY, '/\\\\') : __DIR__;
|
||||||
|
|
||||||
if (!is_dir($save_dir) || !is_writable($save_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);
|
sendResponse(false, 'Error del servidor: El directorio de destino no existe o no tiene permisos de escritura.', '', 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
$safe_filename = preg_replace('/[^\\w\\s._-]/u', '', basename(FILENAME));
|
$safe_filename = preg_replace('/[^\\w\\s._-]/', '', basename(FILENAME));
|
||||||
$safe_filename = preg_replace('/\\s+/', '_', $safe_filename);
|
$safe_filename = preg_replace('/\\s+/', '_', $safe_filename);
|
||||||
$target_path = $save_dir . DIRECTORY_SEPARATOR . $safe_filename;
|
$target_path = $save_dir . DIRECTORY_SEPARATOR . $safe_filename;
|
||||||
|
|
||||||
@ -1587,12 +1528,12 @@ $content_to_write = "";
|
|||||||
if (FILE_ACTION_APPEND) {
|
if (FILE_ACTION_APPEND) {
|
||||||
$file_exists = file_exists($target_path);
|
$file_exists = file_exists($target_path);
|
||||||
if (!$file_exists) {
|
if (!$file_exists) {
|
||||||
$content_to_write .= "#EXTM3U\n";
|
$content_to_write .= "#EXTM3U\\n";
|
||||||
}
|
}
|
||||||
foreach ($data['streams'] as $stream) {
|
foreach ($data['streams'] as $stream) {
|
||||||
if (isset($stream['extinf'], $stream['url'])) {
|
if (isset($stream['extinf'], $stream['url'])) {
|
||||||
$content_to_write .= trim($stream['extinf']) . "\n";
|
$content_to_write .= trim($stream['extinf']) . "\\n";
|
||||||
$content_to_write .= trim($stream['url']) . "\n";
|
$content_to_write .= trim($stream['url']) . "\\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (file_put_contents($target_path, $content_to_write, FILE_APPEND | LOCK_EX) !== false) {
|
if (file_put_contents($target_path, $content_to_write, FILE_APPEND | LOCK_EX) !== false) {
|
||||||
@ -1601,11 +1542,11 @@ if (FILE_ACTION_APPEND) {
|
|||||||
sendResponse(false, 'Error del servidor: No se pudo añadir contenido al archivo.', '', 500);
|
sendResponse(false, 'Error del servidor: No se pudo añadir contenido al archivo.', '', 500);
|
||||||
}
|
}
|
||||||
} else { // Overwrite mode
|
} else { // Overwrite mode
|
||||||
$content_to_write = "#EXTM3U\n";
|
$content_to_write = "#EXTM3U\\n";
|
||||||
foreach ($data['streams'] as $stream) {
|
foreach ($data['streams'] as $stream) {
|
||||||
if (isset($stream['extinf'], $stream['url'])) {
|
if (isset($stream['extinf'], $stream['url'])) {
|
||||||
$content_to_write .= trim($stream['extinf']) . "\n";
|
$content_to_write .= trim($stream['extinf']) . "\\n";
|
||||||
$content_to_write .= trim($stream['url']) . "\n";
|
$content_to_write .= trim($stream['url']) . "\\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (file_put_contents($target_path, $content_to_write, LOCK_EX) !== false) {
|
if (file_put_contents($target_path, $content_to_write, LOCK_EX) !== false) {
|
||||||
@ -1636,14 +1577,12 @@ if (FILE_ACTION_APPEND) {
|
|||||||
|
|
||||||
export function initPhotosView() {
|
export function initPhotosView() {
|
||||||
const select = document.getElementById('photos-token-select');
|
const select = document.getElementById('photos-token-select');
|
||||||
select.innerHTML = '';
|
select.innerHTML = `<option value="">${_('loading')}</option>`;
|
||||||
select.appendChild(createElement('option', { value: '', textContent: _('loading') }));
|
|
||||||
|
|
||||||
const photoServers = [...new Map(state.localPhotos.map(item => [item.tokenPrincipal, item])).values()];
|
const photoServers = [...new Map(state.localPhotos.map(item => [item.tokenPrincipal, item])).values()];
|
||||||
|
|
||||||
if (photoServers.length === 0) {
|
if (photoServers.length === 0) {
|
||||||
select.innerHTML = '';
|
select.innerHTML = `<option value="">${_('noPhotoServers')}</option>`;
|
||||||
select.appendChild(createElement('option', { value: '', textContent: _('noPhotoServers') }));
|
|
||||||
document.getElementById('photos-empty-state').style.display = 'block';
|
document.getElementById('photos-empty-state').style.display = 'block';
|
||||||
document.getElementById('photos-grid').innerHTML = '';
|
document.getElementById('photos-grid').innerHTML = '';
|
||||||
return;
|
return;
|
||||||
@ -1651,10 +1590,12 @@ export function initPhotosView() {
|
|||||||
|
|
||||||
document.getElementById('photos-empty-state').style.display = 'none';
|
document.getElementById('photos-empty-state').style.display = 'none';
|
||||||
|
|
||||||
select.innerHTML = '';
|
select.innerHTML = `<option value="">${_('selectServer')}</option>`;
|
||||||
select.appendChild(createElement('option', { value: '', textContent: _('selectServer') }));
|
|
||||||
photoServers.forEach(server => {
|
photoServers.forEach(server => {
|
||||||
select.appendChild(createElement('option', { value: server.tokenPrincipal, textContent: server.serverName || `Servidor ${server.tokenPrincipal.slice(-4)}` }));
|
const option = document.createElement('option');
|
||||||
|
option.value = server.tokenPrincipal;
|
||||||
|
option.textContent = server.serverName || `Servidor ${server.tokenPrincipal.slice(-4)}`;
|
||||||
|
select.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (state.currentPhotoToken && photoServers.some(s => s.tokenPrincipal === state.currentPhotoToken)) {
|
if (state.currentPhotoToken && photoServers.some(s => s.tokenPrincipal === state.currentPhotoToken)) {
|
||||||
@ -1812,8 +1753,11 @@ function renderPhotoBreadcrumb() {
|
|||||||
const breadcrumb = document.getElementById('photos-breadcrumb');
|
const breadcrumb = document.getElementById('photos-breadcrumb');
|
||||||
breadcrumb.innerHTML = '';
|
breadcrumb.innerHTML = '';
|
||||||
|
|
||||||
const rootItem = createElement('li', { className: 'breadcrumb-item' });
|
const rootItem = document.createElement('li');
|
||||||
const rootLink = createElement('a', { href: '#', innerHTML: `<i class="fas fa-home"></i> ${_('photosBreadcrumbHome')}` });
|
rootItem.className = 'breadcrumb-item';
|
||||||
|
const rootLink = document.createElement('a');
|
||||||
|
rootLink.href = '#';
|
||||||
|
rootLink.innerHTML = `<i class="fas fa-home"></i> ${_('photosBreadcrumbHome')}`;
|
||||||
rootLink.onclick = (e) => {
|
rootLink.onclick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handlePhotoTokenChange();
|
handlePhotoTokenChange();
|
||||||
@ -1822,15 +1766,20 @@ function renderPhotoBreadcrumb() {
|
|||||||
breadcrumb.appendChild(rootItem);
|
breadcrumb.appendChild(rootItem);
|
||||||
|
|
||||||
state.photoStack.forEach((item, index) => {
|
state.photoStack.forEach((item, index) => {
|
||||||
const divider = createElement('li', { className: 'breadcrumb-divider', innerHTML: '<i class="fas fa-chevron-right"></i>' });
|
const divider = document.createElement('li');
|
||||||
|
divider.className = 'breadcrumb-divider';
|
||||||
|
divider.innerHTML = '<i class="fas fa-chevron-right"></i>';
|
||||||
breadcrumb.appendChild(divider);
|
breadcrumb.appendChild(divider);
|
||||||
|
|
||||||
const breadcrumbItem = createElement('li', { className: 'breadcrumb-item' });
|
const breadcrumbItem = document.createElement('li');
|
||||||
|
breadcrumbItem.className = 'breadcrumb-item';
|
||||||
if (index === state.photoStack.length - 1) {
|
if (index === state.photoStack.length - 1) {
|
||||||
breadcrumbItem.classList.add('active');
|
breadcrumbItem.classList.add('active');
|
||||||
breadcrumbItem.textContent = item.title;
|
breadcrumbItem.textContent = item.title;
|
||||||
} else {
|
} else {
|
||||||
const link = createElement('a', { href: '#', textContent: item.title });
|
const link = document.createElement('a');
|
||||||
|
link.href = '#';
|
||||||
|
link.textContent = item.title;
|
||||||
link.onclick = async (e) => {
|
link.onclick = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
state.photoStack = state.photoStack.slice(0, index + 1);
|
state.photoStack = state.photoStack.slice(0, index + 1);
|
||||||
|
19
js/utils.js
19
js/utils.js
@ -93,17 +93,18 @@ export function logToConsole(message) {
|
|||||||
logEntry.className = 'console-log-entry';
|
logEntry.className = 'console-log-entry';
|
||||||
const time = new Date().toLocaleTimeString(_('appLocaleCode'), { hour12: false });
|
const time = new Date().toLocaleTimeString(_('appLocaleCode'), { hour12: false });
|
||||||
|
|
||||||
const timeSpan = document.createElement('span');
|
const timeSpan = `<span class="log-time">[${time}]</span>`;
|
||||||
timeSpan.className = 'log-time';
|
const messageSpan = `<span class="log-message">${message.replace(/</g, "<").replace(/>/g, ">")}</span>`;
|
||||||
timeSpan.textContent = `[${time}]`;
|
|
||||||
|
|
||||||
const messageSpan = document.createElement('span');
|
if (message.toLowerCase().includes("error")) {
|
||||||
messageSpan.className = 'log-message';
|
logEntry.classList.add('log-error');
|
||||||
messageSpan.textContent = message;
|
} else if (message.toLowerCase().includes("advertencia") || message.toLowerCase().includes("warning") || message.toLowerCase().includes("pendiente")) {
|
||||||
|
logEntry.classList.add('log-warning');
|
||||||
|
} else if (message.toLowerCase().includes("success") || message.toLowerCase().includes("finalizado") || message.toLowerCase().includes("éxito") || message.toLowerCase().includes("limpiadas")) {
|
||||||
|
logEntry.classList.add('log-success');
|
||||||
|
}
|
||||||
|
|
||||||
logEntry.appendChild(timeSpan);
|
logEntry.innerHTML = `${timeSpan} ${messageSpan}`;
|
||||||
logEntry.appendChild(document.createTextNode(' '));
|
|
||||||
logEntry.appendChild(messageSpan);
|
|
||||||
consoleOutput.appendChild(logEntry);
|
consoleOutput.appendChild(logEntry);
|
||||||
consoleOutput.scrollTop = consoleOutput.scrollHeight;
|
consoleOutput.scrollTop = consoleOutput.scrollHeight;
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,8 @@
|
|||||||
"notifications"
|
"notifications"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://*.plex.tv/*"
|
"https://*.plex.tv/*",
|
||||||
|
"*://*:*/*"
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "js/background.js",
|
"service_worker": "js/background.js",
|
||||||
|
@ -13,15 +13,6 @@
|
|||||||
<link rel="stylesheet" href="css/main.css">
|
<link rel="stylesheet" href="css/main.css">
|
||||||
<link rel="stylesheet" href="css/equalizer.css">
|
<link rel="stylesheet" href="css/equalizer.css">
|
||||||
<link rel="stylesheet" href="css/photos.css">
|
<link rel="stylesheet" href="css/photos.css">
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.5s ease-in-out;
|
|
||||||
}
|
|
||||||
body.loaded {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user