// 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

`; $('nowPlaying').style.display = 'none'; resetAudioTrackUI(); }); } function setupWizardListeners() { document.querySelectorAll('[data-library]').forEach(btn => { btn.addEventListener('click', () => { selectLibrary(btn.dataset.library).catch(() => {}); }); }); $('backBtn').addEventListener('click', () => { if (state.currentPathParts.length === 0) { showLibraryPicker(); return; } state.currentPathParts.pop(); state.currentNode = getNodeAtPath(state.currentType, state.currentPathParts); renderCurrentDirectory(); }); $('rootBtn').addEventListener('click', () => { if (!state.currentType) return; goToLibraryRoot(); }); $('refreshBtn').addEventListener('click', () => { if (!state.currentType) return; selectLibrary(state.currentType, { forceRefresh: true }).catch(() => {}); }); } function initApp() { updateWizardStep(); showLibraryPicker(); const urlParams = new URLSearchParams(window.location.search); const playPath = urlParams.get('play'); if (!playPath) { return; } const ext = playPath.toLowerCase().split('.').pop(); const type = VIDEO_EXTENSIONS.includes(ext) ? 'videos' : 'music'; selectLibrary(type).then(() => { navigateToFile(playPath); }).catch(() => {}); } async function selectLibrary(type, options = {}) { const { forceRefresh = false } = options; if (!LIBRARY_LABELS[type]) return; setLibraryButtonActive(type); showBrowserStep(); const loaded = await ensureLibraryData(type, forceRefresh); if (!loaded) return; state.currentType = type; goToLibraryRoot(); } function goToLibraryRoot() { state.currentPathParts = []; state.currentNode = getNodeAtPath(state.currentType, state.currentPathParts); renderCurrentDirectory(); } async function ensureLibraryData(type, forceRefresh = false) { if (!forceRefresh && state.filesByType[type] !== null) { logMediaDebug({ type, source: 'memory' }); hideLogin(); return true; } const browser = $('fileBrowser'); browser.innerHTML = '
'; const url = `get_files.php?type=${encodeURIComponent(type)}${forceRefresh ? '&refresh=1' : ''}`; const startedAt = performance.now(); try { const resp = await authedFetch(url); const fetchedAt = performance.now(); const debugHeaders = { cacheStatus: resp.headers.get('X-Media-Cache'), backendMs: resp.headers.get('X-Media-Time-Ms'), forceRefresh: resp.headers.get('X-Media-Force-Refresh'), cacheGeneratedAt: resp.headers.get('X-Media-Cache-Generated-At') }; const data = await resp.json(); const parsedAt = performance.now(); if (data.error) { browser.innerHTML = `

${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 = `${state.currentType === 'videos' ? '🎬' : '🎵'}${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}

`; }); media.addEventListener('ended', playNextItem); if (isVideo) { media.addEventListener('loadedmetadata', () => setupAudioTrackSelector(media)); } container.innerHTML = ''; container.appendChild(media); $('nowPlayingTitle').textContent = name; $('nowPlaying').style.display = 'block'; state.step = 3; updateWizardStep(); resetAudioTrackUI(); } function playNextItem() { const files = Array.from($('fileBrowser').querySelectorAll('.file-item[data-kind="file"]')); if (files.length === 0) return; const currentIdx = files.findIndex(el => el.classList.contains('active')); if (currentIdx < 0) return; const next = files[currentIdx + 1]; if (next) { next.click(); } } function resetAudioTrackUI() { $('audioControls').style.display = 'none'; $('audioHint').style.display = 'none'; $('audioHint').textContent = ''; $('audioSelect').innerHTML = ''; } function setupAudioTrackSelector(videoEl) { const tracks = videoEl.audioTracks; if (!tracks || tracks.length <= 1) { if (!tracks) { $('audioHint').textContent = 'Audio track switching not supported for this browser.'; $('audioHint').style.display = 'block'; } return; } $('audioSelect').innerHTML = ''; for (let i = 0; i < tracks.length; i++) { const opt = document.createElement('option'); opt.value = i; const label = tracks[i].label?.trim() || `Track ${i + 1}`; opt.textContent = label; $('audioSelect').appendChild(opt); if (tracks[i].enabled) opt.selected = true; } $('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'; }