diff --git a/pkg/discord/jukebox.go b/pkg/discord/jukebox.go index 10120c1..a896f76 100644 --- a/pkg/discord/jukebox.go +++ b/pkg/discord/jukebox.go @@ -22,6 +22,7 @@ import ( type JukeboxPlayer struct { bot *Bot currentSong *subsonic.Song + currentSongStarted time.Time playlist []subsonic.Song playlistMutex sync.Mutex playingMutex sync.Mutex @@ -29,6 +30,7 @@ type JukeboxPlayer struct { songHistory []*subsonic.Song historyMutex sync.RWMutex maxHistorySize int + initialized bool } // NewJukeboxPlayer creates a new jukebox player @@ -38,6 +40,7 @@ func NewJukeboxPlayer(bot *Bot) *JukeboxPlayer { playlist: make([]subsonic.Song, 0), songHistory: make([]*subsonic.Song, 0), maxHistorySize: 20, // Store the last 20 songs + initialized: true, } // Register command handlers @@ -236,6 +239,7 @@ func (j *JukeboxPlayer) startPlaying() { // Clear current song when stopping j.playingMutex.Lock() j.currentSong = nil + j.currentSongStarted = time.Time{} // Reset time to zero value j.playingMutex.Unlock() return @@ -273,6 +277,7 @@ func (j *JukeboxPlayer) startPlaying() { j.playingMutex.Lock() j.currentSong = song + j.currentSongStarted = time.Now() j.playingMutex.Unlock() // Add song to history when it starts playing @@ -781,6 +786,13 @@ func (j *JukeboxPlayer) GetCurrentSong() *subsonic.Song { return j.currentSong } +// GetCurrentSongInfo returns the current song and its start time +func (j *JukeboxPlayer) GetCurrentSongInfo() (*subsonic.Song, time.Time) { + j.playingMutex.Lock() + defer j.playingMutex.Unlock() + return j.currentSong, j.currentSongStarted +} + // addToHistory adds a song to the playback history func (j *JukeboxPlayer) addToHistory(song *subsonic.Song) { if song == nil { @@ -790,6 +802,12 @@ func (j *JukeboxPlayer) addToHistory(song *subsonic.Song) { j.historyMutex.Lock() defer j.historyMutex.Unlock() + // Avoid duplicate entries at the beginning of history + if len(j.songHistory) > 0 && j.songHistory[0] != nil && + j.songHistory[0].ID == song.ID { + return + } + // Add the song to the beginning of the history list j.songHistory = append([]*subsonic.Song{song}, j.songHistory...) @@ -804,6 +822,11 @@ func (j *JukeboxPlayer) GetSongHistory() []*subsonic.Song { j.historyMutex.RLock() defer j.historyMutex.RUnlock() + // Ensure we don't return a nil slice even if no history yet + if j.songHistory == nil { + return make([]*subsonic.Song, 0) + } + // Create a copy of the history to return history := make([]*subsonic.Song, len(j.songHistory)) copy(history, j.songHistory) diff --git a/pkg/web/server.go b/pkg/web/server.go index 3cbd17c..8cfbc3e 100644 --- a/pkg/web/server.go +++ b/pkg/web/server.go @@ -149,10 +149,11 @@ func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) { // StatusResponse contains the jukebox status type StatusResponse struct { - IsPlaying bool `json:"isPlaying"` - CurrentSong *subsonic.Song `json:"currentSong"` - SongHistory []*subsonic.Song `json:"songHistory"` - UpdateTime string `json:"updateTime"` + IsPlaying bool `json:"isPlaying"` + CurrentSong *subsonic.Song `json:"currentSong"` + CurrentSongStarted string `json:"currentSongStarted"` + SongHistory []*subsonic.Song `json:"songHistory"` + UpdateTime string `json:"updateTime"` } // handleStatus returns the current jukebox status as JSON @@ -165,15 +166,22 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { // getStatus returns the current status func (s *Server) getStatus() StatusResponse { - currentSong := s.jukebox.GetCurrentSong() + currentSong, songStartTime := s.jukebox.GetCurrentSongInfo() songHistory := s.jukebox.GetSongHistory() isPlaying := s.bot.IsPlaying() + + // Format the start time if it's not zero + startTimeStr := "" + if !songStartTime.IsZero() { + startTimeStr = songStartTime.Format(time.RFC3339) + } return StatusResponse{ - IsPlaying: isPlaying, - CurrentSong: currentSong, - SongHistory: songHistory, - UpdateTime: time.Now().Format(time.RFC3339), + IsPlaying: isPlaying, + CurrentSong: currentSong, + CurrentSongStarted: startTimeStr, + SongHistory: songHistory, + UpdateTime: time.Now().Format(time.RFC3339), } } @@ -208,7 +216,8 @@ func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) { notifyChan <- true }() - // Send initial status update + // Send initial status update as soon as connection is established + // This ensures the client receives current data immediately without waiting for changes status := s.getStatus() data, err := json.Marshal(status) if err == nil { @@ -287,6 +296,7 @@ func (s *Server) broadcastUpdates() { // Keep track of the previous status to only send updates when there are changes var prevIsPlaying bool var prevSongID string + var prevHistoryHash string ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() @@ -309,12 +319,23 @@ func (s *Server) broadcastUpdates() { currentSongID = status.CurrentSong.ID } - hasChanged := (prevIsPlaying != status.IsPlaying) || (prevSongID != currentSongID) + // Create a simple hash of the history to detect changes + historyHash := "" + for _, song := range status.SongHistory { + if song != nil { + historyHash += song.ID + ":" + } + } + + hasChanged := (prevIsPlaying != status.IsPlaying) || + (prevSongID != currentSongID) || + (prevHistoryHash != historyHash) if hasChanged { // Update previous state prevIsPlaying = status.IsPlaying prevSongID = currentSongID + prevHistoryHash = historyHash // Broadcast to all clients data, err := json.Marshal(status) diff --git a/pkg/web/static/app.js b/pkg/web/static/app.js index d923e89..d7e7021 100644 --- a/pkg/web/static/app.js +++ b/pkg/web/static/app.js @@ -17,9 +17,26 @@ const songDuration = document.getElementById('song-duration'); const songGenre = document.getElementById('song-genre'); const songYear = document.getElementById('song-year'); +// Progress bar elements +const progressBar = document.getElementById('progress-bar'); +const currentTime = document.getElementById('current-time'); +const totalTime = document.getElementById('total-time'); + // Base64 encoded default cover art (simple music note icon) const DEFAULT_COVER_ART = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAF+mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0wNC0wMVQxNTozODoxOSswMjowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMDQtMDFUMTU6NDE6MDcrMDI6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMDQtMDFUMTU6NDE6MDcrMDI6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjZiMGE4MDQtZWYxZS1hMDQ0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'; +// Track if initial data has been received +let initialDataReceived = false; + +// Track song progress state +let songStartTime = null; +let songDurationSeconds = 0; +let progressInterval = null; + +// Show loading indicator +const loadingOverlay = document.getElementById('loading-overlay'); +const connectionError = document.getElementById('connection-error'); + // Initial status load fetchStatus(); @@ -32,7 +49,18 @@ function setupEventSource() { eventSource.onmessage = function(event) { const data = JSON.parse(event.data); + initialDataReceived = true; updateUI(data); + + // Hide loading indicator once we've received data + if (loadingOverlay) { + loadingOverlay.classList.add('hidden'); + } + + // Hide connection error message if it was showing + if (connectionError) { + connectionError.classList.add('hidden'); + } }; eventSource.onerror = function() { @@ -41,6 +69,11 @@ function setupEventSource() { statusDot.className = 'status-dot offline'; statusText.textContent = 'Connection lost. Retrying...'; + // Show connection error message + if (connectionError) { + connectionError.classList.remove('hidden'); + } + setTimeout(setupEventSource, 5000); }; } @@ -49,16 +82,43 @@ function fetchStatus() { fetch('/status') .then(response => response.json()) .then(data => { + initialDataReceived = true; updateUI(data); + + // Hide loading indicator once we've received data + if (loadingOverlay) { + loadingOverlay.classList.add('hidden'); + } + + // Hide connection error message if it was showing + if (connectionError) { + connectionError.classList.add('hidden'); + } }) .catch(error => { console.error('Error fetching status:', error); statusDot.className = 'status-dot offline'; statusText.textContent = 'Error connecting to server'; + + // Show connection error message + if (connectionError) { + connectionError.classList.remove('hidden'); + } }); } function updateUI(data) { + // If this is the first data we've received, update everything + const isInitialUpdate = !document.getElementById('data-loaded'); + + if (isInitialUpdate) { + // Mark the page as having received data + const dataLoadedMark = document.createElement('div'); + dataLoadedMark.id = 'data-loaded'; + dataLoadedMark.style.display = 'none'; + document.body.appendChild(dataLoadedMark); + } + // Update status indicator if (data.isPlaying) { statusDot.className = 'status-dot online'; @@ -70,19 +130,23 @@ function updateUI(data) { statusText.textContent = 'Not playing'; nowPlaying.classList.add('hidden'); notPlaying.classList.remove('hidden'); + + // Stop progress bar updates if not playing + stopProgressUpdates(); } // Update song information if we have a current song if (data.currentSong) { - updateSongInfo(data.currentSong); + updateSongInfo(data.currentSong, data.currentSongStarted); } // Update song history if available + // Always show history if it exists, even when nothing is playing if (data.songHistory && data.songHistory.length > 0) { updateSongHistory(data.songHistory, data.currentSong); songHistory.classList.remove('hidden'); } else { - // No history available + // Hide history section only if there's no history songHistory.classList.add('hidden'); } @@ -93,7 +157,7 @@ function updateUI(data) { } } -function updateSongInfo(song) { +function updateSongInfo(song, startTimeStr) { // Update the song title songTitle.textContent = song.title || 'Unknown Title'; @@ -108,8 +172,14 @@ function updateSongInfo(song) { const minutes = Math.floor(song.duration / 60); const seconds = song.duration % 60; songDuration.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; + songDurationSeconds = song.duration; + + // Update total time in progress bar + totalTime.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; } else { songDuration.textContent = '--'; + songDurationSeconds = 0; + totalTime.textContent = '--'; } // Update the genre if available @@ -129,24 +199,86 @@ function updateSongInfo(song) { coverArt.src = DEFAULT_COVER_ART; coverArt.alt = 'Default cover art'; } + + // Set up progress bar + setupProgressBar(startTimeStr); } function formatDate(date) { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } +// Format seconds to MM:SS format +function formatTime(seconds) { + const minutes = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${minutes}:${secs.toString().padStart(2, '0')}`; +} + +// Set up progress bar with song start time +function setupProgressBar(startTimeStr) { + // Stop any existing progress updates + stopProgressUpdates(); + + // Reset progress bar + progressBar.style.width = '0%'; + currentTime.textContent = '0:00'; + + // If we don't have a start time or duration, exit + if (!startTimeStr || songDurationSeconds <= 0) { + return; + } + + // Parse the start time + songStartTime = new Date(startTimeStr); + + // Start progress updates + updateProgress(); + progressInterval = setInterval(updateProgress, 1000); +} + +// Update the progress bar +function updateProgress() { + if (!songStartTime || songDurationSeconds <= 0) { + return; + } + + // Calculate elapsed time in seconds + const now = new Date(); + const elapsedSeconds = (now - songStartTime) / 1000; + + // Calculate progress percentage + const progressPercent = Math.min(100, (elapsedSeconds / songDurationSeconds) * 100); + + // Update progress bar width + progressBar.style.width = `${progressPercent}%`; + + // Update current time display + const displaySeconds = Math.min(songDurationSeconds, elapsedSeconds); + currentTime.textContent = formatTime(displaySeconds); + + // If we've reached the end of the song (with a small buffer) + if (elapsedSeconds >= songDurationSeconds + 2) { + stopProgressUpdates(); + } +} + +// Stop progress updates +function stopProgressUpdates() { + if (progressInterval) { + clearInterval(progressInterval); + progressInterval = null; + } + songStartTime = null; +} + // Update the song history list function updateSongHistory(songs, currentSong) { // Clear the current list historyList.innerHTML = ''; - // Show history even when nothing is playing - - // Start from the first song - const startIndex = 0; - - // If no songs are available after filtering current one, show a message - if (songs.length === 0) { + // If there's no song history yet, show a message + if (!songs || songs.length === 0) { const li = document.createElement('li'); li.className = 'history-item history-empty'; li.textContent = 'No song history available yet'; @@ -154,14 +286,28 @@ function updateSongHistory(songs, currentSong) { return; } - // Add each song to the history list - for (let i = startIndex; i < songs.length; i++) { - const song = songs[i]; + // Create a set of songs for the history that excludes the current song + const historySongs = []; + const currentId = currentSong ? currentSong.id : null; + + // Filter out null entries and the current song + for (const song of songs) { if (!song) continue; - - // Skip if this song is the same as the currently playing song - if (currentSong && song.id === currentSong.id) continue; - + if (currentId && song.id === currentId) continue; + historySongs.push(song); + } + + // If after filtering, we don't have any songs to display + if (historySongs.length === 0) { + const li = document.createElement('li'); + li.className = 'history-item history-empty'; + li.textContent = 'No previous songs to display'; + historyList.appendChild(li); + return; + } + + // Add each song to the history list + for (const song of historySongs) { const li = document.createElement('li'); li.className = 'history-item'; diff --git a/pkg/web/static/index.html b/pkg/web/static/index.html index 821f626..0998a08 100644 --- a/pkg/web/static/index.html +++ b/pkg/web/static/index.html @@ -8,6 +8,10 @@
+Loading jukebox data...
+