Files
AudiOhm/backend/app/static/js/app.js.backup
T
root 801e6a050b prod: UI Optimisée mise en production
- Documentation archivée et réorganisée
- Backend: Ajout tests, migrations, library service, rate limiting
- Frontend: Suppression Flutter, focus sur interface web HTML/JS
- Tailwind CSS ajouté pour le style
- Améliorations UX et corrections bugs

Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-20 09:56:39 +00:00

3838 lines
171 KiB
Plaintext

/**
* ============================================
* 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));
}
function cacheDOM() {
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
// ============================================
function setupEventListeners() {
// 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();
}
});
}
function setupPlayerControls() {
// 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
// ============================================
async function checkAuth() {
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');
}
}
async function handleLogin(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');
}
}
async function handleRegister(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');
}
}
function handleLogout() {
localStorage.removeItem('token');
AppState.isAuthenticated = false;
showScreen('login');
showToast('Déconnexion réussie', 'success');
}
// ============================================
// NAVIGATION
// ============================================
function navigateTo(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));
}
function toggleMobileMenu() {
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
// ============================================
function togglePlayPause() {
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));
}
function updatePlayButton(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));
}
function playPrevious() {
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));
}
function updateLikeButtonState(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');
}
}
}
}
function playNext() {
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));
}
function toggleShuffle() {
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');
}
function toggleRepeat() {
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');
}
function handleSeek() {
if (!DOM.audioPlayer || !DOM.progressBar) return;
const time = (DOM.progressBar.value / 100) * DOM.audioPlayer.duration;
DOM.audioPlayer.currentTime = time;
}
function handleVolumeChange() {
if (!DOM.audioPlayer || !DOM.volumeBar) return;
AppState.volume = DOM.volumeBar.value;
DOM.audioPlayer.volume = AppState.volume / 100;
AppState.isMuted = false;
updateVolumeIcon();
}
function toggleMute() {
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]);
}
}
function updateVolumeIcon() {
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}%`);
}
}
function toggleLike() {
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));
}
function updateProgress() {
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);
}
}
function updateDuration() {
if (!DOM.audioPlayer || !DOM.totalTime) return;
DOM.totalTime.textContent = formatTime(DOM.audioPlayer.duration);
}
function handleTrackEnd() {
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
// ============================================
function formatTime(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')}`;
}
function showScreen(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');
}
}
}
function hideLoadingScreen() {
if (DOM.loadingScreen) {
setTimeout(() => {
DOM.loadingScreen.style.display = 'none';
}, 500);
}
}
function showError(message) {
if (DOM.authError) {
DOM.authError.textContent = message;
DOM.authError.classList.remove('hidden');
}
}
async function loadUserData() {
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));
}
async function loadPlaylists() {
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 = `
<div class="col-span-1 sm:col-span-2 lg:col-span-3 flex flex-col items-center justify-center py-12 text-gray-400">
<i class="fas fa-exclamation-circle text-4xl mb-4 text-red-400"></i>
<p class="text-lg">Erreur de chargement</p>
<p class="text-sm mt-2">${error.detail || 'Impossible de charger les playlists'}</p>
</div>
`;
}
} catch (error) {
console.error('[loadPlaylists] ✗ Exception:', error);
container.innerHTML = `
<div class="col-span-1 sm:col-span-2 lg:col-span-3 flex flex-col items-center justify-center py-12 text-gray-400">
<i class="fas fa-wifi text-4xl mb-4"></i>
<p class="text-lg">Erreur de connexion</p>
<p class="text-sm mt-2">Vérifiez votre connexion internet</p>
</div>
`;
}
console.log('='.repeat(80));
console.log('[loadPlaylists] ╔════════════════════════════════════════════════════════════════════════╗');
console.log('[loadPlaylists] ║ LOADPLAYLISTS FUNCTION COMPLETED ║');
console.log('[loadPlaylists] ╚════════════════════════════════════════════════════════════════════════╝');
console.log('='.repeat(80));
}
function renderPlaylists(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 = `
<div class="col-span-1 sm:col-span-2 lg:col-span-3 flex flex-col items-center justify-center py-12 text-gray-400">
<i class="fas fa-list text-5xl mb-4"></i>
<p class="text-lg">Aucune playlist</p>
<p class="text-sm mt-2">Créez votre première playlist pour commencer</p>
</div>
`;
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 `
<div class="glass-card rounded-xl p-4 hover:bg-gray-700/50 transition-all group animate-fadeIn"
data-playlist-id="${playlist.id}">
<!-- Cover Image -->
<div class="relative aspect-square rounded-lg overflow-hidden mb-3 ${!coverImage ? `bg-gradient-to-br ${gradientClass}` : ''}"
style="${coverStyle}">
<img src="${coverImage || ''}" alt="" class="w-full h-full object-cover ${coverImage ? '' : 'hidden'}" aria-hidden="true">
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-all flex items-center justify-center">
<button onclick="event.stopPropagation(); playPlaylist('${playlist.id}', false)"
class="w-12 h-12 flex items-center justify-center bg-primary-600 hover:bg-primary-500 rounded-full transform hover:scale-110 transition-all"
aria-label="Lire la playlist">
<i class="fas fa-play text-white"></i>
</button>
</div>
</div>
<!-- Info -->
<div class="mb-3">
<h3 class="font-semibold text-white truncate group-hover:text-primary-400 transition-colors" title="${playlist.name}">
${playlist.name}
</h3>
<p class="text-sm text-gray-400 truncate mt-1" title="${playlist.description || ''}">
${playlist.description || 'Aucune description'}
</p>
<p class="text-xs text-gray-500 mt-1">
<i class="fas fa-music mr-1"></i>
${playlist.track_count || 0} piste${(playlist.track_count || 0) !== 1 ? 's' : ''}
</p>
</div>
<!-- Actions -->
<div class="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-all">
<button onclick="event.stopPropagation(); showPlaylistDetails('${playlist.id}')"
class="flex-1 px-3 py-2 text-sm bg-gray-700 hover:bg-gray-600 rounded-lg transition-all flex items-center justify-center gap-2"
title="Voir les détails">
<i class="fas fa-eye"></i>
<span>Voir</span>
</button>
<button onclick="event.stopPropagation(); deletePlaylistWithConfirm('${playlist.id}', '${playlist.name}')"
class="px-3 py-2 text-sm bg-red-600/20 hover:bg-red-600/40 text-red-400 rounded-lg transition-all"
title="Supprimer la playlist"
aria-label="Supprimer ${playlist.name}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
}).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<void>}
*/
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 {
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 = `
<div class="text-center py-8">
<i class="fas fa-exclamation-circle text-red-400 text-4xl mb-3"></i>
<p class="text-gray-400">Erreur de chargement des titres likés</p>
<p class="text-gray-500 text-sm mt-2">${error.message}</p>
</div>
`;
}
}
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
*/
function updateLikedTracksUI(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 = `
<div class="text-center py-12">
<i class="far fa-heart text-gray-600 text-5xl mb-4"></i>
<p class="text-gray-400">Aucun titre liké pour le moment</p>
<p class="text-gray-500 text-sm mt-2">Cliquez sur le cœur pour ajouter des titres</p>
</div>
`;
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 `
<div class="glass-card rounded-xl p-3 sm:p-4 hover:bg-gray-800/50 transition-all cursor-pointer group"
data-id="${trackId}"
data-title="${encodeURIComponent(title)}"
data-artist="${encodeURIComponent(artist)}"
data-cover="${encodeURIComponent(cover)}"
role="listitem"
tabindex="0"
aria-label="${title} de ${artist}">
<div class="flex items-center gap-3 sm:gap-4">
<!-- Cover -->
<div class="relative flex-shrink-0">
<img src="${cover}"
alt="${title}"
class="w-12 h-12 sm:w-16 sm:h-16 rounded-lg object-cover bg-gray-800">
<button class="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg"
onclick="event.stopPropagation(); playTrack('${trackId}', ${isYoutube})"
aria-label="Lire ${title}">
<i class="fas fa-play text-white text-lg"></i>
</button>
</div>
<!-- Track Info -->
<div class="flex-1 min-w-0">
<h3 class="font-medium text-white truncate text-sm sm:text-base">${title}</h3>
<p class="text-gray-400 text-xs sm:text-sm truncate">${artist}</p>
</div>
<!-- Actions -->
<div class="flex items-center gap-2 flex-shrink-0">
<button class="p-2 text-accent-400 hover:bg-gray-700/50 rounded-lg transition-all"
onclick="event.stopPropagation(); toggleLikeTrack('${trackId}')"
aria-label="Retirer des favoris"
aria-pressed="true">
<i class="fas fa-heart"></i>
</button>
</div>
</div>
</div>
`;
}).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
*/
async function toggleLikeTrack(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<void>}
*/
async function loadListeningHistory() {
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 {
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 = `
<div class="text-center py-8">
<i class="fas fa-exclamation-circle text-red-400 text-4xl mb-3"></i>
<p class="text-gray-400">Erreur de chargement de l'historique</p>
<p class="text-gray-500 text-sm mt-2">${error.message}</p>
</div>
`;
}
}
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
*/
function renderListeningHistory(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 = `
<div class="text-center py-12">
<i class="fas fa-history text-gray-600 text-5xl mb-4"></i>
<p class="text-gray-400">Aucun historique d'écoute</p>
<p class="text-gray-500 text-sm mt-2">Vos écoutes récentes apparaîtront ici</p>
</div>
`;
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 += `
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-400 mb-3 sticky top-0 bg-gray-900/95 backdrop-blur-sm py-2 border-b border-gray-800">
${group.display}
</h3>
<div class="space-y-2">
`;
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 += `
<div class="glass-card rounded-xl p-3 sm:p-4 hover:bg-gray-800/50 transition-all cursor-pointer group"
data-id="${trackId}"
data-title="${encodeURIComponent(title)}"
data-artist="${encodeURIComponent(artist)}"
data-cover="${encodeURIComponent(cover)}"
role="listitem"
tabindex="0"
aria-label="${title} de ${artist}, écouté ${timeStr}">
<div class="flex items-center gap-3 sm:gap-4">
<!-- Cover -->
<div class="relative flex-shrink-0">
<img src="${cover}"
alt="${title}"
class="w-12 h-12 sm:w-16 sm:h-16 rounded-lg object-cover bg-gray-800">
<button class="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg"
onclick="event.stopPropagation(); playTrack('${trackId}', ${isYoutube})"
aria-label="Lire ${title}">
<i class="fas fa-play text-white text-lg"></i>
</button>
</div>
<!-- Track Info -->
<div class="flex-1 min-w-0">
<h3 class="font-medium text-white truncate text-sm sm:text-base">${title}</h3>
<p class="text-gray-400 text-xs sm:text-sm truncate">${artist}</p>
</div>
<!-- Time -->
<div class="text-xs text-gray-500 flex-shrink-0">
${timeStr}
</div>
</div>
</div>
`;
});
html += `
</div>
</div>
`;
});
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
*/
function formatDateKey(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
*/
function formatDateDisplay(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
*/
function formatTimeAgo(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));
}
async function loadTrendingTracks() {
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 = '<p style="color: var(--text-secondary);">Erreur de chargement</p>';
}
} catch (error) {
console.error('[loadTrendingTracks] Failed to load trending tracks:', error);
container.innerHTML = '<p style="color: var(--text-secondary);">Erreur de chargement: ' + error.message + '</p>';
}
}
// ============================================
// 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 = `
<div class="loading">
<div class="spinner" style="width: 40px; height: 40px; margin: 0 auto 1rem;"></div>
<p>Recherche en cours...</p>
</div>
`;
}
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 = `
<div class="loading">
<div class="spinner" style="width: 60px; height: 60px; margin: 0 auto 1rem;"></div>
<p style="font-size: 1.1rem; color: var(--text-primary);">Recherche de "${query}" en cours...</p>
<p style="font-size: 0.9rem; color: var(--text-secondary); margin-top: 0.5rem;">Cela peut prendre quelques secondes</p>
</div>
`;
} 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
async function performSearch(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 = `
<div style="text-align: center; padding: var(--space-2xl);">
<i class="fas fa-search" style="font-size: 3rem; color: var(--text-muted); margin-bottom: var(--space-md);"></i>
<p style="color: var(--text-secondary); font-size: 1.1rem;">Aucun résultat pour "${query}"</p>
<p style="color: var(--text-muted); font-size: 0.9rem; margin-top: var(--space-sm);">Essayez d'autres mots-clés</p>
</div>
`;
console.log('[performSearch] ✓ Empty state rendered');
} else {
console.log('[performSearch] → Tracks found, rendering results...');
// Add results header
container.innerHTML = `
<div style="margin-bottom: var(--space-lg); padding: var(--space-md); background: var(--bg-card); border-radius: var(--radius-md);">
<p style="color: var(--text-secondary); margin: 0;">
<i class="fas fa-check-circle" style="color: var(--success);"></i>
${tracks.length} résultat${tracks.length > 1 ? 's' : ''} trouvé${tracks.length > 1 ? 's' : ''} pour "${query}"
</p>
</div>
`;
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 = `
<div style="text-align: center; padding: var(--space-2xl);">
<i class="fas fa-exclamation-triangle" style="font-size: 3rem; color: var(--error); margin-bottom: var(--space-md);"></i>
<p style="color: var(--text-secondary);">Erreur lors de la recherche</p>
<button class="btn btn-primary" onclick="handleMainSearch()" style="margin-top: var(--space-md);">Réessayer</button>
</div>
`;
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 = `
<div style="text-align: center; padding: var(--space-2xl);">
<i class="fas fa-wifi" style="font-size: 3rem; color: var(--error); margin-bottom: var(--space-md);"></i>
<p style="color: var(--text-secondary);">Erreur de connexion</p>
<p style="color: var(--text-muted); font-size: 0.9rem; margin-top: var(--space-sm);">Vérifiez votre connexion internet</p>
<button class="btn btn-primary" onclick="handleMainSearch()" style="margin-top: var(--space-md);">Réessayer</button>
</div>
`;
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));
}
function renderTracks(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 = '<p class="text-gray-400 text-center py-8">Aucun résultat</p>';
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 `
<div class="glass-card rounded-xl p-4 hover:bg-gray-700/50 transition-all cursor-pointer group animate-fadeIn relative"
data-id="${track.id}"
data-is-youtube="${isYoutubeTrack}"
data-youtube-id="${track.youtube_id || ''}"
data-title="${encodedTitle}"
data-artist="${encodedArtist}"
data-cover="${encodedCover}"
onclick="playTrack('${track.id}', ${isYoutubeTrack})">
<div class="flex items-center gap-4">
<!-- Cover -->
<img src="${track.image_url || '/static/img/default-cover.png'}"
alt="${track.title}"
class="w-16 h-16 rounded-lg object-cover bg-gray-800 flex-shrink-0">
<!-- Info -->
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-white truncate group-hover:text-primary-400 transition-colors">${track.title}</h3>
<p class="text-sm text-gray-400 truncate">${artistName}</p>
</div>
<!-- Duration -->
<span class="text-sm text-gray-500 flex-shrink-0">
${track.duration ? formatTime(track.duration) : '--:--'}
</span>
<!-- Actions -->
<div class="flex items-center gap-2">
<!-- Add to Playlist Button -->
<div class="relative">
<button onclick="event.stopPropagation(); toggleAddToPlaylistDropdown(event, '${track.id}')"
class="w-10 h-10 flex items-center justify-center bg-gray-700 hover:bg-gray-600 rounded-full opacity-0 group-hover:opacity-100 transition-all transform hover:scale-110"
aria-label="Ajouter à la playlist">
<i class="fas fa-plus text-sm text-white"></i>
</button>
<div id="playlist-dropdown-${track.id}" class="hidden fixed glass-card rounded-xl shadow-xl border border-gray-700 z-[9999] animate-fadeIn" style="min-width: 14rem;">
<div class="p-2 border-b border-gray-700">
<p class="text-xs font-semibold text-gray-400 px-2 py-1">Ajouter à la playlist</p>
</div>
<div id="playlist-options-${track.id}" class="max-h-48 overflow-y-auto p-1">
<!-- Playlist options will be dynamically inserted here -->
</div>
<div class="p-2 border-t border-gray-700">
<button onclick="event.stopPropagation(); createNewPlaylistFromTrack('${track.id}')"
class="w-full px-3 py-2 text-left text-sm text-primary-400 hover:bg-gray-700/50 rounded-lg transition-all flex items-center gap-2">
<i class="fas fa-plus"></i>
Créer une playlist
</button>
</div>
</div>
</div>
<!-- Play Button -->
<button onclick="event.stopPropagation(); playTrack('${track.id}', ${isYoutubeTrack})"
class="w-10 h-10 flex items-center justify-center bg-primary-600 hover:bg-primary-500 rounded-full opacity-0 group-hover:opacity-100 transition-all transform hover:scale-110">
<i class="fas fa-play text-sm text-white"></i>
</button>
</div>
</div>
</div>
`;
}).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 = `
<div class="flex flex-col items-center justify-center py-12 text-gray-400">
<i class="fas fa-list-ul text-4xl mb-4"></i>
<p class="text-lg">File d'attente vide</p>
<p class="text-sm mt-2">Cliquez sur une piste pour l'ajouter</p>
</div>
`;
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 `
<div class="queue-item flex items-center gap-3 p-3 rounded-lg transition-all cursor-pointer group
${isCurrentTrack ? 'bg-primary-600/20 border-l-4 border-primary-500' : 'hover:bg-gray-800/50'}"
data-index="${index}"
onclick="playTrackFromQueue(${index})"
role="button"
tabindex="0"
aria-label="Lire ${track.title}"
aria-current="${isCurrentTrack ? 'true' : 'false'}">
<div class="flex-shrink-0 w-8 text-center text-sm ${isCurrentTrack ? 'text-primary-400' : 'text-gray-500'}">
${isCurrentTrack
? '<i class="fas fa-volume-up animate-pulse"></i>'
: `<span class="group-hover:hidden">${index + 1}</span><i class="fas fa-play hidden group-hover:inline"></i>`
}
</div>
<img src="${track.image_url || track.cover || '/static/img/default-cover.png'}"
alt=""
class="w-10 h-10 rounded object-cover bg-gray-800 flex-shrink-0"
aria-hidden="true">
<div class="flex-1 min-w-0">
<p class="font-medium text-sm truncate ${isCurrentTrack ? 'text-primary-400' : 'text-white'}">
${track.title}
</p>
<p class="text-xs text-gray-400 truncate">${artistName}</p>
</div>
<button onclick="event.stopPropagation(); removeFromQueue(${index})"
class="p-2 text-gray-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-all"
aria-label="Retirer ${track.title} de la file">
<i class="fas fa-times"></i>
</button>
</div>
`;
}).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<string|null>} - 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 = `
<div class="px-3 py-2 text-sm text-gray-400 text-center">
Aucune playlist
</div>
`;
} else {
optionsContainer.innerHTML = AppState.playlists.map(playlist => `
<button onclick="event.stopPropagation(); addTrackToPlaylist('${trackId}', '${playlist.id}', '${playlist.name.replace(/'/g, "\\'")}')"
class="w-full px-3 py-2 text-left text-sm text-gray-300 hover:bg-gray-700/50 rounded-lg transition-all truncate">
${playlist.name}
</button>
`).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 = `
<div class="flex flex-col items-center justify-center py-12 text-gray-400">
<i class="fas fa-music text-4xl mb-4"></i>
<p class="text-lg">Aucune piste disponible</p>
</div>
`;
}
} else {
tracksContainer.innerHTML = `
<div class="flex flex-col items-center justify-center py-12 text-gray-400">
<i class="fas fa-music text-4xl mb-4"></i>
<p class="text-lg">Aucune piste</p>
<p class="text-sm mt-2">Ajoutez des pistes depuis la recherche</p>
</div>
`;
}
// 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 = `
<i class="fas ${iconClasses[type] || iconClasses.success} text-lg"></i>
<span class="flex-1 text-white">${message}</span>
<button onclick="this.parentElement.remove()" class="text-gray-400 hover:text-white transition-colors">
<i class="fas fa-times"></i>
</button>
`;
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();
}