🚴‍♂️ Music Cycling Analyzer

Analyze music from Spotify or upload MP3 files for cycling cadence and resistance

🔒 Secure Connection: Your Spotify credentials are handled securely through encrypted server-side authentication. No sensitive tokens are stored in your browser.

Connect your Spotify account to search songs or browse playlists

checkAuthStatus() { try { const response = await fetch(AUTH_CONFIG.AUTH_ENDPOINT + '/status', { credentials: 'include' }); if (response.ok) { const data = await response.json(); if (data.authenticated) { isAuthenticated = true; showSpotifyInterface(); } } else { // Check for auth callback const urlParams = new URLSearchParams(window.location.search); const authCode = urlParams.get('code'); const error = urlParams.get('error'); if (error) { showError(`Spotify authentication failed: ${error}`); return; } if (authCode) { handleAuthCallback(authCode); } } } catch (error) { console.error('Auth status check failed:', error); // Show error message if Spotify auth is unavailable showError('Spotify authentication is currently unavailable. Please try again later.'); } } function hideSpotifyTab() { const spotifyTab = document.querySelector('.tab-button[onclick="switchTab(\'spotify\')"]'); if (spotifyTab) { spotifyTab.style.display = 'none'; } } function showSpotifyInterface() { document.getElementById('spotifyAuth').style.display = 'none'; document.getElementById('spotifySearch').style.display = 'block'; } async function // Analysis and Player functions async function analyzeSpotifyTrack(track) { const baseCadence = Math.floor(track.bpm * 0.6); songData = { source: 'spotify', trackId: track.id, name: track.name, artist: track.artist, duration: track.duration, bpm: Math.floor(track.bpm), energy: track.energy, danceability: track.danceability, valence: track.valence, popularity: Math.floor(track.popularity), baseCadence: Math.floor(baseCadence), energyLevel: getEnergyLevelFromValue(track.energy), cadencePattern: generateEnhancedCadencePattern(track.duration, baseCadence, track.energy), resistancePattern: generateEnhancedResistancePattern(track.duration, track.bpm, track.energy) }; updateAnalysisDisplay(); } function generateEnhancedCadencePattern(duration, baseCadence, energy) { const pattern = []; const segments = Math.floor(duration / 30); for (let i = 0; i < segments; i++) { const energyInfluence = energy * 0.3; const randomVariation = (Math.random() - 0.5) * 0.2; const multiplier = 0.85 + energyInfluence + randomVariation; let cadence = baseCadence * multiplier; cadence = Math.round(cadence / 5) * 5; cadence = Math.floor(cadence); cadence = Math.max(70, Math.min(110, cadence)); pattern.push(cadence); } return pattern; } function generateEnhancedResistancePattern(duration, bpm, energy) { const pattern = []; const segments = Math.floor(duration / 30); let baseResistance; if (bpm < 100) { baseResistance = 25; } else if (bpm < 120) { baseResistance = 35; } else if (bpm < 140) { baseResistance = 45; } else { baseResistance = 55; } for (let i = 0; i < segments; i++) { const energyVariation = (energy - 0.5) * 15; const randomVariation = (Math.random() - 0.5) * 10; let resistance = baseResistance + energyVariation + randomVariation; resistance = Math.round(resistance / 5) * 5; resistance = Math.floor(resistance); resistance = Math.max(15, Math.min(70, resistance)); pattern.push(resistance); } return pattern; } function showPlayerSection(source) { document.getElementById('playerSection').style.display = 'block'; document.getElementById('metricsSection').style.display = 'block'; document.getElementById('analysisSection').style.display = 'block'; if (intervalId) { clearInterval(intervalId); } let elapsedTime = 0; intervalId = setInterval(() => { elapsedTime += 1; updateMetricsForTime(elapsedTime); }, 2000); if (isPlaylistMode) { document.getElementById('playlistControls').style.display = 'block'; } else { document.getElementById('playlistControls').style.display = 'none'; } } function updateMetricsForTime(currentTime) { if (!songData || !songData.cadencePattern || !songData.resistancePattern) { return; } const segmentIndex = Math.floor(currentTime / 30); const maxSegments = songData.cadencePattern.length; const currentSegment = Math.min(segmentIndex, maxSegments - 1); let baseCadence = songData.cadencePattern[currentSegment]; let baseResistance = songData.resistancePattern[currentSegment]; if (typeof baseCadence !== 'number') { baseCadence = 85; } if (typeof baseResistance !== 'number') { baseResistance = 35; } let cadenceMin = Math.max(70, baseCadence - 10); let cadenceMax = Math.min(110, baseCadence + 10); let resistanceMin = Math.max(15, baseResistance - 10); let resistanceMax = Math.min(70, baseResistance + 10); cadenceMin = Math.round(Math.floor(cadenceMin) / 5) * 5; cadenceMax = Math.round(Math.floor(cadenceMax) / 5) * 5; resistanceMin = Math.round(Math.floor(resistanceMin) / 5) * 5; resistanceMax = Math.round(Math.floor(resistanceMax) / 5) * 5; const cadenceDisplay = `${cadenceMin}-${cadenceMax}`; const resistanceDisplay = `${resistanceMin}-${resistanceMax}`; document.getElementById('currentCadence').textContent = cadenceDisplay; document.getElementById('currentResistance').textContent = resistanceDisplay; } function estimateBPMFromTrack(track) { const genres = track.artists[0].genres || []; const popularity = track.popularity; let baseBPM = 120; const genreString = genres.join(' ').toLowerCase(); if (genreString.includes('electronic') || genreString.includes('dance')) { baseBPM = 128; } else if (genreString.includes('hip hop') || genreString.includes('rap')) { baseBPM = 90; } else if (genreString.includes('rock') || genreString.includes('metal')) { baseBPM = 140; } else if (genreString.includes('pop')) { baseBPM = 115; } else if (genreString.includes('jazz') || genreString.includes('classical')) { baseBPM = 100; } const popularityFactor = (popularity / 100) * 20; const variation = (Math.random() - 0.5) * 30; const estimatedBPM = Math.round(baseBPM + popularityFactor + variation); return Math.max(80, Math.min(180, estimatedBPM)); } function estimateEnergyFromTrack(track) { const popularity = track.popularity; const duration = track.duration_ms / 1000; let energyScore = 0.5; energyScore += (popularity / 100) * 0.3; if (duration < 180) { energyScore += 0.2; } else if (duration > 300) { energyScore -= 0.1; } energyScore += (Math.random() - 0.5) * 0.4; return Math.max(0.1, Math.min(1.0, energyScore)); } function estimateDanceability(track) { const popularity = track.popularity; let danceability = 0.5 + (popularity / 100) * 0.3; danceability += (Math.random() - 0.5) * 0.4; return Math.max(0.1, Math.min(1.0, danceability)); } function estimateValence(track) { const popularity = track.popularity; let valence = 0.5 + (popularity / 100) * 0.2; valence += (Math.random() - 0.5) * 0.6; return Math.max(0.1, Math.min(1.0, valence)); } function setupSpotifyPlayer(track) { document.getElementById('trackImage').src = track.image || 'https://via.placeholder.com/80x80/667eea/white?text=♪'; document.getElementById('trackTitle').textContent = track.name; document.getElementById('trackArtist').textContent = track.artist; const spotifyPlayerElement = document.getElementById('spotifyPlayer'); const trackId = track.uri ? track.uri.split(':')[2] : track.id; const embedUrl = `https://open.spotify.com/embed/track/${trackId}?utm_source=generator&theme=0&autoplay=1&show_cover_art=true`; spotifyPlayerElement.src = embedUrl; document.getElementById('spotifyPlayer').style.display = 'block'; if (isPlaylistMode) { setTimeout(() => { const iframe = document.getElementById('spotifyPlayer'); if (iframe && iframe.contentWindow) { try { iframe.contentWindow.postMessage({command: 'play'}, 'https://open.spotify.com'); } catch (e) { showPlayInstructions(); } } }, 1000); } } function showPlayInstructions() { if (isPlaylistMode && currentTrackIndex === 0) { showSuccess('🎵 Click the play button in the Spotify player below to start your workout!'); } } function updateAnalysisDisplay() { document.getElementById('songBPM').textContent = songData.bpm; document.getElementById('energyLevel').textContent = songData.energyLevel; document.getElementById('sourceType').textContent = 'Spotify'; document.getElementById('songTitle').textContent = songData.name; document.getElementById('songDuration').textContent = formatTime(songData.duration); document.getElementById('popularity').textContent = songData.popularity + '/100'; const avgCadence = Math.floor(songData.cadencePattern.reduce((a, b) => a + b, 0) / songData.cadencePattern.length); const avgResistance = Math.floor(songData.resistancePattern.reduce((a, b) => a + b, 0) / songData.resistancePattern.length); document.getElementById('avgCadence').textContent = avgCadence; document.getElementById('avgResistance').textContent = avgResistance; } function getEnergyLevelFromValue(energy) { if (energy < 0.3) return 'Low'; if (energy < 0.6) return 'Medium'; if (energy < 0.8) return 'High'; return 'Very High'; } function getEnergyLevel(bpm) { if (bpm < 100) return 'Low'; if (bpm < 120) return 'Medium'; if (bpm < 140) return 'High'; return 'Very High'; }