diff --git a/README.md b/README.md index b007197..b8b96de 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Discord bot written in Go that plays random music from a Subsonic-compatible s - Play random music in a Discord voice channel with real-time audio streaming - Automatic audio format conversion for Discord compatibility - Simple slash commands for controlling the jukebox -- Web interface for viewing current playback status +- Web interface for viewing current playback status and song history - Configurable through environment variables ## Requirements @@ -166,6 +166,7 @@ http://your-server-ip:8080 The web interface features: - Real-time updates when songs change or playback stops - Current song information (artist, album, title, etc.) +- History of the last 20 played songs - Responsive design that works on mobile and desktop - No additional setup required - it works out of the box diff --git a/pkg/discord/jukebox.go b/pkg/discord/jukebox.go index 7990225..10120c1 100644 --- a/pkg/discord/jukebox.go +++ b/pkg/discord/jukebox.go @@ -26,13 +26,18 @@ type JukeboxPlayer struct { playlistMutex sync.Mutex playingMutex sync.Mutex currentStreamCancel context.CancelFunc + songHistory []*subsonic.Song + historyMutex sync.RWMutex + maxHistorySize int } // NewJukeboxPlayer creates a new jukebox player func NewJukeboxPlayer(bot *Bot) *JukeboxPlayer { jukebox := &JukeboxPlayer{ - bot: bot, - playlist: make([]subsonic.Song, 0), + bot: bot, + playlist: make([]subsonic.Song, 0), + songHistory: make([]*subsonic.Song, 0), + maxHistorySize: 20, // Store the last 20 songs } // Register command handlers @@ -232,7 +237,7 @@ func (j *JukeboxPlayer) startPlaying() { j.playingMutex.Lock() j.currentSong = nil j.playingMutex.Unlock() - + return } @@ -269,6 +274,9 @@ func (j *JukeboxPlayer) startPlaying() { j.playingMutex.Lock() j.currentSong = song j.playingMutex.Unlock() + + // Add song to history when it starts playing + j.addToHistory(song) // Update Discord status with the current song information in format "Artist - Title (Album)" var statusText string @@ -772,3 +780,33 @@ func (j *JukeboxPlayer) GetCurrentSong() *subsonic.Song { defer j.playingMutex.Unlock() return j.currentSong } + +// addToHistory adds a song to the playback history +func (j *JukeboxPlayer) addToHistory(song *subsonic.Song) { + if song == nil { + return + } + + j.historyMutex.Lock() + defer j.historyMutex.Unlock() + + // Add the song to the beginning of the history list + j.songHistory = append([]*subsonic.Song{song}, j.songHistory...) + + // Trim the history if it exceeds the maximum size + if len(j.songHistory) > j.maxHistorySize { + j.songHistory = j.songHistory[:j.maxHistorySize] + } +} + +// GetSongHistory returns a copy of the playback history +func (j *JukeboxPlayer) GetSongHistory() []*subsonic.Song { + j.historyMutex.RLock() + defer j.historyMutex.RUnlock() + + // Create a copy of the history to return + history := make([]*subsonic.Song, len(j.songHistory)) + copy(history, j.songHistory) + + return history +} diff --git a/pkg/web/server.go b/pkg/web/server.go index d9067cf..3cbd17c 100644 --- a/pkg/web/server.go +++ b/pkg/web/server.go @@ -149,9 +149,10 @@ 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"` - UpdateTime string `json:"updateTime"` + IsPlaying bool `json:"isPlaying"` + CurrentSong *subsonic.Song `json:"currentSong"` + SongHistory []*subsonic.Song `json:"songHistory"` + UpdateTime string `json:"updateTime"` } // handleStatus returns the current jukebox status as JSON @@ -165,11 +166,13 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { // getStatus returns the current status func (s *Server) getStatus() StatusResponse { currentSong := s.jukebox.GetCurrentSong() + songHistory := s.jukebox.GetSongHistory() isPlaying := s.bot.IsPlaying() return StatusResponse{ IsPlaying: isPlaying, CurrentSong: currentSong, + SongHistory: songHistory, UpdateTime: time.Now().Format(time.RFC3339), } } diff --git a/pkg/web/static/app.js b/pkg/web/static/app.js index c67aeb8..d923e89 100644 --- a/pkg/web/static/app.js +++ b/pkg/web/static/app.js @@ -5,6 +5,8 @@ const statusText = document.getElementById('status-text'); const nowPlaying = document.getElementById('now-playing'); const notPlaying = document.getElementById('not-playing'); const lastUpdate = document.getElementById('last-update'); +const songHistory = document.getElementById('song-history'); +const historyList = document.getElementById('history-list'); // Song info elements const coverArt = document.getElementById('cover-art'); @@ -75,6 +77,15 @@ function updateUI(data) { updateSongInfo(data.currentSong); } + // Update song history if available + if (data.songHistory && data.songHistory.length > 0) { + updateSongHistory(data.songHistory, data.currentSong); + songHistory.classList.remove('hidden'); + } else { + // No history available + songHistory.classList.add('hidden'); + } + // Update last update time if (data.updateTime) { const updateDate = new Date(data.updateTime); @@ -122,4 +133,67 @@ function updateSongInfo(song) { function formatDate(date) { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); +} + +// 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) { + const li = document.createElement('li'); + li.className = 'history-item history-empty'; + li.textContent = 'No song history available yet'; + historyList.appendChild(li); + return; + } + + // Add each song to the history list + for (let i = startIndex; i < songs.length; i++) { + const song = songs[i]; + if (!song) continue; + + // Skip if this song is the same as the currently playing song + if (currentSong && song.id === currentSong.id) continue; + + const li = document.createElement('li'); + li.className = 'history-item'; + + // Create cover art + const img = document.createElement('img'); + if (song.coverArt) { + img.src = `/cover/${song.coverArt}`; + } else { + img.src = DEFAULT_COVER_ART; + } + img.alt = 'Cover'; + img.className = 'history-cover'; + + // Create song details + const details = document.createElement('div'); + details.className = 'history-details'; + + const title = document.createElement('div'); + title.className = 'history-title'; + title.textContent = song.title || 'Unknown Title'; + + const artist = document.createElement('div'); + artist.className = 'history-artist'; + artist.textContent = song.artist || 'Unknown Artist'; + + // Assemble the elements + details.appendChild(title); + details.appendChild(artist); + + li.appendChild(img); + li.appendChild(details); + + historyList.appendChild(li); + } } \ No newline at end of file diff --git a/pkg/web/static/index.html b/pkg/web/static/index.html index cfe3ecf..821f626 100644 --- a/pkg/web/static/index.html +++ b/pkg/web/static/index.html @@ -5,6 +5,7 @@