mirror of
https://github.com/skidoodle/spotify-ws.git
synced 2025-02-15 06:09:14 +01:00
improved function
This commit is contained in:
parent
cdec626875
commit
a12ebd132f
8 changed files with 177 additions and 138 deletions
|
@ -2,3 +2,4 @@
|
||||||
CLIENT_ID=
|
CLIENT_ID=
|
||||||
CLIENT_SECRET=
|
CLIENT_SECRET=
|
||||||
REFRESH_TOKEN=
|
REFRESH_TOKEN=
|
||||||
|
# LOG_LEVEL=DEBUG (optional)
|
||||||
|
|
2
.github/workflows/go.yml
vendored
2
.github/workflows/go.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.22.3'
|
go-version: '1.22.4'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build -v ./...
|
run: go build -v ./...
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
FROM golang:alpine as builder
|
FROM golang:alpine as builder
|
||||||
|
ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go build -o spotify-ws .
|
RUN go build -o spotify-ws .
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM gcr.io/distroless/static:nonroot
|
||||||
RUN apk --no-cache add ca-certificates
|
WORKDIR /app
|
||||||
WORKDIR /root/
|
|
||||||
COPY --from=builder /app/spotify-ws .
|
COPY --from=builder /app/spotify-ws .
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
USER nonroot:nonroot
|
||||||
CMD ["./spotify-ws"]
|
CMD ["./spotify-ws"]
|
||||||
|
|
|
@ -9,3 +9,4 @@ services:
|
||||||
- REFRESH_TOKEN=
|
- REFRESH_TOKEN=
|
||||||
- CLIENT_SECRET=
|
- CLIENT_SECRET=
|
||||||
- CLIENT_ID=
|
- CLIENT_ID=
|
||||||
|
#- LOG_LEVEL=DEBUG
|
||||||
|
|
9
go.mod
9
go.mod
|
@ -1,12 +1,13 @@
|
||||||
module spotify-ws
|
module spotify-ws
|
||||||
|
|
||||||
go 1.22.3
|
go 1.22.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gorilla/websocket v1.5.1
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/zmb3/spotify v1.3.0
|
github.com/zmb3/spotify v1.3.0
|
||||||
golang.org/x/oauth2 v0.20.0
|
golang.org/x/oauth2 v0.24.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/net v0.25.0 // indirect
|
require golang.org/x/sys v0.28.0 // indirect
|
||||||
|
|
18
go.sum
18
go.sum
|
@ -1,15 +1,18 @@
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
@ -17,12 +20,13 @@ github.com/zmb3/spotify v1.3.0 h1:6Z2F1IMx0Hviq/dpf8nFwvKPppFEMXn8yfReSBVi16k=
|
||||||
github.com/zmb3/spotify v1.3.0/go.mod h1:GD7AAEMUJVYc2Z7p2a2S0E3/5f/KxM/vOnErNr4j+Tw=
|
github.com/zmb3/spotify v1.3.0/go.mod h1:GD7AAEMUJVYc2Z7p2a2S0E3/5f/KxM/vOnErNr4j+Tw=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
|
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||||
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|
271
main.go
271
main.go
|
@ -2,202 +2,231 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/zmb3/spotify"
|
"github.com/zmb3/spotify"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
serverPort = ":3000"
|
||||||
|
tokenRefreshURL = "https://accounts.spotify.com/api/token"
|
||||||
|
apiRetryDelay = 3 * time.Second
|
||||||
|
heartbeatDelay = 3 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
clients = make(map[*websocket.Conn]bool) // Map to keep track of connected clients
|
clients = make(map[*websocket.Conn]bool)
|
||||||
clientsMutex sync.Mutex // Mutex to protect access to clients map
|
clientsMutex sync.RWMutex
|
||||||
broadcast = make(chan *spotify.CurrentlyPlaying) // Channel for broadcasting currently playing track
|
broadcast = make(chan *spotify.CurrentlyPlaying)
|
||||||
connect = make(chan *websocket.Conn) // Channel for managing new connections
|
connect = make(chan *websocket.Conn)
|
||||||
disconnect = make(chan *websocket.Conn) // Channel for managing client disconnections
|
disconnect = make(chan *websocket.Conn)
|
||||||
upgrader = websocket.Upgrader{
|
|
||||||
CheckOrigin: func(r *http.Request) bool { return true }, // Allow all origins
|
upgrader = websocket.Upgrader{
|
||||||
HandshakeTimeout: 10 * time.Second, // Timeout for WebSocket handshake
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
ReadBufferSize: 1024, // Buffer size for reading incoming messages
|
return true
|
||||||
WriteBufferSize: 1024, // Buffer size for writing outgoing messages
|
},
|
||||||
Subprotocols: []string{"binary"}, // Supported subprotocols
|
HandshakeTimeout: 10 * time.Second,
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
}
|
}
|
||||||
spotifyClient spotify.Client // Spotify API client
|
|
||||||
tokenSource oauth2.TokenSource // OAuth2 token source
|
spotifyClient spotify.Client
|
||||||
config *oauth2.Config // OAuth2 configuration
|
tokenSource oauth2.TokenSource
|
||||||
|
config *oauth2.Config
|
||||||
|
lastPlayingState *bool
|
||||||
|
lastTrackState *spotify.CurrentlyPlaying
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Load environment variables from .env file if not already set
|
logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})
|
||||||
if os.Getenv("CLIENT_ID") == "" || os.Getenv("CLIENT_SECRET") == "" || os.Getenv("REFRESH_TOKEN") == "" {
|
loadEnv()
|
||||||
if err := godotenv.Load(); err != nil {
|
|
||||||
log.Fatalf("Error loading .env file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clientID := os.Getenv("CLIENT_ID")
|
|
||||||
clientSecret := os.Getenv("CLIENT_SECRET")
|
|
||||||
refreshToken := os.Getenv("REFRESH_TOKEN")
|
|
||||||
|
|
||||||
// Setup OAuth2 configuration for Spotify API
|
|
||||||
config = &oauth2.Config{
|
config = &oauth2.Config{
|
||||||
ClientID: clientID,
|
ClientID: os.Getenv("CLIENT_ID"),
|
||||||
ClientSecret: clientSecret,
|
ClientSecret: os.Getenv("CLIENT_SECRET"),
|
||||||
Endpoint: oauth2.Endpoint{
|
Endpoint: oauth2.Endpoint{
|
||||||
TokenURL: "https://accounts.spotify.com/api/token",
|
TokenURL: tokenRefreshURL,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
token := &oauth2.Token{RefreshToken: os.Getenv("REFRESH_TOKEN")}
|
||||||
token := &oauth2.Token{RefreshToken: refreshToken}
|
|
||||||
tokenSource = config.TokenSource(context.Background(), token)
|
tokenSource = config.TokenSource(context.Background(), token)
|
||||||
|
|
||||||
// Create an OAuth2 HTTP client
|
|
||||||
httpClient := oauth2.NewClient(context.Background(), tokenSource)
|
httpClient := oauth2.NewClient(context.Background(), tokenSource)
|
||||||
spotifyClient = spotify.NewClient(httpClient)
|
spotifyClient = spotify.NewClient(httpClient)
|
||||||
|
|
||||||
// Handle WebSocket connections at the root endpoint
|
http.HandleFunc("/", connectionHandler)
|
||||||
http.HandleFunc("/", ConnectionHandler)
|
http.HandleFunc("/health", healthHandler)
|
||||||
|
|
||||||
// Log server start-up and initialize background tasks
|
stop := make(chan os.Signal, 1)
|
||||||
log.Println("Server started on :3000")
|
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||||
go TrackFetcher() // Periodically fetch currently playing track from Spotify
|
|
||||||
go MessageHandler() // Broadcast messages to connected clients
|
|
||||||
go ConnectionManager() // Manage client connections
|
|
||||||
|
|
||||||
// Start the HTTP server
|
go trackFetcher()
|
||||||
if err := http.ListenAndServe(":3000", nil); err != nil {
|
go messageHandler()
|
||||||
log.Fatalf("Error starting server: %v", err)
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectionHandler upgrades HTTP connections to WebSocket and handles communication with clients
|
func connectionHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
func ConnectionHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ws, err := upgrader.Upgrade(w, r, nil)
|
ws, err := upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logrus.Errorf("Failed to upgrade to WebSocket: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
connect <- ws
|
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() {
|
defer func() {
|
||||||
disconnect <- ws
|
disconnect <- ws
|
||||||
err := ws.Close()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Immediately send the current track to the newly connected client
|
|
||||||
currentTrack, err := spotifyClient.PlayerCurrentlyPlaying()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error getting currently playing track: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the current track information to the client
|
|
||||||
err = ws.WriteJSON(currentTrack)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error sending current track to client: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep the connection open to listen for incoming messages (heartbeat)
|
|
||||||
for {
|
for {
|
||||||
_, _, err := ws.ReadMessage()
|
if _, _, err := ws.ReadMessage(); err != nil {
|
||||||
if err != nil {
|
|
||||||
disconnect <- ws
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectionManager manages client connections and disconnections using channels
|
func connectionManager() {
|
||||||
func ConnectionManager() {
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case client := <-connect:
|
case client := <-connect:
|
||||||
clientsMutex.Lock()
|
clientsMutex.Lock()
|
||||||
clients[client] = true
|
clients[client] = true
|
||||||
clientsMutex.Unlock()
|
clientsMutex.Unlock()
|
||||||
|
logrus.Debugf("New client connected: %v", client.RemoteAddr())
|
||||||
case client := <-disconnect:
|
case client := <-disconnect:
|
||||||
clientsMutex.Lock()
|
clientsMutex.Lock()
|
||||||
if _, ok := clients[client]; ok {
|
if _, ok := clients[client]; ok {
|
||||||
delete(clients, client)
|
delete(clients, client)
|
||||||
err := client.Close()
|
logrus.Debugf("Client disconnected: %v", client.RemoteAddr())
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
clientsMutex.Unlock()
|
clientsMutex.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageHandler continuously listens for messages on the broadcast channel and sends them to all connected clients
|
func messageHandler() {
|
||||||
func MessageHandler() {
|
|
||||||
for msg := range broadcast {
|
for msg := range broadcast {
|
||||||
clientsMutex.Lock()
|
clientsMutex.RLock()
|
||||||
for client := range clients {
|
for client := range clients {
|
||||||
err := client.WriteJSON(msg)
|
if err := client.WriteJSON(msg); err != nil {
|
||||||
if err != nil {
|
logrus.Errorf("Failed to send message to client: %v", err)
|
||||||
err := client.Close()
|
disconnect <- client
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
delete(clients, client)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clientsMutex.Unlock()
|
clientsMutex.RUnlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrackFetcher periodically fetches the currently playing track from the Spotify API and broadcasts it to clients
|
func trackFetcher() {
|
||||||
func TrackFetcher() {
|
|
||||||
var playing bool
|
|
||||||
for {
|
for {
|
||||||
// Fetch the currently playing track
|
current, err := fetchCurrentlyPlaying()
|
||||||
current, err := spotifyClient.PlayerCurrentlyPlaying()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error getting currently playing track: %v", err)
|
logrus.Errorf("Error fetching currently playing track: %v", err)
|
||||||
// Refresh the access token if it has expired
|
time.Sleep(apiRetryDelay)
|
||||||
if err.Error() == "token expired" {
|
|
||||||
log.Println("Token expired, refreshing token...")
|
|
||||||
newToken, err := tokenSource.Token()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Couldn't refresh token: %v", err)
|
|
||||||
}
|
|
||||||
httpClient := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(newToken))
|
|
||||||
spotifyClient = spotify.NewClient(httpClient)
|
|
||||||
}
|
|
||||||
// Wait before retrying to avoid overwhelming the API
|
|
||||||
time.Sleep(30 * time.Minute)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the track is playing
|
if current != nil {
|
||||||
switch {
|
clientsMutex.Lock()
|
||||||
case current.Playing:
|
lastTrackState = current
|
||||||
broadcast <- current
|
clientsMutex.Unlock()
|
||||||
playing = true
|
|
||||||
// Send updates every 3 seconds while playing
|
|
||||||
for current.Playing {
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
current, err = spotifyClient.PlayerCurrentlyPlaying()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error getting currently playing track: %v", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
broadcast <- current
|
|
||||||
}
|
|
||||||
case !current.Playing && playing:
|
|
||||||
playing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait before checking again to avoid overwhelming the API
|
if lastPlayingState == nil || *lastPlayingState != current.Playing {
|
||||||
time.Sleep(3 * time.Second)
|
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"))
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ It's important to note that this project does not purport to resolve the challen
|
||||||
|
|
||||||
### With Docker
|
### With Docker
|
||||||
|
|
||||||
```
|
```sh
|
||||||
git clone https://github.com/skidoodle/spotify-ws
|
git clone https://github.com/skidoodle/spotify-ws
|
||||||
cd spotify-ws
|
cd spotify-ws
|
||||||
docker build -t spotify-ws:main .
|
docker build -t spotify-ws:main .
|
||||||
|
@ -17,7 +17,7 @@ docker run -p 3000:3000 spotify-ws:main
|
||||||
|
|
||||||
### Without Docker
|
### Without Docker
|
||||||
|
|
||||||
```
|
```sh
|
||||||
git clone https://github.com/skidoodle/spotify-ws
|
git clone https://github.com/skidoodle/spotify-ws
|
||||||
cd spotify-ws
|
cd spotify-ws
|
||||||
go get
|
go get
|
||||||
|
@ -38,6 +38,7 @@ services:
|
||||||
- REFRESH_TOKEN=
|
- REFRESH_TOKEN=
|
||||||
- CLIENT_SECRET=
|
- CLIENT_SECRET=
|
||||||
- CLIENT_ID=
|
- CLIENT_ID=
|
||||||
|
#- LOG_LEVEL=debug
|
||||||
ports:
|
ports:
|
||||||
- '3000:3000'
|
- '3000:3000'
|
||||||
```
|
```
|
||||||
|
@ -53,6 +54,7 @@ docker run \
|
||||||
-e CLIENT_ID= \
|
-e CLIENT_ID= \
|
||||||
-e CLIENT_SECRET= \
|
-e CLIENT_SECRET= \
|
||||||
-e REFRESH_TOKEN= \
|
-e REFRESH_TOKEN= \
|
||||||
|
#-e LOG_LEVEL=DEBUG \
|
||||||
ghcr.io/skidoodle/spotify-ws:main
|
ghcr.io/skidoodle/spotify-ws:main
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue