Added auth support and a helper script, targeting frankenphp

This commit is contained in:
markmental 2026-02-02 14:05:21 -05:00
commit 0783e08fe9
8 changed files with 655 additions and 4 deletions

374
index.php
View file

@ -342,6 +342,49 @@
font-weight: 600;
}
.player-controls-row {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
margin-top: 1rem;
z-index: 1;
}
.audio-track-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-secondary);
font-family: 'Orbitron', sans-serif;
}
.audio-track-select {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 0.55rem 0.75rem;
border-radius: 8px;
outline: none;
min-width: 220px;
font-family: 'Archivo', sans-serif;
}
.audio-track-select:focus {
border-color: var(--accent-primary);
box-shadow: var(--shadow-glow);
}
.audio-track-hint {
color: var(--text-secondary);
font-size: 0.8rem;
margin-top: 0.5rem;
z-index: 1;
text-align: center;
}
.loading {
display: inline-block;
width: 20px;
@ -352,6 +395,119 @@
animation: spin 0.8s linear infinite;
}
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.72);
backdrop-filter: blur(6px);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.login-card {
width: 100%;
max-width: 420px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 14px;
box-shadow: 0 10px 40px rgba(0,0,0,0.6);
padding: 1.75rem;
}
.login-title {
font-family: 'Orbitron', sans-serif;
font-size: 1.4rem;
color: var(--accent-primary);
margin-bottom: 0.35rem;
}
.login-subtitle {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 1.25rem;
}
.login-row {
display: flex;
flex-direction: column;
gap: 0.4rem;
margin-bottom: 0.9rem;
}
.login-row label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-secondary);
font-family: 'Orbitron', sans-serif;
}
.login-row input {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 0.75rem 0.85rem;
border-radius: 10px;
outline: none;
font-family: 'Archivo', sans-serif;
}
.login-row input:focus {
border-color: var(--accent-primary);
box-shadow: var(--shadow-glow);
}
.login-actions {
display: flex;
gap: 0.75rem;
margin-top: 1rem;
align-items: center;
justify-content: space-between;
}
.btn {
cursor: pointer;
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 0.75rem 1rem;
font-family: 'Orbitron', sans-serif;
letter-spacing: 0.05em;
text-transform: uppercase;
font-size: 0.85rem;
transition: all 0.2s ease;
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-secondary) 100%);
border-color: var(--accent-primary);
color: var(--bg-primary);
font-weight: 700;
box-shadow: var(--shadow-glow);
}
.btn:hover {
transform: translateY(-1px);
}
.login-error {
color: #ff6b6b;
margin-top: 0.75rem;
font-size: 0.9rem;
display: none;
}
.topbar {
display: flex;
justify-content: flex-end;
margin-bottom: 1rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
@ -392,12 +548,22 @@
</aside>
<main class="player-section">
<div class="topbar">
<button class="btn" id="logoutBtn" style="display:none;">Logout</button>
</div>
<div class="player-container" id="playerContainer">
<div class="player-placeholder">
<div class="player-placeholder-icon">🎵</div>
<p>Select a file to start playing</p>
</div>
</div>
<div class="player-controls-row" id="audioControls" style="display:none;">
<div class="audio-track-label">Audio Track</div>
<select id="audioSelect" class="audio-track-select"></select>
</div>
<div class="audio-track-hint" id="audioHint" style="display:none;"></div>
<div class="now-playing" id="nowPlaying" style="display: none;">
<div class="now-playing-label">Now Playing</div>
<div class="now-playing-title" id="nowPlayingTitle"></div>
@ -406,7 +572,70 @@
</div>
</div>
<div class="overlay" id="loginOverlay" style="display:none;">
<div class="login-card">
<div class="login-title">Login</div>
<div class="login-subtitle">Sign in to access your media library.</div>
<form id="loginForm">
<div class="login-row">
<label for="username">Username</label>
<input id="username" name="username" autocomplete="username" required />
</div>
<div class="login-row">
<label for="password">Password</label>
<input id="password" name="password" type="password" autocomplete="current-password" required />
</div>
<div class="login-actions">
<button class="btn btn-primary" type="submit" id="loginBtn">Login</button>
<span id="loginSpinner" class="loading" style="display:none;"></span>
</div>
<div class="login-error" id="loginError"></div>
</form>
</div>
</div>
<script>
function showLogin(message = '') {
document.getElementById('loginOverlay').style.display = 'flex';
document.getElementById('logoutBtn').style.display = 'none';
const err = document.getElementById('loginError');
if (message) {
err.textContent = message;
err.style.display = 'block';
} else {
err.textContent = '';
err.style.display = 'none';
}
}
function hideLogin() {
document.getElementById('loginOverlay').style.display = 'none';
document.getElementById('loginError').style.display = 'none';
document.getElementById('logoutBtn').style.display = 'inline-block';
}
async function authedFetch(url, options = {}) {
// Ensure cookies (session) are sent
const resp = await fetch(url, {
...options,
credentials: 'same-origin',
});
if (resp.status === 401) {
// Session missing/expired
showLogin('Please log in to continue.');
throw new Error('Unauthorized');
}
return resp;
}
const state = {
currentType: 'videos',
currentFile: null,
@ -415,10 +644,77 @@
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadFiles('videos');
setupTabListeners();
setupLoginHandlers();
// Try loading files; if 401, overlay will appear.
loadFiles('videos');
});
function setupLoginHandlers() {
const form = document.getElementById('loginForm');
const spinner = document.getElementById('loginSpinner');
const err = document.getElementById('loginError');
const logoutBtn = document.getElementById('logoutBtn');
// Show overlay until proven authed
showLogin('');
form.addEventListener('submit', async (e) => {
e.preventDefault();
err.style.display = 'none';
spinner.style.display = 'inline-block';
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
try {
const resp = await fetch('login.php', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok) {
throw new Error(data.error || `Login failed (${resp.status})`);
}
hideLogin();
// Reload file browser after login
loadFiles(state.currentType);
} catch (ex) {
err.textContent = ex.message || 'Login failed';
err.style.display = 'block';
} finally {
spinner.style.display = 'none';
}
});
logoutBtn.addEventListener('click', async () => {
try {
await fetch('logout.php', { method: 'POST', credentials: 'same-origin' });
} catch (_) {
// ignore
}
showLogin('Logged out.');
// Clear UI
document.getElementById('fileBrowser').innerHTML = '';
document.getElementById('playerContainer').innerHTML = `
<div class="player-placeholder">
<div class="player-placeholder-icon">🎵</div>
<p>Select a file to start playing</p>
</div>
`;
document.getElementById('nowPlaying').style.display = 'none';
});
}
function setupTabListeners() {
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
@ -436,7 +732,7 @@
browser.innerHTML = '<div class="loading"></div>';
try {
const response = await fetch(`get_files.php?type=${type}`);
const response = await authedFetch(`get_files.php?type=${type}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@ -516,18 +812,92 @@
}
}
function resetAudioTrackUI() {
const audioControls = document.getElementById('audioControls');
const audioSelect = document.getElementById('audioSelect');
const audioHint = document.getElementById('audioHint');
audioControls.style.display = 'none';
audioHint.style.display = 'none';
audioHint.textContent = '';
audioSelect.innerHTML = '';
}
function setupAudioTrackSelector(videoEl) {
const audioControls = document.getElementById('audioControls');
const audioSelect = document.getElementById('audioSelect');
const audioHint = document.getElementById('audioHint');
// Not all browsers expose audioTracks (and MKV often won't)
const tracks = videoEl.audioTracks;
if (!tracks) {
audioControls.style.display = 'none';
audioHint.style.display = 'block';
audioHint.textContent = 'Audio track switching not supported for this file/browser, open in VLC to change audio track if needed.';
return;
}
if (tracks.length <= 1) {
audioControls.style.display = 'none';
audioHint.style.display = 'none';
return;
}
// Build dropdown
audioSelect.innerHTML = '';
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
const opt = document.createElement('option');
opt.value = String(i);
// label is often empty; language may exist depending on browser/container
const label = (t.label && t.label.trim()) ? t.label.trim() : null;
const lang = (t.language && t.language.trim()) ? t.language.trim() : null;
opt.textContent = label || (lang ? `Track ${i + 1} (${lang})` : `Track ${i + 1}`);
audioSelect.appendChild(opt);
// Set currently enabled track as selected
if (t.enabled) {
audioSelect.value = String(i);
}
}
audioSelect.onchange = () => {
const idx = parseInt(audioSelect.value, 10);
for (let i = 0; i < tracks.length; i++) {
tracks[i].enabled = (i === idx);
}
};
audioHint.style.display = 'none';
audioControls.style.display = 'flex';
}
function playFile(path, name) {
const playerContainer = document.getElementById('playerContainer');
const nowPlaying = document.getElementById('nowPlaying');
const nowPlayingTitle = document.getElementById('nowPlayingTitle');
state.currentFile = { path, name };
resetAudioTrackUI();
const isVideo = state.currentType === 'videos';
const mediaElement = document.createElement(isVideo ? 'video' : 'audio');
mediaElement.controls = true;
mediaElement.autoplay = true;
if (isVideo) {
// Some browsers only populate audioTracks after metadata is available
mediaElement.addEventListener('loadedmetadata', () => {
setupAudioTrackSelector(mediaElement);
});
}
// Add error handling
mediaElement.addEventListener('error', (e) => {