mirror of
https://github.com/skidoodle/spotify-ws.git
synced 2025-02-15 06:09:14 +01:00
232 lines
5.3 KiB
Go
232 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/joho/godotenv"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/zmb3/spotify"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
const (
|
|
serverPort = ":3000"
|
|
tokenRefreshURL = "https://accounts.spotify.com/api/token"
|
|
apiRetryDelay = 3 * time.Second
|
|
heartbeatDelay = 3 * time.Second
|
|
)
|
|
|
|
var (
|
|
clients = make(map[*websocket.Conn]bool)
|
|
clientsMutex sync.RWMutex
|
|
broadcast = make(chan *spotify.CurrentlyPlaying)
|
|
connect = make(chan *websocket.Conn)
|
|
disconnect = make(chan *websocket.Conn)
|
|
|
|
upgrader = websocket.Upgrader{
|
|
CheckOrigin: func(r *http.Request) bool {
|
|
return true
|
|
},
|
|
HandshakeTimeout: 10 * time.Second,
|
|
ReadBufferSize: 1024,
|
|
WriteBufferSize: 1024,
|
|
}
|
|
|
|
spotifyClient spotify.Client
|
|
tokenSource oauth2.TokenSource
|
|
config *oauth2.Config
|
|
lastPlayingState *bool
|
|
lastTrackState *spotify.CurrentlyPlaying
|
|
)
|
|
|
|
func main() {
|
|
logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})
|
|
loadEnv()
|
|
|
|
config = &oauth2.Config{
|
|
ClientID: os.Getenv("CLIENT_ID"),
|
|
ClientSecret: os.Getenv("CLIENT_SECRET"),
|
|
Endpoint: oauth2.Endpoint{
|
|
TokenURL: tokenRefreshURL,
|
|
},
|
|
}
|
|
token := &oauth2.Token{RefreshToken: os.Getenv("REFRESH_TOKEN")}
|
|
tokenSource = config.TokenSource(context.Background(), token)
|
|
|
|
httpClient := oauth2.NewClient(context.Background(), tokenSource)
|
|
spotifyClient = spotify.NewClient(httpClient)
|
|
|
|
http.HandleFunc("/", connectionHandler)
|
|
http.HandleFunc("/health", healthHandler)
|
|
|
|
stop := make(chan os.Signal, 1)
|
|
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
|
|
|
go trackFetcher()
|
|
go messageHandler()
|
|
go connectionManager()
|
|
|
|
server := &http.Server{
|
|
Addr: serverPort,
|
|
}
|
|
|
|
go func() {
|
|
logrus.Infof("Server started on %s", serverPort)
|
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
logrus.Fatalf("Server failed: %v", err)
|
|
}
|
|
}()
|
|
|
|
<-stop
|
|
logrus.Info("Shutting down server...")
|
|
|
|
clientsMutex.Lock()
|
|
for client := range clients {
|
|
_ = client.Close()
|
|
}
|
|
clientsMutex.Unlock()
|
|
|
|
if err := server.Shutdown(context.Background()); err != nil {
|
|
logrus.Fatalf("Server shutdown failed: %v", err)
|
|
}
|
|
logrus.Info("Server exited cleanly")
|
|
}
|
|
|
|
func loadEnv() {
|
|
if err := godotenv.Load(); err != nil {
|
|
logrus.Warn("Could not load .env file, falling back to system environment")
|
|
}
|
|
|
|
requiredVars := []string{"CLIENT_ID", "CLIENT_SECRET", "REFRESH_TOKEN"}
|
|
for _, v := range requiredVars {
|
|
if os.Getenv(v) == "" {
|
|
logrus.Fatalf("Missing required environment variable: %s", v)
|
|
}
|
|
}
|
|
|
|
logLevel := strings.ToLower(os.Getenv("LOG_LEVEL"))
|
|
if logLevel == "debug" {
|
|
logrus.SetLevel(logrus.DebugLevel)
|
|
logrus.Info("Log level set to DEBUG")
|
|
} else {
|
|
logrus.SetLevel(logrus.InfoLevel)
|
|
logrus.Info("Log level set to INFO")
|
|
}
|
|
}
|
|
|
|
func connectionHandler(w http.ResponseWriter, r *http.Request) {
|
|
ws, err := upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
logrus.Errorf("Failed to upgrade to WebSocket: %v", err)
|
|
return
|
|
}
|
|
connect <- ws
|
|
|
|
clientsMutex.RLock()
|
|
if lastTrackState != nil {
|
|
if err := ws.WriteJSON(lastTrackState); err != nil {
|
|
logrus.Errorf("Failed to send initial state to client: %v", err)
|
|
}
|
|
}
|
|
clientsMutex.RUnlock()
|
|
|
|
defer func() {
|
|
disconnect <- ws
|
|
}()
|
|
|
|
for {
|
|
if _, _, err := ws.ReadMessage(); err != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func connectionManager() {
|
|
for {
|
|
select {
|
|
case client := <-connect:
|
|
clientsMutex.Lock()
|
|
clients[client] = true
|
|
clientsMutex.Unlock()
|
|
logrus.Debugf("New client connected: %v", client.RemoteAddr())
|
|
case client := <-disconnect:
|
|
clientsMutex.Lock()
|
|
if _, ok := clients[client]; ok {
|
|
delete(clients, client)
|
|
logrus.Debugf("Client disconnected: %v", client.RemoteAddr())
|
|
}
|
|
clientsMutex.Unlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
func messageHandler() {
|
|
for msg := range broadcast {
|
|
clientsMutex.RLock()
|
|
for client := range clients {
|
|
if err := client.WriteJSON(msg); err != nil {
|
|
logrus.Errorf("Failed to send message to client: %v", err)
|
|
disconnect <- client
|
|
}
|
|
}
|
|
clientsMutex.RUnlock()
|
|
}
|
|
}
|
|
|
|
func trackFetcher() {
|
|
for {
|
|
current, err := fetchCurrentlyPlaying()
|
|
if err != nil {
|
|
logrus.Errorf("Error fetching currently playing track: %v", err)
|
|
time.Sleep(apiRetryDelay)
|
|
continue
|
|
}
|
|
|
|
if current != nil {
|
|
clientsMutex.Lock()
|
|
lastTrackState = current
|
|
clientsMutex.Unlock()
|
|
|
|
if lastPlayingState == nil || *lastPlayingState != current.Playing {
|
|
logrus.Debugf("Playback state changed: is_playing=%v", current.Playing)
|
|
broadcast <- current
|
|
lastPlayingState = ¤t.Playing
|
|
}
|
|
|
|
if current.Playing {
|
|
broadcast <- current
|
|
time.Sleep(heartbeatDelay)
|
|
continue
|
|
}
|
|
}
|
|
time.Sleep(heartbeatDelay)
|
|
}
|
|
}
|
|
|
|
func fetchCurrentlyPlaying() (*spotify.CurrentlyPlaying, error) {
|
|
current, err := spotifyClient.PlayerCurrentlyPlaying()
|
|
if err != nil && err.Error() == "token expired" {
|
|
logrus.Warn("Spotify token expired, refreshing token...")
|
|
newToken, err := tokenSource.Token()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
httpClient := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(newToken))
|
|
spotifyClient = spotify.NewClient(httpClient)
|
|
return spotifyClient.PlayerCurrentlyPlaying()
|
|
}
|
|
return current, err
|
|
}
|
|
|
|
func healthHandler(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("OK"))
|
|
}
|