diff --git a/.env.example b/.env.example index ff1f352..4c79df4 100644 --- a/.env.example +++ b/.env.example @@ -19,8 +19,4 @@ JUKEBOX_SUBSONIC_PASSWORD=your_subsonic_password # Jukebox settings # JUKEBOX_AUDIO_VOLUME=0.5 -# JUKEBOX_TIMEOUT_SEC=30 - -# Web server settings -# JUKEBOX_WEB_ENABLED=true -# JUKEBOX_WEB_ADDR=:8080 \ No newline at end of file +# JUKEBOX_TIMEOUT_SEC=30 \ No newline at end of file diff --git a/README.md b/README.md index b8b96de..4e9a187 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ 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 and song history - Configurable through environment variables ## Requirements @@ -89,8 +88,6 @@ Configure the bot using environment variables with the `JUKEBOX_` prefix. You ca - `JUKEBOX_SUBSONIC_VERSION`: Subsonic API version (default: `1.16.1`) - `JUKEBOX_AUDIO_VOLUME`: Volume level from 0.0 to 1.0 for audio playback (default: `0.5`) - `JUKEBOX_TIMEOUT_SEC`: HTTP request timeout in seconds (default: `30`) -- `JUKEBOX_WEB_ENABLED`: Enable the web interface (default: `true`) -- `JUKEBOX_WEB_ADDR`: Address and port for the web server (default: `:8080`) ### Sample .env File @@ -107,8 +104,6 @@ JUKEBOX_SUBSONIC_PASSWORD=your_subsonic_password # JUKEBOX_SUBSONIC_VERSION=1.16.1 # JUKEBOX_AUDIO_VOLUME=0.5 # JUKEBOX_TIMEOUT_SEC=30 -# JUKEBOX_WEB_ENABLED=true -# JUKEBOX_WEB_ADDR=:8080 ``` ## Troubleshooting @@ -153,26 +148,6 @@ The bot provides the following slash commands: - `/jukebox play` - Starts playing random music from your Subsonic library - `/jukebox stop` - Stops playing music and leaves the voice channel - `/jukebox skip` - Skips the current song and plays the next one -- `/jukebox info` - Displays information about the currently playing song - -### Web Interface - -The bot includes a web interface that displays the current playback status. By default, it runs on port 8080 and can be accessed at: - -``` -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 - -You can configure the web interface using these environment variables: -- `JUKEBOX_WEB_ENABLED`: Set to `false` to disable the web interface -- `JUKEBOX_WEB_ADDR`: Change the address and port (e.g., `:8000` for port 8000) ## License diff --git a/cmd/bot/main.go b/cmd/bot/main.go index 30d0fc5..18926a9 100644 --- a/cmd/bot/main.go +++ b/cmd/bot/main.go @@ -10,21 +10,20 @@ import ( "discord-jukebox-bot/pkg/commands" "discord-jukebox-bot/pkg/config" "discord-jukebox-bot/pkg/logger" - "discord-jukebox-bot/pkg/web" ) func main() { // Parse command-line flags debugMode := flag.Bool("debug", false, "Enable debug mode with verbose logging") flag.Parse() - + // Initialize logger logger.Init(*debugMode) logger.Info("Discord Jukebox Bot - Starting up...") if *debugMode { logger.Debug("Debug mode enabled - verbose logging will be shown") } - + // Load configuration from environment variables and .env file logger.Info("Loading configuration...") cfg, err := config.Load() @@ -34,7 +33,7 @@ func main() { os.Exit(1) } logger.Info("Configuration loaded successfully") - + // Print config summary in debug mode if logger.IsDebug() { logger.Debug("Configuration Summary", @@ -43,14 +42,14 @@ func main() { "subsonic_username", cfg.SubsonicUsername, "subsonic_api_version", cfg.SubsonicVersion) } - + // Check if ffmpeg is available - important for audio transcoding ffmpegPath, err := exec.LookPath("ffmpeg") if err != nil { - logger.Warn("ffmpeg not found in system PATH. Audio streaming may be less reliable.", + logger.Warn("ffmpeg not found in system PATH. Audio streaming may be less reliable.", "error", err.Error()) } else { - logger.Info("ffmpeg found and available for audio transcoding", + logger.Info("ffmpeg found and available for audio transcoding", "path", ffmpegPath) } @@ -70,28 +69,6 @@ func main() { } logger.Info("Discord Jukebox Bot is now running.") logger.Info("Use /jukebox play, /jukebox stop, or /jukebox skip in your Discord server.") - - // Get jukebox player reference - jukebox := commands.GetJukebox() - - // HTTP server reference - var webServer *web.Server - - // Initialize and start web server if enabled - if cfg.WebEnabled { - logger.Info("Starting HTTP server...", "address", cfg.WebAddr) - var err error - webServer, err = web.StartServer(jukebox, bot, cfg.WebAddr) - if err != nil { - logger.Error("Error starting HTTP server", "error", err) - os.Exit(1) - } - - logger.Info("HTTP server is now running", "address", cfg.WebAddr) - } else { - logger.Info("HTTP server is disabled. Set JUKEBOX_WEB_ENABLED=true to enable") - } - logger.Info("Press Ctrl+C to exit.") // Wait for a termination signal @@ -101,19 +78,8 @@ func main() { // Clean up and exit logger.Info("Shutting down Discord Jukebox Bot...") - - // Stop the HTTP server if it was started - if cfg.WebEnabled && webServer != nil { - logger.Info("Stopping HTTP server...") - if err := webServer.Stop(); err != nil { - logger.Error("Error stopping HTTP server", "error", err) - } - logger.Info("HTTP server stopped") - } - - // Stop the Discord bot if err := bot.Stop(); err != nil { logger.Error("Error stopping bot", "error", err) } logger.Info("Bot stopped. Goodbye!") -} +} \ No newline at end of file diff --git a/go.mod b/go.mod index a5db4cb..2a498f1 100644 --- a/go.mod +++ b/go.mod @@ -14,4 +14,4 @@ require ( golang.org/x/crypto v0.17.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sys v0.15.0 // indirect -) \ No newline at end of file +) diff --git a/pkg/commands/setup.go b/pkg/commands/setup.go index 9f8763b..2009e19 100644 --- a/pkg/commands/setup.go +++ b/pkg/commands/setup.go @@ -2,7 +2,6 @@ package commands import ( "fmt" - "sync" "discord-jukebox-bot/pkg/config" "discord-jukebox-bot/pkg/discord" @@ -11,12 +10,6 @@ import ( "github.com/bwmarrin/discordgo" ) -// Store a reference to the jukebox player for external access -var ( - jukeboxPlayer *discord.JukeboxPlayer - jukeboxPlayerLock sync.RWMutex -) - // Setup sets up all commands for the Discord bot func Setup(cfg *config.Config) (*discord.Bot, error) { // Create the Subsonic client @@ -39,11 +32,6 @@ func Setup(cfg *config.Config) (*discord.Bot, error) { if jukebox == nil { return nil, fmt.Errorf("error creating jukebox player") } - - // Store the jukebox player reference - jukeboxPlayerLock.Lock() - jukeboxPlayer = jukebox - jukeboxPlayerLock.Unlock() // Add any additional command initialization here // This is where you can easily add new commands in the future @@ -63,11 +51,4 @@ func RegisterCustomCommand( // Register the command handler bot.RegisterCommand(name, handler) return nil -} - -// GetJukebox returns the current jukebox player instance -func GetJukebox() *discord.JukeboxPlayer { - jukeboxPlayerLock.RLock() - defer jukeboxPlayerLock.RUnlock() - return jukeboxPlayer } \ No newline at end of file diff --git a/pkg/config/config.go b/pkg/config/config.go index 68833d8..cc05d2f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -32,10 +32,6 @@ type Config struct { // Jukebox configuration AudioVolume float64 TimeoutSec int - - // Web server configuration - WebEnabled bool - WebAddr string } // Load loads the configuration from environment variables and .env file @@ -71,10 +67,6 @@ func Load() (*Config, error) { // Jukebox AudioVolume: getEnvFloat(envPrefix+"AUDIO_VOLUME", 0.5), TimeoutSec: getEnvInt(envPrefix+"TIMEOUT_SEC", 30), - - // Web server - WebEnabled: getEnvBool(envPrefix+"WEB_ENABLED", true), - WebAddr: getEnv(envPrefix+"WEB_ADDR", ":8080"), } if err := config.validate(); err != nil { @@ -145,18 +137,6 @@ func getEnvFloat(key string, defaultValue float64) float64 { return defaultValue } -// getEnvBool gets an environment variable as a boolean or returns a default value -func getEnvBool(key string, defaultValue bool) bool { - if value, exists := os.LookupEnv(key); exists { - if strings.ToLower(value) == "true" || value == "1" { - return true - } else if strings.ToLower(value) == "false" || value == "0" { - return false - } - } - return defaultValue -} - // PrintDebugInfo prints helpful debugging information for environment variables func PrintDebugInfo() { slog.Info("=== Configuration Debug Information ===") @@ -172,8 +152,6 @@ func PrintDebugInfo() { checkEnvVar(envPrefix + "SUBSONIC_VERSION") checkEnvVar(envPrefix + "AUDIO_VOLUME") checkEnvVar(envPrefix + "TIMEOUT_SEC") - checkEnvVar(envPrefix + "WEB_ENABLED") - checkEnvVar(envPrefix + "WEB_ADDR") slog.Info("Troubleshooting tips:") slog.Info("1. Your .env file is in the correct directory") diff --git a/pkg/discord/bot.go b/pkg/discord/bot.go index a868f0a..204c8c9 100644 --- a/pkg/discord/bot.go +++ b/pkg/discord/bot.go @@ -232,11 +232,6 @@ func (b *Bot) leaveVoiceChannel() { } } -// GetSubsonicClient returns the Subsonic client -func (b *Bot) GetSubsonicClient() *subsonic.Client { - return b.subsonic -} - // IsPlaying returns true if the bot is currently playing music func (b *Bot) IsPlaying() bool { b.mu.Lock() diff --git a/pkg/discord/jukebox.go b/pkg/discord/jukebox.go index 10120c1..7990225 100644 --- a/pkg/discord/jukebox.go +++ b/pkg/discord/jukebox.go @@ -26,18 +26,13 @@ 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), - songHistory: make([]*subsonic.Song, 0), - maxHistorySize: 20, // Store the last 20 songs + bot: bot, + playlist: make([]subsonic.Song, 0), } // Register command handlers @@ -237,7 +232,7 @@ func (j *JukeboxPlayer) startPlaying() { j.playingMutex.Lock() j.currentSong = nil j.playingMutex.Unlock() - + return } @@ -274,9 +269,6 @@ 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 @@ -780,33 +772,3 @@ 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 deleted file mode 100644 index 3cbd17c..0000000 --- a/pkg/web/server.go +++ /dev/null @@ -1,348 +0,0 @@ -package web - -import ( - "context" - "embed" - "encoding/json" - "fmt" - "io" - "log/slog" - "net/http" - "strings" - "sync" - "time" - - "discord-jukebox-bot/pkg/discord" - "discord-jukebox-bot/pkg/subsonic" -) - -//go:embed static/* -var staticFiles embed.FS - -// Server represents the HTTP server for the jukebox status -type Server struct { - jukebox *discord.JukeboxPlayer - bot *discord.Bot - httpServer *http.Server - addr string - clients map[chan []byte]bool - clientsMutex sync.RWMutex - isRunning bool - stop chan struct{} -} - -// NewServer creates a new HTTP server -func NewServer(jukebox *discord.JukeboxPlayer, bot *discord.Bot, addr string) *Server { - server := &Server{ - jukebox: jukebox, - bot: bot, - addr: addr, - clients: make(map[chan []byte]bool), - isRunning: false, - stop: make(chan struct{}), - } - - return server -} - -// Start starts the HTTP server -func (s *Server) Start() error { - mux := http.NewServeMux() - - // Set up routes - mux.HandleFunc("/", s.handleIndex) - mux.HandleFunc("/status", s.handleStatus) - mux.HandleFunc("/events", s.handleEvents) - mux.HandleFunc("/static/", s.handleStatic) - mux.HandleFunc("/cover/", s.handleCoverArt) - - s.httpServer = &http.Server{ - Addr: s.addr, - Handler: mux, - } - - // Start sending updates to clients - go s.broadcastUpdates() - - slog.Info("Starting HTTP server", "address", s.addr) - s.isRunning = true - - // Start the server in a goroutine so Start doesn't block - go func() { - if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - slog.Error("HTTP server error", "error", err) - } - }() - - return nil -} - -// Stop stops the HTTP server -func (s *Server) Stop() error { - if !s.isRunning { - return nil - } - - slog.Info("Stopping HTTP server") - s.isRunning = false - - // Signal the broadcast loop to stop - close(s.stop) - - // Close all client connections - s.clientsMutex.Lock() - for clientChan := range s.clients { - close(clientChan) - delete(s.clients, clientChan) - } - s.clientsMutex.Unlock() - - // Shutdown the server with a 5 second timeout - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - return s.httpServer.Shutdown(ctx) -} - -// handleIndex serves the main page -func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.NotFound(w, r) - return - } - - html, err := staticFiles.ReadFile("static/index.html") - if err != nil { - http.Error(w, "Error reading index.html", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "text/html") - w.Write(html) -} - -// handleStatic serves static files -func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) { - // Strip the leading "/static/" from the path - filePath := strings.TrimPrefix(r.URL.Path, "/static/") - filePath = "static/" + filePath - - content, err := staticFiles.ReadFile(filePath) - if err != nil { - http.NotFound(w, r) - return - } - - // Set the content type based on file extension - contentType := "text/plain" - if strings.HasSuffix(filePath, ".css") { - contentType = "text/css" - } else if strings.HasSuffix(filePath, ".js") { - contentType = "application/javascript" - } else if strings.HasSuffix(filePath, ".html") { - contentType = "text/html" - } - - w.Header().Set("Content-Type", contentType) - w.Write(content) -} - -// 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"` -} - -// handleStatus returns the current jukebox status as JSON -func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { - status := s.getStatus() - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(status) -} - -// 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), - } -} - -// handleEvents sets up a Server-Sent Events connection -func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) { - // Set required headers for SSE - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.Header().Set("Access-Control-Allow-Origin", "*") - - // Create a channel for this client - messageChan := make(chan []byte) - - // Register the client - s.clientsMutex.Lock() - s.clients[messageChan] = true - s.clientsMutex.Unlock() - - // Clean up when the client disconnects - defer func() { - s.clientsMutex.Lock() - delete(s.clients, messageChan) - close(messageChan) - s.clientsMutex.Unlock() - }() - - // Set up a notification channel for when the client disconnects - notifyChan := make(chan bool, 1) - go func() { - <-r.Context().Done() - notifyChan <- true - }() - - // Send initial status update - status := s.getStatus() - data, err := json.Marshal(status) - if err == nil { - fmt.Fprintf(w, "data: %s\n\n", data) - w.(http.Flusher).Flush() - } - - // Keep the connection open and send updates as they come - for { - select { - case <-notifyChan: - // Client disconnected - return - case message, ok := <-messageChan: - if !ok { - // Channel closed - return - } - fmt.Fprintf(w, "data: %s\n\n", message) - w.(http.Flusher).Flush() - } - } -} - -// handleCoverArt proxies requests for cover art to the Subsonic server -func (s *Server) handleCoverArt(w http.ResponseWriter, r *http.Request) { - // Extract the cover art ID from the URL path - coverArtID := strings.TrimPrefix(r.URL.Path, "/cover/") - if coverArtID == "" { - http.Error(w, "Cover art ID is required", http.StatusBadRequest) - return - } - - // Get the cover art URL from the Subsonic client - coverArtURL := s.bot.GetSubsonicClient().GetCoverArtURL(coverArtID) - - // Create a new request to the Subsonic server - req, err := http.NewRequest("GET", coverArtURL, nil) - if err != nil { - http.Error(w, "Error creating request for cover art", http.StatusInternalServerError) - return - } - - // Forward the request to the Subsonic server - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(req) - if err != nil { - http.Error(w, "Error fetching cover art", http.StatusInternalServerError) - return - } - defer resp.Body.Close() - - // Check if the request was successful - if resp.StatusCode != http.StatusOK { - http.Error(w, "Error fetching cover art from Subsonic", resp.StatusCode) - return - } - - // Copy the content type from the response - contentType := resp.Header.Get("Content-Type") - if contentType != "" { - w.Header().Set("Content-Type", contentType) - } else { - w.Header().Set("Content-Type", "image/jpeg") // Default to JPEG if no content type - } - - // Copy the image data to the response - _, err = io.Copy(w, resp.Body) - if err != nil { - slog.Error("Error copying cover art data", "error", err) - } -} - -// broadcastUpdates periodically sends status updates to all connected clients -func (s *Server) broadcastUpdates() { - // Keep track of the previous status to only send updates when there are changes - var prevIsPlaying bool - var prevSongID string - - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - for { - select { - case <-s.stop: - return - case <-ticker.C: - if !s.isRunning { - return - } - - // Get current status - status := s.getStatus() - - // Determine if there's a change - currentSongID := "" - if status.CurrentSong != nil { - currentSongID = status.CurrentSong.ID - } - - hasChanged := (prevIsPlaying != status.IsPlaying) || (prevSongID != currentSongID) - - if hasChanged { - // Update previous state - prevIsPlaying = status.IsPlaying - prevSongID = currentSongID - - // Broadcast to all clients - data, err := json.Marshal(status) - if err != nil { - slog.Error("Error marshaling status", "error", err) - continue - } - - s.clientsMutex.RLock() - for client := range s.clients { - select { - case client <- data: - // Message sent - default: - // Skip if channel is blocked - } - } - s.clientsMutex.RUnlock() - } - } - } -} - -// StartServer creates and starts a new HTTP server -func StartServer(jukebox *discord.JukeboxPlayer, bot *discord.Bot, addr string) (*Server, error) { - server := NewServer(jukebox, bot, addr) - if err := server.Start(); err != nil { - return nil, err - } - return server, nil -} \ No newline at end of file diff --git a/pkg/web/static/app.js b/pkg/web/static/app.js deleted file mode 100644 index d923e89..0000000 --- a/pkg/web/static/app.js +++ /dev/null @@ -1,199 +0,0 @@ -// DOM Elements -const statusIndicator = document.getElementById('status-indicator'); -const statusDot = document.querySelector('.status-dot'); -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'); -const songTitle = document.getElementById('song-title'); -const songArtist = document.getElementById('song-artist'); -const songAlbum = document.getElementById('song-album'); -const songDuration = document.getElementById('song-duration'); -const songGenre = document.getElementById('song-genre'); -const songYear = document.getElementById('song-year'); - -// Base64 encoded default cover art (simple music note icon) -const DEFAULT_COVER_ART = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAF+mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0wNC0wMVQxNTozODoxOSswMjowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMDQtMDFUMTU6NDE6MDcrMDI6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMDQtMDFUMTU6NDE6MDcrMDI6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjZiMGE4MDQtZWYxZS1hMDQ0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'; - -// Initial status load -fetchStatus(); - -// Set up Server-Sent Events for live updates -setupEventSource(); - -// Functions -function setupEventSource() { - const eventSource = new EventSource('/events'); - - eventSource.onmessage = function(event) { - const data = JSON.parse(event.data); - updateUI(data); - }; - - eventSource.onerror = function() { - // If connection fails, close it and retry after a delay - eventSource.close(); - statusDot.className = 'status-dot offline'; - statusText.textContent = 'Connection lost. Retrying...'; - - setTimeout(setupEventSource, 5000); - }; -} - -function fetchStatus() { - fetch('/status') - .then(response => response.json()) - .then(data => { - updateUI(data); - }) - .catch(error => { - console.error('Error fetching status:', error); - statusDot.className = 'status-dot offline'; - statusText.textContent = 'Error connecting to server'; - }); -} - -function updateUI(data) { - // Update status indicator - if (data.isPlaying) { - statusDot.className = 'status-dot online'; - statusText.textContent = 'Playing music'; - nowPlaying.classList.remove('hidden'); - notPlaying.classList.add('hidden'); - } else { - statusDot.className = 'status-dot offline'; - statusText.textContent = 'Not playing'; - nowPlaying.classList.add('hidden'); - notPlaying.classList.remove('hidden'); - } - - // Update song information if we have a current song - if (data.currentSong) { - 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); - lastUpdate.textContent = formatDate(updateDate); - } -} - -function updateSongInfo(song) { - // Update the song title - songTitle.textContent = song.title || 'Unknown Title'; - - // Update the artist - songArtist.textContent = song.artist || 'Unknown Artist'; - - // Update the album - songAlbum.textContent = song.album || 'Unknown Album'; - - // Update the duration if available - if (song.duration) { - const minutes = Math.floor(song.duration / 60); - const seconds = song.duration % 60; - songDuration.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; - } else { - songDuration.textContent = '--'; - } - - // Update the genre if available - songGenre.textContent = song.genre || '--'; - - // Update the year if available - songYear.textContent = song.year ? song.year.toString() : '--'; - - // Update the cover art if available - if (song.coverArt) { - // This assumes your backend has a route to get cover art - // You might need to adjust this to match your API - const coverArtUrl = `/cover/${song.coverArt}`; - coverArt.src = coverArtUrl; - coverArt.alt = `${song.album} cover`; - } else { - coverArt.src = DEFAULT_COVER_ART; - coverArt.alt = 'Default cover art'; - } -} - -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/default-cover.png b/pkg/web/static/default-cover.png deleted file mode 100644 index fb5519b..0000000 --- a/pkg/web/static/default-cover.png +++ /dev/null @@ -1,13 +0,0 @@ -I can't create or include binary content like PNG files directly in my response, as this would require binary data that can't be represented as text. - -For the default-cover.png file, you'll need to: - -1. Generate a simple placeholder image using an image editor -2. Save it as "default-cover.png" in the "discord-jukebox-bot/pkg/web/static/" directory - -Alternatively, you could: - -1. Use a base64-encoded data URL in your JavaScript to represent a default image -2. Update the app.js file to include a fallback image directly in the code - -Would you like me to update the JavaScript to include a base64-encoded default image instead? This would eliminate the need for a separate PNG file. \ No newline at end of file diff --git a/pkg/web/static/index.html b/pkg/web/static/index.html deleted file mode 100644 index 821f626..0000000 --- a/pkg/web/static/index.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - Discord Jukebox Status - - - - -
-
-

Discord Jukebox

-
- - Checking status... -
-
- - - - - - - - -
- - - - \ No newline at end of file diff --git a/pkg/web/static/styles.css b/pkg/web/static/styles.css deleted file mode 100644 index d83e306..0000000 --- a/pkg/web/static/styles.css +++ /dev/null @@ -1,266 +0,0 @@ -/* Main styles */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - line-height: 1.6; - color: #333; - background-color: #f5f5f5; - min-height: 100vh; -} - -.container { - max-width: 800px; - margin: 0 auto; - padding: 20px; - background-color: white; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - min-height: calc(100vh - 40px); - margin-top: 20px; - margin-bottom: 20px; - display: flex; - flex-direction: column; -} - -header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 30px; - padding-bottom: 15px; - border-bottom: 1px solid #eee; - flex-wrap: wrap; -} - -h1 { - font-size: 28px; - color: #5865F2; /* Discord blue */ -} - -.status-indicator { - display: flex; - align-items: center; - font-size: 14px; - color: #666; -} - -.status-dot { - display: inline-block; - width: 10px; - height: 10px; - border-radius: 50%; - background-color: gray; - margin-right: 6px; -} - -.status-dot.online { - background-color: #43b581; /* Discord green */ - box-shadow: 0 0 5px rgba(67, 181, 129, 0.5); -} - -.status-dot.offline { - background-color: #f04747; /* Discord red */ -} - -/* Now Playing Section */ -#now-playing { - padding: 20px; - background-color: #f9f9f9; - border-radius: 8px; - margin-bottom: 20px; - flex-grow: 1; -} - -h2 { - font-size: 22px; - margin-bottom: 15px; - color: #5865F2; -} - -.song-info { - display: flex; - flex-wrap: wrap; - gap: 20px; -} - -#cover-art-container { - flex: 0 0 200px; -} - -#cover-art { - width: 100%; - max-width: 200px; - height: auto; - border-radius: 4px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); -} - -.song-details { - flex: 1; - min-width: 250px; -} - -h3 { - font-size: 20px; - margin-bottom: 10px; - color: #333; -} - -.song-details p { - margin-bottom: 8px; -} - -.additional-info { - display: flex; - flex-wrap: wrap; - gap: 15px; - margin-top: 15px; - border-top: 1px solid #eee; - padding-top: 15px; -} - -.additional-info p { - 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; - padding: 40px 20px; - background-color: #f9f9f9; - border-radius: 8px; - margin-bottom: 20px; - flex-grow: 1; -} - -.message { - max-width: 400px; - margin: 0 auto; -} - -code { - font-family: monospace; - background-color: #eee; - padding: 2px 6px; - border-radius: 4px; - font-size: 0.9em; -} - -footer { - margin-top: auto; - padding-top: 20px; - border-top: 1px solid #eee; - color: #888; - font-size: 14px; - text-align: center; -} - -.bot-info { - margin-top: 5px; - font-size: 12px; -} - -/* Helper classes */ -.hidden { - display: none; -} - -/* Responsive adjustments */ -@media (max-width: 600px) { - header { - flex-direction: column; - align-items: flex-start; - } - - .status-indicator { - margin-top: 10px; - } - - #cover-art-container { - flex: 0 0 100%; - text-align: center; - margin-bottom: 15px; - } - - #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; - } -} \ No newline at end of file