/** * ============================================ * AUDIOHM WEB PLAYER - OPTIMIZED * Version: 2.0 * Last Updated: 2026-01-19 * ============================================ */ // ============================================ // STATE MANAGEMENT // ============================================ const AppState = { isAuthenticated: false, currentPage: 'home', currentTrack: null, isPlaying: false, isShuffle: false, repeatMode: 'none', // none, one, all volume: 100, isMuted: false, likedTracks: new Set(), playlists: [], queue: [], queuePosition: 0, isQueuePanelOpen: false }; // ============================================ // DOM ELEMENTS // ============================================ const DOM = { // Screens loadingScreen: null, loginScreen: null, mainApp: null, // Forms loginForm: null, registerForm: null, authError: null, // Navigation sidebar: null, navItems: null, mobileMenuBtn: null, logoutBtn: null, // Pages pages: {}, // Player audioPlayer: null, playBtn: null, prevBtn: null, nextBtn: null, shuffleBtn: null, repeatBtn: null, progressBar: null, volumeBar: null, muteBtn: null, likeBtn: null, playerCover: null, playerTitle: null, playerArtist: null, playerCoverDesktop: null, playerTitleDesktop: null, playerArtistDesktop: null, mobilePlayBtn: null, mobileLikeBtn: null, currentTime: null, totalTime: null, // Queue queuePanel: null, queueList: null, queueOpenBtn: null, queueCloseBtn: null, queueShuffleBtn: null, queueClearBtn: null, queueCount: null, // Toast toastContainer: null }; // ============================================ // INITIALIZATION // ============================================ function init() { console.log('='.repeat(80)); console.log('[INIT] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[INIT] ║ AUDIOHM APPLICATION INITIALIZATION STARTING ║'); console.log('[INIT] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[INIT] Timestamp:', new Date().toISOString()); console.log('[INIT] User Agent:', navigator.userAgent); console.log('='.repeat(80)); console.log('[INIT] → Step 1: Caching DOM elements...'); cacheDOM(); console.log('[INIT] ✓ DOM elements cached'); console.log('[INIT] → Step 2: Checking authentication...'); checkAuth(); console.log('[INIT] ✓ Authentication checked'); console.log('[INIT] → Step 3: Loading queue from storage...'); loadQueueFromStorage(); console.log('[INIT] ✓ Queue loaded from storage'); console.log('[INIT] → Step 4: Setting up event listeners...'); setupEventListeners(); console.log('[INIT] ✓ Event listeners set up'); console.log('[INIT] → Step 5: Hiding loading screen...'); hideLoadingScreen(); console.log('[INIT] ✓ Loading screen hidden'); console.log('='.repeat(80)); console.log('[INIT] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[INIT] ║ AUDIOHM APPLICATION INITIALIZED SUCCESSFULLY ║'); console.log('[INIT] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[INIT] Ready for user interaction!'); console.log('='.repeat(80)); } window.cacheDOM = function() { console.log('='.repeat(80)); console.log('[cacheDOM] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[cacheDOM] ║ CACHING DOM ELEMENTS ║'); console.log('[cacheDOM] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('='.repeat(80)); console.log('[cacheDOM] → Caching screen elements...'); DOM.loadingScreen = document.getElementById('loading-screen'); console.log('[cacheDOM] ✓ loading-screen:', !!DOM.loadingScreen); DOM.loginScreen = document.getElementById('login-screen'); console.log('[cacheDOM] ✓ login-screen:', !!DOM.loginScreen); DOM.mainApp = document.getElementById('main-app'); console.log('[cacheDOM] ✓ main-app:', !!DOM.mainApp); console.log('[cacheDOM] → Caching form elements...'); DOM.loginForm = document.getElementById('login-form'); console.log('[cacheDOM] ✓ login-form:', !!DOM.loginForm); DOM.registerForm = document.getElementById('register-form'); console.log('[cacheDOM] ✓ register-form:', !!DOM.registerForm); DOM.authError = document.getElementById('auth-error'); console.log('[cacheDOM] ✓ auth-error:', !!DOM.authError); console.log('[cacheDOM] → Caching navigation elements...'); DOM.sidebar = document.getElementById('sidebar'); console.log('[cacheDOM] ✓ sidebar:', !!DOM.sidebar); DOM.navItems = document.querySelectorAll('.nav-item'); console.log('[cacheDOM] ✓ nav-items:', DOM.navItems.length); DOM.mobileMenuBtn = document.getElementById('mobile-menu-btn'); console.log('[cacheDOM] ✓ mobile-menu-btn:', !!DOM.mobileMenuBtn); DOM.logoutBtn = document.getElementById('logout-btn'); console.log('[cacheDOM] ✓ logout-btn:', !!DOM.logoutBtn); console.log('[cacheDOM] → Caching page elements...'); ['home', 'search', 'library'].forEach(page => { DOM.pages[page] = document.getElementById(`${page}-page`); console.log(`[cacheDOM] ✓ ${page}-page:`, !!DOM.pages[page]); }); console.log('[cacheDOM] → Caching audio player elements...'); DOM.audioPlayer = document.getElementById('audio-player'); console.log('[cacheDOM] ✓ audio-player:', !!DOM.audioPlayer); DOM.playBtn = document.getElementById('play-btn'); console.log('[cacheDOM] ✓ play-btn:', !!DOM.playBtn); DOM.prevBtn = document.getElementById('prev-btn'); console.log('[cacheDOM] ✓ prev-btn:', !!DOM.prevBtn); DOM.nextBtn = document.getElementById('next-btn'); console.log('[cacheDOM] ✓ next-btn:', !!DOM.nextBtn); DOM.shuffleBtn = document.getElementById('shuffle-btn'); console.log('[cacheDOM] ✓ shuffle-btn:', !!DOM.shuffleBtn); DOM.repeatBtn = document.getElementById('repeat-btn'); console.log('[cacheDOM] ✓ repeat-btn:', !!DOM.repeatBtn); DOM.progressBar = document.getElementById('progress-bar'); console.log('[cacheDOM] ✓ progress-bar:', !!DOM.progressBar); DOM.volumeBar = document.getElementById('volume-bar'); console.log('[cacheDOM] ✓ volume-bar:', !!DOM.volumeBar); DOM.muteBtn = document.getElementById('mute-btn'); console.log('[cacheDOM] ✓ mute-btn:', !!DOM.muteBtn); DOM.likeBtn = document.getElementById('like-btn'); console.log('[cacheDOM] ✓ like-btn:', !!DOM.likeBtn); console.log('[cacheDOM] → Caching player UI elements (mobile)...'); DOM.playerCover = document.getElementById('player-cover'); console.log('[cacheDOM] ✓ player-cover:', !!DOM.playerCover); DOM.playerTitle = document.getElementById('player-title'); console.log('[cacheDOM] ✓ player-title:', !!DOM.playerTitle); DOM.playerArtist = document.getElementById('player-artist'); console.log('[cacheDOM] ✓ player-artist:', !!DOM.playerArtist); console.log('[cacheDOM] → Caching player UI elements (desktop)...'); DOM.playerCoverDesktop = document.getElementById('player-cover-desktop'); console.log('[cacheDOM] ✓ player-cover-desktop:', !!DOM.playerCoverDesktop); DOM.playerTitleDesktop = document.getElementById('player-title-desktop'); console.log('[cacheDOM] ✓ player-title-desktop:', !!DOM.playerTitleDesktop); DOM.playerArtistDesktop = document.getElementById('player-artist-desktop'); console.log('[cacheDOM] ✓ player-artist-desktop:', !!DOM.playerArtistDesktop); console.log('[cacheDOM] → Caching mobile controls...'); DOM.mobilePlayBtn = document.getElementById('mobile-play-btn'); console.log('[cacheDOM] ✓ mobile-play-btn:', !!DOM.mobilePlayBtn); DOM.mobileLikeBtn = document.getElementById('mobile-like-btn'); console.log('[cacheDOM] ✓ mobile-like-btn:', !!DOM.mobileLikeBtn); console.log('[cacheDOM] → Caching time display elements...'); DOM.currentTime = document.getElementById('current-time'); console.log('[cacheDOM] ✓ current-time:', !!DOM.currentTime); DOM.totalTime = document.getElementById('total-time'); console.log('[cacheDOM] ✓ total-time:', !!DOM.totalTime); console.log('[cacheDOM] → Caching toast container...'); DOM.toastContainer = document.getElementById('toast-container'); console.log('[cacheDOM] ✓ toast-container:', !!DOM.toastContainer); console.log('[cacheDOM] → Caching queue panel elements...'); DOM.queuePanel = document.getElementById('queue-panel'); console.log('[cacheDOM] ✓ queue-panel:', !!DOM.queuePanel); DOM.queueList = document.getElementById('queue-list'); console.log('[cacheDOM] ✓ queue-list:', !!DOM.queueList); DOM.queueOpenBtn = document.getElementById('queue-open-btn'); console.log('[cacheDOM] ✓ queue-open-btn:', !!DOM.queueOpenBtn); DOM.queueCloseBtn = document.getElementById('queue-close-btn'); console.log('[cacheDOM] ✓ queue-close-btn:', !!DOM.queueCloseBtn); DOM.queueShuffleBtn = document.getElementById('queue-shuffle-btn'); console.log('[cacheDOM] ✓ queue-shuffle-btn:', !!DOM.queueShuffleBtn); DOM.queueClearBtn = document.getElementById('queue-clear-btn'); console.log('[cacheDOM] ✓ queue-clear-btn:', !!DOM.queueClearBtn); DOM.queueCount = document.getElementById('queue-count'); console.log('[cacheDOM] ✓ queue-count:', !!DOM.queueCount); console.log('='.repeat(80)); console.log('[cacheDOM] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[cacheDOM] ║ DOM ELEMENTS CACHED SUCCESSFULLY ║'); console.log('[cacheDOM] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[cacheDOM] Total DOM objects cached:', Object.keys(DOM).length); console.log('='.repeat(80)); } // ============================================ // EVENT LISTENERS // ============================================ window.setupEventListeners = function() { // Auth forms if (DOM.loginForm) { DOM.loginForm.addEventListener('submit', handleLogin); } if (DOM.registerForm) { DOM.registerForm.addEventListener('submit', handleRegister); } // Show/hide register forms const showRegister = document.getElementById('show-register'); const showLogin = document.getElementById('show-login'); if (showRegister) { showRegister.addEventListener('click', (e) => { e.preventDefault(); DOM.loginForm.classList.add('hidden'); DOM.registerForm.classList.remove('hidden'); }); } if (showLogin) { showLogin.addEventListener('click', (e) => { e.preventDefault(); DOM.registerForm.classList.add('hidden'); DOM.loginForm.classList.remove('hidden'); }); } // Navigation DOM.navItems.forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); const page = item.dataset.page; navigateTo(page); }); }); // Mobile menu if (DOM.mobileMenuBtn) { DOM.mobileMenuBtn.addEventListener('click', toggleMobileMenu); } // Logout if (DOM.logoutBtn) { DOM.logoutBtn.addEventListener('click', handleLogout); } // Search functionality const quickSearchBtn = document.getElementById('quick-search-btn'); const quickSearchInput = document.getElementById('quick-search'); const searchBtn = document.getElementById('search-btn'); const searchInput = document.getElementById('search-input'); // Quick search button click if (quickSearchBtn) { quickSearchBtn.addEventListener('click', handleQuickSearch); } // Quick search Enter key if (quickSearchInput) { quickSearchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleQuickSearch(); } }); } // Main search button click if (searchBtn) { searchBtn.addEventListener('click', handleMainSearch); } // Main search Enter key if (searchInput) { searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleMainSearch(); } }); } // Player controls setupPlayerControls(); // Playlist management const createPlaylistBtn = document.getElementById('create-playlist-btn'); if (createPlaylistBtn) { createPlaylistBtn.addEventListener('click', showCreatePlaylistModal); } const createPlaylistForm = document.getElementById('create-playlist-form'); if (createPlaylistForm) { createPlaylistForm.addEventListener('submit', createPlaylist); } const closeCreatePlaylistModal = document.getElementById('close-create-playlist-modal'); if (closeCreatePlaylistModal) { closeCreatePlaylistModal.addEventListener('click', hideCreatePlaylistModal); } const cancelCreatePlaylist = document.getElementById('cancel-create-playlist'); if (cancelCreatePlaylist) { cancelCreatePlaylist.addEventListener('click', hideCreatePlaylistModal); } const closePlaylistDetails = document.getElementById('close-playlist-details'); if (closePlaylistDetails) { closePlaylistDetails.addEventListener('click', hidePlaylistDetails); } const playPlaylistBtn = document.getElementById('play-playlist-btn'); if (playPlaylistBtn) { playPlaylistBtn.addEventListener('click', () => { if (window.currentPlaylistId) { playPlaylist(window.currentPlaylistId, false); } }); } const shufflePlaylistBtn = document.getElementById('shuffle-playlist-btn'); if (shufflePlaylistBtn) { shufflePlaylistBtn.addEventListener('click', () => { if (window.currentPlaylistId) { playPlaylist(window.currentPlaylistId, true); } }); } // Close dropdowns when clicking outside document.addEventListener('click', (e) => { if (!e.target.closest('[id^="playlist-dropdown-"]') && !e.target.closest('button')) { document.querySelectorAll('[id^="playlist-dropdown-"]').forEach(dropdown => { dropdown.classList.add('hidden'); }); } }); // Close dropdowns when scrolling document.addEventListener('scroll', (e) => { document.querySelectorAll('[id^="playlist-dropdown-"]').forEach(dropdown => { dropdown.classList.add('hidden'); }); }, true); // Close modals with Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { hideCreatePlaylistModal(); hidePlaylistDetails(); } }); } window.setupPlayerControls = function() { // Play/Pause if (DOM.playBtn) { DOM.playBtn.addEventListener('click', togglePlayPause); } // Mobile Play/Pause if (DOM.mobilePlayBtn) { DOM.mobilePlayBtn.addEventListener('click', togglePlayPause); } // Previous/Next if (DOM.prevBtn) { DOM.prevBtn.addEventListener('click', playPrevious); } if (DOM.nextBtn) { DOM.nextBtn.addEventListener('click', playNext); } // Shuffle if (DOM.shuffleBtn) { DOM.shuffleBtn.addEventListener('click', toggleShuffle); } // Repeat if (DOM.repeatBtn) { DOM.repeatBtn.addEventListener('click', toggleRepeat); } // Progress bar if (DOM.progressBar) { DOM.progressBar.addEventListener('input', handleSeek); } // Volume if (DOM.volumeBar) { DOM.volumeBar.addEventListener('input', handleVolumeChange); } // Mute if (DOM.muteBtn) { DOM.muteBtn.addEventListener('click', toggleMute); } // Like if (DOM.likeBtn) { DOM.likeBtn.addEventListener('click', toggleLike); } // Mobile Like if (DOM.mobileLikeBtn) { DOM.mobileLikeBtn.addEventListener('click', toggleLike); } // Audio events if (DOM.audioPlayer) { DOM.audioPlayer.addEventListener('timeupdate', updateProgress); DOM.audioPlayer.addEventListener('loadedmetadata', updateDuration); DOM.audioPlayer.addEventListener('ended', handleTrackEnd); } // Queue panel controls if (DOM.queueOpenBtn) { DOM.queueOpenBtn.addEventListener('click', openQueuePanel); } if (DOM.queueCloseBtn) { DOM.queueCloseBtn.addEventListener('click', closeQueuePanel); } if (DOM.queueShuffleBtn) { DOM.queueShuffleBtn.addEventListener('click', shuffleQueue); } if (DOM.queueClearBtn) { DOM.queueClearBtn.addEventListener('click', clearQueue); } } // ============================================ // AUTHENTICATION // ============================================ window.checkAuth = async function() { const token = localStorage.getItem('token'); if (!token) { showScreen('login'); return; } try { const response = await fetch('/api/v1/auth/me', { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { AppState.isAuthenticated = true; showScreen('main'); loadUserData(); } else { localStorage.removeItem('token'); showScreen('login'); } } catch (error) { console.error('Auth check failed:', error); showScreen('login'); } } window.handleLogin = async function(e) { e.preventDefault(); const email = document.getElementById('login-email').value; const password = document.getElementById('login-password').value; try { const response = await fetch('/api/v1/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }); const data = await response.json(); if (response.ok) { localStorage.setItem('token', data.access_token); AppState.isAuthenticated = true; showScreen('main'); showToast('Connexion réussie!', 'success'); } else { showError(data.detail || 'Email ou mot de passe incorrect'); } } catch (error) { console.error('Login failed:', error); showError('Erreur de connexion'); } } window.handleRegister = async function(e) { e.preventDefault(); const username = document.getElementById('register-username').value; const email = document.getElementById('register-email').value; const password = document.getElementById('register-password').value; try { const response = await fetch('/api/v1/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, email, password }) }); const data = await response.json(); if (response.ok) { localStorage.setItem('token', data.access_token); AppState.isAuthenticated = true; showScreen('main'); showToast('Compte créé avec succès!', 'success'); } else { showError(data.detail || 'Erreur lors de la création du compte'); } } catch (error) { console.error('Register failed:', error); showError('Erreur de connexion'); } } window.handleLogout = function() { localStorage.removeItem('token'); AppState.isAuthenticated = false; showScreen('login'); showToast('Déconnexion réussie', 'success'); } // ============================================ // NAVIGATION // ============================================ window.navigateTo = function(page) { // Update active nav item DOM.navItems.forEach(item => { const isActive = item.dataset.page === page; item.classList.remove('active'); item.removeAttribute('aria-current'); if (isActive) { item.classList.add('active'); item.setAttribute('aria-current', 'page'); } }); // Show/hide pages Object.keys(DOM.pages).forEach(key => { if (key === page) { DOM.pages[key].classList.remove('hidden'); DOM.pages[key].classList.add('active'); } else { DOM.pages[key].classList.add('hidden'); DOM.pages[key].classList.remove('active'); } }); AppState.currentPage = page; // Close mobile menu const sidebar = DOM.sidebar; sidebar.classList.remove('open'); sidebar.classList.add('-translate-x-full'); // Update mobile menu button if (DOM.mobileMenuBtn) { DOM.mobileMenuBtn.setAttribute('aria-expanded', 'false'); DOM.mobileMenuBtn.setAttribute('aria-label', 'Ouvrir le menu'); } // Focus management for accessibility const mainContent = document.getElementById('main-content'); if (mainContent) { mainContent.focus(); } } /** * Switch between library tabs (Playlists, Liked, History) * @param {string} tabName - The tab name to switch to ('playlists', 'liked', 'history') */ window.switchLibraryTab = function(tabName) { console.log('='.repeat(80)); console.log('[switchLibraryTab] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[switchLibraryTab] ║ SWITCHLIBRARYTAB FUNCTION CALLED ║'); console.log('[switchLibraryTab] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[switchLibraryTab] Timestamp:', new Date().toISOString()); console.log('[switchLibraryTab] Tab to switch to:', tabName); console.log('='.repeat(80)); const validTabs = ['playlists', 'liked', 'history']; if (!validTabs.includes(tabName)) { console.error('[switchLibraryTab] ✗ Invalid tab name:', tabName); return; } console.log('[switchLibraryTab] ✓ Tab name is valid'); // Update tab buttons console.log('[switchLibraryTab] → Updating tab buttons...'); document.querySelectorAll('.library-tab').forEach(tab => { const isActive = tab.id === `tab-${tabName}`; console.log('[switchLibraryTab] → Tab:', tab.id, 'active:', isActive); tab.classList.remove('active'); tab.setAttribute('aria-selected', 'false'); if (isActive) { tab.classList.add('active'); tab.setAttribute('aria-selected', 'true'); } }); console.log('[switchLibraryTab] ✓ Tab buttons updated'); // Update tab panels console.log('[switchLibraryTab] → Updating tab panels...'); document.querySelectorAll('.tab-panel').forEach(panel => { const isActive = panel.id === `library-${tabName}`; console.log('[switchLibraryTab] → Panel:', panel.id, 'active:', isActive); panel.classList.remove('active'); panel.classList.add('hidden'); if (isActive) { panel.classList.add('active'); panel.classList.remove('hidden'); } }); console.log('[switchLibraryTab] ✓ Tab panels updated'); console.log('[switchLibraryTab] ✓ Tab switched successfully to:', tabName); console.log('='.repeat(80)); } window.toggleMobileMenu = function() { const sidebar = DOM.sidebar; const isOpen = sidebar.classList.contains('open') || !sidebar.classList.contains('-translate-x-full'); if (isOpen) { sidebar.classList.remove('open'); sidebar.classList.add('-translate-x-full'); DOM.mobileMenuBtn?.setAttribute('aria-expanded', 'false'); DOM.mobileMenuBtn?.setAttribute('aria-label', 'Ouvrir le menu'); } else { sidebar.classList.add('open'); sidebar.classList.remove('-translate-x-full'); DOM.mobileMenuBtn?.setAttribute('aria-expanded', 'true'); DOM.mobileMenuBtn?.setAttribute('aria-label', 'Fermer le menu'); } } // Close menu when clicking outside document.addEventListener('click', (e) => { if (!DOM.sidebar?.contains(e.target) && !DOM.mobileMenuBtn?.contains(e.target)) { DOM.sidebar?.classList.remove('open'); } }); // ============================================ // PLAYER CONTROLS // ============================================ window.togglePlayPause = function() { console.log('='.repeat(80)); console.log('[togglePlayPause] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[togglePlayPause] ║ TOGGLEPLAYPAUSE FUNCTION CALLED ║'); console.log('[togglePlayPause] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[togglePlayPause] Timestamp:', new Date().toISOString()); if (!DOM.audioPlayer) { console.error('[togglePlayPause] ✗ Audio player NOT found!'); return; } console.log('[togglePlayPause] ✓ Audio player found'); console.log('[togglePlayPause] → Checking if paused...'); console.log('[togglePlayPause] paused:', DOM.audioPlayer.paused); console.log('[togglePlayPause] currentTime:', DOM.audioPlayer.currentTime); console.log('[togglePlayPause] duration:', DOM.audioPlayer.duration); if (DOM.audioPlayer.paused) { console.log('[togglePlayPause] → Audio is paused, playing...'); DOM.audioPlayer.play(); updatePlayButton(true); console.log('[togglePlayPause] ✓ Play command sent'); } else { console.log('[togglePlayPause] → Audio is playing, pausing...'); DOM.audioPlayer.pause(); updatePlayButton(false); console.log('[togglePlayPause] ✓ Pause command sent'); } console.log('='.repeat(80)); } window.updatePlayButton = function(isPlaying) { console.log('='.repeat(80)); console.log('[updatePlayButton] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[updatePlayButton] ║ UPDATEPLAYBUTTON FUNCTION CALLED ║'); console.log('[updatePlayButton] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[updatePlayButton] Timestamp:', new Date().toISOString()); console.log('[updatePlayButton] Parameter:', { isPlaying }); console.log('='.repeat(80)); // Update desktop play button console.log('[updatePlayButton] → Updating desktop play button...'); const icon = DOM.playBtn?.querySelector('i'); if (icon) { console.log('[updatePlayButton] ✓ Desktop button icon found'); console.log('[updatePlayButton] Current classes:', icon.className); if (isPlaying) { console.log('[updatePlayButton] → Switching to PAUSE icon'); icon.classList.remove('fa-play'); icon.classList.add('fa-pause'); DOM.playBtn?.setAttribute('aria-label', 'Pause'); DOM.playBtn?.setAttribute('aria-pressed', 'true'); console.log('[updatePlayButton] ✓ Desktop button updated to pause'); } else { console.log('[updatePlayButton] → Switching to PLAY icon'); icon.classList.remove('fa-pause'); icon.classList.add('fa-play'); DOM.playBtn?.setAttribute('aria-label', 'Lecture'); DOM.playBtn?.setAttribute('aria-pressed', 'false'); console.log('[updatePlayButton] ✓ Desktop button updated to play'); } } else { console.warn('[updatePlayButton] ✗ Desktop button icon NOT found'); } // Update mobile play button console.log('[updatePlayButton] → Updating mobile play button...'); const mobileIcon = DOM.mobilePlayBtn?.querySelector('i'); if (mobileIcon) { console.log('[updatePlayButton] ✓ Mobile button icon found'); console.log('[updatePlayButton] Current classes:', mobileIcon.className); if (isPlaying) { console.log('[updatePlayButton] → Switching to PAUSE icon (mobile)'); mobileIcon.classList.remove('fa-play'); mobileIcon.classList.add('fa-pause'); DOM.mobilePlayBtn?.setAttribute('aria-label', 'Pause'); DOM.mobilePlayBtn?.setAttribute('aria-pressed', 'true'); console.log('[updatePlayButton] ✓ Mobile button updated to pause'); } else { console.log('[updatePlayButton] → Switching to PLAY icon (mobile)'); mobileIcon.classList.remove('fa-pause'); mobileIcon.classList.add('fa-play'); DOM.mobilePlayBtn?.setAttribute('aria-label', 'Lecture'); DOM.mobilePlayBtn?.setAttribute('aria-pressed', 'false'); console.log('[updatePlayButton] ✓ Mobile button updated to play'); } } else { console.warn('[updatePlayButton] ✗ Mobile button icon NOT found'); } console.log('='.repeat(80)); console.log('[updatePlayButton] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[updatePlayButton] ║ UPDATEPLAYBUTTON FUNCTION COMPLETED ║'); console.log('[updatePlayButton] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('='.repeat(80)); } window.playPrevious = function() { console.log('='.repeat(80)); console.log('[playPrevious] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[playPrevious] ║ PLAYPREVIOUS FUNCTION CALLED ║'); console.log('[playPrevious] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[playPrevious] Timestamp:', new Date().toISOString()); console.log('[playPrevious] Queue position:', AppState.queuePosition); console.log('[playPrevious] Queue length:', AppState.queue.length); console.log('='.repeat(80)); if (AppState.queue.length === 0) { console.warn('[playPrevious] ✗ Queue is empty'); showToast('File d\'attente vide', 'info'); return; } // If we're more than 3 seconds into the track, restart it if (DOM.audioPlayer && DOM.audioPlayer.currentTime > 3) { console.log('[playPrevious] → Restarting current track (more than 3 seconds played)'); DOM.audioPlayer.currentTime = 0; return; } // Move to previous track if (AppState.queuePosition > 0) { console.log('[playPrevious] → Moving to previous track'); AppState.queuePosition--; console.log('[playPrevious] New position:', AppState.queuePosition); const track = AppState.queue[AppState.queuePosition]; console.log('[playPrevious] Track to play:', track); if (track) { // Determine if this is a YouTube track const isYoutubeTrack = !!track.youtube_id; const trackId = track.youtube_id || track.id; console.log('[playPrevious] → Calling playTrack with skipQueuePositionUpdate=true...'); playTrack(trackId, isYoutubeTrack, true); console.log('[playPrevious] ✓ Previous track playing'); } else { console.error('[playPrevious] ✗ Track not found at position', AppState.queuePosition); } } else { console.log('[playPrevious] → Already at first track, restarting'); if (DOM.audioPlayer) { DOM.audioPlayer.currentTime = 0; } } updateQueueUI(); console.log('='.repeat(80)); } window.updateLikeButtonState = function(trackId, isLiked) { // Update desktop button if (DOM.likeBtn) { const icon = DOM.likeBtn.querySelector('i'); if (icon) { if (isLiked) { DOM.likeBtn.classList.add('text-accent-400'); icon.classList.remove('far'); icon.classList.add('fas'); } else { DOM.likeBtn.classList.remove('text-accent-400'); icon.classList.remove('fas'); icon.classList.add('far'); } } } // Update mobile button if (DOM.mobileLikeBtn) { const mobileIcon = DOM.mobileLikeBtn.querySelector('i'); if (mobileIcon) { if (isLiked) { DOM.mobileLikeBtn.classList.add('text-accent-400'); mobileIcon.classList.remove('far'); mobileIcon.classList.add('fas'); } else { DOM.mobileLikeBtn.classList.remove('text-accent-400'); mobileIcon.classList.remove('fas'); mobileIcon.classList.add('far'); } } } } window.playNext = function() { console.log('='.repeat(80)); console.log('[playNext] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[playNext] ║ PLAYNEXT FUNCTION CALLED ║'); console.log('[playNext] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[playNext] Timestamp:', new Date().toISOString()); console.log('[playNext] Queue position:', AppState.queuePosition); console.log('[playNext] Queue length:', AppState.queue.length); console.log('[playNext] Repeat mode:', AppState.repeatMode); console.log('='.repeat(80)); if (AppState.queue.length === 0) { console.warn('[playNext] ✗ Queue is empty'); showToast('File d\'attente vide', 'info'); return; } // Move to next track if (AppState.queuePosition < AppState.queue.length - 1) { console.log('[playNext] → Moving to next track'); AppState.queuePosition++; console.log('[playNext] New position:', AppState.queuePosition); const track = AppState.queue[AppState.queuePosition]; console.log('[playNext] Track to play:', track); if (track) { // Determine if this is a YouTube track const isYoutubeTrack = !!track.youtube_id; const trackId = track.youtube_id || track.id; console.log('[playNext] → Calling playTrack with skipQueuePositionUpdate=true...'); playTrack(trackId, isYoutubeTrack, true); console.log('[playNext] ✓ Next track playing'); } else { console.error('[playNext] ✗ Track not found at position', AppState.queuePosition); } } else { // At the end of queue if (AppState.repeatMode === 'all') { console.log('[playNext] → Repeat all mode, going back to start'); AppState.queuePosition = 0; const track = AppState.queue[0]; if (track) { const isYoutubeTrack = !!track.youtube_id; const trackId = track.youtube_id || track.id; console.log('[playNext] → Calling playTrack with skipQueuePositionUpdate=true...'); playTrack(trackId, isYoutubeTrack, true); } } else { console.log('[playNext] → End of queue, stopping playback'); updatePlayButton(false); showToast('Fin de la file d\'attente', 'info'); } } updateQueueUI(); console.log('='.repeat(80)); } window.toggleShuffle = function() { AppState.isShuffle = !AppState.isShuffle; if (DOM.shuffleBtn) { DOM.shuffleBtn.classList.toggle('active', AppState.isShuffle); DOM.shuffleBtn.classList.toggle('text-primary-400', AppState.isShuffle); DOM.shuffleBtn.setAttribute('aria-pressed', AppState.isShuffle.toString()); } showToast(AppState.isShuffle ? 'Aléatoire activé' : 'Aléatoire désactivé', 'success'); } window.toggleRepeat = function() { const modes = ['none', 'all', 'one']; const currentIndex = modes.indexOf(AppState.repeatMode); const nextIndex = (currentIndex + 1) % modes.length; AppState.repeatMode = modes[nextIndex]; if (DOM.repeatBtn) { DOM.repeatBtn.classList.remove('active', 'text-primary-400'); if (AppState.repeatMode !== 'none') { DOM.repeatBtn.classList.add('active', 'text-primary-400'); } DOM.repeatBtn.setAttribute('aria-pressed', (AppState.repeatMode !== 'none').toString()); } const messages = { none: 'Répétition désactivée', all: 'Répétition de toutes les pistes', one: 'Répétition de la piste actuelle' }; showToast(messages[AppState.repeatMode], 'success'); } window.handleSeek = function() { if (!DOM.audioPlayer || !DOM.progressBar) return; const time = (DOM.progressBar.value / 100) * DOM.audioPlayer.duration; DOM.audioPlayer.currentTime = time; } window.handleVolumeChange = function() { if (!DOM.audioPlayer || !DOM.volumeBar) return; AppState.volume = DOM.volumeBar.value; DOM.audioPlayer.volume = AppState.volume / 100; AppState.isMuted = false; updateVolumeIcon(); } window.toggleMute = function() { if (!DOM.audioPlayer) return; AppState.isMuted = !AppState.isMuted; DOM.audioPlayer.muted = AppState.isMuted; updateVolumeIcon(); if (DOM.muteBtn) { DOM.muteBtn.setAttribute('aria-pressed', AppState.isMuted.toString()); const labels = { true: 'Activer le son', false: 'Couper le son' }; DOM.muteBtn.setAttribute('aria-label', labels[AppState.isMuted]); } } window.updateVolumeIcon = function() { const icon = DOM.muteBtn?.querySelector('i'); if (!icon) return; icon.className = 'fas'; if (AppState.isMuted || AppState.volume === 0) { icon.classList.add('fa-volume-mute'); } else if (AppState.volume < 50) { icon.classList.add('fa-volume-down'); } else { icon.classList.add('fa-volume-up'); } // Update ARIA valuetext for volume slider if (DOM.volumeBar) { DOM.volumeBar.setAttribute('aria-valuenow', AppState.volume.toString()); DOM.volumeBar.setAttribute('aria-valuetext', `${AppState.volume}%`); } } window.toggleLike = function() { console.log('='.repeat(80)); console.log('[toggleLike] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[toggleLike] ║ TOGGLELIKE FUNCTION CALLED (PLAYER BUTTON) ║'); console.log('[toggleLike] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[toggleLike] Timestamp:', new Date().toISOString()); console.log('='.repeat(80)); if (!DOM.likeBtn && !DOM.mobileLikeBtn) { console.error('[toggleLike] ✗ No like button found'); return; } // Use either desktop or mobile button const btn = DOM.likeBtn || DOM.mobileLikeBtn; const trackId = btn?.dataset.trackId; if (!trackId) { console.error('[toggleLike] ✗ No track ID found in button dataset'); return; } console.log('[toggleLike] ✓ Track ID found:', trackId); // Call the API function console.log('[toggleLike] → Calling toggleLikeTrack API function...'); toggleLikeTrack(trackId); console.log('[toggleLike] ✓ toggleLikeTrack called'); console.log('='.repeat(80)); } window.updateProgress = function() { if (!DOM.audioPlayer || !DOM.progressBar) return; const progress = (DOM.audioPlayer.currentTime / DOM.audioPlayer.duration) * 100; DOM.progressBar.value = progress; // Update ARIA attributes for progress bar DOM.progressBar.setAttribute('aria-valuenow', Math.round(progress).toString()); DOM.progressBar.setAttribute('aria-valuetext', `${Math.round(progress)}%`); if (DOM.currentTime) { DOM.currentTime.textContent = formatTime(DOM.audioPlayer.currentTime); } } window.updateDuration = function() { if (!DOM.audioPlayer || !DOM.totalTime) return; DOM.totalTime.textContent = formatTime(DOM.audioPlayer.duration); } window.handleTrackEnd = function() { console.log('='.repeat(80)); console.log('[handleTrackEnd] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[handleTrackEnd] ║ HANDLETRACKEND FUNCTION CALLED ║'); console.log('[handleTrackEnd] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[handleTrackEnd] Timestamp:', new Date().toISOString()); console.log('[handleTrackEnd] Repeat mode:', AppState.repeatMode); console.log('='.repeat(80)); if (AppState.repeatMode === 'one') { console.log('[handleTrackEnd] → Repeat one mode, restarting track'); DOM.audioPlayer.currentTime = 0; DOM.audioPlayer.play(); } else { console.log('[handleTrackEnd] → Playing next track in queue'); playNext(); } console.log('='.repeat(80)); } // ============================================ // UTILITY FUNCTIONS // ============================================ window.formatTime = function(seconds) { if (!seconds || isNaN(seconds)) return '0:00'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; } window.showScreen = function(screen) { if (DOM.loadingScreen) DOM.loadingScreen.classList.add('hidden'); if (DOM.loginScreen) DOM.loginScreen.classList.toggle('hidden', screen !== 'login'); if (DOM.mainApp) { DOM.mainApp.classList.toggle('hidden', screen !== 'main'); if (screen === 'main') { DOM.mainApp.classList.add('visible'); } } // Show/hide player based on authentication const player = document.getElementById('player'); if (player) { if (screen === 'main') { player.classList.remove('hidden'); } else { player.classList.add('hidden'); } } } window.hideLoadingScreen = function() { if (DOM.loadingScreen) { setTimeout(() => { DOM.loadingScreen.style.display = 'none'; }, 500); } } window.showError = function(message) { if (DOM.authError) { DOM.authError.textContent = message; DOM.authError.classList.remove('hidden'); } } window.loadUserData = async function() { console.log('='.repeat(80)); console.log('[loadUserData] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[loadUserData] ║ LOADING USER DATA ║'); console.log('[loadUserData] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[loadUserData] Timestamp:', new Date().toISOString()); console.log('='.repeat(80)); console.log('[loadUserData] → Loading playlists...'); await loadPlaylists(); console.log('[loadUserData] ✓ Playlists loaded'); console.log('[loadUserData] → Loading trending tracks...'); await loadTrendingTracks(); console.log('[loadUserData] ✓ Trending tracks loaded'); console.log('[loadUserData] → Loading liked tracks...'); await loadLikedTracks(); console.log('[loadUserData] ✓ Liked tracks loaded'); console.log('[loadUserData] → Loading listening history...'); await loadListeningHistory(); console.log('[loadUserData] ✓ Listening history loaded'); console.log('[loadUserData] ✓ All user data loaded successfully'); console.log('='.repeat(80)); } window.loadPlaylists = async function() { console.log('='.repeat(80)); console.log('[loadPlaylists] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[loadPlaylists] ║ LOADING USER PLAYLISTS ║'); console.log('[loadPlaylists] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[loadPlaylists] Timestamp:', new Date().toISOString()); console.log('='.repeat(80)); const container = document.getElementById('my-playlists'); if (!container) { console.error('[loadPlaylists] ✗ Container not found'); return; } console.log('[loadPlaylists] ✓ Container found'); try { console.log('[loadPlaylists] → Fetching playlists from API...'); const token = localStorage.getItem('token'); const response = await fetch('/api/v1/playlists', { headers: { 'Authorization': `Bearer ${token}` } }); console.log('[loadPlaylists] → Response status:', response.status); if (response.ok) { const playlists = await response.json(); console.log('[loadPlaylists] ✓ Playlists loaded:', playlists.length); AppState.playlists = playlists; renderPlaylists(playlists); console.log('[loadPlaylists] ✓ Playlists rendered'); } else { const error = await response.json(); console.error('[loadPlaylists] ✗ Error loading playlists:', error); container.innerHTML = `

Erreur de chargement

${error.detail || 'Impossible de charger les playlists'}

`; } } catch (error) { console.error('[loadPlaylists] ✗ Exception:', error); container.innerHTML = `

Erreur de connexion

Vérifiez votre connexion internet

`; } console.log('='.repeat(80)); console.log('[loadPlaylists] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[loadPlaylists] ║ LOADPLAYLISTS FUNCTION COMPLETED ║'); console.log('[loadPlaylists] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('='.repeat(80)); } window.renderPlaylists = function(playlists) { console.log('='.repeat(80)); console.log('[renderPlaylists] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[renderPlaylists] ║ RENDERPLAYLISTS FUNCTION STARTED ║'); console.log('[renderPlaylists] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[renderPlaylists] Timestamp:', new Date().toISOString()); console.log('[renderPlaylists] Playlists to render:', playlists.length); console.log('='.repeat(80)); const container = document.getElementById('my-playlists'); if (!container) { console.error('[renderPlaylists] ✗ Container not found'); return; } console.log('[renderPlaylists] ✓ Container found'); if (playlists.length === 0) { console.log('[renderPlaylists] → No playlists to render'); container.innerHTML = `

Aucune playlist

Créez votre première playlist pour commencer

`; console.log('[renderPlaylists] ✓ Empty state rendered'); return; } console.log('[renderPlaylists] → Rendering playlist cards...'); container.innerHTML = playlists.map((playlist, index) => { console.log(`[renderPlaylists] ┌─ Playlist #${index + 1}: ${playlist.name}`); console.log(`[renderPlaylists] │ ID: ${playlist.id}`); console.log(`[renderPlaylists] │ Description: ${playlist.description || 'none'}`); console.log(`[renderPlaylists] │ Image: ${playlist.image_url || 'default'}`); // Generate gradient based on playlist name for visual variety const gradients = [ 'from-purple-500 to-pink-500', 'from-blue-500 to-cyan-500', 'from-green-500 to-teal-500', 'from-orange-500 to-red-500', 'from-indigo-500 to-purple-500', 'from-yellow-500 to-orange-500' ]; const gradientIndex = index % gradients.length; const gradientClass = gradients[gradientIndex]; // Use provided image or create gradient placeholder const coverImage = playlist.image_url || null; const coverStyle = coverImage ? `background-image: url('${coverImage}'); background-size: cover; background-position: center;` : `background: linear-gradient(135deg, var(--tw-gradient-stops));`; return `

${playlist.name}

${playlist.description || 'Aucune description'}

${playlist.track_count || 0} piste${(playlist.track_count || 0) !== 1 ? 's' : ''}

`; }).join(''); console.log('[renderPlaylists] ✓ All playlists rendered'); console.log('='.repeat(80)); console.log('[renderPlaylists] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[renderPlaylists] ║ RENDERPLAYLISTS FUNCTION COMPLETED ║'); console.log('[renderPlaylists] ╚════════════════════════════════════════════════════════════════════════╝'); // ============================================ // LIKED TRACKS FUNCTIONALITY // ============================================ /** * Load liked tracks from the API * @async * @returns {Promise} */ window.loadLikedTracks = async function() { console.log('='.repeat(80)); console.log('[loadLikedTracks] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[loadLikedTracks] ║ LOADLIKEDTRACKS FUNCTION STARTED ║'); console.log('[loadLikedTracks] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[loadLikedTracks] Timestamp:', new Date().toISOString()); console.log('='.repeat(80)); const container = document.getElementById('liked-tracks'); if (!container) { console.error('[loadLikedTracks] ✗ Container liked-tracks not found'); return; } console.log('[loadLikedTracks] ✓ Container found:', container.id); try { console.log('[loadLikedTracks] → Getting token from localStorage...'); const token = localStorage.getItem('token'); if (!token) { console.error('[loadLikedTracks] ✗ No token found in localStorage'); throw new Error('No authentication token'); } console.log('[loadLikedTracks] ✓ Token found'); console.log('[loadLikedTracks] → Fetching liked tracks from API...'); console.log('[loadLikedTracks] → Endpoint: GET /api/v1/library/liked-tracks'); const response = await fetch('/api/v1/library/liked-tracks', { headers: { 'Authorization': `Bearer ${token}` } }); console.log('[loadLikedTracks] → Response status:', response.status); console.log('[loadLikedTracks] → Response ok:', response.ok); if (response.ok) { const likedTracks = await response.json(); console.log('[loadLikedTracks] ✓ Liked tracks loaded:', likedTracks.length, 'tracks'); // Update AppState.likedTracks Set console.log('[loadLikedTracks] → Updating AppState.likedTracks Set...'); AppState.likedTracks.clear(); likedTracks.forEach(track => { const trackId = track.youtube_id || track.id; AppState.likedTracks.add(String(trackId)); console.log('[loadLikedTracks] ✓ Added to Set:', trackId); }); console.log('[loadLikedTracks] ✓ AppState.likedTracks updated:', AppState.likedTracks.size, 'tracks'); // Render liked tracks UI console.log('[loadLikedTracks] → Rendering liked tracks UI...'); updateLikedTracksUI(likedTracks); console.log('[loadLikedTracks] ✓ Liked tracks UI rendered'); } else if (response.status === 401) { console.warn('[loadLikedTracks] ⚠ Session expired - skipping liked tracks load'); return; } else { console.error('[loadLikedTracks] ✗ Failed to load liked tracks'); console.error('[loadLikedTracks] → Status:', response.status); console.error('[loadLikedTracks] → Status text:', response.statusText); throw new Error('Failed to load liked tracks'); } } catch (error) { console.error('[loadLikedTracks] ✗ Error loading liked tracks:', error); console.error('[loadLikedTracks] → Error name:', error.name); console.error('[loadLikedTracks] → Error message:', error.message); console.error('[loadLikedTracks] → Error stack:', error.stack); if (container) { container.innerHTML = `

Erreur de chargement des titres likés

${error.message}

`; } } console.log('='.repeat(80)); console.log('[loadLikedTracks] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[loadLikedTracks] ║ LOADLIKEDTRACKS FUNCTION COMPLETED ║'); console.log('[loadLikedTracks] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('='.repeat(80)); } /** * Update the liked tracks UI * @param {Array} likedTracks - Array of liked track objects */ window.updateLikedTracksUI = function(likedTracks) { console.log('='.repeat(80)); console.log('[updateLikedTracksUI] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[updateLikedTracksUI] ║ UPDATELIKEDTRACKSUI FUNCTION CALLED ║'); console.log('[updateLikedTracksUI] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[updateLikedTracksUI] Timestamp:', new Date().toISOString()); console.log('[updateLikedTracksUI] Liked tracks count:', likedTracks.length); console.log('='.repeat(80)); const container = document.getElementById('liked-tracks'); if (!container) { console.error('[updateLikedTracksUI] ✗ Container liked-tracks not found'); return; } console.log('[updateLikedTracksUI] ✓ Container found'); if (!likedTracks || likedTracks.length === 0) { console.log('[updateLikedTracksUI] → No liked tracks to display'); container.innerHTML = `

Aucun titre liké pour le moment

Cliquez sur le cœur pour ajouter des titres

`; console.log('[updateLikedTracksUI] ✓ Empty state rendered'); return; } console.log('[updateLikedTracksUI] → Rendering liked tracks...'); container.innerHTML = likedTracks.map(track => { // Handle nested track object from API const trackInfo = track.track || track; const trackId = trackInfo.youtube_id || trackInfo.id; const title = trackInfo.title || 'Titre inconnu'; const artist = trackInfo.artist ? trackInfo.artist.name : (trackInfo.artist_name || 'Artiste inconnu'); const cover = trackInfo.image_url || trackInfo.cover || '/static/img/default-cover.png'; const isYoutube = !!trackInfo.youtube_id; console.log('[updateLikedTracksUI] → Rendering track:', { id: trackId, title: title, artist: artist, isYoutube: isYoutube, hasTrack: !!track.track }); return `
${title}

${title}

${artist}

`; }).join(''); console.log('[updateLikedTracksUI] ✓ Liked tracks rendered:', likedTracks.length, 'tracks'); console.log('='.repeat(80)); } /** * Toggle like status for a track (called from UI) * @param {string} trackId - The track ID to toggle * @async */ window.toggleLikeTrack = async function(trackId) { console.log('='.repeat(80)); console.log('[toggleLikeTrack] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[toggleLikeTrack] ║ TOGGLELIKETRACK FUNCTION CALLED ║'); console.log('[toggleLikeTrack] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[toggleLikeTrack] Timestamp:', new Date().toISOString()); console.log('[toggleLikeTrack] Track ID:', trackId); console.log('='.repeat(80)); const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; let actualTrackId = trackId; // If not a UUID, try to create the track first if (!uuidRegex.test(trackId)) { console.log('[toggleLikeTrack] → Track ID is not a UUID, attempting to create track from YouTube ID'); // Get track info from DOM element const trackElement = document.querySelector(`[data-id="${trackId}"]`) || document.querySelector(`[data-youtube-id="${trackId}"]`); if (!trackElement) { console.error('[toggleLikeTrack] ✗ Track element not found in DOM'); showToast('Impossible de trouver les informations de la piste', 'error'); return; } const title = decodeURIComponent(trackElement.dataset.title || 'Unknown Track'); const artist = decodeURIComponent(trackElement.dataset.artist || 'Unknown Artist'); console.log('[toggleLikeTrack] → Creating track from YouTube...'); console.log('[toggleLikeTrack] YouTube ID:', trackId); console.log('[toggleLikeTrack] Title:', title); console.log('[toggleLikeTrack] Artist:', artist); showToast('Création de la piste en cours...', 'info'); // Create the track in database actualTrackId = await createTrackFromYouTube(trackId, title, artist); if (!actualTrackId) { console.error('[toggleLikeTrack] ✗ Failed to create track'); showToast('Erreur lors de la création de la piste', 'error'); return; } console.log('[toggleLikeTrack] ✓ Track created with UUID:', actualTrackId); // Update the DOM element with the new UUID if (trackElement) { trackElement.setAttribute('data-id', actualTrackId); trackElement.setAttribute('data-uuid-created', 'true'); console.log('[toggleLikeTrack] ✓ DOM element updated with UUID'); } } const isLiked = AppState.likedTracks.has(String(actualTrackId)); console.log('[toggleLikeTrack] Current like status:', isLiked); try { const token = localStorage.getItem('token'); if (!token) { console.error('[toggleLikeTrack] ✗ No token found'); showToast('Non authentifié', 'error'); return; } console.log('[toggleLikeTrack] ✓ Token found'); const url = `/api/v1/library/liked-tracks/${actualTrackId}`; console.log('[toggleLikeTrack] → API call:', isLiked ? `DELETE ${url}` : `POST ${url}`); const response = await fetch(url, { method: isLiked ? 'DELETE' : 'POST', headers: { 'Authorization': `Bearer ${token}` } }); console.log('[toggleLikeTrack] → Response status:', response.status); if (response.ok) { if (isLiked) { AppState.likedTracks.delete(String(actualTrackId)); console.log('[toggleLikeTrack] ✓ Track removed from liked tracks'); showToast('Retiré des titres likés', 'success'); } else { AppState.likedTracks.add(String(actualTrackId)); console.log('[toggleLikeTrack] ✓ Track added to liked tracks'); showToast('Ajouté aux titres likés', 'success'); } // Update UI console.log('[toggleLikeTrack] → Updating UI...'); updateLikeButtonState(actualTrackId, !isLiked); // If on library page, reload liked tracks if (AppState.currentPage === 'library') { console.log('[toggleLikeTrack] → Reloading liked tracks...'); await loadLikedTracks(); } } else { console.error('[toggleLikeTrack] ✗ API call failed'); const error = await response.json(); console.error('[toggleLikeTrack] → Error:', error.detail); showToast(error.detail || 'Erreur lors de la modification', 'error'); } } catch (error) { console.error('[toggleLikeTrack] ✗ Error:', error); showToast('Erreur de connexion', 'error'); } console.log('='.repeat(80)); } // ============================================ // LISTENING HISTORY FUNCTIONALITY // ============================================ /** * Load listening history from the API * @async * @returns {Promise} */ window.loadListeningHistory = async function() { console.log('='.repeat(80)); console.log('[loadListeningHistory] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[loadListeningHistory] ║ LOADLISTENINGHISTORY FUNCTION STARTED ║'); console.log('[loadListeningHistory] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[loadListeningHistory] Timestamp:', new Date().toISOString()); console.log('='.repeat(80)); const container = document.getElementById('listening-history'); if (!container) { console.error('[loadListeningHistory] ✗ Container listening-history not found'); return; } console.log('[loadListeningHistory] ✓ Container found'); try { console.log('[loadListeningHistory] → Getting token from localStorage...'); const token = localStorage.getItem('token'); if (!token) { console.error('[loadListeningHistory] ✗ No token found in localStorage'); throw new Error('No authentication token'); } console.log('[loadListeningHistory] ✓ Token found'); console.log('[loadListeningHistory] → Fetching listening history from API...'); console.log('[loadListeningHistory] → Endpoint: GET /api/v1/library/history'); const response = await fetch('/api/v1/library/history', { headers: { 'Authorization': `Bearer ${token}` } }); console.log('[loadListeningHistory] → Response status:', response.status); console.log('[loadListeningHistory] → Response ok:', response.ok); if (response.ok) { const history = await response.json(); console.log('[loadListeningHistory] ✓ History loaded:', history.length, 'entries'); // Render history UI console.log('[loadListeningHistory] → Rendering listening history UI...'); renderListeningHistory(history); console.log('[loadListeningHistory] ✓ Listening history UI rendered'); } else if (response.status === 401) { console.warn('[loadListeningHistory] ⚠ Session expired - skipping history load'); return; } else { console.error('[loadListeningHistory] ✗ Failed to load history'); console.error('[loadListeningHistory] → Status:', response.status); throw new Error('Failed to load listening history'); } } catch (error) { console.error('[loadListeningHistory] ✗ Error loading history:', error); console.error('[loadListeningHistory] → Error name:', error.name); console.error('[loadListeningHistory] → Error message:', error.message); if (container) { container.innerHTML = `

Erreur de chargement de l'historique

${error.message}

`; } } console.log('='.repeat(80)); console.log('[loadListeningHistory] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[loadListeningHistory] ║ LOADLISTENINGHISTORY FUNCTION COMPLETED ║'); console.log('[loadListeningHistory] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('='.repeat(80)); } /** * Render listening history grouped by date * @param {Array} history - Array of history entries */ window.renderListeningHistory = function(history) { console.log('='.repeat(80)); console.log('[renderListeningHistory] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[renderListeningHistory] ║ RENDERLISTENINGHISTORY FUNCTION CALLED ║'); console.log('[renderListeningHistory] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[renderListeningHistory] Timestamp:', new Date().toISOString()); console.log('[renderListeningHistory] History entries:', history.length); console.log('='.repeat(80)); const container = document.getElementById('listening-history'); if (!container) { console.error('[renderListeningHistory] ✗ Container listening-history not found'); return; } console.log('[renderListeningHistory] ✓ Container found'); if (!history || history.length === 0) { console.log('[renderListeningHistory] → No history to display'); container.innerHTML = `

Aucun historique d'écoute

Vos écoutes récentes apparaîtront ici

`; console.log('[renderListeningHistory] ✓ Empty state rendered'); return; } console.log('[renderListeningHistory] → Grouping history by date...'); // Group history by date const groupedHistory = {}; history.forEach(entry => { const date = new Date(entry.played_at); const dateKey = formatDateKey(date); const displayDate = formatDateDisplay(date); if (!groupedHistory[dateKey]) { groupedHistory[dateKey] = { display: displayDate, entries: [] }; } groupedHistory[dateKey].entries.push(entry); }); console.log('[renderListeningHistory] ✓ History grouped into', Object.keys(groupedHistory).length, 'dates'); // Sort dates (most recent first) const sortedDates = Object.keys(groupedHistory).sort((a, b) => new Date(b) - new Date(a)); console.log('[renderListeningHistory] → Dates sorted:', sortedDates); console.log('[renderListeningHistory] → Rendering history...'); // Build HTML let html = ''; sortedDates.forEach(dateKey => { const group = groupedHistory[dateKey]; console.log('[renderListeningHistory] → Rendering date:', group.display, 'with', group.entries.length, 'entries'); html += `

${group.display}

`; group.entries.forEach(entry => { const track = entry.track; const trackId = track.youtube_id || track.id; const title = track.title || 'Titre inconnu'; const artist = track.artist_name || track.artist || 'Artiste inconnu'; const cover = track.image_url || track.cover || '/static/img/default-cover.png'; const isYoutube = !!track.youtube_id; const playedAt = new Date(entry.played_at); const timeStr = formatTimeAgo(playedAt); html += `
${title}

${title}

${artist}

${timeStr}
`; }); html += `
`; }); container.innerHTML = html; console.log('[renderListeningHistory] ✓ History rendered:', history.length, 'entries across', sortedDates.length, 'days'); console.log('='.repeat(80)); } // ============================================ // UTILITY FUNCTIONS FOR HISTORY // ============================================ /** * Format date to key for grouping (YYYY-MM-DD) * @param {Date} date - The date to format * @returns {string} Formatted date key */ window.formatDateKey = function(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } /** * Format date for display * @param {Date} date - The date to format * @returns {string} Formatted date string */ window.formatDateDisplay = function(date) { const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); // Reset time parts for accurate comparison today.setHours(0, 0, 0, 0); yesterday.setHours(0, 0, 0, 0); const compareDate = new Date(date); compareDate.setHours(0, 0, 0, 0); if (compareDate.getTime() === today.getTime()) { return "Aujourd'hui"; } else if (compareDate.getTime() === yesterday.getTime()) { return 'Hier'; } else { const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; return date.toLocaleDateString('fr-FR', options); } } /** * Format time ago for display * @param {Date} date - The date to format * @returns {string} Time ago string */ window.formatTimeAgo = function(date) { const now = new Date(); const seconds = Math.floor((now - date) / 1000); if (seconds < 60) { return "À l'instant"; } const minutes = Math.floor(seconds / 60); if (minutes < 60) { return `Il y a ${minutes} min`; } const hours = Math.floor(minutes / 60); if (hours < 24) { return `Il y a ${hours}h`; } const days = Math.floor(hours / 24); if (days === 1) { return 'Hier'; } else if (days < 7) { return `Il y a ${days} j`; } return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }); } // ============================================ // SEARCH FUNCTIONALITY // ============================================ console.log('='.repeat(80)); } window.loadTrendingTracks = async function() { const container = document.getElementById('trending-tracks'); if (!container) { console.error('Container trending-tracks not found'); return; } try { console.log('[loadTrendingTracks] Starting...'); const token = localStorage.getItem('token'); console.log('[loadTrendingTracks] Token:', token ? token.substring(0, 20) + '...' : 'none'); const response = await fetch('/api/v1/music/trending', { headers: { 'Authorization': `Bearer ${token}` } }); console.log('[loadTrendingTracks] Response status:', response.status); if (response.ok) { const tracks = await response.json(); console.log('[loadTrendingTracks] Tracks received:', tracks.length, tracks); renderTracks(tracks, container); } else { console.error('[loadTrendingTracks] Response not OK:', response.status); container.innerHTML = '

Erreur de chargement

'; } } catch (error) { console.error('[loadTrendingTracks] Failed to load trending tracks:', error); container.innerHTML = '

Erreur de chargement: ' + error.message + '

'; } } // ============================================ // SEARCH FUNCTIONALITY // ============================================ // Quick search from home page async function handleQuickSearch() { const searchInput = document.getElementById('quick-search'); if (!searchInput) return; const query = searchInput.value.trim(); if (!query) { showToast('Veuillez entrer une recherche', 'error'); return; } // Show loading state const container = document.getElementById('trending-tracks'); if (container) { container.innerHTML = `

Recherche en cours...

`; } await performSearch(query, container); } // Main search from search page async function handleMainSearch() { console.log('='.repeat(80)); console.log('[handleMainSearch] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[handleMainSearch] ║ HANDLEMAINSEARCH FUNCTION STARTED ║'); console.log('[handleMainSearch] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[handleMainSearch] Timestamp:', new Date().toISOString()); console.log('='.repeat(80)); console.log('[handleMainSearch] → Getting search input element...'); const searchInput = document.getElementById('search-input'); if (!searchInput) { console.error('[handleMainSearch] ✗ Search input element NOT found!'); return; } console.log('[handleMainSearch] ✓ Search input element found'); console.log('[handleMainSearch] → Getting search query...'); const query = searchInput.value.trim(); console.log('[handleMainSearch] Raw value:', searchInput.value); console.log('[handleMainSearch] Trimmed query:', query); if (!query) { console.warn('[handleMainSearch] ✗ Empty query, showing error toast'); showToast('Veuillez entrer une recherche', 'error'); return; } console.log('[handleMainSearch] ✓ Query is valid'); // Show loading state console.log('[handleMainSearch] → Getting search results container...'); const container = document.getElementById('search-results'); if (container) { console.log('[handleMainSearch] ✓ Container found, showing loading state'); container.innerHTML = `

Recherche de "${query}" en cours...

Cela peut prendre quelques secondes

`; } else { console.error('[handleMainSearch] ✗ Search results container NOT found!'); } console.log('[handleMainSearch] → Calling performSearch...'); await performSearch(query, container); console.log('[handleMainSearch] ✓ performSearch completed'); console.log('='.repeat(80)); console.log('[handleMainSearch] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[handleMainSearch] ║ HANDLEMAINSEARCH FUNCTION COMPLETED ║'); console.log('[handleMainSearch] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('='.repeat(80)); } // Perform the actual search window.performSearch = async function(query, container) { console.log('='.repeat(80)); console.log('[performSearch] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[performSearch] ║ PERFORMSEARCH FUNCTION STARTED ║'); console.log('[performSearch] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[performSearch] Timestamp:', new Date().toISOString()); console.log('[performSearch] Query:', query); console.log('='.repeat(80)); if (!container) { console.error('[performSearch] ✗ No container provided'); return; } console.log('[performSearch] ✓ Container provided'); try { console.log('[performSearch] → Getting auth token...'); const token = localStorage.getItem('token'); console.log('[performSearch] Token present:', !!token); console.log('[performSearch] Token length:', token ? token.length : 0); const searchUrl = `/api/v1/music/search?q=${encodeURIComponent(query)}`; console.log('[performSearch] → Fetching from API...'); console.log('[performSearch] URL:', searchUrl); const response = await fetch(searchUrl, { headers: { 'Authorization': `Bearer ${token}` } }); console.log('[performSearch] → Response received'); console.log('[performSearch] Status:', response.status); console.log('[performSearch] Status text:', response.statusText); console.log('[performSearch] OK:', response.ok); if (response.ok) { console.log('[performSearch] → Parsing JSON response...'); const results = await response.json(); console.log('[performSearch] ✓ JSON parsed'); console.log('[performSearch] Full results:', results); const tracks = results.tracks || []; // Extract tracks array from response console.log('[performSearch] → Extracted tracks array'); console.log('[performSearch] Number of tracks:', tracks.length); console.log('[performSearch] Tracks:', tracks); if (tracks.length === 0) { console.log('[performSearch] → No tracks found, showing empty state'); container.innerHTML = `

Aucun résultat pour "${query}"

Essayez d'autres mots-clés

`; console.log('[performSearch] ✓ Empty state rendered'); } else { console.log('[performSearch] → Tracks found, rendering results...'); // Add results header container.innerHTML = `

${tracks.length} résultat${tracks.length > 1 ? 's' : ''} trouvé${tracks.length > 1 ? 's' : ''} pour "${query}"

`; console.log('[performSearch] ✓ Results header rendered'); const resultsContainer = document.createElement('div'); resultsContainer.className = 'track-list'; container.appendChild(resultsContainer); console.log('[performSearch] ✓ Results container created and appended'); console.log('[performSearch] → Calling renderTracks...'); renderTracks(tracks, resultsContainer); console.log('[performSearch] ✓ renderTracks completed'); } } else { console.error('[performSearch] ✗ API response not OK'); console.error('[performSearch] Status:', response.status); console.error('[performSearch] Status text:', response.statusText); container.innerHTML = `

Erreur lors de la recherche

`; console.log('[performSearch] ✗ Error state rendered'); } } catch (error) { console.error('='.repeat(80)); console.error('[performSearch] ╔════════════════════════════════════════════════════════════════════════╗'); console.error('[performSearch] ║ PERFORMSEARCH FUNCTION FAILED ║'); console.error('[performSearch] ╚════════════════════════════════════════════════════════════════════════╝'); console.error('[performSearch] Error name:', error.name); console.error('[performSearch] Error message:', error.message); console.error('[performSearch] Error stack:', error.stack); console.error('='.repeat(80)); container.innerHTML = `

Erreur de connexion

Vérifiez votre connexion internet

`; console.log('[performSearch] ✗ Connection error state rendered'); } console.log('='.repeat(80)); console.log('[performSearch] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[performSearch] ║ PERFORMSEARCH FUNCTION COMPLETED ║'); console.log('[performSearch] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('='.repeat(80)); } window.renderTracks = function(tracks, container) { console.log('='.repeat(80)); console.log('[renderTracks] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[renderTracks] ║ RENDERTRACKS FUNCTION STARTED ║'); console.log('[renderTracks] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[renderTracks] Timestamp:', new Date().toISOString()); console.log('='.repeat(80)); if (!container) { console.error('[renderTracks] ✗ ERROR: No container provided'); return; } console.log('[renderTracks] ✓ Container provided'); console.log('[renderTracks] → Number of tracks to render:', tracks.length); console.log('[renderTracks] Tracks array:', tracks); if (tracks.length === 0) { console.log('[renderTracks] → No tracks to render, showing "Aucun résultat"'); container.innerHTML = '

Aucun résultat

'; console.log('[renderTracks] ✓ Empty state rendered'); return; } console.log('[renderTracks] → Starting to map tracks to HTML...'); container.innerHTML = tracks.map((track, index) => { // Get artist name - handle both nested object and flat structure const artistName = track.artist?.name || track.artist || track.artist_name || 'Artiste inconnu'; // Use youtube_id to determine if this is a YouTube track const isYoutubeTrack = !!track.youtube_id; console.log('[renderTracks] ┌─────────────────────────────────────────────────────────────────'); console.log('[renderTracks] │ Track #' + (index + 1) + ':'); console.log('[renderTracks] │ - ID:', track.id); console.log('[renderTracks] │ - Title:', track.title); console.log('[renderTracks] │ - Artist:', artistName); console.log('[renderTracks] │ - YouTube ID:', track.youtube_id); console.log('[renderTracks] │ - Is YouTube Track:', isYoutubeTrack); console.log('[renderTracks] │ - Duration:', track.duration); console.log('[renderTracks] │ - Image URL:', track.image_url); console.log('[renderTracks] │ - Full track object:', track); console.log('[renderTracks] └─────────────────────────────────────────────────────────────────'); // Encode data attributes for proper JSON storage console.log('[renderTracks] │ → Encoding data attributes...'); const encodedTitle = encodeURIComponent(track.title || 'Unknown Track'); const encodedArtist = encodeURIComponent(artistName); const encodedCover = encodeURIComponent(track.image_url || '/static/img/default-cover.png'); console.log('[renderTracks] │ Encoded title:', encodedTitle); console.log('[renderTracks] │ Encoded artist:', encodedArtist); console.log('[renderTracks] │ Encoded cover:', encodedCover); console.log('[renderTracks] │ ✓ Data attributes encoded'); console.log('[renderTracks] │ → Building HTML element...'); return `
${track.title}

${track.title}

${artistName}

${track.duration ? formatTime(track.duration) : '--:--'}
`; }).join(''); console.log('[renderTracks] ✓ All tracks rendered to HTML'); console.log('[renderTracks] → Container innerHTML length:', container.innerHTML.length); console.log('='.repeat(80)); console.log('[renderTracks] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[renderTracks] ║ RENDERTRACKS FUNCTION COMPLETED ║'); console.log('[renderTracks] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('='.repeat(80)); } // Global function to play a track // trackId: either database UUID or youtube_id // isYoutubeTrack: boolean indicating if this is a YouTube track (default: false) // skipQueuePositionUpdate: boolean to prevent updating queue position (for auto-advance) window.playTrack = async function(trackId, isYoutubeTrack = false, skipQueuePositionUpdate = false) { console.log('='.repeat(80)); console.log('[playTrack] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[playTrack] ║ STARTING PLAYTRACK FUNCTION ║'); console.log('[playTrack] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[playTrack] Timestamp:', new Date().toISOString()); console.log('[playTrack] Parameters received:', { trackId: trackId, trackIdType: typeof trackId, isYoutubeTrack: isYoutubeTrack, isYoutubeTrackType: typeof isYoutubeTrack }); console.log('='.repeat(80)); try { console.log('[playTrack] ✓ Function started successfully'); const token = localStorage.getItem('token'); console.log('[playTrack] ✓ Token retrieved:', { hasToken: !!token, tokenLength: token ? token.length : 0, tokenPreview: token ? token.substring(0, 20) + '...' : 'none' }); console.log('[playTrack] → Showing loading toast...'); showToast('Chargement de la piste...', 'info'); let track; let streamUrl; console.log('[playTrack] ✓ Variables initialized (track, streamUrl)'); console.log('[playTrack] ├─ Checking track type...'); console.log('[playTrack] │ isYoutubeTrack:', isYoutubeTrack); if (isYoutubeTrack) { console.log('[playTrack] │ → This is a YouTube track'); console.log('[playTrack] │ → Building stream URL...'); // This is a YouTube track - use the stream endpoint directly streamUrl = `/api/v1/music/youtube/${trackId}/stream`; console.log('[playTrack] │ ✓ Stream URL built:', streamUrl); console.log('[playTrack] │ → Searching for track element in DOM...'); console.log('[playTrack] │ → Selector:', `[data-id="${trackId}"]`); // Get track info from the clicked element's data attributes const trackElement = document.querySelector(`[data-id="${trackId}"]`); if (trackElement) { console.log('[playTrack] │ ✓ Track element found!'); console.log('[playTrack] │ → Reading data attributes...'); console.log('[playTrack] │ → Raw dataset.title:', trackElement.dataset.title); console.log('[playTrack] │ → Raw dataset.artist:', trackElement.dataset.artist); console.log('[playTrack] │ → Raw dataset.cover:', trackElement.dataset.cover); const title = decodeURIComponent(trackElement.dataset.title || 'Unknown Track'); const artist = decodeURIComponent(trackElement.dataset.artist || 'Unknown Artist'); const cover = decodeURIComponent(trackElement.dataset.cover || '/static/img/default-cover.png'); console.log('[playTrack] │ ✓ Data decoded:'); console.log('[playTrack] │ - title:', title); console.log('[playTrack] │ - artist:', artist); console.log('[playTrack] │ - cover:', cover); track = { title: title, artist_name: artist, image_url: cover, youtube_id: trackId }; console.log('[playTrack] │ ✓ Track object created:', track); } else { console.error('[playTrack] │ ✗ Track element NOT found in DOM!'); console.error('[playTrack] │ → Elements with data-id attribute:'); document.querySelectorAll('[data-id]').forEach(el => { console.error('[playTrack] │ -', el.dataset.id); }); throw new Error('Track element not found'); } } else { console.log('[playTrack] │ → This is a database track'); console.log('[playTrack] │ → Fetching from API...'); console.log('[playTrack] │ → Endpoint:', `/api/v1/music/${trackId}`); // This is a database track - fetch from API const response = await fetch(`/api/v1/music/${trackId}`, { headers: { 'Authorization': `Bearer ${token}` } }); console.log('[playTrack] │ → API Response status:', response.status); console.log('[playTrack] │ → API Response ok:', response.ok); if (response.ok) { track = await response.json(); // Check if this is a YouTube track and use stream endpoint if (track.youtube_id) { streamUrl = `/api/v1/music/youtube/${track.youtube_id}/stream`; console.log('[playTrack] │ ✓ YouTube track detected, using stream endpoint'); } else { streamUrl = track.audio_url || track.stream_url; console.log('[playTrack] │ ✓ Database track with direct audio URL'); } console.log('[playTrack] │ ✓ Track loaded from database:', track); console.log('[playTrack] │ → Stream URL:', streamUrl); } else { console.error('[playTrack] │ ✗ Failed to load track from database'); console.error('[playTrack] │ → Status:', response.status); console.error('[playTrack] │ → Status text:', response.statusText); showToast('Erreur lors du chargement de la piste', 'error'); return; } } console.log('[playTrack] ├─ Setting up audio player...'); // Update player and play if (DOM.audioPlayer) { console.log('[playTrack] │ ✓ Audio player element found'); console.log('[playTrack] │ → Setting audio src...'); console.log('[playTrack] │ Stream URL (truncated):', streamUrl ? streamUrl.substring(0, 100) + '...' : 'none'); DOM.audioPlayer.src = streamUrl; console.log('[playTrack] │ ✓ Audio src set'); // Add error handler for audio element console.log('[playTrack] │ → Setting up error handler...'); DOM.audioPlayer.onerror = function(e) { console.error('[playTrack] Audio error:', e); console.error('[playTrack] Audio error code:', DOM.audioPlayer.error); console.error('[playTrack] Audio error message:', DOM.audioPlayer.error?.message); showToast('Erreur de lecture: format non supporté', 'error'); }; console.log('[playTrack] │ → Setting up metadata loaded handler...'); DOM.audioPlayer.onloadedmetadata = function() { console.log('[playTrack] ✓ Audio metadata loaded'); console.log('[playTrack] Duration:', DOM.audioPlayer.duration); console.log('[playTrack] ReadyState:', DOM.audioPlayer.readyState); }; console.log('[playTrack] │ → Attempting to play audio...'); try { await DOM.audioPlayer.play(); console.log('[playTrack] │ ✓ Audio.play() succeeded'); updatePlayButton(true); console.log('[playTrack] │ ✓ Play button updated'); } catch (playError) { console.error('[playTrack] │ ✗ Audio.play() failed:', playError); console.error('[playTrack] │ Error name:', playError.name); console.error('[playTrack] │ Error message:', playError.message); showToast('Erreur lors de la lecture', 'error'); } } else { console.error('[playTrack] │ ✗ Audio player element NOT found!'); } console.log('[playTrack] ├─ Updating player UI...'); // Update mobile player console.log('[playTrack] │ → Updating mobile player elements...'); if (DOM.playerTitle) { DOM.playerTitle.textContent = track.title; console.log('[playTrack] │ ✓ playerTitle updated:', track.title); } else { console.warn('[playTrack] │ ✗ playerTitle element not found'); } if (DOM.playerArtist) { DOM.playerArtist.textContent = track.artist_name || track.artist || 'Artiste inconnu'; console.log('[playTrack] │ ✓ playerArtist updated:', track.artist_name || track.artist || 'Artiste inconnu'); } else { console.warn('[playTrack] │ ✗ playerArtist element not found'); } if (DOM.playerCover) { DOM.playerCover.src = track.image_url || track.cover || '/static/img/default-cover.png'; console.log('[playTrack] │ ✓ playerCover updated'); } else { console.warn('[playTrack] │ ✗ playerCover element not found'); } // Update desktop player console.log('[playTrack] │ → Updating desktop player elements...'); if (DOM.playerTitleDesktop) { DOM.playerTitleDesktop.textContent = track.title; console.log('[playTrack] │ ✓ playerTitleDesktop updated:', track.title); } else { console.warn('[playTrack] │ ✗ playerTitleDesktop element not found'); } if (DOM.playerArtistDesktop) { DOM.playerArtistDesktop.textContent = track.artist_name || track.artist || 'Artiste inconnu'; console.log('[playTrack] │ ✓ playerArtistDesktop updated:', track.artist_name || track.artist || 'Artiste inconnu'); } else { console.warn('[playTrack] │ ✗ playerArtistDesktop element not found'); } if (DOM.playerCoverDesktop) { DOM.playerCoverDesktop.src = track.image_url || track.cover || '/static/img/default-cover.png'; console.log('[playTrack] │ ✓ playerCoverDesktop updated'); } else { console.warn('[playTrack] │ ✗ playerCoverDesktop element not found'); } // Update like buttons dataset console.log('[playTrack] │ → Updating like buttons dataset...'); if (DOM.likeBtn) { DOM.likeBtn.dataset.trackId = trackId; console.log('[playTrack] │ ✓ likeBtn.dataset.trackId updated:', trackId); } else { console.warn('[playTrack] │ ✗ likeBtn element not found'); } if (DOM.mobileLikeBtn) { DOM.mobileLikeBtn.dataset.trackId = trackId; console.log('[playTrack] │ ✓ mobileLikeBtn.dataset.trackId updated:', trackId); } else { console.warn('[playTrack] │ ✗ mobileLikeBtn element not found'); } // Update like button state based on whether track is liked console.log('[playTrack] │ → Checking if track is liked...'); const isLiked = AppState.likedTracks.has(trackId); console.log('[playTrack] │ Track liked:', isLiked); console.log('[playTrack] │ Liked tracks count:', AppState.likedTracks.size); updateLikeButtonState(trackId, isLiked); console.log('[playTrack] │ ✓ Like button state updated'); console.log('[playTrack] ├─ Updating AppState...'); AppState.currentTrack = track; console.log('[playTrack] │ ✓ AppState.currentTrack updated'); // Add to queue if not already present // Skip queue position update if called from playNext() to avoid overriding the position if (!skipQueuePositionUpdate) { console.log('[playTrack] ├─ Checking if track should be added to queue...'); const trackIndexInQueue = AppState.queue.findIndex(t => (t.youtube_id && t.youtube_id === trackId) || (t.id && t.id === trackId) ); if (trackIndexInQueue === -1) { console.log('[playTrack] → Track not in queue, adding it'); addToQueue([track], AppState.queue.length, false); } else { console.log('[playTrack] → Track already in queue at position', trackIndexInQueue); AppState.queuePosition = trackIndexInQueue; } console.log('[playTrack] │ ✓ Queue position updated:', AppState.queuePosition); } else { console.log('[playTrack] ├─ Skipping queue position update (skipQueuePositionUpdate=true)'); } // Track listening history (to be implemented with API) console.log('[playTrack] ├─ Tracking listen in history...'); trackListenHistory(trackId, isYoutubeTrack); console.log('[playTrack] │ ✓ Listen tracked'); console.log('[playTrack] → Showing success toast...'); showToast(`En lecture: ${track.title}`, 'success'); console.log('[playTrack] ✓ Success toast shown'); console.log('='.repeat(80)); console.log('[playTrack] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[playTrack] ║ PLAYTRACK FUNCTION COMPLETED SUCCESSFULLY ║'); console.log('[playTrack] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[playTrack] Final state:', { trackId: trackId, title: track.title, artist: track.artist_name, streamUrl: streamUrl.substring(0, 50) + '...' }); console.log('='.repeat(80)); } catch (error) { console.error('='.repeat(80)); console.error('[playTrack] ╔════════════════════════════════════════════════════════════════════════╗'); console.error('[playTrack] ║ PLAYTRACK FUNCTION FAILED ║'); console.error('[playTrack] ╚════════════════════════════════════════════════════════════════════════╝'); console.error('[playTrack] Error name:', error.name); console.error('[playTrack] Error message:', error.message); console.error('[playTrack] Error stack:', error.stack); console.error('='.repeat(80)); showToast('Erreur de connexion au serveur', 'error'); } }; // ============================================ // QUEUE MANAGEMENT // ============================================ /** * Add tracks to the queue * @param {Array} tracks - Array of track objects to add * @param {number|null} position - Position to insert at (null = end of queue) * @param {boolean} clear - Clear existing queue before adding */ function addToQueue(tracks, position = null, clear = false) { console.log('='.repeat(80)); console.log('[addToQueue] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[addToQueue] ║ ADDTOQUEUE FUNCTION CALLED ║'); console.log('[addToQueue] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[addToQueue] Timestamp:', new Date().toISOString()); console.log('[addToQueue] Parameters:', { tracksCount: tracks.length, position: position, clear: clear, currentQueueLength: AppState.queue.length }); console.log('='.repeat(80)); try { if (clear) { console.log('[addToQueue] → Clearing existing queue...'); AppState.queue = []; AppState.queuePosition = 0; console.log('[addToQueue] ✓ Queue cleared'); } if (!tracks || tracks.length === 0) { console.warn('[addToQueue] ✗ No tracks to add'); return; } console.log('[addToQueue] → Processing', tracks.length, 'tracks...'); // Filter out duplicates if not clearing const tracksToAdd = clear ? tracks : tracks.filter(track => { const exists = AppState.queue.some(t => (t.youtube_id && t.youtube_id === track.youtube_id) || (t.id && t.id === track.id) ); if (exists) { console.log('[addToQueue] Skipping duplicate track:', track.title); } return !exists; }); console.log('[addToQueue] → Unique tracks to add:', tracksToAdd.length); if (tracksToAdd.length === 0) { console.log('[addToQueue] → All tracks are duplicates, nothing to add'); showToast('Toutes les pistes sont déjà dans la file', 'info'); return; } // Add tracks at specified position or at the end const insertPosition = position !== null ? position : AppState.queue.length; console.log('[addToQueue] → Insert position:', insertPosition); AppState.queue.splice(insertPosition, 0, ...tracksToAdd); console.log('[addToQueue] ✓ Tracks added to queue'); console.log('[addToQueue] New queue length:', AppState.queue.length); // Save to storage console.log('[addToQueue] → Saving to localStorage...'); saveQueueToStorage(); console.log('[addToQueue] ✓ Queue saved'); // Update UI console.log('[addToQueue] → Updating queue UI...'); updateQueueUI(); console.log('[addToQueue] ✓ UI updated'); // Show toast const message = clear ? `${tracksToAdd.length} piste${tracksToAdd.length > 1 ? 's' : ''} mise${tracksToAdd.length > 1 ? 's' : ''} en file` : `${tracksToAdd.length} piste${tracksToAdd.length > 1 ? 's' : ''} ajoutée${tracksToAdd.length > 1 ? 's' : ''}`; showToast(message, 'success'); console.log('[addToQueue] ✓ Toast shown:', message); } catch (error) { console.error('[addToQueue] ✗ Error:', error); console.error('[addToQueue] Error message:', error.message); console.error('[addToQueue] Error stack:', error.stack); showToast('Erreur lors de l\'ajout à la file', 'error'); } console.log('='.repeat(80)); } /** * Remove a track from the queue * @param {number} index - Index of track to remove */ function removeFromQueue(index) { console.log('='.repeat(80)); console.log('[removeFromQueue] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[removeFromQueue] ║ REMOVEFROMQUEUE FUNCTION CALLED ║'); console.log('[removeFromQueue] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[removeFromQueue] Timestamp:', new Date().toISOString()); console.log('[removeFromQueue] Index:', index); console.log('[removeFromQueue] Queue length:', AppState.queue.length); console.log('[removeFromQueue] Current position:', AppState.queuePosition); console.log('='.repeat(80)); if (index < 0 || index >= AppState.queue.length) { console.error('[removeFromQueue] ✗ Invalid index:', index); return; } const removedTrack = AppState.queue[index]; console.log('[removeFromQueue] → Removing track:', removedTrack.title); AppState.queue.splice(index, 1); console.log('[removeFromQueue] ✓ Track removed'); // Adjust position if needed if (index < AppState.queuePosition) { AppState.queuePosition--; console.log('[removeFromQueue] → Position adjusted:', AppState.queuePosition); } else if (index === AppState.queuePosition && AppState.queue.length > 0) { // If removing current track, play next console.log('[removeFromQueue] → Removing current track, playing next...'); if (AppState.queuePosition >= AppState.queue.length) { AppState.queuePosition = Math.max(0, AppState.queue.length - 1); } if (AppState.queue.length > 0) { const nextTrack = AppState.queue[AppState.queuePosition]; const isYoutubeTrack = !!nextTrack.youtube_id; const trackId = nextTrack.youtube_id || nextTrack.id; playTrack(trackId, isYoutubeTrack); } } console.log('[removeFromQueue] → Saving to storage...'); saveQueueToStorage(); console.log('[removeFromQueue] ✓ Saved'); console.log('[removeFromQueue] → Updating UI...'); updateQueueUI(); console.log('[removeFromQueue] ✓ UI updated'); showToast('Piste retirée de la file', 'success'); console.log('='.repeat(80)); } /** * Shuffle the current queue */ function shuffleQueue() { console.log('='.repeat(80)); console.log('[shuffleQueue] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[shuffleQueue] ║ SHUFFLEQUEUE FUNCTION CALLED ║'); console.log('[shuffleQueue] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[shuffleQueue] Timestamp:', new Date().toISOString()); console.log('[shuffleQueue] Queue length:', AppState.queue.length); console.log('='.repeat(80)); if (AppState.queue.length < 2) { console.log('[shuffleQueue] → Queue too small to shuffle'); showToast('Pas assez de pistes à mélanger', 'info'); return; } // Keep track of current track const currentTrack = AppState.queue[AppState.queuePosition]; console.log('[shuffleQueue] → Current track:', currentTrack.title); // Fisher-Yates shuffle for (let i = AppState.queue.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [AppState.queue[i], AppState.queue[j]] = [AppState.queue[j], AppState.queue[i]]; } console.log('[shuffleQueue] ✓ Queue shuffled'); // Move current track to position 0 const newCurrentIndex = AppState.queue.findIndex(t => (t.youtube_id && t.youtube_id === currentTrack.youtube_id) || (t.id && t.id === currentTrack.id) ); if (newCurrentIndex > 0) { AppState.queue.splice(newCurrentIndex, 1); AppState.queue.splice(0, 0, currentTrack); AppState.queuePosition = 0; console.log('[shuffleQueue] → Current track moved to position 0'); } console.log('[shuffleQueue] → Saving to storage...'); saveQueueToStorage(); console.log('[shuffleQueue] ✓ Saved'); console.log('[shuffleQueue] → Updating UI...'); updateQueueUI(); console.log('[shuffleQueue] ✓ UI updated'); showToast('File d\'attente mélangée', 'success'); console.log('='.repeat(80)); } /** * Clear the entire queue */ function clearQueue() { console.log('='.repeat(80)); console.log('[clearQueue] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[clearQueue] ║ CLEARQUEUE FUNCTION CALLED ║'); console.log('[clearQueue] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[clearQueue] Timestamp:', new Date().toISOString()); console.log('[clearQueue] Queue length:', AppState.queue.length); console.log('='.repeat(80)); if (AppState.queue.length === 0) { console.log('[clearQueue] → Queue already empty'); showToast('File d\'attente déjà vide', 'info'); return; } // Stop playback if playing if (DOM.audioPlayer && !DOM.audioPlayer.paused) { console.log('[clearQueue] → Stopping playback...'); DOM.audioPlayer.pause(); updatePlayButton(false); console.log('[clearQueue] ✓ Playback stopped'); } AppState.queue = []; AppState.queuePosition = 0; console.log('[clearQueue] ✓ Queue cleared'); console.log('[clearQueue] → Saving to storage...'); saveQueueToStorage(); console.log('[clearQueue] ✓ Saved'); console.log('[clearQueue] → Updating UI...'); updateQueueUI(); console.log('[clearQueue] ✓ UI updated'); showToast('File d\'attente vidée', 'success'); console.log('='.repeat(80)); } /** * Save queue to localStorage */ function saveQueueToStorage() { console.log('='.repeat(80)); console.log('[saveQueueToStorage] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[saveQueueToStorage] ║ SAVEQUEUETOSTORAGE FUNCTION CALLED ║'); console.log('[saveQueueToStorage] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[saveQueueToStorage] Timestamp:', new Date().toISOString()); console.log('[saveQueueToStorage] Queue length:', AppState.queue.length); console.log('='.repeat(80)); try { const queueData = { queue: AppState.queue, position: AppState.queuePosition }; const json = JSON.stringify(queueData); console.log('[saveQueueToStorage] → Queue data size:', json.length, 'bytes'); localStorage.setItem('audiohm_queue', json); console.log('[saveQueueToStorage] ✓ Queue saved to localStorage'); } catch (error) { console.error('[saveQueueToStorage] ✗ Error saving queue:', error); console.error('[saveQueueToStorage] Error message:', error.message); } console.log('='.repeat(80)); } /** * Load queue from localStorage */ function loadQueueFromStorage() { console.log('='.repeat(80)); console.log('[loadQueueFromStorage] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[loadQueueFromStorage] ║ LOADQUEUEFROMSTORAGE FUNCTION CALLED ║'); console.log('[loadQueueFromStorage] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[loadQueueFromStorage] Timestamp:', new Date().toISOString()); console.log('='.repeat(80)); try { const data = localStorage.getItem('audiohm_queue'); if (!data) { console.log('[loadQueueFromStorage] → No queue found in storage'); AppState.queue = []; AppState.queuePosition = 0; return; } console.log('[loadQueueFromStorage] → Queue data found, parsing...'); const queueData = JSON.parse(data); if (queueData.queue && Array.isArray(queueData.queue)) { AppState.queue = queueData.queue; AppState.queuePosition = queueData.position || 0; console.log('[loadQueueFromStorage] ✓ Queue loaded'); console.log('[loadQueueFromStorage] Tracks:', AppState.queue.length); console.log('[loadQueueFromStorage] Position:', AppState.queuePosition); // Update UI after a short delay to ensure DOM is ready setTimeout(() => { updateQueueUI(); }, 100); } else { console.warn('[loadQueueFromStorage] ✗ Invalid queue data format'); AppState.queue = []; AppState.queuePosition = 0; } } catch (error) { console.error('[loadQueueFromStorage] ✗ Error loading queue:', error); console.error('[loadQueueFromStorage] Error message:', error.message); AppState.queue = []; AppState.queuePosition = 0; } console.log('='.repeat(80)); } /** * Update queue UI */ function updateQueueUI() { console.log('='.repeat(80)); console.log('[updateQueueUI] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[updateQueueUI] ║ UPDATEQUEUEUI FUNCTION CALLED ║'); console.log('[updateQueueUI] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[updateQueueUI] Timestamp:', new Date().toISOString()); console.log('[updateQueueUI] Queue length:', AppState.queue.length); console.log('[updateQueueUI] Current position:', AppState.queuePosition); console.log('='.repeat(80)); // Update queue count if (DOM.queueCount) { DOM.queueCount.textContent = AppState.queue.length; console.log('[updateQueueUI] ✓ Queue count updated'); } // Update queue list if (!DOM.queueList) { console.warn('[updateQueueUI] ✗ Queue list element not found'); console.log('='.repeat(80)); return; } if (AppState.queue.length === 0) { console.log('[updateQueueUI] → Queue empty, showing empty state'); DOM.queueList.innerHTML = `

File d'attente vide

Cliquez sur une piste pour l'ajouter

`; console.log('[updateQueueUI] ✓ Empty state rendered'); console.log('='.repeat(80)); return; } console.log('[updateQueueUI] → Rendering queue items...'); DOM.queueList.innerHTML = AppState.queue.map((track, index) => { const isCurrentTrack = index === AppState.queuePosition; const artistName = track.artist_name || track.artist || track.artist?.name || 'Artiste inconnu'; console.log('[updateQueueUI] Track', index + 1, ':', track.title, '(current:', isCurrentTrack + ')'); return `
${isCurrentTrack ? '' : `${index + 1}` }

${track.title}

${artistName}

`; }).join(''); console.log('[updateQueueUI] ✓ Queue items rendered'); // Scroll to current track if (AppState.queuePosition > 0) { const currentItem = DOM.queueList.querySelector(`[data-index="${AppState.queuePosition}"]`); if (currentItem) { currentItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); console.log('[updateQueueUI] ✓ Scrolled to current track'); } } console.log('='.repeat(80)); } /** * Play a track from the queue * @param {number} index - Index of track to play */ window.playTrackFromQueue = function(index) { console.log('='.repeat(80)); console.log('[playTrackFromQueue] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[playTrackFromQueue] ║ PLAYTRACKFROMQUEUE FUNCTION CALLED ║'); console.log('[playTrackFromQueue] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[playTrackFromQueue] Timestamp:', new Date().toISOString()); console.log('[playTrackFromQueue] Index:', index); console.log('='.repeat(80)); if (index < 0 || index >= AppState.queue.length) { console.error('[playTrackFromQueue] ✗ Invalid index:', index); return; } AppState.queuePosition = index; const track = AppState.queue[index]; console.log('[playTrackFromQueue] → Track:', track.title); const isYoutubeTrack = !!track.youtube_id; const trackId = track.youtube_id || track.id; playTrack(trackId, isYoutubeTrack); updateQueueUI(); console.log('='.repeat(80)); }; /** * Open the queue panel */ function openQueuePanel() { console.log('[openQueuePanel] Opening queue panel...'); AppState.isQueuePanelOpen = true; if (DOM.queuePanel) { DOM.queuePanel.classList.remove('translate-x-full'); DOM.queuePanel.classList.add('translate-x-0'); console.log('[openQueuePanel] ✓ Panel opened'); } if (DOM.queueOpenBtn) { DOM.queueOpenBtn.setAttribute('aria-expanded', 'true'); } updateQueueUI(); } /** * Close the queue panel */ function closeQueuePanel() { console.log('[closeQueuePanel] Closing queue panel...'); AppState.isQueuePanelOpen = false; if (DOM.queuePanel) { DOM.queuePanel.classList.add('translate-x-full'); DOM.queuePanel.classList.remove('translate-x-0'); console.log('[closeQueuePanel] ✓ Panel closed'); } if (DOM.queueOpenBtn) { DOM.queueOpenBtn.setAttribute('aria-expanded', 'false'); } } /** * Track a listening event in the history * @param {string} trackId - The track ID * @param {boolean} isYoutubeTrack - Whether it's a YouTube track * @async */ async function trackListenHistory(trackId, isYoutubeTrack) { console.log('='.repeat(80)); console.log('[trackListenHistory] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[trackListenHistory] ║ TRACKLISTENHISTORY FUNCTION CALLED ║'); console.log('[trackListenHistory] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[trackListenHistory] Timestamp:', new Date().toISOString()); console.log('[trackListenHistory] Track ID:', trackId); console.log('[trackListenHistory] Is YouTube:', isYoutubeTrack); console.log('='.repeat(80)); try { const token = localStorage.getItem('token'); if (!token) { console.log('[trackListenHistory] → No token found, skipping history tracking'); return; } console.log('[trackListenHistory] ✓ Token found'); console.log('[trackListenHistory] → Sending history event to API...'); console.log('[trackListenHistory] → Endpoint: POST /api/v1/library/history'); const response = await fetch('/api/v1/library/history', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ track_id: trackId, played_for: 0, completed: false, source: isYoutubeTrack ? 'youtube' : 'library' }) }); console.log('[trackListenHistory] → Response status:', response.status); if (response.ok) { console.log('[trackListenHistory] ✓ Listen event tracked successfully'); } else { console.warn('[trackListenHistory] → Failed to track listen event'); console.warn('[trackListenHistory] → Status:', response.status); // Don't show error toast to user, this is non-critical } } catch (error) { console.warn('[trackListenHistory] → Error tracking listen:', error.message); // Don't show error toast to user, this is non-critical } console.log('='.repeat(80)); } // ============================================ // PLAYLIST MANAGEMENT // ============================================ // Show create playlist modal window.showCreatePlaylistModal = function() { console.log('[showCreatePlaylistModal] Showing modal'); const modal = document.getElementById('create-playlist-modal'); if (modal) { modal.classList.remove('hidden'); modal.setAttribute('aria-hidden', 'false'); document.getElementById('playlist-name').focus(); } }; // Hide create playlist modal window.hideCreatePlaylistModal = function() { console.log('[hideCreatePlaylistModal] Hiding modal'); const modal = document.getElementById('create-playlist-modal'); if (modal) { modal.classList.add('hidden'); modal.setAttribute('aria-hidden', 'true'); // Reset form const form = document.getElementById('create-playlist-form'); if (form) form.reset(); } }; // Create a new playlist window.createPlaylist = async function(e) { console.log('='.repeat(80)); console.log('[createPlaylist] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[createPlaylist] ║ CREATING NEW PLAYLIST ║'); console.log('[createPlaylist] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('='.repeat(80)); e.preventDefault(); const name = document.getElementById('playlist-name').value.trim(); const description = document.getElementById('playlist-description').value.trim(); if (!name) { showToast('Le nom de la playlist est requis', 'error'); return; } console.log('[createPlaylist] → Creating playlist:', { name, description }); try { const token = localStorage.getItem('token'); const response = await fetch('/api/v1/playlists', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ name, description: description || null }) }); if (response.ok) { const newPlaylist = await response.json(); console.log('[createPlaylist] ✓ Playlist created successfully:', newPlaylist); showToast(`Playlist "${name}" créée avec succès!`, 'success'); hideCreatePlaylistModal(); // If there's a pending track to add, add it now if (window.pendingTrackToAdd) { console.log('[createPlaylist] → Adding pending track to new playlist'); await addTrackToPlaylist(window.pendingTrackToAdd, newPlaylist.id, newPlaylist.name); window.pendingTrackToAdd = null; } // Reload playlists await loadPlaylists(); } else { const error = await response.json(); console.error('[createPlaylist] ✗ Error creating playlist:', error); showToast(error.detail || 'Erreur lors de la création', 'error'); } } catch (error) { console.error('[createPlaylist] ✗ Exception:', error); showToast('Erreur de connexion', 'error'); } console.log('='.repeat(80)); }; /** * Create a track from YouTube ID in the database * This ensures the track has a valid UUID for playlist/liked operations * @param {string} youtubeId - YouTube video ID * @param {string} title - Track title * @param {string} artist - Artist name * @returns {Promise} - Returns UUID if successful, null otherwise */ async function createTrackFromYouTube(youtubeId, title, artist) { console.log('='.repeat(80)); console.log('[createTrackFromYouTube] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[createTrackFromYouTube] ║ CREATING TRACK FROM YOUTUBE ║'); console.log('[createTrackFromYouTube] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[createTrackFromYouTube] YouTube ID:', youtubeId); console.log('[createTrackFromYouTube] Title:', title); console.log('[createTrackFromYouTube] Artist:', artist); console.log('='.repeat(80)); try { const token = localStorage.getItem('token'); if (!token) { console.error('[createTrackFromYouTube] ✗ No token found'); return null; } // Build query parameters const params = new URLSearchParams({ youtube_id: youtubeId, title: title, artist: artist || 'Unknown Artist' }); const response = await fetch(`/api/v1/music/tracks/from-youtube?${params}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { const track = await response.json(); console.log('[createTrackFromYouTube] ✓ Track created successfully'); console.log('[createTrackFromYouTube] → Track UUID:', track.id); return track.id; } else { const error = await response.json(); console.error('[createTrackFromYouTube] ✗ Failed to create track'); console.error('[createTrackFromYouTube] → Error:', error.detail); return null; } } catch (error) { console.error('[createTrackFromYouTube] ✗ Exception:', error); return null; } } // Add track to playlist window.addTrackToPlaylist = async function(trackId, playlistId, playlistName) { console.log('='.repeat(80)); console.log('[addTrackToPlaylist] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[addTrackToPlaylist] ║ ADDING TRACK TO PLAYLIST ║'); console.log('[addTrackToPlaylist] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[addTrackToPlaylist] Track ID:', trackId, 'Playlist ID:', playlistId); console.log('='.repeat(80)); try { const token = localStorage.getItem('token'); // Validate UUID format const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; let actualTrackId = trackId; if (!uuidRegex.test(trackId)) { console.log('[addTrackToPlaylist] → Track ID is not a UUID, attempting to create track from YouTube ID'); // Get track info from DOM element const trackElement = document.querySelector(`[data-id="${trackId}"]`) || document.querySelector(`[data-youtube-id="${trackId}"]`); if (!trackElement) { console.error('[addTrackToPlaylist] ✗ Track element not found in DOM'); showToast('Impossible de trouver les informations de la piste', 'error'); const dropdown = document.getElementById(`playlist-dropdown-${trackId}`); if (dropdown) dropdown.classList.add('hidden'); return; } const title = decodeURIComponent(trackElement.dataset.title || 'Unknown Track'); const artist = decodeURIComponent(trackElement.dataset.artist || 'Unknown Artist'); console.log('[addTrackToPlaylist] → Creating track from YouTube...'); console.log('[addTrackToPlaylist] YouTube ID:', trackId); console.log('[addTrackToPlaylist] Title:', title); console.log('[addTrackToPlaylist] Artist:', artist); showToast('Création de la piste en cours...', 'info'); // Create the track in database actualTrackId = await createTrackFromYouTube(trackId, title, artist); if (!actualTrackId) { console.error('[addTrackToPlaylist] ✗ Failed to create track'); showToast('Erreur lors de la création de la piste', 'error'); const dropdown = document.getElementById(`playlist-dropdown-${trackId}`); if (dropdown) dropdown.classList.add('hidden'); return; } console.log('[addTrackToPlaylist] ✓ Track created with UUID:', actualTrackId); // Update the DOM element with the new UUID if (trackElement) { trackElement.setAttribute('data-id', actualTrackId); trackElement.setAttribute('data-uuid-created', 'true'); console.log('[addTrackToPlaylist] ✓ DOM element updated with UUID'); } } const response = await fetch(`/api/v1/playlists/${playlistId}/tracks`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ track_ids: [actualTrackId] }) }); if (response.ok) { console.log('[addTrackToPlaylist] ✓ Track added successfully'); showToast(`Ajouté à "${playlistName}"`, 'success'); // Close dropdown const dropdown = document.getElementById(`playlist-dropdown-${trackId}`); if (dropdown) dropdown.classList.add('hidden'); // Reload playlists to update track count await loadPlaylists(); } else { const error = await response.json(); console.error('[addTrackToPlaylist] ✗ Error adding track:', error); showToast(error.detail || 'Erreur lors de l\'ajout', 'error'); } } catch (error) { console.error('[addTrackToPlaylist] ✗ Exception:', error); showToast('Erreur de connexion', 'error'); } console.log('='.repeat(80)); }; // Toggle add to playlist dropdown window.toggleAddToPlaylistDropdown = async function(event, trackId) { console.log('[toggleAddToPlaylistDropdown] Toggling dropdown for track:', trackId); event.stopPropagation(); // Close all other dropdowns first document.querySelectorAll('[id^="playlist-dropdown-"]').forEach(dropdown => { if (dropdown.id !== `playlist-dropdown-${trackId}`) { dropdown.classList.add('hidden'); } }); const dropdown = document.getElementById(`playlist-dropdown-${trackId}`); if (!dropdown) { console.error('[toggleAddToPlaylistDropdown] ✗ Dropdown not found'); return; } if (dropdown.classList.contains('hidden')) { console.log('[toggleAddToPlaylistDropdown] → Showing dropdown and loading playlists'); // Position the dropdown above the button const button = event.target.closest('button'); if (button) { const rect = button.getBoundingClientRect(); dropdown.style.top = `${rect.bottom + 8}px`; dropdown.style.right = `${window.innerWidth - rect.right}px`; } // Load playlists into dropdown const optionsContainer = document.getElementById(`playlist-options-${trackId}`); if (AppState.playlists.length === 0) { optionsContainer.innerHTML = `
Aucune playlist
`; } else { optionsContainer.innerHTML = AppState.playlists.map(playlist => ` `).join(''); } dropdown.classList.remove('hidden'); } else { dropdown.classList.add('hidden'); } }; // Create new playlist from track (opens modal) window.createNewPlaylistFromTrack = function(trackId) { console.log('[createNewPlaylistFromTrack] Opening modal for track:', trackId); // Store track ID to add after playlist creation window.pendingTrackToAdd = trackId; // Close dropdown const dropdown = document.getElementById(`playlist-dropdown-${trackId}`); if (dropdown) dropdown.classList.add('hidden'); // Show modal showCreatePlaylistModal(); }; // Show playlist details modal window.showPlaylistDetails = async function(playlistId) { console.log('='.repeat(80)); console.log('[showPlaylistDetails] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[showPlaylistDetails] ║ SHOWING PLAYLIST DETAILS ║'); console.log('[showPlaylistDetails] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[showPlaylistDetails] Playlist ID:', playlistId); console.log('='.repeat(80)); try { const token = localStorage.getItem('token'); const response = await fetch(`/api/v1/playlists/${playlistId}?include_tracks=true`, { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { const playlist = await response.json(); console.log('[showPlaylistDetails] ✓ Playlist data loaded:', playlist); // Update modal content document.getElementById('playlist-details-title').textContent = playlist.name; document.getElementById('playlist-details-description').textContent = playlist.description || 'Aucune description'; // Store playlist ID for play buttons window.currentPlaylistId = playlistId; // Render tracks const tracksContainer = document.getElementById('playlist-tracks'); if (playlist.tracks && playlist.tracks.length > 0) { // Extract track objects from the response const trackObjects = playlist.tracks.map(pt => pt.track).filter(t => t !== null); console.log('[showPlaylistDetails] → Tracks to render:', trackObjects.length); if (trackObjects.length > 0) { // Use renderTracks function renderTracks(trackObjects, tracksContainer); } else { tracksContainer.innerHTML = `

Aucune piste disponible

`; } } else { tracksContainer.innerHTML = `

Aucune piste

Ajoutez des pistes depuis la recherche

`; } // Show modal const modal = document.getElementById('playlist-details-modal'); modal.classList.remove('hidden'); modal.setAttribute('aria-hidden', 'false'); console.log('[showPlaylistDetails] ✓ Modal shown'); } else { const error = await response.json(); console.error('[showPlaylistDetails] ✗ Error loading playlist:', error); showToast(error.detail || 'Erreur lors du chargement', 'error'); } } catch (error) { console.error('[showPlaylistDetails] ✗ Exception:', error); showToast('Erreur de connexion', 'error'); } console.log('='.repeat(80)); }; // Hide playlist details modal window.hidePlaylistDetails = function() { console.log('[hidePlaylistDetails] Hiding modal'); const modal = document.getElementById('playlist-details-modal'); if (modal) { modal.classList.add('hidden'); modal.setAttribute('aria-hidden', 'true'); window.currentPlaylistId = null; } }; // Play playlist window.playPlaylist = async function(playlistId, shuffle = false) { console.log('='.repeat(80)); console.log('[playPlaylist] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[playPlaylist] ║ PLAYING PLAYLIST ║'); console.log('[playPlaylist] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[playPlaylist] Playlist ID:', playlistId, 'Shuffle:', shuffle); console.log('='.repeat(80)); try { const token = localStorage.getItem('token'); const response = await fetch(`/api/v1/playlists/${playlistId}?include_tracks=true`, { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { const playlist = await response.json(); console.log('[playPlaylist] ✓ Playlist loaded:', playlist.name); if (playlist.tracks && playlist.tracks.length > 0) { // Extract track objects const trackObjects = playlist.tracks .map(pt => pt.track) .filter(t => t !== null); if (trackObjects.length > 0) { console.log('[playPlaylist] → Tracks to play:', trackObjects.length); // Clear queue and add tracks AppState.queue = []; AppState.queuePosition = 0; // Add tracks to queue trackObjects.forEach(track => { AppState.queue.push({ id: track.id, youtube_id: track.youtube_id, title: track.title, artist: track.artist, image_url: track.image_url, duration: track.duration }); }); // Shuffle if requested if (shuffle) { console.log('[playPlaylist] → Shuffling queue'); shuffleQueue(); } // Update queue UI updateQueueUI(); // Play first track const firstTrack = AppState.queue[0]; console.log('[playPlaylist] → Playing first track:', firstTrack.title); await playTrack(firstTrack.id, !!firstTrack.youtube_id); showToast(`Lecture de "${playlist.name}"`, 'success'); } else { showToast('Aucune piste à jouer', 'error'); } } else { showToast('Playlist vide', 'error'); } } else { const error = await response.json(); console.error('[playPlaylist] ✗ Error:', error); showToast(error.detail || 'Erreur lors du chargement', 'error'); } } catch (error) { console.error('[playPlaylist] ✗ Exception:', error); showToast('Erreur de connexion', 'error'); } console.log('='.repeat(80)); }; // Delete playlist with confirmation window.deletePlaylistWithConfirm = function(playlistId, playlistName) { console.log('[deletePlaylistWithConfirm] Playlist:', playlistId, playlistName); if (confirm(`Êtes-vous sûr de vouloir supprimer "${playlistName}" ?\n\nCette action est irréversible.`)) { deletePlaylist(playlistId); } }; // Delete playlist window.deletePlaylist = async function(playlistId) { console.log('='.repeat(80)); console.log('[deletePlaylist] ╔════════════════════════════════════════════════════════════════════════╗'); console.log('[deletePlaylist] ║ DELETING PLAYLIST ║'); console.log('[deletePlaylist] ╚════════════════════════════════════════════════════════════════════════╝'); console.log('[deletePlaylist] Playlist ID:', playlistId); console.log('='.repeat(80)); try { const token = localStorage.getItem('token'); const response = await fetch(`/api/v1/playlists/${playlistId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { console.log('[deletePlaylist] ✓ Playlist deleted successfully'); showToast('Playlist supprimée', 'success'); // Reload playlists await loadPlaylists(); } else { const error = await response.json(); console.error('[deletePlaylist] ✗ Error:', error); showToast(error.detail || 'Erreur lors de la suppression', 'error'); } } catch (error) { console.error('[deletePlaylist] ✗ Exception:', error); showToast('Erreur de connexion', 'error'); } console.log('='.repeat(80)); }; // ============================================ // TOAST NOTIFICATIONS // ============================================ function showToast(message, type = 'success') { if (!DOM.toastContainer) return; const toast = document.createElement('div'); // Tailwind classes based on type const baseClasses = 'glass-card rounded-xl px-4 py-3 shadow-lg flex items-center gap-3 min-w-[300px] animate-fadeIn'; const typeClasses = { success: 'border-l-4 border-emerald-500 text-emerald-400', error: 'border-l-4 border-red-500 text-red-400', info: 'border-l-4 border-primary-500 text-primary-400' }; const iconClasses = { success: 'fa-check-circle text-emerald-400', error: 'fa-exclamation-circle text-red-400', info: 'fa-info-circle text-primary-400' }; toast.className = `${baseClasses} ${typeClasses[type] || typeClasses.success}`; toast.innerHTML = ` ${message} `; DOM.toastContainer.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateX(100%)'; toast.style.transition = 'all 0.3s ease-out'; setTimeout(() => toast.remove(), 300); }, 4000); } // ============================================ // KEYBOARD SHORTCUTS // ============================================ document.addEventListener('keydown', (e) => { // Close queue panel with Escape if (e.code === 'Escape' && AppState.isQueuePanelOpen) { closeQueuePanel(); return; } // Don't trigger if typing in input (except Enter which is handled separately) if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { // Allow space in search inputs (for searching terms with spaces) if (e.target.id.includes('search') && e.code === 'Space') { return; } // Return early for other shortcuts, but let Enter be handled by input event listeners if (e.code !== 'Enter') return; } switch(e.code) { case 'Space': e.preventDefault(); togglePlayPause(); break; case 'ArrowRight': if (e.shiftKey) playNext(); else if (DOM.audioPlayer) DOM.audioPlayer.currentTime += 10; break; case 'ArrowLeft': if (e.shiftKey) playPrevious(); else if (DOM.audioPlayer) DOM.audioPlayer.currentTime -= 10; break; case 'ArrowUp': e.preventDefault(); if (DOM.volumeBar) { DOM.volumeBar.value = Math.min(100, parseInt(DOM.volumeBar.value) + 10); handleVolumeChange(); } break; case 'ArrowDown': e.preventDefault(); if (DOM.volumeBar) { DOM.volumeBar.value = Math.max(0, parseInt(DOM.volumeBar.value) - 10); handleVolumeChange(); } break; case 'KeyM': toggleMute(); break; } }); // ============================================ // INIT // ============================================ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }