class SpotifyPlayer { constructor() { this.token = null; this.player = null; this.deviceId = null; this.currentTrack = null; this.isPlaying = false; this.currentPosition = 0; this.duration = 0; this.volume = 0.5; this.queue = []; this.currentQueueIndex = -1; this.isRadioMode = false; this.playbackContext = null; this.init(); } init() { this.checkToken(); this.setupEventListeners(); } checkToken() { // Check localStorage first const storedToken = localStorage.getItem('spotify_access_token'); if (storedToken) { this.token = storedToken; this.showMainApp(); return; } // Check URL hash const hash = window.location.hash.substring(1); const params = new URLSearchParams(hash); const token = params.get('access_token'); if (token) { this.token = token; localStorage.setItem('spotify_access_token', token); window.location.hash = ''; this.showMainApp(); } else { this.showLoginScreen(); } } showLoginScreen() { document.getElementById('login-screen').classList.remove('hidden'); document.getElementById('main-app').classList.add('hidden'); } async showMainApp() { document.getElementById('login-screen').classList.add('hidden'); document.getElementById('main-app').classList.remove('hidden'); await this.initializeSpotifyPlayer(); await this.loadUserProfile(); await this.loadLikedSongs(); } async initializeSpotifyPlayer() { return new Promise((resolve) => { window.onSpotifyWebPlaybackSDKReady = () => { this.player = new Spotify.Player({ name: 'Spotify Web Player', getOAuthToken: cb => { cb(this.token); }, volume: this.volume }); // Error handling this.player.addListener('initialization_error', ({ message }) => { console.error('Failed to initialize:', message); }); this.player.addListener('authentication_error', ({ message }) => { console.error('Failed to authenticate:', message); this.logout(); }); this.player.addListener('account_error', ({ message }) => { console.error('Failed to validate Spotify account:', message); }); this.player.addListener('playback_error', ({ message }) => { console.error('Failed to perform playback:', message); }); // Playback status updates this.player.addListener('player_state_changed', (state) => { if (!state) return; this.currentTrack = state.track_window.current_track; this.isPlaying = !state.paused; this.currentPosition = state.position; this.duration = state.duration; // Store context for next/previous functionality if (state.context && state.context.uri) { this.playbackContext = state.context; } this.updateUI(); // Check if track ended if (state.paused && state.position === 0 && this.previousPosition > 0) { this.handleTrackEnd(); } this.previousPosition = state.position; }); // Ready this.player.addListener('ready', ({ device_id }) => { console.log('Ready with Device ID', device_id); this.deviceId = device_id; resolve(); }); // Connect to the player! this.player.connect(); }; }); } async loadUserProfile() { try { const response = await fetch('https://api.spotify.com/v1/me', { headers: { 'Authorization': `Bearer ${this.token}` } }); const user = await response.json(); document.getElementById('user-name').textContent = user.display_name || 'User'; if (user.images && user.images.length > 0) { document.getElementById('user-avatar').src = user.images[0].url; } } catch (error) { console.error('Error loading user profile:', error); } } async loadLikedSongs() { try { const response = await fetch('https://api.spotify.com/v1/me/tracks?limit=50', { headers: { 'Authorization': `Bearer ${this.token}` } }); const data = await response.json(); const container = document.getElementById('liked-songs'); container.innerHTML = ''; if (data.items && data.items.length > 0) { // Store liked songs as a playlist this.likedSongs = data.items.map(item => item.track); data.items.forEach((item, index) => { const trackElement = this.createTrackElement(item.track, true, index); container.appendChild(trackElement); }); } else { container.innerHTML = '
No liked songs found
'; } } catch (error) { console.error('Error loading liked songs:', error); document.getElementById('liked-songs').innerHTML = '
Error loading liked songs
'; } } async search(query) { if (!query.trim()) return; this.showLoading(); try { const response = await fetch(`https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=20`, { headers: { 'Authorization': `Bearer ${this.token}` } }); const data = await response.json(); const container = document.getElementById('search-results'); container.innerHTML = ''; if (data.tracks && data.tracks.items.length > 0) { data.tracks.items.forEach(track => { const trackElement = this.createTrackElement(track); container.appendChild(trackElement); }); } else { container.innerHTML = '
No results found
'; } } catch (error) { console.error('Error searching:', error); document.getElementById('search-results').innerHTML = '
Error searching
'; } this.hideLoading(); } createTrackElement(track, isLiked = false, index = -1) { const div = document.createElement('div'); div.className = 'track-item'; if (this.currentTrack && this.currentTrack.id === track.id) { div.classList.add('playing'); } div.innerHTML = ` ${track.name}
${track.name}
${track.artists.map(a => a.name).join(', ')}
`; div.addEventListener('click', (e) => { if (!e.target.classList.contains('like-btn')) { if (isLiked && index >= 0) { // Playing from liked songs - set up queue this.playFromLikedSongs(index); } else { // Playing from search - enable radio mode this.playTrack(track, true); } } }); div.querySelector('.like-btn').addEventListener('click', (e) => { e.stopPropagation(); this.toggleLike(track.id, e.target); }); return div; } async playFromLikedSongs(index) { if (!this.likedSongs || index >= this.likedSongs.length) return; try { // Play all liked songs starting from the selected one const uris = this.likedSongs.slice(index).map(track => track.uri); await fetch(`https://api.spotify.com/v1/me/player/play?device_id=${this.deviceId}`, { method: 'PUT', body: JSON.stringify({ uris: uris }), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.token}` } }); this.isRadioMode = false; this.queue = this.likedSongs.slice(index); this.currentQueueIndex = 0; } catch (error) { console.error('Error playing from liked songs:', error); } } async playTrack(track, startRadioMode = true) { try { if (startRadioMode) { // Get recommendations first await this.loadSimilarTracks(track); // Play the queue if we have recommendations if (this.queue.length > 0) { const uris = this.queue.map(t => t.uri); await fetch(`https://api.spotify.com/v1/me/player/play?device_id=${this.deviceId}`, { method: 'PUT', body: JSON.stringify({ uris: uris }), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.token}` } }); this.isRadioMode = true; this.currentQueueIndex = 0; } else { // Fallback to single track if recommendations fail await fetch(`https://api.spotify.com/v1/me/player/play?device_id=${this.deviceId}`, { method: 'PUT', body: JSON.stringify({ uris: [track.uri] }), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.token}` } }); this.isRadioMode = false; } } else { // Just play single track await fetch(`https://api.spotify.com/v1/me/player/play?device_id=${this.deviceId}`, { method: 'PUT', body: JSON.stringify({ uris: [track.uri] }), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.token}` } }); } this.currentTrack = track; this.updateUI(); } catch (error) { console.error('Error playing track:', error); } } async loadSimilarTracks(seedTrack) { try { const response = await fetch('/recommendations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ access_token: this.token, seed_tracks: seedTrack.id, seed_artists: seedTrack.artists[0].id }) }); if (response.ok) { const data = await response.json(); if (data.tracks) { this.queue = [seedTrack, ...data.tracks]; this.currentQueueIndex = 0; return; } } // Fallback - just use the seed track this.queue = [seedTrack]; this.currentQueueIndex = 0; } catch (error) { console.error('Error loading similar tracks:', error); // Fallback - just use the seed track this.queue = [seedTrack]; this.currentQueueIndex = 0; } } async handleTrackEnd() { // This is handled by the SDK's queue now } async togglePlayPause() { try { if (this.isPlaying) { await this.player.pause(); } else { await this.player.resume(); } } catch (error) { console.error('Error toggling play/pause:', error); } } async nextTrack() { try { await this.player.nextTrack(); } catch (error) { console.error('Error skipping to next track:', error); } } async previousTrack() { try { await this.player.previousTrack(); } catch (error) { console.error('Error skipping to previous track:', error); } } async setVolume(volume) { this.volume = volume / 100; try { await this.player.setVolume(this.volume); } catch (error) { console.error('Error setting volume:', error); } } async toggleLike(trackId, button) { const isLiked = button.classList.contains('liked'); try { const method = isLiked ? 'DELETE' : 'PUT'; await fetch(`https://api.spotify.com/v1/me/tracks?ids=${trackId}`, { method: method, headers: { 'Authorization': `Bearer ${this.token}` } }); button.classList.toggle('liked'); button.textContent = button.classList.contains('liked') ? '♥' : '♡'; // Update current track like button if it's the same track if (this.currentTrack && this.currentTrack.id === trackId) { const currentLikeBtn = document.getElementById('like-current-btn'); currentLikeBtn.classList.toggle('liked'); currentLikeBtn.textContent = currentLikeBtn.classList.contains('liked') ? '♥' : '♡'; } // Reload liked songs if we're on that tab const likedTab = document.getElementById('liked-tab'); if (likedTab.classList.contains('active')) { await this.loadLikedSongs(); } } catch (error) { console.error('Error toggling like:', error); } } updateUI() { // Update current track info if (this.currentTrack) { document.getElementById('current-track-info').classList.remove('hidden'); document.getElementById('current-track-image').src = this.currentTrack.album.images[2]?.url || this.currentTrack.album.images[0]?.url; document.getElementById('current-track-name').textContent = this.currentTrack.name; document.getElementById('current-track-artist').textContent = this.currentTrack.artists.map(a => a.name).join(', '); // Check if current track is liked this.checkIfTrackIsLiked(this.currentTrack.id); } // Update play button document.getElementById('play-btn').textContent = this.isPlaying ? '⏸️' : '▶️'; // Update progress const progressPercent = this.duration ? (this.currentPosition / this.duration) * 100 : 0; document.getElementById('progress').style.width = progressPercent + '%'; // Update time display document.getElementById('current-time').textContent = this.formatTime(this.currentPosition); document.getElementById('total-time').textContent = this.formatTime(this.duration); // Update playing indicators document.querySelectorAll('.track-item').forEach(item => { item.classList.remove('playing'); }); if (this.currentTrack) { document.querySelectorAll('.track-item').forEach(item => { const trackName = item.querySelector('.track-name').textContent; if (trackName === this.currentTrack.name) { item.classList.add('playing'); } }); } } async checkIfTrackIsLiked(trackId) { try { const response = await fetch(`https://api.spotify.com/v1/me/tracks/contains?ids=${trackId}`, { headers: { 'Authorization': `Bearer ${this.token}` } }); const [isLiked] = await response.json(); const currentLikeBtn = document.getElementById('like-current-btn'); if (isLiked) { currentLikeBtn.classList.add('liked'); currentLikeBtn.textContent = '♥'; } else { currentLikeBtn.classList.remove('liked'); currentLikeBtn.textContent = '♡'; } } catch (error) { console.error('Error checking if track is liked:', error); } } formatTime(ms) { const minutes = Math.floor(ms / 60000); const seconds = Math.floor((ms % 60000) / 1000); return `${minutes}:${seconds.toString().padStart(2, '0')}`; } showLoading() { document.getElementById('loading-overlay').classList.remove('hidden'); } hideLoading() { document.getElementById('loading-overlay').classList.add('hidden'); } logout() { localStorage.removeItem('spotify_access_token'); this.token = null; if (this.player) { this.player.disconnect(); } this.showLoginScreen(); } setupEventListeners() { // Login document.getElementById('login-btn').addEventListener('click', () => { window.location.href = '/login'; }); // Logout document.getElementById('logout-btn').addEventListener('click', () => { this.logout(); }); // Tab switching document.querySelectorAll('.nav-tab').forEach(tab => { tab.addEventListener('click', () => { const tabName = tab.dataset.tab; // Update active tab document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); // Show corresponding content document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.getElementById(`${tabName}-tab`).classList.add('active'); }); }); // Search const searchInput = document.getElementById('search-input'); const searchBtn = document.getElementById('search-btn'); const performSearch = () => { this.search(searchInput.value); }; searchBtn.addEventListener('click', performSearch); searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { performSearch(); } }); // Player controls document.getElementById('play-btn').addEventListener('click', () => { this.togglePlayPause(); }); document.getElementById('next-btn').addEventListener('click', () => { this.nextTrack(); }); document.getElementById('prev-btn').addEventListener('click', () => { this.previousTrack(); }); // Volume control document.getElementById('volume-slider').addEventListener('input', (e) => { this.setVolume(e.target.value); }); // Progress bar click document.querySelector('.progress-bar').addEventListener('click', async (e) => { const rect = e.target.getBoundingClientRect(); const percent = (e.clientX - rect.left) / rect.width; const position = percent * this.duration; try { await this.player.seek(position); } catch (error) { console.error('Error seeking:', error); } }); // Current track like button document.getElementById('like-current-btn').addEventListener('click', () => { if (this.currentTrack) { this.toggleLike(this.currentTrack.id, document.getElementById('like-current-btn')); } }); // Update progress periodically setInterval(() => { if (this.isPlaying) { this.player.getCurrentState().then(state => { if (state) { this.currentPosition = state.position; this.updateUI(); } }); } }, 1000); } } // Initialize the app const spotifyPlayer = new SpotifyPlayer();