feat: show last played songs in web ui
This commit is contained in:
parent
7f16452a99
commit
bcc1bce743
6 changed files with 206 additions and 7 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue