Spaces:
Paused
Paused
Download views/admin.ejs from Streamixph05/pops: direct link, hf CLI and curl.
- Browser
- Download file 47 kB
-
https://huggingface.co/spaces/Streamixph05/pops/resolve/main/views/admin.ejs
- Command line
-
hf download hf://spaces/Streamixph05/pops/views/admin.ejs
-
curl -L -o admin.ejs https://huggingface.co/spaces/Streamixph05/pops/resolve/main/views/admin.ejs
47 kB
| <html lang="en" class="h-full bg-[#080808]"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title><%= pageTitle %></title> | |
| <meta name="description" content="StreamPortal Admin - Firebase Realtime Database Content & Stream Manager"> | |
| <!-- Tailwind CSS CDN --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| bg: '#080808', | |
| card: '#121212', | |
| cardHover: '#171717', | |
| border: '#262626', | |
| accent: { | |
| DEFAULT: '#e11d48', | |
| hover: '#f43f5e', | |
| glow: 'rgba(225, 29, 72, 0.25)', | |
| dim: 'rgba(225, 29, 72, 0.1)' | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <!-- Lucide Icons --> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #080808; | |
| --card: #121212; | |
| --card-hover: #171717; | |
| --border: #262626; | |
| --border-hover: #3a3a3a; | |
| --accent: #e11d48; | |
| --text: #f5f5f5; | |
| --text-dim: #a3a3a3; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background-color: var(--bg); | |
| color: var(--text); | |
| } | |
| .bento-card { | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| transition: all 0.2s ease-in-out; | |
| } | |
| .bento-card:hover { | |
| border-color: var(--border-hover); | |
| } | |
| .bento-badge { | |
| padding: 4px 8px; | |
| border-radius: 6px; | |
| background: #1a1a1a; | |
| font-size: 11px; | |
| color: #e5e5e5; | |
| border: 1px solid var(--border); | |
| } | |
| .bento-input { | |
| background: #181818; | |
| border: 1px solid var(--border); | |
| color: #f5f5f5; | |
| } | |
| .bento-input:focus { | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 2px rgba(225, 29, 72, 0.2); | |
| } | |
| .accent-dot { | |
| width: 6px; | |
| height: 6px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| box-shadow: 0 0 8px var(--accent); | |
| } | |
| .card-title-bento { | |
| font-size: 11px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: var(--text-dim); | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-[#080808] text-[#f5f5f5] min-h-screen flex flex-col antialiased selection:bg-rose-600 selection:text-white"> | |
| <!-- Bento Navigation Bar --> | |
| <header class="sticky top-0 z-40 border-b border-[#262626] bg-[#080808]/90 backdrop-blur-md"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between gap-4"> | |
| <a href="/" class="flex items-center gap-3 group"> | |
| <div class="w-9 h-9 rounded-xl bg-rose-600 flex items-center justify-center font-extrabold text-white text-sm shadow-[0_0_15px_rgba(225,29,72,0.4)] group-hover:scale-105 transition-transform"> | |
| SP | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <span class="text-base font-extrabold tracking-tight text-white flex items-center"> | |
| STREAM<span class="text-rose-500">PORTAL</span> | |
| </span> | |
| <span class="bento-badge text-[10px] font-mono text-rose-400 border-rose-500/30">FIREBASE ADMIN</span> | |
| </div> | |
| </a> | |
| <!-- Center Tabs --> | |
| <nav class="hidden md:flex items-center gap-6 text-xs font-semibold tracking-wider text-zinc-400"> | |
| <a href="/" class="hover:text-white transition flex items-center gap-1"> | |
| <i data-lucide="layout-dashboard" class="w-3.5 h-3.5"></i> MAIN PORTAL | |
| </a> | |
| <a href="/admin" class="text-white flex items-center gap-1.5 font-bold"> | |
| <div class="accent-dot"></div> ADMIN CONSOLE | |
| </a> | |
| <a href="#database-inventory" class="hover:text-white transition flex items-center gap-1"> | |
| <i data-lucide="database" class="w-3.5 h-3.5"></i> INVENTORY (<%= items.length %>) | |
| </a> | |
| </nav> | |
| <!-- Status Pills --> | |
| <div class="flex items-center gap-3"> | |
| <div class="flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium bg-[#121212] text-emerald-400 border border-[#262626]" title="Firebase Realtime Database: Connected"> | |
| <span class="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_8px_#22c55e] animate-pulse"></span> | |
| <span class="font-mono text-[11px]">RTDB: LIVE</span> | |
| </div> | |
| <a href="/" class="px-3.5 py-1.5 rounded-xl text-xs font-semibold bg-[#181818] hover:bg-zinc-800 text-zinc-200 border border-[#262626] transition flex items-center gap-1.5"> | |
| <i data-lucide="external-link" class="w-3.5 h-3.5 text-rose-500"></i> | |
| View Portal | |
| </a> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Admin Container --> | |
| <main class="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8"> | |
| <!-- Flash Notifications --> | |
| <% if (flashMessage) { %> | |
| <div class="p-4 rounded-2xl <%= flashType === 'error' ? 'bg-rose-950/40 border-rose-600/40 text-rose-200' : 'bg-emerald-950/40 border-emerald-600/40 text-emerald-200' %> border text-sm flex items-center justify-between gap-3 animate-fade-in"> | |
| <div class="flex items-center gap-3"> | |
| <i data-lucide="<%= flashType === 'error' ? 'alert-circle' : 'check-circle-2' %>" class="w-5 h-5 flex-shrink-0 <%= flashType === 'error' ? 'text-rose-400' : 'text-emerald-400' %>"></i> | |
| <span><%= flashMessage %></span> | |
| </div> | |
| <button onclick="this.parentElement.remove()" class="text-zinc-400 hover:text-white">✕</button> | |
| </div> | |
| <% } %> | |
| <!-- Top Stats Row --> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> | |
| <div class="bento-card p-5 space-y-2 bg-gradient-to-br from-[#141414] to-[#0c0c0c]"> | |
| <div class="card-title-bento text-rose-400"><div class="accent-dot"></div> TOTAL TITLES</div> | |
| <div class="text-3xl font-extrabold text-white"><%= totalCount %></div> | |
| <p class="text-[11px] text-zinc-400">Aggregated across all 7 Firebase nodes</p> | |
| </div> | |
| <div class="bento-card p-5 space-y-2"> | |
| <div class="card-title-bento"><div class="accent-dot"></div> 🇵🇭 TAGALOG MOVIES</div> | |
| <div class="text-3xl font-extrabold text-white"><%= (nodeStats['admin_tagalog_movies'] || 0) + (nodeStats['community_tagalog'] || 0) %></div> | |
| <p class="text-[11px] text-zinc-400"><%= nodeStats['admin_tagalog_movies'] || 0 %> Admin + <%= nodeStats['community_tagalog'] || 0 %> Community</p> | |
| </div> | |
| <div class="bento-card p-5 space-y-2"> | |
| <div class="card-title-bento"><div class="accent-dot"></div> 📺 TV SERIES</div> | |
| <div class="text-3xl font-extrabold text-white"><%= (nodeStats['admin_community_tagalog_series'] || 0) + (nodeStats['community_series'] || 0) %></div> | |
| <p class="text-[11px] text-zinc-400"><%= nodeStats['admin_community_tagalog_series'] || 0 %> Tagalog + <%= nodeStats['community_series'] || 0 %> Series</p> | |
| </div> | |
| <div class="bento-card p-5 space-y-2"> | |
| <div class="card-title-bento"><div class="accent-dot"></div> 🍿 ANIME & VIVAMAX</div> | |
| <div class="text-3xl font-extrabold text-white"><%= (nodeStats['community_anime'] || 0) + (nodeStats['vivamax'] || 0) + (nodeStats['community_movies'] || 0) %></div> | |
| <p class="text-[11px] text-zinc-400"><%= nodeStats['community_anime'] || 0 %> Anime + <%= nodeStats['vivamax'] || 0 %> Vivamax</p> | |
| </div> | |
| </div> | |
| <!-- Main Content Section: Form & Live TMDB Search (Left) + Node Overview (Right) --> | |
| <div class="grid grid-cols-1 lg:grid-cols-12 gap-8"> | |
| <!-- Left Column: Add / Update Stream Form (7 Cols) --> | |
| <div class="lg:col-span-7 space-y-6"> | |
| <!-- TMDB Live Search Bar Bento Card --> | |
| <div class="bento-card p-5 space-y-3 bg-gradient-to-br from-[#141414] to-[#0f0f0f] border-rose-500/25 shadow-lg shadow-rose-950/15"> | |
| <div class="flex items-center justify-between"> | |
| <div class="card-title-bento text-rose-400"> | |
| <div class="accent-dot"></div> TMDB TITLE SEARCH & 1-CLICK AUTO-FILL | |
| </div> | |
| <span class="bento-badge font-mono text-[10px] text-rose-300 border-rose-500/30 bg-rose-950/30"> | |
| TMDB MULTI-SEARCH | |
| </span> | |
| </div> | |
| <form onsubmit="event.preventDefault(); triggerAdminSearch();" class="flex gap-2"> | |
| <div class="relative flex-1"> | |
| <i data-lucide="search" class="w-4 h-4 text-zinc-500 absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none"></i> | |
| <input | |
| type="text" | |
| id="admin-search-input" | |
| placeholder="🔍 Search Movie / TV Title on TMDB (e.g. Inception, Dune, Stranger Things)..." | |
| class="w-full pl-10 pr-4 py-2.5 rounded-xl bento-input text-sm font-medium focus:outline-none focus:border-rose-500" | |
| > | |
| </div> | |
| <button | |
| type="button" | |
| onclick="triggerAdminSearch()" | |
| class="px-4 py-2.5 rounded-xl bg-rose-600 hover:bg-rose-500 text-white font-semibold text-xs transition flex items-center gap-1.5 shadow-md shadow-rose-600/25 flex-shrink-0 cursor-pointer" | |
| > | |
| <i data-lucide="sparkles" class="w-3.5 h-3.5"></i> | |
| <span>Search TMDB</span> | |
| </button> | |
| </form> | |
| <!-- Quick Discovery Tags --> | |
| <div class="flex items-center gap-1.5 flex-wrap pt-0.5 text-[11px] text-zinc-400"> | |
| <span class="text-zinc-500 text-[10px] uppercase font-mono">Quick:</span> | |
| <button type="button" onclick="quickSearchModal('Fight Club')" class="px-2 py-0.5 rounded-md bg-[#1a1a1a] hover:bg-zinc-800 text-zinc-300 hover:text-rose-400 border border-[#2b2b2b] transition">Fight Club</button> | |
| <button type="button" onclick="quickSearchModal('Interstellar')" class="px-2 py-0.5 rounded-md bg-[#1a1a1a] hover:bg-zinc-800 text-zinc-300 hover:text-rose-400 border border-[#2b2b2b] transition">Interstellar</button> | |
| <button type="button" onclick="quickSearchModal('Dune')" class="px-2 py-0.5 rounded-md bg-[#1a1a1a] hover:bg-zinc-800 text-zinc-300 hover:text-rose-400 border border-[#2b2b2b] transition">Dune</button> | |
| <button type="button" onclick="quickSearchModal('Descendants of the Sun')" class="px-2 py-0.5 rounded-md bg-[#1a1a1a] hover:bg-zinc-800 text-zinc-300 hover:text-rose-400 border border-[#2b2b2b] transition">Descendants of the Sun</button> | |
| </div> | |
| </div> | |
| <!-- Entry Management Form --> | |
| <div class="bento-card p-6 space-y-5" id="stream-form-container"> | |
| <div class="flex items-center justify-between border-b border-[#262626] pb-3"> | |
| <div class="card-title-bento text-white"> | |
| <div class="accent-dot"></div> <span id="form-heading-text">ADD / EDIT FIREBASE MEDIA ENTRY</span> | |
| </div> | |
| <span class="bento-badge font-mono text-[10px] text-rose-400 border-rose-500/30" id="form-mode-badge"> | |
| NEW ENTRY | |
| </span> | |
| </div> | |
| <form action="/admin/save" method="POST" class="space-y-4" id="media-form"> | |
| <!-- Hidden Existing ID for Updates --> | |
| <input type="hidden" name="existingId" id="field-existing-id" value=""> | |
| <!-- Row 1: Target Node & Type --> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-xs font-semibold text-zinc-300 uppercase tracking-wider mb-2"> | |
| Target Firebase Node * | |
| </label> | |
| <select | |
| name="targetNode" | |
| id="field-target-node" | |
| onchange="handleTargetNodeChange(this.value)" | |
| class="w-full px-3.5 py-2.5 rounded-xl bento-input text-xs font-semibold focus:outline-none focus:border-rose-500 cursor-pointer" | |
| > | |
| <optgroup label="Movies"> | |
| <option value="admin_tagalog_movies">admin_tagalog_movies (VIP Tagalog)</option> | |
| <option value="community_movies">community_movies (General)</option> | |
| <option value="community_tagalog">community_tagalog (Community)</option> | |
| <option value="community_anime_movies">community_anime_movies (Anime Movies)</option> | |
| <option value="vivamax">vivamax (Premium)</option> | |
| </optgroup> | |
| <optgroup label="TV Series"> | |
| <option value="admin_community_tagalog_series">admin_community_tagalog_series (Tagalog Series)</option> | |
| <option value="community_series">community_series (General Series)</option> | |
| <option value="community_anime">community_anime (Anime)</option> | |
| </optgroup> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-semibold text-zinc-300 uppercase tracking-wider mb-2"> | |
| Media Type * | |
| </label> | |
| <select | |
| name="type" | |
| id="field-type" | |
| onchange="toggleMediaType(this.value)" | |
| class="w-full px-3.5 py-2.5 rounded-xl bento-input text-xs font-semibold focus:outline-none focus:border-rose-500 cursor-pointer" | |
| > | |
| <option value="movie">🎬 Movie</option> | |
| <option value="tv">📺 TV Series</option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Row 2: Title & Category --> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-xs font-semibold text-zinc-300 uppercase tracking-wider mb-2"> | |
| Title * | |
| </label> | |
| <input | |
| type="text" | |
| name="title" | |
| id="field-title" | |
| required | |
| placeholder="e.g. Acts of Vengeance Tagalog dubbed" | |
| class="w-full px-3.5 py-2.5 rounded-xl bento-input text-xs focus:outline-none focus:border-rose-500" | |
| > | |
| </div> | |
| <div> | |
| <label class="block text-xs font-semibold text-zinc-300 uppercase tracking-wider mb-2"> | |
| Category Tag | |
| </label> | |
| <input | |
| type="text" | |
| name="category" | |
| id="field-category" | |
| placeholder="e.g. VIP Tagalog (Admin) or tagalog_series" | |
| class="w-full px-3.5 py-2.5 rounded-xl bento-input text-xs focus:outline-none focus:border-rose-500" | |
| > | |
| </div> | |
| </div> | |
| <!-- Row 3: Genre, Year, Rating --> | |
| <div class="grid grid-cols-3 gap-3"> | |
| <div> | |
| <label class="block text-xs font-semibold text-zinc-300 uppercase tracking-wider mb-2"> | |
| Genre | |
| </label> | |
| <input | |
| type="text" | |
| name="genre" | |
| id="field-genre" | |
| placeholder="Action, Drama" | |
| class="w-full px-3 py-2.5 rounded-xl bento-input text-xs focus:outline-none focus:border-rose-500" | |
| > | |
| </div> | |
| <div> | |
| <label class="block text-xs font-semibold text-zinc-300 uppercase tracking-wider mb-2"> | |
| Year | |
| </label> | |
| <input | |
| type="text" | |
| name="year" | |
| id="field-year" | |
| placeholder="2024" | |
| class="w-full px-3 py-2.5 rounded-xl bento-input text-xs focus:outline-none focus:border-rose-500" | |
| > | |
| </div> | |
| <div> | |
| <label class="block text-xs font-semibold text-zinc-300 uppercase tracking-wider mb-2"> | |
| Rating | |
| </label> | |
| <input | |
| type="text" | |
| name="rating" | |
| id="field-rating" | |
| placeholder="8.5" | |
| class="w-full px-3 py-2.5 rounded-xl bento-input text-xs focus:outline-none focus:border-rose-500" | |
| > | |
| </div> | |
| </div> | |
| <!-- Row 4: Poster & Backdrop Images --> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-xs font-semibold text-zinc-300 uppercase tracking-wider mb-2"> | |
| Poster Image URL (image) | |
| </label> | |
| <input | |
| type="url" | |
| name="image" | |
| id="field-image" | |
| placeholder="https://image.tmdb.org/t/p/w500/..." | |
| class="w-full px-3.5 py-2.5 rounded-xl bento-input text-xs focus:outline-none focus:border-rose-500" | |
| > | |
| </div> | |
| <div> | |
| <label class="block text-xs font-semibold text-zinc-300 uppercase tracking-wider mb-2"> | |
| Backdrop Image / Trailer URL | |
| </label> | |
| <input | |
| type="text" | |
| name="trailer" | |
| id="field-trailer" | |
| placeholder="https://www.youtube.com/watch?v=..." | |
| class="w-full px-3.5 py-2.5 rounded-xl bento-input text-xs focus:outline-none focus:border-rose-500" | |
| > | |
| </div> | |
| </div> | |
| <!-- Row 5: Synopsis Overview --> | |
| <div> | |
| <label class="block text-xs font-semibold text-zinc-300 uppercase tracking-wider mb-2"> | |
| Synopsis / Description | |
| </label> | |
| <textarea | |
| name="description" | |
| id="field-description" | |
| rows="2" | |
| placeholder="Plot synopsis..." | |
| class="w-full px-3.5 py-2.5 rounded-xl bento-input text-xs focus:outline-none focus:border-rose-500" | |
| ></textarea> | |
| </div> | |
| <!-- Movie Stream Link Section --> | |
| <div id="movie-stream-container" class="space-y-2"> | |
| <label class="block text-xs font-semibold text-zinc-300 uppercase tracking-wider mb-1 flex items-center justify-between"> | |
| <span>Pipe-Separated Stream Links (streamLink)</span> | |
| <span class="text-[10px] text-zinc-500">Delimited by " | "</span> | |
| </label> | |
| <input | |
| type="text" | |
| name="streamLink" | |
| id="field-streamlink" | |
| placeholder="https://short.icu/... | https://vidlink.pro/... | https://vidsrc.xyz/..." | |
| class="w-full px-3.5 py-2.5 rounded-xl bento-input text-xs font-mono text-rose-300 focus:outline-none focus:border-rose-500" | |
| > | |
| <p class="text-[11px] text-zinc-500"> | |
| You can provide multiple mirrors separated by pipes. The Smart Dual Player will render Server 1, Server 2, Server 3 buttons. | |
| </p> | |
| </div> | |
| <!-- TV Series Seasons JSON Section (Visible if TV Series) --> | |
| <div id="series-seasons-container" class="space-y-2 hidden"> | |
| <label class="block text-xs font-semibold text-zinc-300 uppercase tracking-wider mb-1 flex items-center justify-between"> | |
| <span>Series Seasons & Episodes JSON (seasons)</span> | |
| <button type="button" onclick="loadSampleSeasonsJson()" class="text-[10px] text-blue-400 hover:text-blue-300"> | |
| Insert Sample JSON | |
| </button> | |
| </label> | |
| <textarea | |
| name="seasonsJson" | |
| id="field-seasons-json" | |
| rows="4" | |
| placeholder='{"Season 1": {"Ep 1": "https://short.icu/... | https://vidlink.pro/..."}}' | |
| class="w-full px-3.5 py-2.5 rounded-xl bento-input text-xs font-mono text-blue-300 focus:outline-none focus:border-blue-500" | |
| ></textarea> | |
| </div> | |
| <!-- Submit & Reset Actions --> | |
| <div class="pt-3 border-t border-[#262626] flex items-center justify-between gap-3"> | |
| <button | |
| type="button" | |
| onclick="resetAdminForm()" | |
| class="px-4 py-2.5 rounded-xl bg-[#181818] hover:bg-zinc-800 text-zinc-400 hover:text-white text-xs font-semibold border border-[#262626] transition cursor-pointer" | |
| > | |
| Clear Form | |
| </button> | |
| <button | |
| type="submit" | |
| class="px-6 py-2.5 rounded-xl bg-rose-600 hover:bg-rose-500 text-white font-bold text-xs transition flex items-center gap-2 shadow-lg shadow-rose-600/30 cursor-pointer" | |
| > | |
| <i data-lucide="save" class="w-4 h-4"></i> | |
| <span id="submit-btn-text">Save to Firebase Database</span> | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- Right Column: Database Info & Schema Guide (5 Cols) --> | |
| <div class="lg:col-span-5 space-y-6"> | |
| <div class="bento-card p-5 space-y-4"> | |
| <div class="card-title-bento text-rose-400"> | |
| <div class="accent-dot"></div> FIREBASE REALTIME DATABASE TARGET | |
| </div> | |
| <div class="p-3.5 rounded-xl bg-[#181818] border border-[#262626] space-y-1.5 text-xs font-mono"> | |
| <div class="text-[11px] text-zinc-400">Base REST Endpoint:</div> | |
| <div class="text-rose-400 break-all text-xs font-bold"><%= firebaseDatabaseUrl %></div> | |
| </div> | |
| <div class="space-y-2 text-xs"> | |
| <div class="text-zinc-400 font-semibold uppercase tracking-wider text-[10px]">Supported Content Nodes:</div> | |
| <div class="space-y-1.5"> | |
| <% Object.entries(firebaseNodes).forEach(function([k, m]) { %> | |
| <div class="flex items-center justify-between p-2 rounded-lg bg-[#141414] border border-[#262626] text-xs"> | |
| <div class="flex items-center gap-2"> | |
| <span class="w-1.5 h-1.5 rounded-full bg-rose-500"></span> | |
| <span class="font-mono text-zinc-200"><%= k %></span> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <span class="bento-badge font-mono text-[9px] text-zinc-400"><%= nodeStats[k] || 0 %> items</span> | |
| <span class="text-[10px] text-rose-400"><%= m.badge %></span> | |
| </div> | |
| </div> | |
| <% }); %> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bento-card p-5 space-y-3 bg-[#101010]"> | |
| <div class="card-title-bento text-zinc-300"> | |
| <div class="accent-dot"></div> SMART DUAL PLAYER ENGINE RULES | |
| </div> | |
| <ul class="text-[11px] text-zinc-400 space-y-2 list-disc list-inside leading-relaxed"> | |
| <li><strong class="text-zinc-200">Direct Media:</strong> URLs ending in <code class="text-rose-400">.mp4</code> or <code class="text-rose-400">.m3u8</code> launch Artplayer with HLS.js adaptive streaming and download controls.</li> | |
| <li><strong class="text-zinc-200">Web Embeds:</strong> URLs containing <code class="text-rose-400">short.icu</code>, <code class="text-rose-400">vidlink.pro</code>, <code class="text-rose-400">vidsrc</code>, or <code class="text-rose-400">/embed/</code> render a responsive sandboxed iFrame.</li> | |
| <li><strong class="text-zinc-200">Multi-Mirroring:</strong> Split multiple links with <code class="text-rose-400"> | </code> to give viewers Server 1, Server 2, Server 3 switcher buttons.</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Live Inventory Section --> | |
| <div class="bento-card p-6 space-y-5" id="database-inventory"> | |
| <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-[#262626] pb-4"> | |
| <div> | |
| <div class="card-title-bento text-white"><div class="accent-dot"></div> FIREBASE CONTENT INVENTORY</div> | |
| <p class="text-xs text-zinc-400 mt-0.5">Manage, preview, edit, or delete items from the database</p> | |
| </div> | |
| <!-- Inventory Search & Filter --> | |
| <div class="flex items-center gap-2"> | |
| <input | |
| type="text" | |
| id="inventory-filter-input" | |
| oninput="filterInventoryList()" | |
| placeholder="Filter by title, node, or genre..." | |
| class="px-3.5 py-1.5 rounded-xl bento-input text-xs focus:outline-none focus:border-rose-500 w-48 sm:w-64" | |
| > | |
| <select | |
| id="inventory-node-filter" | |
| onchange="filterInventoryList()" | |
| class="px-3 py-1.5 rounded-xl bento-input text-xs font-semibold focus:outline-none focus:border-rose-500 cursor-pointer" | |
| > | |
| <option value="ALL">All Nodes (<%= items.length %>)</option> | |
| <% Object.entries(firebaseNodes).forEach(function([k, m]) { %> | |
| <option value="<%= k %>"><%= k %> (<%= nodeStats[k] || 0 %>)</option> | |
| <% }); %> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Inventory Grid --> | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" id="inventory-items-container"> | |
| <% items.forEach(function(item) { %> | |
| <div | |
| class="inventory-item-card bento-card p-4 flex gap-3.5 relative group hover:border-zinc-700 transition" | |
| data-title="<%= (item.title || '').toLowerCase() %>" | |
| data-node="<%= item.node %>" | |
| data-genre="<%= (item.genre || '').toLowerCase() %>" | |
| > | |
| <!-- Poster Image --> | |
| <div class="w-16 h-24 rounded-lg overflow-hidden bg-[#181818] flex-shrink-0 border border-[#262626]"> | |
| <img | |
| src="<%= item.image || item.poster %>" | |
| alt="<%= item.title %>" | |
| loading="lazy" | |
| class="w-full h-full object-cover" | |
| onerror="this.src='https://images.unsplash.com/photo-1536440136628-849c177e76a1?auto=format&fit=crop&w=500&q=80'" | |
| > | |
| </div> | |
| <!-- Details --> | |
| <div class="flex-1 min-w-0 flex flex-col justify-between"> | |
| <div> | |
| <div class="flex items-center gap-1.5 mb-1 flex-wrap"> | |
| <span class="bento-badge font-mono text-[9px] text-rose-400 border-rose-500/30"><%= item.node %></span> | |
| <span class="text-[10px] text-zinc-500 font-mono">(<%= item.year %>)</span> | |
| </div> | |
| <h4 class="text-xs font-bold text-white truncate" title="<%= item.title %>"><%= item.title %></h4> | |
| <p class="text-[10px] text-zinc-400 truncate mt-0.5"><%= item.genre %></p> | |
| </div> | |
| <!-- Action Buttons --> | |
| <div class="pt-2 border-t border-[#222] flex items-center justify-between gap-2"> | |
| <a | |
| href="/watch/<%= item.type %>/<%= item.node %>/<%= item.id %>" | |
| target="_blank" | |
| class="text-[10px] text-zinc-400 hover:text-white flex items-center gap-1" | |
| > | |
| <i data-lucide="play" class="w-3 h-3 text-rose-500"></i> Watch | |
| </a> | |
| <div class="flex items-center gap-1.5"> | |
| <button | |
| type="button" | |
| data-item="<%- encodeURIComponent(JSON.stringify(item)) %>" | |
| onclick="handleEditItem(this)" | |
| class="px-2.5 py-1 rounded-lg bg-[#181818] hover:bg-zinc-800 text-zinc-300 text-[10px] font-semibold border border-[#262626] transition cursor-pointer" | |
| > | |
| Edit | |
| </button> | |
| <form action="/admin/delete" method="POST" onsubmit="return confirm('Are you sure you want to delete this item from <%= item.node %>?');"> | |
| <input type="hidden" name="id" value="<%= item.id %>"> | |
| <input type="hidden" name="node" value="<%= item.node %>"> | |
| <button | |
| type="submit" | |
| class="px-2.5 py-1 rounded-lg bg-rose-950/40 hover:bg-rose-900/60 text-rose-300 text-[10px] font-semibold border border-rose-600/30 transition cursor-pointer" | |
| > | |
| Delete | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <% }); %> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Live TMDB Search Modal Dialog --> | |
| <div | |
| id="tmdb-search-modal" | |
| class="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-3 sm:p-6 opacity-0 pointer-events-none transition-opacity duration-200" | |
| role="dialog" | |
| aria-modal="true" | |
| aria-labelledby="modal-title" | |
| onclick="handleModalBackdropClick(event)" | |
| > | |
| <div | |
| id="tmdb-modal-card" | |
| class="bg-[#0e0e0e] border border-[#2b2b2b] rounded-2xl w-full max-w-4xl max-h-[90vh] flex flex-col shadow-2xl shadow-rose-950/20 transform scale-95 transition-transform duration-200 overflow-hidden" | |
| onclick="event.stopPropagation()" | |
| > | |
| <!-- Modal Header --> | |
| <div class="p-4 sm:p-5 border-b border-[#262626] bg-[#121212] flex items-center justify-between gap-3"> | |
| <div class="flex items-center gap-2.5"> | |
| <div class="accent-dot"></div> | |
| <div> | |
| <h2 id="modal-title" class="text-sm sm:text-base font-bold text-white flex items-center gap-2"> | |
| TMDB Title Search & Auto-Fill | |
| </h2> | |
| <p class="text-[11px] text-zinc-400">Search global movies and TV shows to auto-populate metadata</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <span id="modal-result-count" class="hidden sm:inline-flex bento-badge font-mono text-[10px] text-zinc-400"> | |
| Ready | |
| </span> | |
| <button | |
| type="button" | |
| onclick="closeTmdbSearchModal()" | |
| class="p-2 rounded-xl bg-[#181818] hover:bg-zinc-800 text-zinc-400 hover:text-white border border-[#262626] transition flex items-center justify-center cursor-pointer" | |
| title="Close (Esc)" | |
| > | |
| <i data-lucide="x" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Modal Search Bar --> | |
| <div class="p-4 sm:p-5 border-b border-[#262626] bg-[#0c0c0c]"> | |
| <form onsubmit="event.preventDefault(); executeModalSearch();" class="flex gap-2"> | |
| <div class="relative flex-1"> | |
| <i data-lucide="search" class="w-4 h-4 text-zinc-500 absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none"></i> | |
| <input | |
| type="text" | |
| id="modal-search-input" | |
| placeholder="Search movie or TV show title (e.g. Fight Club, Inception, Breaking Bad)..." | |
| class="w-full pl-10 pr-4 py-2.5 rounded-xl bento-input text-sm font-medium focus:outline-none focus:border-rose-500" | |
| > | |
| </div> | |
| <button | |
| type="submit" | |
| id="modal-search-btn" | |
| class="px-5 py-2.5 rounded-xl bg-rose-600 hover:bg-rose-500 text-white font-semibold text-xs transition flex items-center gap-1.5 shadow-md shadow-rose-600/25 flex-shrink-0 cursor-pointer" | |
| > | |
| <i data-lucide="search" class="w-3.5 h-3.5"></i> | |
| <span>Search</span> | |
| </button> | |
| </form> | |
| </div> | |
| <!-- Modal Body / Results Grid --> | |
| <div id="modal-body-scroll" class="p-4 sm:p-5 overflow-y-auto flex-1 max-h-[60vh] space-y-4"> | |
| <div id="modal-loading-state" class="hidden py-16 text-center space-y-3"> | |
| <div class="inline-block w-8 h-8 border-2 border-rose-500 border-t-transparent rounded-full animate-spin"></div> | |
| <div class="text-xs font-mono text-zinc-400">Querying TMDB Multi-Search API...</div> | |
| </div> | |
| <div id="modal-empty-initial" class="py-16 text-center space-y-2"> | |
| <i data-lucide="film" class="w-10 h-10 text-zinc-700 mx-auto"></i> | |
| <p class="text-xs text-zinc-400">Type a title above and press Search</p> | |
| </div> | |
| <div id="modal-not-found-state" class="hidden py-16 text-center space-y-2"> | |
| <i data-lucide="alert-circle" class="w-10 h-10 text-amber-500/70 mx-auto"></i> | |
| <p class="text-xs text-zinc-300 font-semibold">No matching media titles found on TMDB</p> | |
| </div> | |
| <div id="modal-results-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3.5"></div> | |
| </div> | |
| <!-- Modal Footer --> | |
| <div class="p-3.5 px-5 border-t border-[#262626] bg-[#121212] flex items-center justify-between text-xs text-zinc-400"> | |
| <span class="flex items-center gap-1 text-[11px]"> | |
| <span class="text-rose-400">💡</span> Click any title to auto-fill metadata into the form. | |
| </span> | |
| <button | |
| type="button" | |
| onclick="closeTmdbSearchModal()" | |
| class="text-xs text-zinc-400 hover:text-white transition font-medium cursor-pointer" | |
| > | |
| Cancel [Esc] | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Footer --> | |
| <footer class="border-t border-[#262626] bg-[#080808] py-6 mt-12 text-xs text-zinc-500 text-center"> | |
| <div class="max-w-7xl mx-auto px-4 flex flex-col sm:flex-row items-center justify-between gap-4"> | |
| <div>StreamPortal Admin CP • Firebase Realtime Database Engine</div> | |
| <div class="font-mono text-[10px] text-zinc-600">RTDB: <%= firebaseDatabaseUrl %></div> | |
| </div> | |
| </footer> | |
| <script> | |
| lucide.createIcons(); | |
| // Toggle fields based on media type | |
| function toggleMediaType(type) { | |
| const isTv = type === 'tv'; | |
| const movieStreamBox = document.getElementById('movie-stream-container'); | |
| const seriesSeasonsBox = document.getElementById('series-seasons-container'); | |
| const nodeSelect = document.getElementById('field-target-node'); | |
| if (isTv) { | |
| seriesSeasonsBox.classList.remove('hidden'); | |
| if (nodeSelect.value.startsWith('admin_tagalog_movies') || nodeSelect.value.startsWith('community_movies') || nodeSelect.value.startsWith('vivamax')) { | |
| nodeSelect.value = 'admin_community_tagalog_series'; | |
| } | |
| } else { | |
| seriesSeasonsBox.classList.add('hidden'); | |
| if (nodeSelect.value.startsWith('admin_community_tagalog_series') || nodeSelect.value.startsWith('community_series') || nodeSelect.value.startsWith('community_anime')) { | |
| nodeSelect.value = 'admin_tagalog_movies'; | |
| } | |
| } | |
| } | |
| function handleTargetNodeChange(node) { | |
| const isTvNode = node.includes('series') || node.includes('anime'); | |
| const typeSelect = document.getElementById('field-type'); | |
| typeSelect.value = isTvNode ? 'tv' : 'movie'; | |
| toggleMediaType(typeSelect.value); | |
| } | |
| function resetAdminForm() { | |
| document.getElementById('media-form').reset(); | |
| document.getElementById('field-existing-id').value = ''; | |
| document.getElementById('form-heading-text').innerText = 'ADD / EDIT FIREBASE MEDIA ENTRY'; | |
| document.getElementById('form-mode-badge').innerText = 'NEW ENTRY'; | |
| document.getElementById('submit-btn-text').innerText = 'Save to Firebase Database'; | |
| toggleMediaType('movie'); | |
| } | |
| function handleEditItem(btn) { | |
| try { | |
| const raw = btn.getAttribute('data-item'); | |
| if (!raw) return; | |
| const item = JSON.parse(decodeURIComponent(raw)); | |
| document.getElementById('field-existing-id').value = item.id || ''; | |
| document.getElementById('field-target-node').value = item.node || 'admin_tagalog_movies'; | |
| document.getElementById('field-type').value = item.type || 'movie'; | |
| document.getElementById('field-title').value = item.title || ''; | |
| document.getElementById('field-category').value = item.category || ''; | |
| document.getElementById('field-genre').value = item.genre || ''; | |
| document.getElementById('field-year').value = item.year || ''; | |
| document.getElementById('field-rating').value = item.rating || ''; | |
| document.getElementById('field-image').value = item.image || item.poster || ''; | |
| document.getElementById('field-trailer').value = item.trailer || ''; | |
| document.getElementById('field-description').value = item.description || ''; | |
| document.getElementById('field-streamlink').value = item.streamLink || ''; | |
| if (item.seasons) { | |
| document.getElementById('field-seasons-json').value = JSON.stringify(item.seasons, null, 2); | |
| } | |
| toggleMediaType(item.type || 'movie'); | |
| document.getElementById('form-heading-text').innerText = `EDITING "${item.title}" (#${item.id})`; | |
| document.getElementById('form-mode-badge').innerText = 'EDIT MODE'; | |
| document.getElementById('submit-btn-text').innerText = 'Update in Firebase'; | |
| document.getElementById('stream-form-container').scrollIntoView({ behavior: 'smooth' }); | |
| } catch (err) { | |
| console.error('Error preparing edit:', err); | |
| } | |
| } | |
| function loadSampleSeasonsJson() { | |
| const sample = { | |
| "Season 1": { | |
| "Ep 1": "https://short.icu/77lkzxVf5 | https://vidlink.pro/tv/65143/1/1", | |
| "Ep 2": "https://short.icu/77lkzxVf5 | https://vidlink.pro/tv/65143/1/2" | |
| } | |
| }; | |
| document.getElementById('field-seasons-json').value = JSON.stringify(sample, null, 2); | |
| } | |
| function filterInventoryList() { | |
| const textQuery = (document.getElementById('inventory-filter-input').value || '').trim().toLowerCase(); | |
| const nodeFilter = document.getElementById('inventory-node-filter').value; | |
| const cards = document.querySelectorAll('.inventory-item-card'); | |
| cards.forEach(card => { | |
| const title = card.getAttribute('data-title') || ''; | |
| const node = card.getAttribute('data-node') || ''; | |
| const genre = card.getAttribute('data-genre') || ''; | |
| const matchesText = !textQuery || title.includes(textQuery) || genre.includes(textQuery) || node.includes(textQuery); | |
| const matchesNode = nodeFilter === 'ALL' || node === nodeFilter; | |
| if (matchesText && matchesNode) { | |
| card.classList.remove('hidden'); | |
| } else { | |
| card.classList.add('hidden'); | |
| } | |
| }); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // TMDB MODAL LOGIC | |
| // --------------------------------------------------------------------------- | |
| let currentSearchResults = []; | |
| function triggerAdminSearch() { | |
| const q = document.getElementById('admin-search-input').value.trim(); | |
| openTmdbSearchModal(q); | |
| } | |
| function quickSearchModal(query) { | |
| openTmdbSearchModal(query); | |
| } | |
| function openTmdbSearchModal(initialQuery = '') { | |
| const modal = document.getElementById('tmdb-search-modal'); | |
| const modalCard = document.getElementById('tmdb-modal-card'); | |
| const searchInput = document.getElementById('modal-search-input'); | |
| modal.classList.remove('opacity-0', 'pointer-events-none'); | |
| modalCard.classList.remove('scale-95'); | |
| modalCard.classList.add('scale-100'); | |
| document.body.style.overflow = 'hidden'; | |
| if (initialQuery && initialQuery.trim()) { | |
| searchInput.value = initialQuery.trim(); | |
| executeModalSearch(); | |
| } else { | |
| searchInput.focus(); | |
| } | |
| setTimeout(() => lucide.createIcons(), 50); | |
| } | |
| function closeTmdbSearchModal() { | |
| const modal = document.getElementById('tmdb-search-modal'); | |
| const modalCard = document.getElementById('tmdb-modal-card'); | |
| modalCard.classList.remove('scale-100'); | |
| modalCard.classList.add('scale-95'); | |
| modal.classList.add('opacity-0', 'pointer-events-none'); | |
| document.body.style.overflow = ''; | |
| } | |
| function handleModalBackdropClick(event) { | |
| if (event.target === document.getElementById('tmdb-search-modal')) { | |
| closeTmdbSearchModal(); | |
| } | |
| } | |
| window.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') { | |
| const modal = document.getElementById('tmdb-search-modal'); | |
| if (!modal.classList.contains('pointer-events-none')) { | |
| closeTmdbSearchModal(); | |
| } | |
| } | |
| }); | |
| async function executeModalSearch() { | |
| const query = document.getElementById('modal-search-input').value.trim(); | |
| const resultsGrid = document.getElementById('modal-results-grid'); | |
| const loadingState = document.getElementById('modal-loading-state'); | |
| const emptyInitial = document.getElementById('modal-empty-initial'); | |
| const notFoundState = document.getElementById('modal-not-found-state'); | |
| const resultCountBadge = document.getElementById('modal-result-count'); | |
| if (!query) { | |
| resultsGrid.innerHTML = ''; | |
| emptyInitial.classList.remove('hidden'); | |
| notFoundState.classList.add('hidden'); | |
| loadingState.classList.add('hidden'); | |
| resultCountBadge.innerText = 'Ready'; | |
| return; | |
| } | |
| emptyInitial.classList.add('hidden'); | |
| notFoundState.classList.add('hidden'); | |
| loadingState.classList.remove('hidden'); | |
| resultsGrid.innerHTML = ''; | |
| resultCountBadge.innerText = 'Searching...'; | |
| try { | |
| const response = await fetch(`/api/tmdb/search?query=${encodeURIComponent(query)}`); | |
| const data = await response.json(); | |
| loadingState.classList.add('hidden'); | |
| currentSearchResults = data.results || []; | |
| resultCountBadge.innerText = `${currentSearchResults.length} Results`; | |
| if (currentSearchResults.length === 0) { | |
| notFoundState.classList.remove('hidden'); | |
| return; | |
| } | |
| renderModalSearchResults(currentSearchResults); | |
| } catch (err) { | |
| console.error('Error in TMDB search:', err); | |
| loadingState.classList.add('hidden'); | |
| notFoundState.classList.remove('hidden'); | |
| } | |
| } | |
| function renderModalSearchResults(items) { | |
| const grid = document.getElementById('modal-results-grid'); | |
| grid.innerHTML = ''; | |
| items.forEach((item, index) => { | |
| const isTv = item.media_type === 'tv'; | |
| const posterSrc = item.poster_url || (item.poster_path ? `https://image.tmdb.org/t/p/w500${item.poster_path}` : ''); | |
| const yearStr = item.year ? item.year : ''; | |
| const titleEscaped = (item.title || item.name || '').replace(/"/g, '"'); | |
| const overviewSnippet = item.overview ? (item.overview.length > 80 ? item.overview.substring(0, 80) + '...' : item.overview) : 'No description.'; | |
| const card = document.createElement('div'); | |
| card.className = 'bento-card p-3 flex gap-3 cursor-pointer hover:border-rose-500 hover:bg-[#161616] transition group relative'; | |
| card.onclick = () => selectTmdbResult(index); | |
| card.innerHTML = ` | |
| <div class="w-16 h-24 rounded-lg bg-[#1a1a1a] flex-shrink-0 overflow-hidden relative border border-[#2b2b2b]"> | |
| ${posterSrc ? ` | |
| <img src="${posterSrc}" alt="${titleEscaped}" loading="lazy" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"> | |
| ` : ` | |
| <div class="w-full h-full flex items-center justify-center text-zinc-600 text-[10px]">No Poster</div> | |
| `} | |
| </div> | |
| <div class="flex-1 flex flex-col justify-between min-w-0"> | |
| <div> | |
| <div class="flex items-center gap-1.5 mb-1"> | |
| <span class="bento-badge font-mono text-[9px] ${isTv ? 'text-blue-400 border-blue-500/30' : 'text-rose-400 border-rose-500/30'}"> | |
| ${isTv ? '📺 TV SERIES' : '🎬 MOVIE'} | |
| </span> | |
| ${yearStr ? `<span class="text-[10px] text-zinc-400 font-mono">(${yearStr})</span>` : ''} | |
| </div> | |
| <h3 class="text-xs font-bold text-white group-hover:text-rose-400 transition truncate" title="${titleEscaped}"> | |
| ${item.title || item.name} | |
| </h3> | |
| <p class="text-[11px] text-zinc-400 line-clamp-2 mt-1 leading-tight"> | |
| ${overviewSnippet} | |
| </p> | |
| </div> | |
| <div class="pt-2 border-t border-[#222] flex items-center justify-between text-[10px]"> | |
| <span class="text-amber-400">⭐ ${item.vote_average || '7.5'}</span> | |
| <span class="text-rose-400 font-semibold">Auto-Fill Form →</span> | |
| </div> | |
| </div> | |
| `; | |
| grid.appendChild(card); | |
| }); | |
| lucide.createIcons(); | |
| } | |
| function selectTmdbResult(index) { | |
| const item = currentSearchResults[index]; | |
| if (!item) return; | |
| const isTv = item.media_type === 'tv'; | |
| const yearStr = item.year ? ` (${item.year})` : ''; | |
| const formattedTitle = `${item.title || item.name || 'Media'}${yearStr}`; | |
| const posterUrl = item.poster_url || (item.poster_path ? `https://image.tmdb.org/t/p/w500${item.poster_path}` : ''); | |
| document.getElementById('field-title').value = formattedTitle; | |
| document.getElementById('field-type').value = isTv ? 'tv' : 'movie'; | |
| document.getElementById('field-year').value = item.year || ''; | |
| document.getElementById('field-rating').value = item.vote_average || '8.0'; | |
| document.getElementById('field-image').value = posterUrl; | |
| document.getElementById('field-description').value = item.overview || ''; | |
| toggleMediaType(isTv ? 'tv' : 'movie'); | |
| closeTmdbSearchModal(); | |
| const streamInput = document.getElementById('field-streamlink'); | |
| if (streamInput) { | |
| streamInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| setTimeout(() => { | |
| streamInput.focus(); | |
| streamInput.classList.add('ring-2', 'ring-rose-500'); | |
| setTimeout(() => streamInput.classList.remove('ring-2', 'ring-rose-500'), 1500); | |
| }, 300); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |