diff --git a/README.md b/README.md index 18d2d37..e9556c2 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,67 @@ # 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, 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! +* **🎬 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). * **✅ "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! -* **🖼️ Your Personal Photo Gallery:** Connect to your Plex photo libraries and browse your albums and pictures with a beautiful, integrated lightbox viewer. Relive your memories in style! -* **📜 The Scroll of Power (M3U):** For the power users. Found that series you wanted to watch? Generate an `.m3u` file with all the episode links with a single click. Power in your hands! -* **🔥 Stream Straight to Your Server:** This is where it gets wild. Configure a simple PHP script on your server, and you can send streams from CinePlex directly to your M3U playlist file with a single click. We even give you a **PHP script generator** to make it foolproof! -* **❤️ Favorites & Goldfish Memory:** Save your favorite movies and shows. Plus, we've got a "History" section so you can remember what you were watching last night before you fell asleep on the couch. Never lose track again! -* **🧠 AI-Powered Recommendations:** Based on your viewing history and favorites, CinePlex will suggest new content you might love. It's like having a personal movie critic living in your browser. -* **🔧 Customization Tuning Shop:** Don't like the dark theme? Switch to light mode! Don't want the giant hero banner? Hide it! Add your own TMDB API key. You're the boss, this is your extension! +* **🎶 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 fancy charts. +* **🖼️ 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. +* **🔥 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. +* **🔧 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. +* **💾 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!). -2. **Open Chrome Extensions:** In a new tab, type `chrome://extensions` and press Enter. Welcome to the control center of your browser superpowers! -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! +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:** 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**. 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). -6. **Mission Ready!** You should now see the CinePlex card in your extensions list. Congratulations, you've installed your first piece of space technology! +5. **Select the Folder:** A file browser will open. Navigate to the folder where you unzipped the repository and select it. +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 **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. - * 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 { "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!** - * 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! +4. **Run Your First Scan:** + * Still in the Plex tab, check the boxes for the content you want to scan (e.g., Películas, Series). + * 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 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. -2. Configure the options (like the filename, save path, and security key) and click **"Generate Script"**. Watch as the magic of code materializes! -3. Copy the generated PHP code. It's your secret recipe! -4. Save that code as a `.php` file (e.g., `playlist.php`) and upload it to a web server you control. Your own corner on the web for your streams! -5. Go to the **"General"** tab in settings and paste the public URL to your new script in the **"Stream Server URL"** field. Connecting the dots! -6. Save, and you're ready! Now you can add streams with a single click. It's like teleporting your content wherever you need it! - ---- - -## 🔒 Permissions: What Does CinePlex Need? - -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. \ No newline at end of file +1. In the CinePlex settings, go to the **"Generador PHP"** tab. +2. Configure the options (like the filename) and click **"Generar Script"**. +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. +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 to add streams with a single click! \ No newline at end of file diff --git a/_locales/de/messages.json b/_locales/de/messages.json index b553f83..ec3197e 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -256,8 +256,8 @@ "writer": {"message": "Autor:"}, "viewOnImdb": {"message": "Auf IMDb ansehen"}, "watchTrailer": {"message": "Trailer ansehen"}, - "addToFavorites": {"message": "Zu Favoriten hinzufügen"}, - "removeFromFavorites": {"message": "Aus Favoriten entfernen"}, + "addToFavorites": {"message": "Favorit"}, + "removeFromFavorites": {"message": "Entfernen"}, "notAvailable": {"message": "Nicht verfügbar"}, "mainCast": {"message": "Hauptbesetzung"}, "seasonsAndEpisodes": {"message": "Staffeln & Episoden"}, diff --git a/_locales/en/messages.json b/_locales/en/messages.json index bc6000d..0f0a19a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -256,8 +256,8 @@ "writer": {"message": "Writer:"}, "viewOnImdb": {"message": "View on IMDb"}, "watchTrailer": {"message": "Watch Trailer"}, - "addToFavorites": {"message": "Add to Favorites"}, - "removeFromFavorites": {"message": "Remove from Favorites"}, + "addToFavorites": {"message": "Favorite"}, + "removeFromFavorites": {"message": "Unfavorite"}, "notAvailable": {"message": "Not Available"}, "mainCast": {"message": "Main Cast"}, "seasonsAndEpisodes": {"message": "Seasons & Episodes"}, diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 717cf08..a79b510 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -117,7 +117,7 @@ "miniplayerOpenList": { "message": "Abrir lista" }, "eqTitle": { "message": "Ecualizador Gráfico" }, "eqPresetsLabel": { "message": "Presets" }, - "eqPresetFlat": { "message": "Plano" }, + "eqPresetFlat": { "message": "Plano (Flat)" }, "eqPresetRock": { "message": "Rock" }, "eqPresetPop": { "message": "Pop" }, "eqPresetJazz": { "message": "Jazz" }, @@ -256,8 +256,8 @@ "writer": {"message": "Escritor:"}, "viewOnImdb": {"message": "Ver en IMDb"}, "watchTrailer": {"message": "Ver Tráiler"}, - "addToFavorites": {"message": "Añadir a favoritos"}, - "removeFromFavorites": {"message": "Quitar de favoritos"}, + "addToFavorites": {"message": "Favorito"}, + "removeFromFavorites": {"message": "Quitar Fav."}, "notAvailable": {"message": "No disponible"}, "mainCast": {"message": "Reparto Principal"}, "seasonsAndEpisodes": {"message": "Temporadas y Episodios"}, diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index c8a5655..8d36b22 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -257,7 +257,7 @@ "viewOnImdb": {"message": "Voir sur IMDb"}, "watchTrailer": {"message": "Voir la bande-annonce"}, "addToFavorites": {"message": "Ajouter aux favoris"}, - "removeFromFavorites": {"message": "Supprimer des favoris"}, + "removeFromFavorites": {"message": "Retirer des favoris"}, "notAvailable": {"message": "Non disponible"}, "mainCast": {"message": "Distribution Principale"}, "seasonsAndEpisodes": {"message": "Saisons & Épisodes"}, diff --git a/css/components.css b/css/components.css index 4bf09cc..0400c99 100644 --- a/css/components.css +++ b/css/components.css @@ -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"); } -.filter-select option, .form-control option { +.filter-select option { background: var(--primary); color: var(--text-primary); border: none; diff --git a/img/no-poster.png b/img/no-poster.png index a87f54a..12140ff 100644 Binary files a/img/no-poster.png and b/img/no-poster.png differ diff --git a/img/no-profile.png b/img/no-profile.png index a147b69..12140ff 100644 Binary files a/img/no-profile.png and b/img/no-profile.png differ diff --git a/js/api.js b/js/api.js index 0c39a3a..e12d331 100644 --- a/js/api.js +++ b/js/api.js @@ -84,129 +84,116 @@ export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artista return tracks; } catch (error) { + console.error("Error in getMusicUrlsFromPlex:", error); throw error; } } - -async function searchAndProcessServer(server, busqueda, tipoContenido) { - const { ip, puerto, token, protocolo = 'http', nombre: serverName = 'Servidor Desconocido' } = server; - 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}`; - - try { - const response = await fetchWithTimeout(searchUrl, { headers: { 'Accept': 'application/xml' } }); - if (!response.ok) return []; - - const data = await response.text(); - const parser = new DOMParser(); - const xml = parser.parseFromString(data, "text/xml"); - if (xml.querySelector('parsererror')) return []; - - 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")); - let videosToProcess = videos; - const exactMatch = videos.find(v => v.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase()); - if (exactMatch) { - videosToProcess = [exactMatch]; - } - - return videosToProcess.map(video => { - const part = video.querySelector("Part"); - if (!part || !part.getAttribute("key")) return null; - - const movieTitle = video.getAttribute("title") || busqueda; - const movieYear = video.getAttribute("year"); - const streamUrl = `${protocolo}://${ip}:${puerto}${part.getAttribute("key")}?X-Plex-Token=${token}`; - const extinfName = `${movieTitle}${movieYear ? ` (${movieYear})` : ''}`; - const logoUrl = video.getAttribute("thumb") ? `${protocolo}://${ip}:${puerto}${video.getAttribute("thumb")}?X-Plex-Token=${token}` : ''; - const groupTitle = extinfName.replace(/"/g, "'"); - - return { - url: streamUrl, - title: extinfName, - extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${logoUrl}" group-title="${groupTitle}",${extinfName}` - }; - }).filter(Boolean); -} - -async function processShowResults(xml, busqueda, protocolo, ip, puerto, token) { - const directories = Array.from(xml.querySelectorAll('Directory[type="show"]')); - let directoryToProcess = directories.find(d => d.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase()); - if (!directoryToProcess && directories.length > 0) { - directoryToProcess = directories[0]; - } - - if (directoryToProcess && directoryToProcess.getAttribute("ratingKey")) { - const serieKey = directoryToProcess.getAttribute("ratingKey"); - const leavesUrl = `${protocolo}://${ip}:${puerto}/library/metadata/${serieKey}/allLeaves?X-Plex-Token=${token}`; - - const leavesResponse = await fetchWithTimeout(leavesUrl, { headers: { 'Accept': 'application/xml' } }); - if (leavesResponse.ok) { - const leavesData = await leavesResponse.text(); - const leavesXml = new DOMParser().parseFromString(leavesData, "text/xml"); - if (!leavesXml.querySelector('parsererror')) { - return processShowEpisodes(leavesXml, busqueda, protocolo, ip, puerto, token); - } - } - } - return []; -} - -function processShowEpisodes(xml, busqueda, protocolo, ip, puerto, token) { - const episodes = Array.from(xml.querySelectorAll("Video")); - episodes.sort((a, b) => { - const seasonA = parseInt(a.getAttribute("parentIndex") || 0, 10); - const seasonB = parseInt(b.getAttribute("parentIndex") || 0, 10); - if (seasonA !== seasonB) return seasonA - seasonB; - const episodeA = parseInt(a.getAttribute("index") || 0, 10); - const episodeB = parseInt(b.getAttribute("index") || 0, 10); - return episodeA - episodeB; - }); - - return episodes.map(episode => { - const part = episode.querySelector("Part"); - if (!part || !part.getAttribute("key")) return null; - - const serieTitulo = episode.getAttribute("grandparentTitle") || busqueda; - const serieYear = episode.getAttribute("parentYear"); - const seasonNum = episode.getAttribute("parentIndex") || 'S'; - const episodeNum = episode.getAttribute("index") || 'E'; - const episodeTitle = episode.getAttribute("title") || 'Episodio'; - const streamUrl = `${protocolo}://${ip}:${puerto}${part.getAttribute("key")}?X-Plex-Token=${token}`; - const groupTitle = `${serieTitulo}${serieYear ? ` (${serieYear})` : ''} - Temporada ${seasonNum}`.replace(/"/g, "'"); - const extinfName = `${serieTitulo} T${seasonNum}E${episodeNum} ${episodeTitle}`; - const logoUrl = episode.getAttribute("grandparentThumb") || episode.getAttribute("parentThumb") || episode.getAttribute("thumb"); - const fullLogoUrl = logoUrl ? `${protocolo}://${ip}:${puerto}${logoUrl}?X-Plex-Token=${token}` : ''; - - return { - url: streamUrl, - title: 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') }; + 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(server => searchAndProcessServer(server, busqueda, tipoContenido)); + const searchTasks = servers.map(async (server) => { + const { ip, puerto, token, protocolo = 'http', nombre: serverName = 'Servidor Desconocido' } = server; + if (!ip || !puerto || !token) return []; + + const searchUrl = `${protocolo}://${ip}:${puerto}/search?type=${plexSearchType}&query=${encodeURIComponent(busqueda)}&X-Plex-Token=${token}`; + let serverStreams = []; + + try { + const response = await fetchWithTimeout(searchUrl, { headers: { 'Accept': 'application/xml' } }); + if (!response.ok) return []; + + const data = await response.text(); + const parser = new DOMParser(); + const xml = parser.parseFromString(data, "text/xml"); + if (xml.querySelector('parsererror')) return []; + + if (tipoContenido === 'movie') { + const videos = Array.from(xml.querySelectorAll("Video")); + let videosToProcess = videos; + const exactMatch = videos.find(v => v.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase()); + if (exactMatch) { + videosToProcess = [exactMatch]; + } + + videosToProcess.forEach(video => { + const part = video.querySelector("Part"); + if (part && part.getAttribute("key")) { + const movieTitle = video.getAttribute("title") || busqueda; + const movieYear = video.getAttribute("year"); + const streamUrl = `${protocolo}://${ip}:${puerto}${part.getAttribute("key")}?X-Plex-Token=${token}`; + const extinfName = `${movieTitle}${movieYear ? ` (${movieYear})` : ''}`; + const logoUrl = video.getAttribute("thumb") ? `${protocolo}://${ip}:${puerto}${video.getAttribute("thumb")}?X-Plex-Token=${token}` : ''; + const groupTitle = extinfName.replace(/"/g, "'"); + serverStreams.push({ + url: streamUrl, + title: extinfName, + extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${logoUrl}" group-title="${groupTitle}",${extinfName}` + }); + } + }); + } else { + const directories = Array.from(xml.querySelectorAll('Directory[type="show"]')); + let directoryToProcess = directories.find(d => d.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase()); + if (!directoryToProcess && directories.length > 0) { + directoryToProcess = directories[0]; + } + + if (directoryToProcess && directoryToProcess.getAttribute("ratingKey")) { + const serieKey = directoryToProcess.getAttribute("ratingKey"); + const serieTitulo = directoryToProcess.getAttribute("title") || busqueda; + const serieYear = directoryToProcess.getAttribute("year"); + const leavesUrl = `${protocolo}://${ip}:${puerto}/library/metadata/${serieKey}/allLeaves?X-Plex-Token=${token}`; + + const leavesResponse = await fetchWithTimeout(leavesUrl, { headers: { 'Accept': 'application/xml' } }); + if (leavesResponse.ok) { + const leavesData = await leavesResponse.text(); + const leavesXml = parser.parseFromString(leavesData, "text/xml"); + if (!leavesXml.querySelector('parsererror')) { + const episodes = Array.from(leavesXml.querySelectorAll("Video")); + + episodes.sort((a,b) => { + const seasonA = parseInt(a.getAttribute("parentIndex") || 0, 10); + const seasonB = parseInt(b.getAttribute("parentIndex") || 0, 10); + if(seasonA !== seasonB) return seasonA - seasonB; + const episodeA = parseInt(a.getAttribute("index") || 0, 10); + const episodeB = parseInt(b.getAttribute("index") || 0, 10); + return episodeA - episodeB; + }); + + episodes.forEach(episode => { + const part = episode.querySelector("Part"); + if (part && part.getAttribute("key")) { + const seasonNum = episode.getAttribute("parentIndex") || 'S'; + const episodeNum = episode.getAttribute("index") || 'E'; + const episodeTitle = episode.getAttribute("title") || 'Episodio'; + const streamUrl = `${protocolo}://${ip}:${puerto}${part.getAttribute("key")}?X-Plex-Token=${token}`; + const groupTitle = `${serieTitulo}${serieYear ? ` (${serieYear})` : ''} - Temporada ${seasonNum}`.replace(/"/g, "'"); + const extinfName = `${serieTitulo} T${seasonNum}E${episodeNum} ${episodeTitle}`; + const logoUrl = episode.getAttribute("grandparentThumb") || episode.getAttribute("parentThumb") || episode.getAttribute("thumb"); + const fullLogoUrl = logoUrl ? `${protocolo}://${ip}:${puerto}${logoUrl}?X-Plex-Token=${token}` : ''; + serverStreams.push({ + url: streamUrl, + title: extinfName, + extinf: `#EXTINF:-1 tvg-name="${extinfName.replace(/"/g, "'")}" tvg-logo="${fullLogoUrl}" group-title="${groupTitle}",${extinfName}` + }); + } + }); + } + } + } + } + return serverStreams; + } catch (error) { + console.warn(`Error buscando streams en ${serverName}:`, error.message); + return []; + } + }); const results = await Promise.allSettled(searchTasks); const allFoundStreams = results @@ -223,7 +210,7 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) { } if (tipoContenido === 'movie') { - uniqueStreams.sort((a, b) => (a.title || '').localeCompare(b.title || '')); + uniqueStreams.sort((a, b) => (a.title || '').localeCompare(b.title || '')); } if (uniqueStreams.length > 0) { diff --git a/js/constants.js b/js/constants.js deleted file mode 100644 index 3d1aeaa..0000000 --- a/js/constants.js +++ /dev/null @@ -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' -}; \ No newline at end of file diff --git a/js/db.js b/js/db.js index 8d77019..ef61075 100644 --- a/js/db.js +++ b/js/db.js @@ -55,6 +55,7 @@ export function initDB() { window.location.reload(); }; state.db.onerror = event => { + console.error(`Database error: ${event.target.errorCode}`); reject(event.target.error); }; resolve(); @@ -113,7 +114,7 @@ export function addItemsToStore(storeName, items) { if (item !== undefined && item !== null) { const request = store.put(item); request.onsuccess = () => successCount++; - request.onerror = (e) => {}; + request.onerror = (e) => console.warn(`Error adding/updating item in ${storeName}:`, e.target.error, item); } }); diff --git a/js/equalizer.js b/js/equalizer.js index 0336033..5a1a28d 100644 --- a/js/equalizer.js +++ b/js/equalizer.js @@ -54,6 +54,7 @@ export class Equalizer { this.drawVisualizer(); return true; } catch (e) { + console.error("Error initializing Web Audio API:", e); return false; } } diff --git a/js/i18n.js b/js/i18n.js index 15b1341..0a0cd0b 100644 --- a/js/i18n.js +++ b/js/i18n.js @@ -5,6 +5,7 @@ function localizeHtmlPage() { try { return p1 ? chrome.i18n.getMessage(p1) : match; } catch (e) { + console.warn(`Could not find message for: ${p1}`); return match; } } @@ -33,7 +34,6 @@ function localizeHtmlPage() { document.documentElement.lang = chrome.i18n.getUILanguage().split('-')[0]; document.title = document.title.replace(i18nRegex, replaceMsg); - document.body.classList.add('loaded'); } document.addEventListener('DOMContentLoaded', localizeHtmlPage); \ No newline at end of file diff --git a/js/main.js b/js/main.js index 0307aea..0ac021d 100644 --- a/js/main.js +++ b/js/main.js @@ -19,6 +19,7 @@ async function loadSettings() { state.settings.apiKey = config.defaultApiKey; } } catch (error) { + console.error("Could not load settings from DB, using defaults.", error); state.settings.language = chrome.i18n.getUILanguage().split('-')[0]; } } @@ -46,6 +47,7 @@ document.addEventListener('DOMContentLoaded', async () => { initializeThirdPartyLibs(); } catch (error) { + console.error("Fatal Initialization failed:", error); showNotification(_("fatalInitError"), "error"); document.getElementById('main-container').innerHTML = `

${_("fatalInitError")}

${_("fatalInitErrorSub")}

`; } diff --git a/js/ui.js b/js/ui.js index ff27b55..aa2c54b 100644 --- a/js/ui.js +++ b/js/ui.js @@ -5,27 +5,6 @@ import { getFromDB, addItemsToStore } from './db.js'; 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() { await Promise.all([loadGenres(), loadYears()]); await Promise.all([loadContent(), initializeHeroSection()]); @@ -248,29 +227,31 @@ function updateSortOptions() { async function loadGenres() { const type = state.currentParams.contentType; const select = document.getElementById('genre-filter'); - select.innerHTML = ''; - select.appendChild(createElement('option', { value: '', textContent: _('loadingGenres') })); + select.innerHTML = ``; try { const data = await fetchTMDB(`genre/${type}/list`); - select.innerHTML = ''; - select.appendChild(createElement('option', { value: '', textContent: _('allGenres') })); + select.innerHTML = ``; 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 || ""; } catch (error) { - select.innerHTML = ''; - select.appendChild(createElement('option', { value: '', textContent: _('errorLoadingGenres') })); + select.innerHTML = ``; } } function loadYears() { const select = document.getElementById('year-filter'); - select.innerHTML = ''; - select.appendChild(createElement('option', { value: '', textContent: _('allYears') })); + select.innerHTML = ``; const currentYear = new Date().getFullYear(); 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 || ""; } @@ -287,14 +268,11 @@ export async function loadContent(append = false) { const loadMoreButton = document.getElementById('load-more'); if (!append) { - grid.innerHTML = ''; - grid.appendChild(createElement('div', { className: 'col-12 text-center mt-5' }, [createElement('div', { className: 'spinner', style: 'position: static; margin: auto; display: block;' })])); + grid.innerHTML = '
'; loadMoreButton.style.display = 'none'; } else { loadMoreButton.disabled = true; - loadMoreButton.innerHTML = ''; - loadMoreButton.appendChild(createElement('span', { className: 'spinner-border spinner-border-sm', role: 'status', "aria-hidden": 'true' })); - loadMoreButton.appendChild(document.createTextNode(` ${_('loading')}`)); + loadMoreButton.innerHTML = ` ${_('loading')}`; } try { @@ -316,13 +294,7 @@ export async function loadContent(append = false) { } catch (error) { if (error.name !== 'AbortError') { - if (!append) { - grid.innerHTML = ''; - grid.appendChild(createElement('div', { className: 'empty-state' }, [ - createElement('i', { className: 'fas fa-exclamation-triangle' }), - createElement('p', { textContent: _('couldNotLoadContent') }) - ])); - } + if (!append) grid.innerHTML = `

${_('couldNotLoadContent')}

`; } } finally { state.isLoading = false; @@ -355,11 +327,7 @@ function renderGrid(items, append = false) { const emptyStateTarget = document.getElementById('recommendations-grid') || grid; const emptyIcon = state.currentView === 'recommendations' ? 'fa-user-astronaut' : 'fa-film'; const emptyText = state.currentView === 'recommendations' ? _('noRecommendations') : _('noContentFound'); - emptyStateTarget.innerHTML = ''; - emptyStateTarget.appendChild(createElement('div', { className: 'empty-state' }, [ - createElement('i', { className: `fas ${emptyIcon} fa-3x mb-3` }), - createElement('p', { className: 'lead', textContent: emptyText }) - ])); + emptyStateTarget.innerHTML = `

${emptyText}

`; } document.getElementById('load-more').style.display = 'none'; return; @@ -380,7 +348,10 @@ function renderGrid(items, append = false) { 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 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 = `
${voteAvg >= 7.8 ? 'TOP' : ''} @@ -470,8 +441,7 @@ export async function showItemDetails(itemId, contentType) { if (mainView.style.display !== 'none') state.lastScrollPosition = window.scrollY; document.body.classList.add('details-view-active'); - detailsContent.innerHTML = ''; - detailsContent.appendChild(createElement('div', { className: 'text-center py-5' }, [createElement('div', { className: 'spinner', style: 'display: block; margin: auto; position: static;' })])); + detailsContent.innerHTML = '
'; detailsView.classList.add('active'); @@ -529,7 +499,7 @@ export async function showItemDetails(itemId, contentType) { if (!isMovie && item.seasons && item.seasons.length > 0) { const seasonPromises = item.seasons .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); item.seasons_with_episodes = seasonsData.filter(s => s !== null); } @@ -538,11 +508,7 @@ export async function showItemDetails(itemId, contentType) { await renderItemDetails(item); } catch (error) { - detailsContent.innerHTML = ''; - detailsContent.appendChild(createElement('div', { className: 'alert alert-danger mx-3 my-5 text-center' }, [ - createElement('h4', { textContent: _('errorLoadingDetails') }), - createElement('p', { textContent: error.message }) - ])); + detailsContent.innerHTML = `

${_('errorLoadingDetails')}

${error.message}

`; } finally { state.isLoading = false; } @@ -569,7 +535,6 @@ async function renderItemDetails(item) { const director = crew.find(c => c.job === 'Director'); const writer = crew.find(c => c.job === 'Screenplay' || c.job === 'Writer' || c.job === 'Story'); - detailsContent.innerHTML = ''; detailsContent.innerHTML = ` ${backdropPath ? `
` : ''}
@@ -745,15 +710,10 @@ function updateFavoriteButtonVisuals(itemId, itemType, isFavorite) { export async function loadFavorites() { const grid = document.getElementById('content-grid'); - grid.innerHTML = ''; - grid.appendChild(createElement('div', { className: 'col-12 text-center mt-5' }, [createElement('div', { className: 'spinner', style: 'position: static; margin: auto; display: block;' })])); + grid.innerHTML = '
'; if (state.favorites.length === 0) { - grid.innerHTML = ''; - grid.appendChild(createElement('div', { className: 'empty-state' }, [ - createElement('i', { className: 'far fa-heart fa-3x mb-3' }), - createElement('p', { className: 'lead', textContent: _('noFavorites') }) - ])); + grid.innerHTML = `

${_('noFavorites')}

`; return; } @@ -762,11 +722,7 @@ export async function loadFavorites() { const favoriteItems = (await Promise.all(favoritePromises)).filter(item => item !== null); renderGrid(favoriteItems, false); } catch (error) { - grid.innerHTML = ''; - grid.appendChild(createElement('div', { className: 'empty-state' }, [ - createElement('i', { className: 'fas fa-exclamation-triangle' }), - createElement('p', { textContent: _('errorLoadingFavorites') }) - ])); + grid.innerHTML = `

${_('errorLoadingFavorites')}

`; } } @@ -774,11 +730,7 @@ export function displayHistory() { const listContainer = document.getElementById('history-list'); listContainer.innerHTML = ""; if (state.userHistory.length === 0) { - listContainer.appendChild(createElement('div', { className: 'empty-state' }, [ - createElement('i', { className: 'fas fa-history fa-3x mb-3' }), - createElement('p', { className: 'lead', textContent: _('historyEmpty') }), - createElement('p', { className: 'text-muted', textContent: _('historyEmptySub') }) - ])); + listContainer.innerHTML = `

${_('historyEmpty')}

${_('historyEmptySub')}

`; document.getElementById('clear-history-btn').style.display = 'none'; return; } @@ -788,7 +740,11 @@ export function displayHistory() { [...state.userHistory].sort((a,b) => b.timestamp - a.timestamp).forEach(item => { const posterUrl = item.poster ? `https://image.tmdb.org/t/p/w92${item.poster}` : 'img/no-poster.png'; const isAvailable = !!buscarContenidoLocal(item.title, item.type); - const 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 = `
@@ -856,8 +812,7 @@ export async function getTrailerKey(id, type) { export async function loadRecommendations() { const grid = document.getElementById('recommendations-grid'); - grid.innerHTML = ''; - grid.appendChild(createElement('div', { className: 'col-12 text-center mt-5' }, [createElement('div', { className: 'spinner', style: 'position: static; margin: auto; display: block;' })])); + grid.innerHTML = '
'; const cachedRecs = sessionStorage.getItem('cineplex_recommendations'); if (cachedRecs) { @@ -889,19 +844,14 @@ export async function loadRecommendations() { renderGrid([]); } } catch(error) { - grid.innerHTML = ''; - grid.appendChild(createElement('div', { className: 'empty-state' }, [ - createElement('i', { className: 'fas fa-exclamation-triangle' }), - createElement('p', { textContent: _('errorGeneratingRecommendations') }) - ])); + grid.innerHTML = `

${_('errorGeneratingRecommendations')}

`; } } async function populateStatsTokenFilter() { const select = document.getElementById('stats-token-filter'); const currentValue = select.value; - select.innerHTML = ''; - select.appendChild(createElement('option', { value: 'all', textContent: _('statsAllTokens') })); + select.innerHTML = ``; try { const tokensData = await getFromDB('tokens'); @@ -916,7 +866,10 @@ async function populateStatsTokenFilter() { }); 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) { @@ -924,8 +877,7 @@ async function populateStatsTokenFilter() { } } catch(e) { - select.innerHTML = ''; - select.appendChild(createElement('option', { value: 'all', textContent: _('errorLoadingTokens') })); + select.innerHTML = ``; } } @@ -998,11 +950,7 @@ export async function generateStatistics() { }); } catch (error) { - loader.innerHTML = ''; - loader.appendChild(createElement('div', { className: 'empty-state' }, [ - createElement('i', { className: 'fas fa-exclamation-triangle' }), - createElement('p', { textContent: _('errorGeneratingStats') }) - ])); + loader.innerHTML = `

${_('errorGeneratingStats')}

`; } } @@ -1017,15 +965,12 @@ function updateTokenDetailsCard(selectedToken, allConnections) { const associatedServers = allConnections.filter(c => c.tokenPrincipal === selectedToken); - serverList.innerHTML = ''; if (associatedServers.length === 0) { - serverList.appendChild(createElement('li', { textContent: _('noServersForToken') })); + serverList.innerHTML = `
  • ${_('noServersForToken')}
  • `; } else { - associatedServers.forEach(s => { - const li = createElement('li'); - li.innerHTML = `${s.nombre}: ${s.token.slice(0, 5)}...${s.token.slice(-5)}`; - serverList.appendChild(li); - }); + serverList.innerHTML = associatedServers.map(s => + `
  • ${s.nombre}: ${s.token.slice(0, 5)}...${s.token.slice(-5)}
  • ` + ).join(''); } card.style.display = 'block'; } @@ -1147,8 +1092,7 @@ export async function searchByActor(actorId, actorName) { updateActiveNav(state.currentParams.contentType); const grid = document.getElementById('content-grid'); - grid.innerHTML = ''; - grid.appendChild(createElement('div', { className: 'col-12 text-center mt-5' }, [createElement('div', { className: 'spinner', style: 'display: block; margin: auto; position: static;' })])); + grid.innerHTML = '
    '; document.querySelector('.filters').style.display = 'none'; try { @@ -1157,11 +1101,7 @@ export async function searchByActor(actorId, actorName) { renderGrid(data.results, false); document.getElementById('load-more').style.display = (data.page < data.total_pages) ? 'block' : 'none'; } catch (error) { - grid.innerHTML = ''; - grid.appendChild(createElement('div', { className: 'empty-state' }, [ - createElement('i', { className: 'fas fa-exclamation-triangle' }), - createElement('p', { textContent: _('errorLoadingActorContent', actorName) }) - ])); + grid.innerHTML = `

    ${_('errorLoadingActorContent', actorName)}

    `; } } @@ -1251,6 +1191,7 @@ export async function initializeHeroSection() { setInterval(() => changeHeroSlide(false), 12000); } catch (error) { + console.error("Error initializing hero section:", error); 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.'); } -$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)) { 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); $target_path = $save_dir . DIRECTORY_SEPARATOR . $safe_filename; @@ -1587,12 +1528,12 @@ $content_to_write = ""; if (FILE_ACTION_APPEND) { $file_exists = file_exists($target_path); if (!$file_exists) { - $content_to_write .= "#EXTM3U\n"; + $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"; + $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) { @@ -1601,11 +1542,11 @@ if (FILE_ACTION_APPEND) { sendResponse(false, 'Error del servidor: No se pudo añadir contenido al archivo.', '', 500); } } else { // Overwrite mode - $content_to_write = "#EXTM3U\n"; + $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"; + $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) { @@ -1636,14 +1577,12 @@ if (FILE_ACTION_APPEND) { export function initPhotosView() { const select = document.getElementById('photos-token-select'); - select.innerHTML = ''; - select.appendChild(createElement('option', { value: '', textContent: _('loading') })); + select.innerHTML = ``; const photoServers = [...new Map(state.localPhotos.map(item => [item.tokenPrincipal, item])).values()]; if (photoServers.length === 0) { - select.innerHTML = ''; - select.appendChild(createElement('option', { value: '', textContent: _('noPhotoServers') })); + select.innerHTML = ``; document.getElementById('photos-empty-state').style.display = 'block'; document.getElementById('photos-grid').innerHTML = ''; return; @@ -1651,10 +1590,12 @@ export function initPhotosView() { document.getElementById('photos-empty-state').style.display = 'none'; - select.innerHTML = ''; - select.appendChild(createElement('option', { value: '', textContent: _('selectServer') })); + select.innerHTML = ``; 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)) { @@ -1812,8 +1753,11 @@ function renderPhotoBreadcrumb() { const breadcrumb = document.getElementById('photos-breadcrumb'); breadcrumb.innerHTML = ''; - const rootItem = createElement('li', { className: 'breadcrumb-item' }); - const rootLink = createElement('a', { href: '#', innerHTML: ` ${_('photosBreadcrumbHome')}` }); + const rootItem = document.createElement('li'); + rootItem.className = 'breadcrumb-item'; + const rootLink = document.createElement('a'); + rootLink.href = '#'; + rootLink.innerHTML = ` ${_('photosBreadcrumbHome')}`; rootLink.onclick = (e) => { e.preventDefault(); handlePhotoTokenChange(); @@ -1822,15 +1766,20 @@ function renderPhotoBreadcrumb() { breadcrumb.appendChild(rootItem); state.photoStack.forEach((item, index) => { - const divider = createElement('li', { className: 'breadcrumb-divider', innerHTML: '' }); + const divider = document.createElement('li'); + divider.className = 'breadcrumb-divider'; + divider.innerHTML = ''; 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) { breadcrumbItem.classList.add('active'); breadcrumbItem.textContent = item.title; } else { - const link = createElement('a', { href: '#', textContent: item.title }); + const link = document.createElement('a'); + link.href = '#'; + link.textContent = item.title; link.onclick = async (e) => { e.preventDefault(); state.photoStack = state.photoStack.slice(0, index + 1); diff --git a/js/utils.js b/js/utils.js index 9ae0faa..45cee4d 100644 --- a/js/utils.js +++ b/js/utils.js @@ -93,17 +93,18 @@ export function logToConsole(message) { logEntry.className = 'console-log-entry'; const time = new Date().toLocaleTimeString(_('appLocaleCode'), { hour12: false }); - const timeSpan = document.createElement('span'); - timeSpan.className = 'log-time'; - timeSpan.textContent = `[${time}]`; + const timeSpan = `[${time}]`; + const messageSpan = `${message.replace(//g, ">")}`; - const messageSpan = document.createElement('span'); - messageSpan.className = 'log-message'; - messageSpan.textContent = message; - - logEntry.appendChild(timeSpan); - logEntry.appendChild(document.createTextNode(' ')); - logEntry.appendChild(messageSpan); + if (message.toLowerCase().includes("error")) { + logEntry.classList.add('log-error'); + } 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.innerHTML = `${timeSpan} ${messageSpan}`; consoleOutput.appendChild(logEntry); consoleOutput.scrollTop = consoleOutput.scrollHeight; } diff --git a/manifest.json b/manifest.json index 6219fd0..defbfe4 100644 --- a/manifest.json +++ b/manifest.json @@ -14,7 +14,8 @@ "notifications" ], "host_permissions": [ - "https://*.plex.tv/*" + "https://*.plex.tv/*", + "*://*:*/*" ], "background": { "service_worker": "js/background.js", diff --git a/plex.html b/plex.html index 1f4ceaf..7f86f27 100644 --- a/plex.html +++ b/plex.html @@ -13,15 +13,6 @@ -