// Freax Media Player - Wizard Frontend const VIDEO_EXTENSIONS = ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v']; const LIBRARY_LABELS = { videos: 'Videos', music: 'Music' }; const state = { step: 1, currentType: null, filesByType: { videos: null, music: null }, currentNode: null, currentPathParts: [], selectedFilePath: null }; const mediaDebugEnabled = (() => { const params = new URLSearchParams(window.location.search); if (params.get('debug') === '1') return true; try { return window.localStorage.getItem('MEDIA_DEBUG') === '1'; } catch (_) { return false; } })(); const $ = id => document.getElementById(id); function logMediaDebug(details) { if (!mediaDebugEnabled) return; const { type, source, url, status, cacheStatus, backendMs, frontendFetchMs, frontendParseMs, renderMs, payloadBytes, forceRefresh, cacheGeneratedAt, error } = details; console.groupCollapsed(`[media-debug] ${type}`); if (source) console.log('source:', source); if (url) console.log('url:', url); if (typeof status !== 'undefined') console.log('status:', status); if (cacheStatus) console.log('cache:', cacheStatus); if (backendMs) console.log('backend_ms:', backendMs); if (typeof frontendFetchMs === 'number') console.log('frontend_fetch_ms:', Math.round(frontendFetchMs)); if (typeof frontendParseMs === 'number') console.log('frontend_parse_ms:', Math.round(frontendParseMs)); if (typeof renderMs === 'number') console.log('render_ms:', Math.round(renderMs)); if (typeof payloadBytes === 'number') console.log('payload_bytes:', payloadBytes); if (forceRefresh) console.log('force_refresh:', forceRefresh); if (cacheGeneratedAt) console.log('cache_generated_at:', cacheGeneratedAt); if (error) console.log('error:', error); console.groupEnd(); } function showLogin(message = '') { $('loginOverlay').style.display = 'flex'; $('logoutBtn').style.display = 'none'; $('loginError').textContent = message; $('loginError').style.display = message ? 'block' : 'none'; } function hideLogin() { $('loginOverlay').style.display = 'none'; $('loginError').style.display = 'none'; $('logoutBtn').style.display = 'inline-block'; } async function authedFetch(url, options = {}) { const resp = await fetch(url, { ...options, credentials: 'same-origin' }); if (resp.status === 401) { showLogin('Please log in to continue.'); throw new Error('Unauthorized'); } return resp; } document.addEventListener('DOMContentLoaded', () => { setupLoginHandlers(); setupWizardListeners(); initApp(); }); function setupLoginHandlers() { $('loginForm').addEventListener('submit', async (e) => { e.preventDefault(); const username = $('username').value.trim(); const password = $('password').value; $('loginError').style.display = 'none'; $('loginSpinner').style.display = 'inline-block'; 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'); } hideLogin(); initApp(); } catch (ex) { $('loginError').textContent = ex.message || 'Login failed'; $('loginError').style.display = 'block'; } finally { $('loginSpinner').style.display = 'none'; } }); $('logoutBtn').addEventListener('click', async () => { try { await fetch('logout.php', { method: 'POST', credentials: 'same-origin' }); } catch (_) {} state.step = 1; state.currentType = null; state.currentNode = null; state.currentPathParts = []; state.selectedFilePath = null; state.filesByType = { videos: null, music: null }; updateWizardStep(); showLibraryPicker(); $('fileBrowser').innerHTML = '
Choose a library to browse files.
'; showLogin('Logged out.'); $('playerContainer').innerHTML = `Select a file to start playing
${data.error}
`; logMediaDebug({ type, source: 'network', url, status: resp.status, cacheStatus: debugHeaders.cacheStatus, backendMs: debugHeaders.backendMs, frontendFetchMs: fetchedAt - startedAt, frontendParseMs: parsedAt - fetchedAt, forceRefresh: debugHeaders.forceRefresh, cacheGeneratedAt: debugHeaders.cacheGeneratedAt, error: data.error }); return false; } const normalizedTree = data.message ? {} : data; state.filesByType[type] = normalizedTree; hideLogin(); let payloadBytes = null; try { payloadBytes = JSON.stringify(data).length; } catch (_) { payloadBytes = null; } logMediaDebug({ type, source: 'network', url, status: resp.status, cacheStatus: debugHeaders.cacheStatus, backendMs: debugHeaders.backendMs, frontendFetchMs: fetchedAt - startedAt, frontendParseMs: parsedAt - fetchedAt, payloadBytes, forceRefresh: debugHeaders.forceRefresh, cacheGeneratedAt: debugHeaders.cacheGeneratedAt }); return true; } catch (error) { console.error('Error loading files:', error); logMediaDebug({ type, source: 'network', url, frontendFetchMs: performance.now() - startedAt, error: error.message || String(error) }); if (error.message !== 'Unauthorized') { browser.innerHTML = 'Error loading files
'; } return false; } } function getNodeAtPath(type, pathParts) { let node = state.filesByType[type] || {}; for (const part of pathParts) { if (!node || typeof node !== 'object' || Array.isArray(node)) { return {}; } const next = node[part]; if (!next || typeof next !== 'object' || Array.isArray(next)) { return {}; } node = next; } return node; } function renderCurrentDirectory() { const renderStartedAt = performance.now(); const browser = $('fileBrowser'); const node = state.currentNode; updateWizardStep(); updateWizardPath(); browser.innerHTML = ''; if (!node || typeof node !== 'object' || Array.isArray(node)) { browser.innerHTML = 'No entries found.
'; return; } const entries = Object.entries(node); if (entries.length === 0) { browser.innerHTML = 'No media files found.
'; return; } for (const [name, content] of entries) { const item = document.createElement('div'); item.className = 'file-item'; if (typeof content === 'object' && !Array.isArray(content)) { item.classList.add('folder-entry'); item.innerHTML = `${name}`; item.addEventListener('click', () => { state.currentPathParts.push(name); state.currentNode = content; renderCurrentDirectory(); }); } else if (typeof content === 'string') { item.dataset.kind = 'file'; item.dataset.path = content; item.dataset.name = name; item.innerHTML = `${name}`; if (content === state.selectedFilePath) { item.classList.add('active'); } item.addEventListener('click', () => { navigateToPlayback(content); }); } else { continue; } browser.appendChild(item); } logMediaDebug({ type: state.currentType, source: 'memory', renderMs: performance.now() - renderStartedAt }); } function navigateToFile(filePath) { if (!state.currentType) return; const parts = filePath.split('/').filter(Boolean); const rootName = state.currentType === 'videos' ? 'Videos' : 'Music'; if (parts[0] && parts[0].toLowerCase() === rootName.toLowerCase()) { parts.shift(); } const fileName = parts.pop(); if (!fileName) return; let node = state.filesByType[state.currentType] || {}; const folderParts = []; for (const part of parts) { if (!node || typeof node !== 'object' || Array.isArray(node)) { break; } const next = node[part]; if (!next || typeof next !== 'object' || Array.isArray(next)) { break; } folderParts.push(part); node = next; } state.currentPathParts = folderParts; state.currentNode = node; const fileValue = node[fileName]; if (typeof fileValue === 'string') { state.selectedFilePath = fileValue; playFile(fileValue, fileName); requestAnimationFrame(() => { renderCurrentDirectory(); }); return; } renderCurrentDirectory(); } function updateWizardStep() { const stepText = { 1: 'Step 1 - Choose Library', 2: 'Step 2 - Browse', 3: 'Step 3 - Play' }[state.step] || 'Step 1 - Choose Library'; $('wizardStep').textContent = stepText; } function showLibraryPicker() { state.step = 1; updateWizardStep(); $('libraryPicker').style.display = 'grid'; $('browserStep').style.display = 'none'; document.querySelectorAll('[data-library]').forEach(btn => btn.classList.remove('active')); } function showBrowserStep() { state.step = 2; updateWizardStep(); $('libraryPicker').style.display = 'none'; $('browserStep').style.display = 'grid'; } function updateWizardPath() { if (!state.currentType) return; const label = LIBRARY_LABELS[state.currentType] || state.currentType; const path = state.currentPathParts.length ? `/${state.currentPathParts.join('/')}` : '/'; $('currentLibraryLabel').textContent = label; $('breadcrumb').textContent = path; $('backBtn').disabled = state.currentPathParts.length === 0; $('rootBtn').disabled = state.currentPathParts.length === 0; } function setLibraryButtonActive(type) { document.querySelectorAll('[data-library]').forEach(btn => { btn.classList.toggle('active', btn.dataset.library === type); }); } function navigateToPlayback(path) { const params = new URLSearchParams(); params.set('play', path); const currentParams = new URLSearchParams(window.location.search); if (currentParams.get('debug') === '1') { params.set('debug', '1'); } window.location.href = `index.php?${params.toString()}`; } function buildMediaStreamUrl(path) { const normalized = String(path || '').replace(/\\/g, '/'); const encodedPath = normalized .split('/') .filter(Boolean) .map(part => encodeURIComponent(part)) .join('/'); return `/media/${encodedPath}`; } function playFile(path, name) { const container = $('playerContainer'); const ext = path.toLowerCase().split('.').pop(); const isVideo = VIDEO_EXTENSIONS.includes(ext); const media = document.createElement(isVideo ? 'video' : 'audio'); media.controls = true; media.autoplay = true; media.src = buildMediaStreamUrl(path); media.addEventListener('error', () => { container.innerHTML = `Error playing: ${name}