Full overhaul of the repo please see changelog

This commit is contained in:
Inside4ndroid
2025-09-16 18:54:12 +01:00
parent 2119398460
commit e0ad35d2b1
115 changed files with 26066 additions and 6426 deletions
+209
View File
@@ -0,0 +1,209 @@
<!DOCTYPE html><html lang="en"><head>
<meta charset="utf-8" />
<title>TMDB Embed Admin</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="stylesheet" href="/style.css" />
<script defer src="/index.js"></script>
</head>
<body>
<button class="burger" aria-label="Toggle navigation">☰</button>
<div class="overlay"></div>
<aside class="sidebar" id="sidebar">
<div class="brand">TMDB Embed API</div>
<nav class="nav">
<button class="nav-item" id="logoutBtn" data-action="logout">Logout</button>
<button class="nav-item" data-target="panel-status">Server Status</button>
<button class="nav-item" data-target="panel-core">Core</button>
<button class="nav-item" data-target="panel-quality">Quality / Filters</button>
<button class="nav-item" data-target="panel-keys">Keys</button>
<button class="nav-item" data-target="panel-febbox">FebBox / PStream</button>
<button class="nav-item" data-target="panel-advanced">Advanced</button>
<button class="nav-item" data-target="panel-live">Live Config</button>
<div class="grow"></div>
</nav>
<footer class="side-footer">
<a class="hint" style="margin-top: .5rem; color: var(--primary-fg);" href="https://github.com/Inside4ndroid" target="_blank">Github</a>
<a class="hint" style="margin-top: .5rem; color: var(--primary-fg);" href="https://x.com/Inside_4ndroid" target="_blank">X (Twitter)</a>
</footer>
<footer class="side-footer">Credits to :<a class="hint" style="margin-top: .5rem; color: var(--primary-fg);" href="https://github.com/tapframe/NuvioStreamsAddon" target="_blank">NuvioStreamsAddon for Stremio</a></footer>
<div class="bmc-widget-container">
<script type="text/javascript" src="https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js" data-name="bmc-button" data-slug="K3QSoR2" data-color="#5F7FFF" data-emoji="☕" data-font="Bree" data-text="Buy me a coffee" data-outline-color="#000000" data-font-color="#ffffff" data-coffee-color="#FFDD00" ></script>
</div>
<footer class="side-footer">Made with ❤️ by Inside4ndroid</footer>
</aside>
<main class="main">
<div id="bootWarn" class="alert warning" style="display:none;">No FEBBOX_COOKIES configured. Showbox / PStream streams may be unavailable.</div>
<div id="cacheDisabledBanner" class="alert info" style="display:none;">Cache is <strong>DISABLED</strong>; expect slower responses & higher source load.</div>
<div id="tmdbMissingBanner" class="alert warning" style="display:none;">No TMDB API Keys configured. Metadata-dependent providers (Showbox, 4khdhub, MoviesMod, MP4Hydra) may degrade.</div>
<form id="cfgForm" onsubmit="return false;" class="panels">
<section id="panel-status" class="panel">
<h1>Server Status<button style="margin-left: 1rem;" id="refreshStatusBtn" type="button">Refresh Status</button></h1>
<div class="status-grid">
<div class="status-block">
<h2>Metrics</h2>
<pre id="statusMetrics" class="status-pre">loading...</pre>
</div>
<div class="status-block">
<h2>Providers</h2>
<button style="margin-bottom: 1rem;" id="runProviderChecksBtn" type="button">Run Provider Functional Checks</button>
<span style="margin-left: .5rem; margin-bottom: 1rem;" id="providerCheckSummary" class="hint"></span>
<div id="statusProvidersEnhanced" class="providers-table" aria-live="polite"></div>
</div>
<div class="status-block wide">
<h2>Endpoints</h2>
<pre id="statusEndpoints" class="status-pre">loading...</pre>
</div>
</div>
</section>
<section id="panel-core" class="panel">
<h1>Core Settings</h1>
<div class="field"><label>API Port<p class="hint">Provider enable/disable & feature flags apply immediately. Click toggles to enable (highlighted) or disable (dim).</p><input type="text" name="port" placeholder="8787" /></label></div>
<div class="field"><label>Default Region<p class="hint">This is best left set to auto/none.</p>
<select name="defaultRegion">
<option value="">(auto / none)</option>
<option value="USA7">US East</option>
<option value="USA6">US West</option>
<option value="USA5">US Middle</option>
<option value="UK3">United Kingdom</option>
<option value="CA1">Canada</option>
<option value="FR1">France</option>
<option value="DE2">Germany</option>
<option value="HK1">Hong Kong</option>
<option value="IN1">India</option>
<option value="AU1">Australia</option>
<option value="SZ">China</option>
</select>
</label></div>
<div class="field"><label>Providers (Enable + Default)<p class="hint">Click power icon to enable/disable, star to mark as default. If no defaults selected all enabled providers aggregate.</p>
<div id="providerMatrix" class="provider-matrix" aria-live="polite"></div>
<input type="hidden" name="defaultProviders" />
</label>
</div>
</section>
<section id="panel-quality" class="panel">
<h1>Quality & Filtering</h1>
<div class="field quality-group"><label>Minimum Quality<p class="hint">Preset radios serialize to a simple string. Custom & advanced modes use the JSON fields.</p>
<div class="quality-presets" id="qualityPresets">
<label class="q-item"><input type="radio" name="qualityPreset" value="all"> <span>All</span></label>
<label class="q-item"><input type="radio" name="qualityPreset" value="480p"> <span>480p+</span></label>
<label class="q-item"><input type="radio" name="qualityPreset" value="720p"> <span>720p+</span></label>
<label class="q-item"><input type="radio" name="qualityPreset" value="1080p"> <span>1080p+</span></label>
<label class="q-item"><input type="radio" name="qualityPreset" value="1440p"> <span>1440p+</span></label>
<label class="q-item"><input type="radio" name="qualityPreset" value="2160p"> <span>4K+</span></label>
<label class="q-item adv"><input type="radio" name="qualityPreset" value="custom"> <span>Custom JSON</span></label>
</div>
</label>
<textarea name="minQualitiesRaw" class="mini quality-json" placeholder='{"default":"900p"}' disabled></textarea>
</div>
<div class="field codec-group"><label>Exclude Codecs<p class="hint">Preset radios serialize to a simple string. Custom & advanced modes use the JSON fields.</p>
<div class="codec-presets" id="codecPresets">
<label class="c-item"><input type="radio" name="codecPreset" value="all"> <span>All</span></label>
<label class="c-item"><input type="radio" name="codecPreset" value="dv"> <span>Dolby Vision (DV)</span></label>
<label class="c-item"><input type="radio" name="codecPreset" value="hdr"> <span>HDR / HDR10(+)</span></label>
<label class="c-item adv"><input type="radio" name="codecPreset" value="advanced"> <span>Advanced JSON</span></label>
</div>
</label>
<textarea name="excludeCodecsRaw" class="mini json-adv" placeholder='{"excludeDV":true,"excludeHDR":false}' disabled></textarea>
</div>
</section>
<section id="panel-keys" class="panel">
<h1>API Keys</h1>
<div class="field"><label>TMDB API Keys<p class="hint">Add multiple TMDB API keys. They rotate randomly per request to spread usage.</p>
<div class="key-add-row">
<input id="tmdbKeyInput" type="text" placeholder="Enter TMDB key" autocomplete="off" />
<button id="addTmdbKeyBtn" type="button" class="small">Add Key</button>
</div>
<div id="tmdbKeyList" class="key-list" aria-live="polite"></div>
<input type="hidden" name="tmdbApiKeysHidden" />
</label></div>
</section>
<section id="panel-febbox" class="panel">
<h1>FebBox / PStream</h1>
<p class="hint">ShowBox fetches content from a cloud storage provider called FebBox. Each FebBox user gets 100GB/month of fast streaming before speeds are reduced.</p>
<a class="hint" style="margin-bottom: 1rem;" href="https://febbox.com">Febbox Website</a>
<div class="field"><label>FebBox Cookies<p class="hint">Add multiple FebBox ui tokens. They will rotate / be evaluated. Removal updates overrides after saving.</p>
<div class="cookie-add-row">
<input id="febboxCookieInput" type="text" placeholder="Enter ui token (without ui=)" autocomplete="off" />
<button id="addFebboxCookieBtn" type="button" class="small">Add Cookie</button>
</div>
<div id="febboxCookieList" class="cookie-list" aria-live="polite"></div>
<input type="hidden" name="febboxCookiesHidden" />
</label></div>
<div class="field">
<label><strong>How to find your cookie</strong></label>
</div>
<ol class="hint">
<li>
<strong>Open Febbox.com</strong><br>
Go to https://febbox.com in your web browser.
</li>
<li>
<strong>Log in with Google</strong><br>
Use a Google account to log in to the Febbox site.<br>
<em>Tip:</em> For safety, it’s recommended to use a fresh account.
</li>
<li>
<strong>Open Developer Tools</strong><br>
Press the <kbd>F12</kbd> key on your keyboard.<br>
Alternatively, right-click anywhere on the page and select
<code>Inspect</code> or <code>Inspect Element</code>.
</li>
<li>
<strong>Navigate to Cookies</strong><br>
In the Developer Tools panel, go to the <code>Application</code> tab.<br>
Expand the <code>Storage</code> section and click on <code>Cookies</code>.
</li>
<li>
<strong>Find the "ui" Cookie</strong><br>
In the list of cookies, look for the cookie with the name <code>ui</code>.
</li>
<li>
<strong>Copy the value</strong><br>
Copy the cookie value and paste it above then click add cookie.
</li>
</ol>
</section>
<section id="panel-advanced" class="panel">
<h1>Advanced & Providers</h1>
<p class="hint">These settings allow you to customize the behavior of the api and will effect timings.</p>
<div class="field adv-flag-grid" id="advFlags" aria-label="Core Feature Flags">
<label class="adv-flag-item"><input type="checkbox" name="adv_disableCache" value="1" /> <span>Disable Cache</span></label>
<label class="adv-flag-item"><input type="checkbox" name="adv_showboxRotatingProxy" value="1" /> <span>Showbox Rotating Proxy</span></label>
<label class="adv-flag-item"><input type="checkbox" name="adv_disableUrlValidation" value="1" /> <span>Disable URL Validation</span></label>
<label class="adv-flag-item"><input type="checkbox" name="adv_disable4khdhubValidation" value="1" /> <span>Disable 4KHDHub Validation</span></label>
</div>
<h1>Proxy / Paths</h1>
<p class="hint">Provider enable/disable & feature flags apply immediately. Click toggles to enable (highlighted) or disable (dim).</p>
<div class="adv-group">
<div class="field small"><label>Showbox Cache Dir <input type="text" name="adv_showboxCacheDir" placeholder="/data/showbox-cache"/></label></div>
<div class="field small"><label>Showbox Proxy (Primary) <input type="text" name="adv_showboxProxyPrimary" placeholder="https://proxy.example/"/></label></div>
<div class="field small"><label>Showbox Proxy (Alternate) <input type="text" name="adv_showboxProxyAlternate" placeholder="https://proxy2.example/"/></label></div>
<div class="field small"><label>Xprime Proxy URL <input type="text" name="adv_xprimeProxyUrl" placeholder="https://..."/></label></div>
<div class="field small"><label>VidZee Proxy URL <input type="text" name="adv_vidzeeProxyUrl" placeholder="https://..."/></label></div>
<div class="field small"><label>VidSrc Proxy URL <input type="text" name="adv_vidsrcProxyUrl" placeholder="https://..."/></label></div>
<div class="field small"><label>MoviesMod Proxy URL <input type="text" name="adv_moviesmodProxyUrl" placeholder="https://..."/></label></div>
</div>
</section>
<section id="panel-live" class="panel">
<h1>Live Configuration</h1>
<h2>Merged</h2>
<p class="hint">This section shows the currently active configuration, including all merged settings from various sources.</p>
<pre id="currentCfg">loading...</pre>
<h2>Overrides (user-config.json)</h2>
<p class="hint">These are the user-defined settings from this panel that override the default configuration.</p>
<pre id="overrideCfg">(none)</pre>
</section>
</form>
<div class="actions-bar">
<button id="saveBtn" type="button" class="primary" disabled>Save Overrides</button>
<button id="reloadBtn" type="button">Reload</button>
<button id="clearBtn" type="button" class="danger">Clear All</button>
<span id="status" class="status"></span>
<span id="validationSummary" class="validation-summary"></span>
</div>
</main>
</body></html>
+65
View File
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TMDB Embed Login</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="stylesheet" href="/style.css" />
<style>
body { display:flex; align-items:center; justify-content:center; min-height:100vh; background:#0f141a; font-family: system-ui, sans-serif; }
.login-card { background:#1e1e24; padding:2rem 2.25rem; border-radius:12px; width:100%; max-width:380px; box-shadow:0 4px 24px -4px rgba(0,0,0,.6); }
.login-card h1 { margin:0 0 1rem; font-size:1.3rem; color: white; text-align: center;}
.pw-change { display:none; background:#1e1e24; padding:2rem 2.25rem; border-radius:12px; width:100%; max-width:380px; box-shadow:0 4px 24px -4px rgba(0,0,0,.6); }
.pw-change h2 { margin:0 0 1rem; font-size:1.3rem; color: white; text-align: center;}
.field { display:flex; flex-direction:column; gap:.35rem; margin-bottom:.9rem; }
.field label { font-size:.85rem; text-transform:uppercase; letter-spacing:.08em; color:#bbb; text-align: center;}
.field input { padding:.65rem .75rem !important; border:1px solid #333 !important; color:#bbb !important; border-radius:6px !important; background:#111 !important; font-size:.95rem !important; }
.actions { display:block; gap:.75rem; align-items:center; text-align: center;}
button.primary { background:#2563eb; color:#fff; padding:.65rem 1rem; border:none; border-radius:6px; cursor:pointer; font-weight:600; font-size:.95rem; }
button.primary:disabled { opacity:.55; cursor:default; }
.status { min-height:1.1rem; font-size:.8rem; color:#eee; }
.error { color:#f87171; }
.success { color:#4ade80; }
.meta { margin-top:1.25rem; font-size:.7rem; line-height:1.2; color:#666; text-align:center; }
.link-btn { background:none; border:none; color:#60a5fa; cursor:pointer; font-size:.75rem; padding:0; }
.expiry-text { margin-top:.5rem; margin-bottom: .5rem; font-weight: 500; color: yellow;}
</style>
<script defer src="/login.js"></script>
</head>
<body>
<div class="login-card" id="loginCard">
<div id="lockoutBanner" style="display:none; background:#7f1d1d; color:#fca5a5; padding:.6rem .75rem; border-radius:8px; font-size:.8rem; line-height:1.2; margin-bottom:1rem;">
<strong>Too Many Attempts.</strong> Try again in <span id="lockoutCountdown">00:00</span>.
</div>
<h1>TMDB Embed Admin</h1>
<form id="loginForm" autocomplete="off">
<div class="field">
<label for="username">Username</label>
<input id="username" name="username" type="text" required autofocus />
</div>
<div class="field">
<label for="password">Password</label>
<input id="password" name="password" type="password" required />
</div>
<div class="actions">
<button id="loginBtn" class="primary" type="submit">Login</button>
<div class="status" id="status" aria-live="polite"></div>
</div>
</form>
<div class="pw-change" id="pwChange">
<h2>Change Password</h2>
<form id="pwForm" autocomplete="off">
<div class="field"><label for="oldPw">Old Password</label><input id="oldPw" type="password" required /></div>
<div class="field"><label for="newPw">New Password</label><input id="newPw" type="password" minlength="8" required /></div>
<div class="actions"><button id="pwBtn" class="primary" type="submit">Update</button><div class="status" id="pwStatus" aria-live="polite"></div></div>
</form>
</div>
<div class="meta">
<button id="togglePw" class="link-btn" type="button">Change Password</button>
<div class="expiry-text">Sessions expire after 12h or on server restart.</div>
<p class="login-hint">Default user created if none exists: admin / change-me (change it!).</p>
</div>
</div>
</body>
</html>
+573
View File
@@ -0,0 +1,573 @@
// Admin panel logic (refactored into IIFE, no inline handlers)
(function(){
async function fetchConfig(){
const r = await fetch('/api/config');
if(!r.ok) throw new Error('Failed to fetch config');
return r.json();
}
async function fetchProviders(){
const r = await fetch('/api/providers');
if(!r.ok) throw new Error('Failed to fetch providers');
return r.json();
}
function setFebboxCookies(cookiesArray){
const listEl = document.getElementById('febboxCookieList');
const hidden = document.querySelector('input[name="febboxCookiesHidden"]');
if(!listEl || !hidden) return;
const arr = Array.isArray(cookiesArray)? cookiesArray.filter(c=>c && typeof c==='string') : [];
hidden.value = arr.join(',');
listEl.innerHTML = arr.length? arr.map((c,i)=>`<div class="cookie-item" data-idx="${i}"><code>${c}</code><button type="button" class="remove" title="Remove" data-action="rm" data-idx="${i}">✕</button></div>`).join('') : '<div class="empty">No cookies added</div>';
}
function addFebboxCookie(){
const input = document.getElementById('febboxCookieInput');
const val = (input.value||'').trim();
if(!val) return;
const hidden = document.querySelector('input[name="febboxCookiesHidden"]');
const existing = hidden.value? hidden.value.split(',').filter(Boolean):[];
if(existing.includes(val)) { input.value=''; return; }
existing.push(val);
setFebboxCookies(existing);
input.value='';
validate();
}
function removeFebboxCookie(idx){
const hidden = document.querySelector('input[name="febboxCookiesHidden"]');
const existing = hidden.value? hidden.value.split(',').filter(Boolean):[];
const next = existing.filter((_,i)=> i!=idx);
setFebboxCookies(next);
validate();
}
document.addEventListener('click', (e)=>{
const btn = e.target.closest('#addFebboxCookieBtn');
if(btn){ addFebboxCookie(); }
const rm = e.target.closest('.cookie-item button.remove');
if(rm){ const idx = rm.dataset.idx; removeFebboxCookie(idx); }
});
document.addEventListener('keydown',(e)=>{ if(e.key==='Enter' && e.target.id==='febboxCookieInput'){ e.preventDefault(); addFebboxCookie(); }});
// TMDB multi-key management
function setTmdbKeys(keys){
const listEl = document.getElementById('tmdbKeyList');
const hidden = document.querySelector('input[name="tmdbApiKeysHidden"]');
if(!listEl || !hidden) return;
const arr = Array.isArray(keys)? keys.filter(k=>k && typeof k==='string') : [];
hidden.value = arr.join(',');
listEl.innerHTML = arr.length? arr.map((k,i)=>`<div class="key-item" data-idx="${i}"><code>${k}</code><button type="button" class="remove" data-action="rm-tmdb" data-idx="${i}" title="Remove">✕</button></div>`).join('') : '<div class="empty">No TMDB keys</div>';
}
function addTmdbKey(){
const input = document.getElementById('tmdbKeyInput');
const val = (input.value||'').trim();
if(!val) return;
const hidden = document.querySelector('input[name="tmdbApiKeysHidden"]');
const existing = hidden.value? hidden.value.split(',').filter(Boolean):[];
if(existing.includes(val)) { input.value=''; return; }
existing.push(val);
setTmdbKeys(existing);
input.value='';
validate();
}
function removeTmdbKey(idx){
const hidden = document.querySelector('input[name="tmdbApiKeysHidden"]');
const existing = hidden.value? hidden.value.split(',').filter(Boolean):[];
setTmdbKeys(existing.filter((_,i)=> i!=idx));
validate();
}
document.addEventListener('click',(e)=>{
if(e.target.closest('#addTmdbKeyBtn')) addTmdbKey();
const rm = e.target.closest('.key-item button.remove');
if(rm){ removeTmdbKey(rm.dataset.idx); }
});
document.addEventListener('keydown',(e)=>{ if(e.key==='Enter' && e.target.id==='tmdbKeyInput'){ e.preventDefault(); addTmdbKey(); }});
function fillForm(data){
const merged = data.merged || {};
const override = data.override || {};
const f = document.getElementById('cfgForm');
const fields = ['port'];
fields.forEach(k=>{ if(!f.elements[k]) return; const ov = override[k]; f.elements[k].value = ov !== undefined ? ov : (merged[k]||''); });
if (f.elements['defaultRegion']) {
const val = override.defaultRegion !== undefined ? override.defaultRegion : (merged.defaultRegion || '');
f.elements['defaultRegion'].value = val || '';
}
f.elements['defaultProviders'].value = (override.defaultProviders || merged.defaultProviders || []).join(',');
(function(){
let cookiesVal = override.febboxCookies || merged.febboxCookies || [];
if(typeof cookiesVal === 'string') {
const trimmed = cookiesVal.trim();
if(trimmed.startsWith('[') && trimmed.endsWith(']')) { try { const arr = JSON.parse(trimmed); if(Array.isArray(arr)) cookiesVal = arr; } catch{} }
else if (trimmed.includes(',') || trimmed.includes('\n') || trimmed.includes(';')) {
cookiesVal = trimmed.split(/[\n;,]+/).map(s=>s.trim()).filter(Boolean);
} else if(trimmed) cookiesVal = [trimmed]; else cookiesVal = [];
}
if(!Array.isArray(cookiesVal)) cookiesVal = [];
setFebboxCookies(cookiesVal);
})();
(function(){
let keys = override.tmdbApiKeys || merged.tmdbApiKeys || [];
if(!Array.isArray(keys)) {
if (override.tmdbApiKey) keys = [override.tmdbApiKey];
else if (merged.tmdbApiKey) keys = [merged.tmdbApiKey];
else keys = [];
}
setTmdbKeys(keys);
})();
const mqRaw = override.minQualitiesRaw !== undefined ? override.minQualitiesRaw : (merged.minQualitiesRaw || '');
const presetValues = ['all','480p','720p','1080p','1440p','2160p'];
let matchedPreset = presetValues.includes((mqRaw||'').toLowerCase()) ? mqRaw.toLowerCase() : (mqRaw? 'custom':'all');
const presetInput = document.querySelector(`input[name="qualityPreset"][value="${matchedPreset}"]`) || document.querySelector('input[name="qualityPreset"][value="all"]');
if (presetInput) presetInput.checked = true;
const customField = document.querySelector('textarea[name="minQualitiesRaw"]');
if (customField) {
if (matchedPreset === 'custom') { customField.disabled = false; customField.value = mqRaw; }
else { customField.disabled = true; customField.value = ''; }
}
const excRaw = override.excludeCodecsRaw !== undefined ? override.excludeCodecsRaw : (merged.excludeCodecsRaw || '');
let parsed = null; try { if(excRaw && /[\{\[]/.test(excRaw)) parsed = JSON.parse(excRaw); } catch {}
let mode = 'all';
if(parsed){
const dv = parsed.excludeDV === true;
const hdr = parsed.excludeHDR === true;
if(dv && !hdr) mode = 'dv';
else if(hdr && !dv) mode = 'hdr';
else if(dv && hdr) mode = 'advanced';
else mode = 'advanced';
} else if(excRaw){
if(/excludeDV":true/.test(excRaw) && !/excludeHDR":true/.test(excRaw)) mode='dv';
else if(/excludeHDR":true/.test(excRaw) && !/excludeDV":true/.test(excRaw)) mode='hdr';
else mode='advanced';
}
const presetInputC = document.querySelector(`input[name="codecPreset"][value="${mode}"]`) || document.querySelector('input[name="codecPreset"][value="all"]');
if(presetInputC) presetInputC.checked = true;
const codecTa = document.querySelector('textarea[name="excludeCodecsRaw"]');
if(mode==='advanced') { codecTa.disabled=false; codecTa.value = excRaw || JSON.stringify({excludeDV:false,excludeHDR:false}); }
else { codecTa.disabled=true; codecTa.value=''; }
document.getElementById('currentCfg').textContent = JSON.stringify(merged, null, 2);
document.getElementById('overrideCfg').textContent = Object.keys(override).length? JSON.stringify(override,null,2): '(none)';
document.getElementById('bootWarn').style.display = (!merged.febboxCookies || merged.febboxCookies.length===0)? 'block':'none';
// Removed version & overrides pills from UI; logic deleted
const cacheBanner = document.getElementById('cacheDisabledBanner'); if(cacheBanner) cacheBanner.style.display = merged.disableCache? 'block':'none';
const tmdbBanner = document.getElementById('tmdbMissingBanner');
if(tmdbBanner){
const tmdbMissing = !((override.tmdbApiKeys && override.tmdbApiKeys.length) || (merged.tmdbApiKeys && merged.tmdbApiKeys.length) || override.tmdbApiKey || merged.tmdbApiKey);
const tmdbProvidersEnabled = [
merged.enableShowboxProvider !== false,
merged.enable4khdhubProvider !== false,
merged.enableMoviesmodProvider !== false,
merged.enableMp4hydraProvider !== false
].some(Boolean);
tmdbBanner.style.display = (tmdbMissing && tmdbProvidersEnabled)? 'block':'none';
}
const advBools = [
['enableXprimeProvider','adv_enableXprime'],
['enable4khdhubProvider','adv_enable4khdhub'],
['enableMoviesmodProvider','adv_enableMoviesmod'],
['enableMp4hydraProvider','adv_enableMp4hydra'],
['enableVidzeeProvider','adv_enableVidzee'],
['enableVixsrcProvider','adv_enableVixsrc'],
['disableCache','adv_disableCache'],
['enablePStreamApi','adv_enablePStream'],
['showboxUseRotatingProxy','adv_showboxRotatingProxy'],
['disableUrlValidation','adv_disableUrlValidation'],
['disable4khdhubUrlValidation','adv_disable4khdhubValidation']
];
advBools.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const src = (override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]); if(el.type==='checkbox'){ el.checked = !!src; } else { el.value = src? 'true':''; } });
const advTexts = [
['showboxCacheDir','adv_showboxCacheDir'],
['showboxProxyPrimary','adv_showboxProxyPrimary'],
['showboxProxyAlternate','adv_showboxProxyAlternate'],
['xprimeProxyUrl','adv_xprimeProxyUrl'],
['vidzeeProxyUrl','adv_vidzeeProxyUrl'],
['vidsrcProxyUrl','adv_vidsrcProxyUrl'],
['moviesmodProxyUrl','adv_moviesmodProxyUrl']
];
advTexts.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const val = override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]; el.value = val || ''; });
// Sync checkbox style advanced flags
const advFlagMap = {
adv_disableCache: 'disableCache',
adv_showboxRotatingProxy: 'showboxUseRotatingProxy',
adv_disableUrlValidation: 'disableUrlValidation',
adv_disable4khdhubValidation: 'disable4khdhubUrlValidation'
};
Object.entries(advFlagMap).forEach(([inputName, cfgKey]) => {
const el = f.elements[inputName];
if(!el) return;
const val = override[cfgKey] !== undefined ? override[cfgKey] : merged[cfgKey];
el.checked = !!val;
});
}
async function reload(){
try {
const data = await fetchConfig();
fillForm(data);
renderProviderMatrix(data.merged);
validate();
setStatus('');
} catch(e){ setStatus('Load failed: '+e.message,true); }
}
function setStatus(msg, isErr){
const el = document.getElementById('status');
el.textContent = msg;
el.style.color = isErr? '#dc2626':'inherit';
}
async function save(){
const f = document.getElementById('cfgForm');
const payload = {};
const dp = f.elements['defaultProviders'].value.trim();
if (dp) payload.defaultProviders = dp.split(/[\s,]+/).filter(Boolean);
['port'].forEach(k=>{
if(!f.elements[k]) return;
const v = f.elements[k].value.trim();
payload[k] = v? v : null;
});
const selPreset = document.querySelector('input[name="qualityPreset"]:checked');
if (selPreset) {
if (selPreset.value === 'custom') {
const cv = f.elements['minQualitiesRaw'].value.trim();
if (cv) payload.minQualitiesRaw = cv; else payload.minQualitiesRaw = null;
} else {
payload.minQualitiesRaw = selPreset.value === 'all' ? 'all' : selPreset.value;
}
}
const cPreset = document.querySelector('input[name="codecPreset"]:checked');
if(cPreset){
if(cPreset.value==='all') payload.excludeCodecsRaw = null;
else if(cPreset.value==='dv') payload.excludeCodecsRaw = JSON.stringify({excludeDV:true,excludeHDR:false});
else if(cPreset.value==='hdr') payload.excludeCodecsRaw = JSON.stringify({excludeDV:false,excludeHDR:true});
else if(cPreset.value==='advanced') {
const raw = document.querySelector('textarea[name="excludeCodecsRaw"]').value.trim();
payload.excludeCodecsRaw = raw || null;
}
}
const drSel = f.elements['defaultRegion'];
if (drSel) payload.defaultRegion = drSel.value ? drSel.value : null;
const cookiesHidden = f.elements['febboxCookiesHidden'];
if(cookiesHidden){
const cookiesVal = cookiesHidden.value.trim();
payload.febboxCookies = cookiesVal? cookiesVal.split(',').filter(Boolean) : [];
}
const boolMap = {
adv_enableXprime:'enableXprimeProvider',
adv_enable4khdhub:'enable4khdhubProvider',
adv_enableMoviesmod:'enableMoviesmodProvider',
adv_enableMp4hydra:'enableMp4hydraProvider',
adv_enableVidzee:'enableVidzeeProvider',
adv_enableVixsrc:'enableVixsrcProvider',
adv_disableCache:'disableCache',
adv_enablePStream:'enablePStreamApi',
adv_showboxRotatingProxy:'showboxUseRotatingProxy',
adv_disableUrlValidation:'disableUrlValidation',
adv_disable4khdhubValidation:'disable4khdhubUrlValidation'
};
Object.entries(boolMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(el) payload[cfgKey] = !!el.checked; });
const textMap = {
adv_showboxCacheDir:'showboxCacheDir',
adv_showboxProxyPrimary:'showboxProxyPrimary',
adv_showboxProxyAlternate:'showboxProxyAlternate',
adv_xprimeProxyUrl:'xprimeProxyUrl',
adv_vidzeeProxyUrl:'vidzeeProxyUrl',
adv_vidsrcProxyUrl:'vidsrcProxyUrl',
adv_moviesmodProxyUrl:'moviesmodProxyUrl'
};
Object.entries(textMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(!el) return; const v = el.value.trim(); payload[cfgKey] = v? v : null; });
const tmdbHidden = f.elements['tmdbApiKeysHidden'];
if (tmdbHidden){
const raw = tmdbHidden.value.trim();
payload.tmdbApiKeys = raw? raw.split(',').filter(Boolean) : [];
payload.tmdbApiKey = null;
}
try {
const r = await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const js = await r.json();
if(!js.success) throw new Error(js.error||'Save failed');
setStatus('Saved ✔');
fillForm(js);
} catch(e){ setStatus(e.message,true); }
}
async function clearAll(){
if(!confirm('Remove ALL overrides?')) return;
const payload = { port:null, defaultRegion:null, defaultProviders:[], minQualitiesRaw:null, excludeCodecsRaw:null, febboxCookies:null,
enableXprimeProvider:null, enable4khdhubProvider:null, enableMoviesmodProvider:null, enableMp4hydraProvider:null, enableVidzeeProvider:null, enableVixsrcProvider:null,
disableCache:null, enablePStreamApi:null, showboxUseRotatingProxy:null, disableUrlValidation:null, disable4khdhubUrlValidation:null,
showboxCacheDir:null, showboxProxyPrimary:null, showboxProxyAlternate:null, xprimeProxyUrl:null, vidzeeProxyUrl:null, vidsrcProxyUrl:null, moviesmodProxyUrl:null };
try {
const r = await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const js = await r.json();
if(!js.success) throw new Error(js.error||'Clear failed');
setStatus('Overrides cleared');
fillForm(js);
} catch(e){ setStatus(e.message,true); }
}
function setTheme(t){
document.documentElement.dataset.theme = t;
localStorage.setItem('ns_theme', t);
}
function initTheme(){
const saved = localStorage.getItem('ns_theme');
setTheme(saved || 'dark');
}
function toggleTheme(){
setTheme(document.documentElement.dataset.theme === 'dark' ? 'light':'dark');
}
function navTo(id){
document.querySelectorAll('.panel').forEach(p=>p.classList.toggle('active', p.id===id));
document.querySelectorAll('.nav-item').forEach(a=>a.classList.toggle('active', a.dataset.target===id));
if(history.replaceState) history.replaceState(null,'', '#'+id);
if(id==='panel-status') loadServerStatus();
}
function attachCoreListeners(){
const saveBtn = document.getElementById('saveBtn');
const reloadBtn = document.getElementById('reloadBtn');
const clearBtn = document.getElementById('clearBtn');
if(saveBtn) saveBtn.addEventListener('click', save);
if(reloadBtn) reloadBtn.addEventListener('click', reload);
if(clearBtn) clearBtn.addEventListener('click', clearAll);
}
function bindNavigation(){
document.querySelectorAll('.nav .nav-item[data-target]').forEach(btn=>{
btn.addEventListener('click', ()=> navTo(btn.dataset.target));
});
const themeBtn = document.querySelector('.nav .theme-toggle');
if(themeBtn) themeBtn.addEventListener('click', toggleTheme);
}
function bindShellControls(){
const burger = document.querySelector('.burger');
if(burger) burger.addEventListener('click', ()=> toggleSidebar());
const overlay = document.querySelector('.overlay');
if(overlay) overlay.addEventListener('click', ()=> toggleSidebar(false));
}
async function verifySession(){
try {
const r = await fetch('/auth/session',{cache:'no-store'});
const j = await r.json();
if(!j.authenticated){ location.replace('/?expired=1'); }
} catch { location.replace('/?expired=1'); }
}
async function loadServerStatus(){
const metricsEl = document.getElementById('statusMetrics');
const endpointsEl = document.getElementById('statusEndpoints');
const provEnhanced = document.getElementById('statusProvidersEnhanced');
if(!metricsEl) return;
metricsEl.textContent = 'Loading...';
if(endpointsEl) endpointsEl.textContent = 'Loading...';
if(provEnhanced) provEnhanced.innerHTML = '<div class="prov-row">Loading...</div>';
try {
const r = await fetch('/api/status');
if(!r.ok) throw new Error('Status fetch failed');
const j = await r.json();
if(!j.success) throw new Error('Status error');
metricsEl.textContent = JSON.stringify(j.metrics, null, 2);
if(endpointsEl) endpointsEl.textContent = (j.endpoints||[]).join('\n');
if(provEnhanced){
provEnhanced.innerHTML = '';
const header = document.createElement('div');
header.className='prov-row header';
header.innerHTML = '<span>Name</span><span>Enabled</span><span>Cookie Req</span><span>Cookie OK</span><span>Functional</span><span>Notes</span>';
provEnhanced.appendChild(header);
j.providers.forEach(p=>{
const row = document.createElement('div');
row.className = 'prov-row';
row.dataset.provider = p.name;
row.innerHTML = `<strong>${p.name}</strong>`+
`<span class="badge ${p.enabled? 'ok':'off'}">${p.enabled? 'on':'off'}</span>`+
`<span class="badge ${p.cookieRequired? 'warn':'off'}">${p.cookieRequired? 'yes':'no'}</span>`+
`<span class="badge ${p.cookieOk? 'ok':'fail'}">${p.cookieOk? 'ok':'missing'}</span>`+
`<span class="badge" data-role="functional" title="Run provider checks first">?</span>`+
`<span class="notes" data-role="notes"></span>`;
provEnhanced.appendChild(row);
});
}
} catch(e){
metricsEl.textContent = 'Failed: '+e.message;
if(endpointsEl) endpointsEl.textContent = 'n/a';
if(provEnhanced) provEnhanced.innerHTML = '<div class="prov-row">Status load failed</div>';
}
}
async function runProviderFunctionalChecks(){
const targetTmdb = '293660'; // TMDB ID provided
const provEnhanced = document.getElementById('statusProvidersEnhanced');
const summary = document.getElementById('providerCheckSummary');
if(!provEnhanced) return;
summary.textContent = 'Running checks...';
const rows = Array.from(provEnhanced.querySelectorAll('.prov-row:not(.header)'));
let completed=0, passed=0;
for(const row of rows){
const name = row.dataset.provider;
const funcBadge = row.querySelector('[data-role="functional"]');
const notesEl = row.querySelector('[data-role="notes"]');
if(funcBadge){ funcBadge.textContent='…'; funcBadge.className='badge'; }
if(notesEl) notesEl.textContent='';
// Skip disabled providers
if(row.querySelector('.badge.ok, .badge.off') && row.children[1].textContent==='off'){
if(funcBadge){ funcBadge.textContent='skip'; funcBadge.className='badge off'; }
completed++; continue;
}
try {
const r = await fetch(`/api/streams/${name}/movie/${targetTmdb}`);
if(!r.ok) throw new Error('HTTP '+r.status);
const j = await r.json();
const isOk = j && Array.isArray(j.streams) && j.streams.length>0;
if(isOk){
passed++;
if(funcBadge){ funcBadge.textContent='ok'; funcBadge.className='badge ok'; }
if(notesEl) notesEl.textContent = `${j.streams.length} streams`;
}else{
if(funcBadge){ funcBadge.textContent='none'; funcBadge.className='badge fail'; }
if(notesEl) notesEl.textContent = 'No streams array';
}
}catch(err){
if(funcBadge){ funcBadge.textContent='fail'; funcBadge.className='badge fail'; }
if(notesEl) notesEl.textContent = err.message;
}
completed++;
summary.textContent = `Progress: ${completed}/${rows.length} (passed: ${passed})`;
}
summary.textContent = `Checks complete: ${passed}/${rows.length} passed`;
}
document.addEventListener('DOMContentLoaded', ()=> {
initTheme();
attachCoreListeners();
bindNavigation();
bindShellControls();
const logoutBtn = document.getElementById('logoutBtn');
if(logoutBtn){
logoutBtn.addEventListener('click', async ()=>{
try { await fetch('/auth/logout',{method:'POST'}); } catch {}
// Use replace to avoid creating extra history entry
location.replace('/?loggedOut=1');
});
}
reload();
const h = location.hash.slice(1);
navTo(h||'panel-core');
// Revalidate session on back-forward cache navigation or visibility
window.addEventListener('pageshow', (e)=>{
if(e.persisted){ verifySession(); }
});
document.addEventListener('visibilitychange', ()=>{ if(document.visibilityState==='visible') verifySession(); });
// Advanced flag checkbox change -> validation
document.getElementById('advFlags')?.addEventListener('change', (e)=>{
if(e.target.matches('.adv-flag-item input[type="checkbox"]')) {
validate();
}
});
const rsb = document.getElementById('refreshStatusBtn');
if(rsb) rsb.addEventListener('click', loadServerStatus);
const rpc = document.getElementById('runProviderChecksBtn');
if(rpc) rpc.addEventListener('click', runProviderFunctionalChecks);
});
function toggleSidebar(force){
const sb = document.getElementById('sidebar');
const showing = force !== undefined ? force : !sb.classList.contains('open');
sb.classList.toggle('open', showing);
document.querySelector('.overlay').classList.toggle('show', showing);
}
function validate(){
const f = document.getElementById('cfgForm');
const errors = [];
const qp = document.querySelector('input[name="qualityPreset"]:checked');
if(qp && qp.value==='custom') {
const mq = f.elements['minQualitiesRaw'].value.trim();
if(mq && /[\{\[]/.test(mq)) { try { JSON.parse(mq); } catch(e){ errors.push('Min Qualities invalid JSON'); markInvalid(f.elements['minQualitiesRaw']); } }
}
const codecPreset = document.querySelector('input[name="codecPreset"]:checked');
if(codecPreset && codecPreset.value==='advanced') {
const ec = f.elements['excludeCodecsRaw'].value.trim();
if(ec && /[\{\[]/.test(ec)) { try { JSON.parse(ec); } catch(e){ errors.push('Exclude Codecs invalid JSON'); markInvalid(f.elements['excludeCodecsRaw']); } }
}
const p = f.elements['port'].value.trim();
if(p) { const pn = Number(p); if(!(pn>0 && pn<=65535)) { errors.push('Port must be 1-65535'); markInvalid(f.elements['port']); } }
const dp = f.elements['defaultProviders'].value.trim();
if(dp && !/^[a-z0-9_,\-\s]+$/.test(dp)) { errors.push('Default Providers contains invalid chars'); markInvalid(f.elements['defaultProviders']); }
const sum = document.getElementById('validationSummary');
if(errors.length){ sum.textContent = errors.join(' • '); sum.classList.add('has-errors'); }
else { sum.textContent=''; sum.classList.remove('has-errors'); }
document.getElementById('saveBtn').disabled = errors.length>0;
return errors.length===0;
}
function markInvalid(el){ el.classList.add('invalid'); setTimeout(()=>el.classList.remove('invalid'), 3000); }
document.addEventListener('input', (e)=>{
if(e.target.closest('#cfgForm')) validate();
});
document.addEventListener('change', (e)=>{
const t = e.target;
if(t.name === 'qualityPreset') {
const customField = document.querySelector('textarea[name="minQualitiesRaw"]');
if(t.value === 'custom') { customField.disabled = false; } else { customField.disabled = true; customField.value=''; }
validate();
}
if(t.name === 'codecPreset') {
const ta = document.querySelector('textarea[name="excludeCodecsRaw"]');
if(t.value==='advanced') { ta.disabled=false; if(!ta.value.trim()) ta.value = JSON.stringify({excludeDV:false,excludeHDR:false}); }
else { ta.disabled=true; ta.value=''; }
validate();
}
});
async function renderProviderMatrix(merged){
if(!merged){ try { const data = await fetchConfig(); merged = data.merged||{}; } catch { return; } }
const container = document.getElementById('providerMatrix'); if(!container) return;
const defaults = (merged.defaultProviders||[]).map(s=>s.toLowerCase());
const providers = [
['showbox','enableShowboxProvider'],
['4khdhub','enable4khdhubProvider'],
['moviesmod','enableMoviesmodProvider'],
['mp4hydra','enableMp4hydraProvider'],
['vidzee','enableVidzeeProvider'],
['vixsrc','enableVixsrcProvider'],
['xprime','enableXprimeProvider']
];
container.innerHTML = providers.map(([name,flag])=>{
const enabled = merged[flag] !== false;
const isDefault = defaults.includes(name);
return `<div class=\"pm-item\" data-prov=\"${name}\" data-flag=\"${flag}\" data-enabled=\"${enabled}\" data-default=\"${isDefault}\">`
+`<button type=\"button\" class=\"pm-enable\" title=\"Enable/Disable\" data-action=\"toggle-enable\">${enabled? '⏻':'✖'}</button>`
+`<span class=\"pm-name\">${name}</span>`
+`<button type=\"button\" class=\"pm-default\" title=\"Toggle Default\" data-action=\"toggle-default\">${isDefault? '★':'☆'}</button>`
+`</div>`;
}).join('');
const hidden = document.querySelector('input[name="defaultProviders"]'); if(hidden) hidden.value = defaults.join(',');
if(!container._bound){
container.addEventListener('click', async (e)=>{
const btn = e.target.closest('button[data-action]'); if(!btn) return;
const item = btn.closest('.pm-item'); if(!item) return;
const prov = item.dataset.prov; const flag = item.dataset.flag;
let data; try { data = await fetchConfig(); } catch(err){ setStatus('Provider fetch failed',true); return; }
const mergedNow = data.merged||{};
let defaultsNow = (mergedNow.defaultProviders||[]).slice();
const patch = {};
if(btn.dataset.action==='toggle-enable'){
const currentlyEnabled = item.dataset.enabled==='true';
patch[flag] = !currentlyEnabled;
if(currentlyEnabled){ defaultsNow = defaultsNow.filter(d=>d!==prov); patch.defaultProviders = defaultsNow; }
} else if(btn.dataset.action==='toggle-default'){
const isDefault = item.dataset.default==='true';
if(isDefault){ defaultsNow = defaultsNow.filter(d=>d!==prov); }
else { if(!defaultsNow.includes(prov)) defaultsNow.push(prov); patch[flag] = true; }
patch.defaultProviders = defaultsNow;
}
try {
await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(patch)});
const updated = await fetchConfig();
fillForm(updated);
renderProviderMatrix(updated.merged);
reloadLiveOnly();
} catch(err){ setStatus('Update failed: '+err.message,true); }
});
container._bound = true;
}
}
function reloadLiveOnly(){
fetchConfig().then(c=>{
document.getElementById('currentCfg').textContent = JSON.stringify(c.merged||{},null,2);
document.getElementById('overrideCfg').textContent = (c.override && Object.keys(c.override).length)? JSON.stringify(c.override,null,2):'(none)';
}).catch(()=>{});
}
// No globals exported; all handlers bound programmatically
})();
+127
View File
@@ -0,0 +1,127 @@
/* eslint-env browser */
(function(){
const $ = sel => document.querySelector(sel);
async function checkSession(){
try {
const r = await fetch('/auth/session');
const j = await r.json();
if (j.authenticated) {
window.location.href = '/config.html';
}
} catch(e) { /* ignore */ }
}
async function login(username, password){
const status = $('#status');
status.textContent = 'Authenticating...';
status.className = 'status';
try {
const r = await fetch('/auth/login', { method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify({ username, password }) });
const j = await r.json();
if (!j.success) {
if (j.error === 'LOCKED' || j.error === 'TOO_MANY_ATTEMPTS') {
showLockout(j.retryAfter);
status.textContent = 'Locked due to excessive attempts';
} else if (j.error === 'INVALID_CREDENTIALS') {
status.textContent = 'Invalid credentials';
if (typeof j.remaining === 'number') {
status.textContent += ` (${j.remaining} attempts left)`;
}
} else {
status.textContent = j.error || 'Login failed';
}
status.classList.add('error');
return false;
}
status.textContent = 'Success! Redirecting...';
status.classList.add('success');
setTimeout(()=>{ window.location.href = '/config.html'; }, 400);
return true;
} catch (e) {
status.textContent = 'Network error';
status.classList.add('error');
return false;
}
}
// Lockout handling
let lockoutTimer = null;
function showLockout(retryAfterSeconds){
if (!retryAfterSeconds || retryAfterSeconds <= 0) return;
const banner = $('#lockoutBanner');
const countdownEl = $('#lockoutCountdown');
banner.style.display = 'block';
const end = Date.now() + (retryAfterSeconds * 1000);
if (lockoutTimer) clearInterval(lockoutTimer);
lockoutTimer = setInterval(()=>{
const remain = end - Date.now();
if (remain <= 0){
clearInterval(lockoutTimer);
banner.style.display = 'none';
countdownEl.textContent = '00:00';
return;
}
const sec = Math.ceil(remain/1000);
const m = String(Math.floor(sec/60)).padStart(2,'0');
const s = String(sec%60).padStart(2,'0');
countdownEl.textContent = `${m}:${s}`;
}, 1000);
}
async function changePassword(oldPassword, newPassword){
const pwStatus = $('#pwStatus');
pwStatus.textContent = 'Updating...';
pwStatus.className = 'status';
try {
const r = await fetch('/auth/change-password', { method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify({ oldPassword, newPassword }) });
const j = await r.json();
if (!j.success) {
pwStatus.textContent = j.error || 'Update failed';
pwStatus.classList.add('error');
return;
}
pwStatus.textContent = 'Password updated';
pwStatus.classList.add('success');
setTimeout(()=>{ $('#pwForm').reset(); }, 600);
} catch(e){
pwStatus.textContent = 'Network error';
pwStatus.classList.add('error');
}
}
document.addEventListener('DOMContentLoaded', () => {
// Flash messaging based on query params
const params = new URLSearchParams(location.search);
const status = $('#status');
if(params.has('loggedOut')) { status.textContent = 'Logged out successfully'; status.classList.add('success'); }
else if(params.has('expired')) { status.textContent = 'Session expired. Please login again.'; status.classList.add('error'); }
if(params.has('loggedOut') || params.has('expired')) {
// Remove params from URL without reloading
history.replaceState(null,'', location.pathname);
}
checkSession();
$('#loginForm').addEventListener('submit', e => {
e.preventDefault();
const u = $('#username').value.trim();
const p = $('#password').value;
if (!u || !p) return;
login(u,p);
});
$('#togglePw').addEventListener('click', () => {
const chg = $('#pwChange');
const visible = chg.style.display === 'block';
chg.style.display = visible ? 'none' : 'block';
});
$('#pwForm').addEventListener('submit', e => {
e.preventDefault();
const oldPw = $('#oldPw').value;
const newPw = $('#newPw').value;
if (newPw.length < 8) {
$('#pwStatus').textContent = 'Password too short';
$('#pwStatus').classList.add('error');
return;
}
changePassword(oldPw, newPw);
});
});
})();
+366
View File
@@ -0,0 +1,366 @@
/*TMDB Embed Admin Panel Styles */
:root {
--bg: #f7f9fc;
--bg-alt: #ffffff;
--bg-accent: #eef2f7;
--border: #d8e0ea;
--border-strong: #b6c2cf;
--text: #1f2933;
--text-dim: #5a6b7b;
--primary: #2563eb;
--primary-accent: #1d4ed8;
--primary-fg: #ffffff;
--danger: #dc2626;
--warn: #b45309;
--radius-sm: 4px;
--radius: 8px;
--shadow: 0 1px 2px rgba(0,0,0,.05), 0 4px 16px -4px rgba(0,0,0,.08);
--mono: "SFMono-Regular",Consolas,Menlo,monospace;
--transition: .18s cubic-bezier(.4,0,.2,1);
}
:root[data-theme='dark'] {
--bg: #0f141a;
--bg-alt: #161d25;
--bg-accent: #1e2630;
--border: #29323d;
--border-strong: #3a4654;
--text: #e5edf3;
--text-dim: #a0b1c0;
--primary: #3b82f6;
--primary-accent: #1d4ed8;
--primary-fg: #f0f6ff;
--danger: #f87171;
--warn: #f59e0b;
--shadow: 0 2px 4px rgba(0,0,0,.4), 0 8px 28px -6px rgba(0,0,0,.55);
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif;
background: var(--bg);
color: var(--text);
display: flex;
min-height: 100vh;
overflow: hidden; /* Prevent body scroll; main will scroll */
}
.bmc-btn {
min-width: 50px !important;
color: #ffffff !important;
background-color: #5F7FFF !important;
height: 40px !important;
border-radius: 12px !important;
font-size: 12px !important;
font-weight: Bold !important;
border: none !important;
padding: 0px 24px !important;
line-height: 27px !important;
text-decoration: none !important;
display: inline-flex !important;
align-items: center !important;
font-family: 'Bree Serif', cursive !important;
-webkit-box-sizing: border-box !important;
box-sizing: border-box !important;
}
.bmc-btn-container{
text-align: center;
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--bg-alt); }
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 20px; }
::-webkit-scrollbar-thumb:hover { background: var(--primary); }
.sidebar {
width: 250px;
background: linear-gradient(180deg, var(--bg-alt), var(--bg-accent));
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 16px 14px 10px;
gap: 12px;
position: fixed;
top: 0; left: 0; bottom: 0;
overflow-y: auto;
z-index: 45;
}
/* Mobile burger */
.burger {
position: fixed; top:10px; left:12px; z-index:60; background: var(--primary); color: var(--primary-fg);
border:1px solid var(--primary-accent); border-radius: var(--radius-sm); padding:8px 10px; font-size:1rem; cursor:pointer; display:none;
box-shadow: var(--shadow);
}
.overlay { position: fixed; inset:0; background:rgba(0,0,0,.45); backdrop-filter: blur(2px); z-index:40; display:none; }
.overlay.show { display:block; }
#sidebar { transition: transform var(--transition); }
@media (max-width: 880px){
.burger { display:block; }
#sidebar { position:fixed; inset:0 auto 0 0; transform: translateX(-100%); z-index:50; width:240px; }
#sidebar.open { transform: translateX(0); }
body { padding-left:0; }
.actions-bar { left:0; }
}
.brand {
font-weight: 700;
font-size: 1.05rem;
letter-spacing: .5px;
display: flex;
align-items: center;
gap: 8px;
}
.pill { background: var(--primary); color: var(--primary-fg); padding: 2px 8px; border-radius: 999px; font-size: .65rem; letter-spacing: .5px; text-transform: uppercase; }
.nav { display: flex; flex-direction: column; gap: 4px; }
.nav-item {
background: transparent;
border: 1px solid transparent;
color: var(--text-dim);
padding: 10px 12px;
text-align: left;
border-radius: var(--radius-sm);
font-size: .85rem;
font-weight: 600;
letter-spacing: .5px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: background var(--transition), color var(--transition), border-color var(--transition);
}
.nav-item:hover { background: var(--bg-accent); color: var(--text); }
.nav-item.active { background: var(--primary); color: var(--primary-fg); }
.nav-item.theme-toggle { margin-top: 12px; }
.nav-item#logoutBtn { border:1px solid var(--danger); color: var(--danger); font-weight:700; }
.nav-item#logoutBtn:hover { background: var(--danger); color:#fff; }
.grow { flex-grow: 1; }
.side-footer { font-size: .65rem; opacity: .6; line-height: 1.4; text-align: center;}
.main {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
min-width: 0;
max-width: 1400px;
margin-left: 250px; /* compensate for fixed sidebar */
height: 100vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.panels { flex: 1; display: flex; flex-direction: column; padding: 28px 36px 140px; gap: 0; }
.panel { display: none; animation: fade .35s ease; max-width: 760px; }
.panel.active { display: block; }
@keyframes fade { from { opacity: 0; transform: translateY(6px);} to { opacity:1; transform:translateY(0);} }
.panel h1 { margin: 0 0 1.2rem; font-size: 1.4rem; }
.field { margin-bottom: 18px; }
.field label { font-size: .75rem; letter-spacing: .5px; text-transform: uppercase; font-weight: 700; display: flex; flex-direction: column; gap: 6px; color: var(--text-dim); }
.field input[type=text], .field textarea, .field select {
background: var(--bg-alt);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 12px;
font-family: var(--mono);
color: var(--text);
font-size: .85rem;
outline: none;
transition: border-color var(--transition),
background var(--transition);
resize: vertical;
}
.field select { cursor: pointer; appearance: none; -webkit-appearance: none; line-height:1.2; padding-right: 36px; background-image: linear-gradient(45deg, transparent 50%, var(--text-dim) 50%), linear-gradient(135deg, var(--text-dim) 50%, transparent 50%); background-position: calc(100% - 20px) 16px, calc(100% - 14px) 16px; background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; }
.field input[type=text]:focus, .field input[type=password]:focus, .field textarea:focus, .field select:focus { border-color: var(--primary); outline: none; }
.field select:disabled { opacity:.55; cursor: not-allowed; }
textarea { min-height: 110px; }
.hint {
font-size: .65rem;
opacity: .75;
margin-top: -4px;
letter-spacing: .5px;
text-transform: none;
font-weight: 500;
display: flex;
flex-direction: column;
gap: 6px;
color: var(--text-dim);
}
.login-hint {
font-size: .65rem;
opacity: .75;
margin-top: -4px;
letter-spacing: .5px;
text-transform: none;
font-weight: 500;
display: flex;
flex-direction: column;
gap: 6px;
color: orange;
}
.provider-toggle-list { display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }
/* Uniform provider width: auto-fill responsive sizing */
.prov-item { background: var(--bg-alt); border:1px solid var(--border); padding:6px 10px; border-radius: var(--radius-sm); font-size:.65rem; letter-spacing:.5px; text-transform:uppercase; display:flex; align-items:center; gap:6px; cursor:pointer; user-select:none; box-shadow: var(--shadow); flex: 0 0 132px; width:132px; justify-content:center; }
.prov-item input { accent-color: var(--primary); cursor:pointer; }
.prov-item:hover { border-color: var(--primary); }
.disabled-hint { font-style:normal; color: var(--warn); }
/* Quality presets */
.quality-presets, .codec-toggles { display:flex; flex-wrap:wrap; gap:10px; margin-top:10px; }
.quality-presets label, .codec-toggles label.codec-item { text-align: center; background: var(--bg-alt); border:1px solid var(--border); padding:6px 12px; border-radius: var(--radius-sm); font-size:.65rem; letter-spacing:.5px; text-transform:uppercase; display:flex; align-items:center; gap:6px; cursor:pointer; user-select:none; box-shadow: var(--shadow); }
.quality-presets .q-item { flex:0 0 100px; justify-content:center; }
.quality-presets .q-item.adv { background: var(--bg-accent); }
.quality-json { display:block; width:100%; margin-top:10px; font-size:.65rem; font-family: var(--mono); min-height:90px; background: var(--bg-alt); border:1px solid var(--border); border-radius: var(--radius-sm); padding:10px 12px; }
.quality-json[disabled] { opacity:.4; }
.json-adv {
display: block;
width: 100%;
margin-top: 10px;
font-size: .65rem;
font-family: var(--mono);
min-height: 90px;
background: var(--bg-alt);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 12px;
}
.codec-presets { display:flex; flex-wrap:wrap; gap:10px; margin-top:10px; }
.codec-presets .c-item { background: var(--bg-alt); border:1px solid var(--border); padding:6px 12px; border-radius: var(--radius-sm); font-size:.65rem; letter-spacing:.5px; text-transform:uppercase; display:flex; align-items:center; gap:6px; cursor:pointer; user-select:none; box-shadow: var(--shadow); flex:0 0 130px; justify-content:center; }
.codec-presets .c-item.adv { background: var(--bg-accent); }
.codec-presets input[type=radio] { accent-color: var(--primary); cursor:pointer; }
.codec-presets .c-item:hover { border-color: var(--primary); }
.quality-presets input[type=radio], .codec-toggles input[type=checkbox] { accent-color: var(--primary); cursor:pointer; }
.quality-presets label:hover, .codec-toggles label.codec-item:hover { border-color: var(--primary); }
.quality-presets label.custom-q { position:relative; }
.quality-presets .mini { width:170px; margin-left:4px; font-size:.65rem; padding:6px 8px; font-family: var(--mono); }
.codec-toggles .json-adv { flex: 1 1 100%; margin-top:4px; font-size:.65rem; font-family: var(--mono); min-height:90px; display:block; }
.codec-toggles .json-adv[disabled] { opacity:.4; }
.codec-toggles label.adv { background: var(--bg-accent); }
.alert { padding: 12px 16px; border-radius: var(--radius-sm); margin: 20px 36px 0; font-size: .75rem; font-weight: 600; letter-spacing: .5px; background: var(--bg-accent); border: 1px solid var(--border); }
.alert.warning { color: var(--warn); border-color: var(--warn); background: rgba(180,83,9,.08); }
[data-theme='dark'] .alert.warning { background: rgba(245,158,11,.12); }
.actions-bar {
position: sticky;
bottom: 0; left: 0; right: 0;
background: linear-gradient(90deg,var(--bg-alt),var(--bg-accent));
border-top: 1px solid var(--border);
display: flex;
gap: 14px;
align-items: center;
padding: 14px 36px;
backdrop-filter: blur(6px);
margin-left: 0;
}
button { font-family: inherit; }
button.primary { background: var(--primary); border: 1px solid var(--primary-accent); color: var(--primary-fg); }
button.primary:hover { background: var(--primary-accent); }
button.danger { background: var(--danger); border: 1px solid var(--danger); color: #fff; }
button.danger:hover { filter: brightness(.9); }
button { border-radius: var(--radius-sm); padding: 10px 18px; font-size: .75rem; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; border: 1px solid var(--border); background: var(--bg-alt); color: var(--text-dim); cursor: pointer; transition: background var(--transition), color var(--transition), border-color var(--transition), transform .15s ease; }
button:hover { background: var(--bg-accent); color: var(--text); }
.status { font-size: .7rem; letter-spacing: .5px; font-weight: 600; }
pre { background: var(--bg-alt); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px 16px; line-height: 1.35; font-size: .7rem; box-shadow: var(--shadow); overflow: auto; }
pre code { font-family: var(--mono); }
#currentCfg, #overrideCfg { max-height: 380px; }
::-moz-selection { background: var(--primary); color: var(--primary-fg); }
::selection { background: var(--primary); color: var(--primary-fg); }
@media (max-width: 880px) {
body { overflow: auto; }
.sidebar { position: fixed; inset:0 auto 0 0; width:210px; transform: translateX(-100%); transition: transform var(--transition); }
#sidebar.open { transform: translateX(0); }
.main { margin-left:0; height:auto; overflow-y:visible; }
.actions-bar { position: fixed; left:0; right:0; bottom:0; }
.panels { padding: 24px 20px 140px; }
}
/* Validation */
.validation-summary { font-size:.65rem; margin-left:12px; color: var(--danger); font-weight:600; letter-spacing:.5px; }
.adv-flag-grid { display:flex; flex-wrap:wrap; gap:14px; margin:14px 0 28px; }
.adv-flag-item { position:relative; background: var(--bg-alt); border:1px solid var(--border); padding:16px 18px 14px; border-radius: var(--radius-sm); font-size:.55rem; letter-spacing:.55px; text-transform:uppercase; font-weight:700; display:flex; flex-direction:column; align-items:center; gap:10px; cursor:pointer; user-select:none; box-shadow: var(--shadow); width:170px; min-height:92px; transition: background var(--transition), border-color var(--transition), color var(--transition), transform .15s ease; }
.adv-flag-item:hover { border-color: var(--primary); }
.adv-flag-item.on { background: var(--primary); border-color: var(--primary-accent); color: var(--primary-fg); }
[data-theme='dark'] .adv-flag-item.on { background: var(--primary); }
.adv-flag-item input { appearance:none; -webkit-appearance:none; accent-color: var(--primary); width:15px; height:15px; border:2px solid var(--primary); border-radius:6px; display:inline-flex; align-items:center; justify-content:center; position:relative; background:transparent; cursor:pointer; margin-top:2px; }
.adv-flag-item input:focus-visible { outline:2px solid var(--primary); outline-offset:3px; }
.adv-flag-item input:checked { background: var(--primary); }
.adv-flag-item input:checked:after { width:7px; height:12px; border:2px solid var(--primary-fg); border-top:none; border-left:none; transform: rotate(45deg); position:absolute; }
.adv-flag-item span { text-align:center; line-height:1.25; display:block; width:100%; }
.adv-flag-item.on input { border-color: var(--primary-fg); }
.adv-group .field.small input { background: var(--bg-alt); border:1px solid var(--border); }
.adv-group .field.small input:focus { border-color: var(--primary); outline:none; }
.validation-summary.has-errors:before { content:'⚠ '; }
input.invalid, textarea.invalid { border-color: var(--danger)!important; animation: pulse-invalid 1.2s ease; }
@keyframes pulse-invalid { 0%{ box-shadow:0 0 0 0 rgba(220,38,38,.6);} 70%{ box-shadow:0 0 0 6px rgba(220,38,38,0);} 100%{ box-shadow:0 0 0 0 rgba(220,38,38,0);} }
.adv-flag-item input:checked { border-color: var(--primary); }
/* Provider Matrix */
.provider-matrix { display:flex; flex-wrap:wrap; gap:10px; margin-top:10px; }
.pm-item { background: var(--bg-alt); border:1px solid var(--border); border-radius: var(--radius-sm); padding:6px 10px; display:flex; align-items:center; gap:8px; font-size:.65rem; letter-spacing:.5px; text-transform:uppercase; box-shadow: var(--shadow); }
.pm-item[data-enabled='false'] { opacity:.45; filter:saturate(.5); }
.pm-item .pm-enable, .pm-item .pm-default { background: var(--bg-accent); border:1px solid var(--border); padding:4px 8px; font-size:.7rem; cursor:pointer; border-radius: var(--radius-sm); line-height:1; display:flex; align-items:center; justify-content:center; }
.pm-item .pm-enable { color: var(--primary); }
.pm-item[data-enabled='false'] .pm-enable { color: var(--danger); }
.pm-item .pm-default { color: gold; }
.pm-item[data-default='false'] .pm-default { opacity:.5; color: var(--text-dim); }
.pm-item .pm-enable:hover, .pm-item .pm-default:hover { border-color: var(--primary); }
.pm-name { min-width:70px; }
/* FebBox Cookie Manager */
.cookie-add-row { display:flex; gap:10px; align-items:center; }
.cookie-add-row input { flex:1; }
.cookie-add-row button.small { padding:8px 14px; font-size:.65rem; }
.cookie-list { margin-top:10px; display:flex; flex-direction:column; gap:6px; background: var(--bg-alt); border:1px solid var(--border); padding:10px 12px; border-radius: var(--radius-sm); max-height:220px; overflow:auto; }
.cookie-item { display:flex; align-items:center; justify-content:space-between; gap:12px; font-size:.65rem; background: var(--bg-accent); border:1px solid var(--border); padding:6px 10px; border-radius: var(--radius-sm); font-family: var(--mono); }
.cookie-item code { font-family: var(--mono); word-break: break-all; flex:1; }
.cookie-item button.remove { background: var(--danger); border:1px solid var(--danger); color:#fff; padding:4px 8px; font-size:.6rem; cursor:pointer; border-radius: var(--radius-sm); }
.cookie-item button.remove:hover { filter:brightness(.9); }
.cookie-list .empty { opacity:.55; font-size:.65rem; font-style:italic; }
/* TMDB Keys Manager */
.key-add-row { display:flex; gap:10px; align-items:center; }
.key-add-row input { flex:1; }
.key-add-row button.small { padding:8px 14px; font-size:.65rem; }
.key-list { margin-top:10px; display:flex; flex-direction:column; gap:6px; background: var(--bg-alt); border:1px solid var(--border); padding:10px 12px; border-radius: var(--radius-sm); max-height:220px; overflow:auto; }
.key-item { display:flex; align-items:center; justify-content:space-between; gap:12px; font-size:.65rem; background: var(--bg-accent); border:1px solid var(--border); padding:6px 10px; border-radius: var(--radius-sm); font-family: var(--mono); }
.key-item code { font-family: var(--mono); word-break: break-all; flex:1; }
.key-item button.remove { background: var(--danger); border:1px solid var(--danger); color:#fff; padding:4px 8px; font-size:.6rem; cursor:pointer; border-radius: var(--radius-sm); }
.key-item button.remove:hover { filter:brightness(.9); }
.key-list .empty { opacity:.55; font-size:.65rem; font-style:italic; }
/* Server Status Enhancements */
.status-grid { display:grid; grid-template-columns: 1fr; gap:24px; margin-bottom:24px; }
.status-block { background: var(--bg-alt); border:1px solid var(--border); border-radius: var(--radius); padding:18px 20px 20px; box-shadow: var(--shadow); position:relative; }
.status-block h2 { margin:0 0 12px; font-size:1rem; letter-spacing:.5px; }
.status-block.wide { grid-column: 1 / -1; }
.status-pre { max-height:260px; }
.providers-table { display:flex; flex-direction:column; gap:6px; font-size:.65rem; }
.prov-row { display:grid; grid-template-columns: 110px 70px 90px 90px 110px 1fr; gap:6px; align-items:center; background: var(--bg-accent); border:1px solid var(--border); padding:6px 8px; border-radius: var(--radius-sm); }
.prov-row.header { background: var(--primary); color: var(--primary-fg); font-weight:700; text-transform:uppercase; letter-spacing:.5px; border-color: var(--primary-accent); }
.badge { display:inline-flex; align-items:center; justify-content:center; padding:2px 8px; border-radius:999px; font-size:.55rem; font-weight:600; letter-spacing:.5px; text-transform:uppercase; background: var(--bg-accent); border:1px solid var(--border); min-width:54px; }
.badge.ok { background:#065f46; border-color:#065f46; color:#ecfdf5; }
[data-theme='dark'] .badge.ok { background:#059669; border-color:#059669; }
.badge.fail { background:#7f1d1d; border-color:#7f1d1d; color:#fee2e2; }
[data-theme='dark'] .badge.fail { background:#dc2626; border-color:#dc2626; }
.badge.warn { background:#92400e; border-color:#92400e; color:#fef3c7; }
[data-theme='dark'] .badge.warn { background:#d97706; border-color:#d97706; }
.badge.off { background:#374151; border-color:#374151; color:#e5e7eb; }
[data-theme='dark'] .badge.off { background:#4b5563; border-color:#4b5563; }
.inline-actions { display:flex; gap:14px; align-items:center; }
.inline-actions button { margin:0; }
.functional-status { display:flex; flex-direction:column; gap:6px; margin-top:10px; }
.prov-row .spinner { width:14px; height:14px; border:2px solid rgba(255,255,255,.3); border-top-color:#fff; border-radius:50%; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Responsive adjustments for Server Status */
/* Previously two-column; intentionally stacked for readability even on wide screens */
/* If a future toggle is desired, we can add a class to re-enable multi-column */
@media (max-width: 760px){
.prov-row { grid-template-columns: 90px 60px 70px 70px 90px 1fr; font-size:.55rem; }
.status-block { padding:14px 14px 16px; }
.status-block h2 { font-size:.9rem; }
.badge { min-width:40px; padding:2px 6px; font-size:.5rem; }
}
@media (max-width: 560px){
.providers-table { gap:10px; }
.prov-row.header { display:none; }
.prov-row { grid-template-columns: 1fr 1fr; grid-template-areas: 'name name' 'enabled cookieReq' 'cookieOk functional' 'notes notes'; position:relative; padding:10px 12px; }
.prov-row strong { grid-area: name; }
.prov-row > span:nth-child(2){ grid-area: enabled; }
.prov-row > span:nth-child(3){ grid-area: cookieReq; }
.prov-row > span:nth-child(4){ grid-area: cookieOk; }
.prov-row > span:nth-child(5){ grid-area: functional; }
.prov-row > span:nth-child(6){ grid-area: notes; justify-self:start; }
.prov-row { background: var(--bg-alt); }
.providers-table .prov-row { border:1px solid var(--border); }
.prov-row strong { font-size:.7rem; letter-spacing:.5px; }
}