feat: show last played songs in web ui

This commit is contained in:
Felipe M 2025-05-13 12:49:59 +02:00
parent 7f16452a99
commit bcc1bce743
Signed by: fmartingr
GPG key ID: CCFBC5637D4000A8
6 changed files with 206 additions and 7 deletions

View file

@ -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

View file

@ -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
}

View file

@ -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),
}
}

View file

@ -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);
}
}

View file

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Discord Jukebox Status</title>
<link rel="stylesheet" href="/static/styles.css">
<meta http-equiv="refresh" content="900"> <!-- Refresh page every 15 minutes to prevent memory issues -->
</head>
<body>
<div class="container">
@ -35,6 +36,15 @@
</div>
</div>
<div id="song-history" class="hidden">
<h2>Recently Played</h2>
<div class="history-container">
<ul id="history-list" class="history-list">
<!-- Song history will be populated here by JavaScript -->
</ul>
</div>
</div>
<div id="not-playing" class="hidden">
<div class="message">
<h2>No Song Playing</h2>

View file

@ -128,6 +128,65 @@ h3 {
margin-right: 15px;
}
/* Song History Section */
#song-history {
padding: 20px;
background-color: #f9f9f9;
border-radius: 8px;
margin-top: 20px;
margin-bottom: 20px;
}
.history-container {
max-height: 300px;
overflow-y: auto;
}
.history-list {
list-style-type: none;
padding: 0;
margin: 0;
}
.history-empty {
text-align: center;
color: #666;
font-style: italic;
padding: 20px 0;
}
.history-item {
padding: 10px;
margin-bottom: 10px;
background-color: white;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
}
.history-cover {
width: 50px;
height: 50px;
border-radius: 4px;
margin-right: 15px;
object-fit: cover;
}
.history-details {
flex: 1;
}
.history-title {
font-weight: bold;
margin-bottom: 3px;
}
.history-artist {
color: #666;
font-size: 0.9em;
}
/* Not Playing Section */
#not-playing {
text-align: center;
@ -190,4 +249,18 @@ footer {
#cover-art {
max-width: 150px;
}
.history-item {
padding: 8px;
}
.history-cover {
width: 40px;
height: 40px;
margin-right: 10px;
}
.history-title, .history-artist {
font-size: 0.9em;
}
}