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, filesData: null, currentItemEl: null }; // Initialize document.addEventListener('DOMContentLoaded', () => { 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 = `
🎵

Select a file to start playing

`; document.getElementById('nowPlaying').style.display = 'none'; }); } function setupTabListeners() { document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { const type = btn.dataset.type; document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.currentType = type; loadFiles(type); }); }); } async function loadFiles(type) { const browser = document.getElementById('fileBrowser'); browser.innerHTML = '
'; try { const response = await authedFetch(`get_files.php?type=${type}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { const text = await response.text(); console.error('Non-JSON response:', text); throw new Error('Server returned non-JSON response'); } const data = await response.json(); console.log('Loaded files:', data); if (data.error) { browser.innerHTML = `

${data.error}

`; return; } if (data.message) { browser.innerHTML = `

${data.message}

`; return; } state.filesData = data; renderFileTree(data); } catch (error) { console.error('Error loading files:', error); browser.innerHTML = `

Error loading files: ${error.message}

`; } } function setActiveFileItem(fileItemEl) { // Clear existing "active" state document.querySelectorAll('.file-item').forEach(f => f.classList.remove('active')); // Set new "active" if (fileItemEl) { fileItemEl.classList.add('active'); fileItemEl.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } state.currentItemEl = fileItemEl || null; } /** * Finds the "next" file-item in UI order, preferring the same folder/div * the current item is in. If there's no next sibling, it walks up the DOM * and continues searching. */ function findNextFileItem(currentEl) { if (!currentEl) return null; // 1) Prefer "next siblings" within the same container let n = currentEl.nextElementSibling; while (n) { if (n.classList && n.classList.contains('file-item')) return n; n = n.nextElementSibling; } // 2) If none, walk upward and look for the next file-item after the // parent container (e.g., next folder section) let parent = currentEl.parentElement; while (parent) { // Stop at the file browser root if (parent.id === 'fileBrowser') break; let pNext = parent.nextElementSibling; while (pNext) { // If the next sibling is a file-item, play it if (pNext.classList && pNext.classList.contains('file-item')) return pNext; // If it's a folder, search inside it for the first file-item const inside = pNext.querySelector && pNext.querySelector('.file-item'); if (inside) return inside; pNext = pNext.nextElementSibling; } parent = parent.parentElement; } // No next item found return null; } function playNextItem() { const next = findNextFileItem(state.currentItemEl); if (!next) { console.log('Reached end of list; no next item.'); return; } // Trigger the same behavior as clicking it next.click(); } function renderFileTree(files, container = document.getElementById('fileBrowser'), depth = 0) { if (depth === 0) { container.innerHTML = ''; } for (const [name, content] of Object.entries(files)) { if (typeof content === 'object' && !Array.isArray(content)) { // It's a folder const folder = document.createElement('div'); folder.className = 'folder'; const header = document.createElement('div'); header.className = 'folder-header'; header.innerHTML = `${name}`; const folderContent = document.createElement('div'); folderContent.className = 'folder-content'; header.addEventListener('click', () => { folder.classList.toggle('open'); }); folder.appendChild(header); folder.appendChild(folderContent); container.appendChild(folder); renderFileTree(content, folderContent, depth + 1); } else if (typeof content === 'string') { // It's a file const fileItem = document.createElement('div'); fileItem.className = 'file-item'; const icon = state.currentType === 'videos' ? '🎬' : '🎵'; fileItem.innerHTML = `${icon}${name}`; fileItem.dataset.path = content; fileItem.dataset.name = name; fileItem.addEventListener('click', () => { // Play and mark active playFile(content, name, fileItem); setActiveFileItem(fileItem); }); container.appendChild(fileItem); } } } 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, sourceItemEl = null) { 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; // Keep track of which UI element launched playback (used for "next") if (sourceItemEl) { state.currentItemEl = sourceItemEl; } if (isVideo) { // Some browsers only populate audioTracks after metadata is available mediaElement.addEventListener('loadedmetadata', () => { setupAudioTrackSelector(mediaElement); }); } // Add error handling mediaElement.addEventListener('error', (e) => { console.error('Media playback error:', { error: mediaElement.error, code: mediaElement.error?.code, message: mediaElement.error?.message, path: path, name: name }); playerContainer.innerHTML = `
⚠️

Error playing: ${name}

${mediaElement.error?.message || 'Unknown error'}

`; }); // Add loading state mediaElement.addEventListener('loadstart', () => { console.log('Loading media:', name); }); mediaElement.addEventListener('canplay', () => { console.log('Media ready to play:', name); }); mediaElement.addEventListener('ended', () => { // Auto-advance to next item when current finishes playNextItem(); }); // URL encode the path properly const encodedPath = encodeURIComponent(path); mediaElement.src = `serve_media.php?file=${encodedPath}`; console.log('Playing file:', { name: name, path: path, encodedPath: encodedPath, fullUrl: `serve_media.php?file=${encodedPath}` }); playerContainer.innerHTML = ''; playerContainer.appendChild(mediaElement); nowPlayingTitle.textContent = name; nowPlaying.style.display = 'block'; }