diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5e51677 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.env +*.db +.env* \ No newline at end of file diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..662fde6 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,57 @@ +name: Build Docker Image + +on: + push: + branches: [ "main" ] + # pull_request: + # branches: [ "main" ] + +jobs: + + build-api: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Build and push API + uses: docker/build-push-action@v5 + with: + context: . + file: resources/docker/api/Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/lanops-spotify-jukebox-api:latest + + build-ui: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Build and push UI + uses: docker/build-push-action@v5 + with: + context: . + file: resources/docker/ui/Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/lanops-spotify-jukebox-ui:latest diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..31522f0 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +build: go-install npm-install + +docker-pull: + docker compose pull + +docker-build: + docker compose build + +go-install: + docker compose run --rm go mod download + +npm-install: + docker compose run --rm npm install diff --git a/api/auth.go b/api/auth.go index e5e6db9..fc70494 100644 --- a/api/auth.go +++ b/api/auth.go @@ -1,7 +1,7 @@ package main import ( - "log" + "fmt" "net/http" "github.com/gin-gonic/gin" @@ -16,11 +16,11 @@ func serveLoginLink(c *gin.Context) { func handleAuth(c *gin.Context) { tok, err := auth.Token(c.Request.Context(), state, c.Request) if err != nil { - log.Fatal(err) + logger.Fatal().Err(err) c.JSON(http.StatusInternalServerError, err) } if st := c.Request.FormValue("state"); st != state { - log.Fatalf("State mismatch: %s != %s\n", st, state) + logger.Fatal().Msg(fmt.Sprintf("State mismatch: %s != %s\n", st, state)) c.JSON(http.StatusNotFound, err) } client = spotify.New(auth.Client(c.Request.Context(), tok)) diff --git a/api/go.mod b/api/go.mod index dd545bf..dbaed9b 100644 --- a/api/go.mod +++ b/api/go.mod @@ -1,4 +1,4 @@ -module lanjukebox +module lanops/spotify-jukebox go 1.18 @@ -31,17 +31,19 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/redis/go-redis/v9 v9.0.2 // indirect + github.com/rs/zerolog v1.32.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.9.0 // indirect golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.30.0 // indirect diff --git a/api/go.sum b/api/go.sum index 4938397..2956d2e 100644 --- a/api/go.sum +++ b/api/go.sum @@ -51,6 +51,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= @@ -87,6 +88,7 @@ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QX github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -166,7 +168,10 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= @@ -180,6 +185,7 @@ github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZO github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -189,6 +195,9 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= +github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -335,9 +344,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/api/main.go b/api/main.go index fc2c7ac..ff1b25b 100644 --- a/api/main.go +++ b/api/main.go @@ -2,23 +2,24 @@ package main import ( "context" - "log" "os" "strconv" "strings" "time" + ratelimit "github.com/JGLTechnologies/gin-rate-limit" + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" "github.com/joho/godotenv" - spotifyauth "github.com/zmb3/spotify/v2/auth" + "github.com/rs/zerolog" "golang.org/x/oauth2" - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" "github.com/zmb3/spotify/v2" - ratelimit "github.com/JGLTechnologies/gin-rate-limit" "gorm.io/driver/sqlite" "gorm.io/gorm" + + spotifyauth "github.com/zmb3/spotify/v2/auth" ) var ( @@ -30,6 +31,9 @@ var ( currentTrackURI spotify.URI client *spotify.Client oauthToken LoginToken + logger zerolog.Logger + rateLimit uint64 + adminPassword string ) var ( @@ -37,39 +41,29 @@ var ( pollingSpotify = false ) -func main() { - // Load Env - err := godotenv.Load() - if err != nil { - log.Fatal("Error loading .env file") - } +func init() { + var err error + + logger = zerolog.New( + zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}, + ).Level(zerolog.TraceLevel).With().Timestamp().Caller().Logger() + logger.Info().Msg("Initializing Jukebox API") + + // Env Variables + logger.Info().Msg("Loading Environment Variables") + godotenv.Load() // Load Database & Migrate the schema db, err = gorm.Open(sqlite.Open(os.Getenv("DB_PATH")), &gorm.Config{}) + logger.Info().Msg("Connecting to Database") if err != nil { - log.Fatal("failed to connect database") + logger.Fatal().Err(err).Msg("Error Connecting to Database") } db.AutoMigrate(&Track{}) db.AutoMigrate(&TrackImage{}) db.AutoMigrate(&Device{}) db.AutoMigrate(&LoginToken{}) - // Set Rate Limiting - rateLimit, _ := strconv.ParseUint(os.Getenv("MAXIMUM_VOTES_PER_HOUR"), 10, 32) - store := ratelimit.InMemoryStore(&ratelimit.InMemoryOptions{ - Rate: time.Hour, - Limit: uint(rateLimit), - }) - - rateLimitMiddleWare := ratelimit.RateLimiter(store, &ratelimit.Options{ - ErrorHandler: func(c *gin.Context, info ratelimit.Info) { - c.JSON(429, "Too many requests. Try again in "+time.Until(info.ResetTime).String()) - }, - KeyFunc: func(c *gin.Context) string { - return c.ClientIP() + c.Request.UserAgent() - }, - }) - // Load Spotify API auth = spotifyauth.New( spotifyauth.WithRedirectURL(os.Getenv("CALLBACK_URL")), @@ -86,9 +80,7 @@ func main() { dbLoginToken := LoginToken{} if err := db.First(&dbLoginToken).Error; err != nil { // Assume no Login is Set - log.Println("-------------") - log.Println("NO LOGIN SET") - log.Println("-------------") + logger.Info().Msg("NO LOGIN SET") } else { oauthToken.AccessToken = dbLoginToken.AccessToken oauthToken.TokenType = dbLoginToken.TokenType @@ -100,29 +92,26 @@ func main() { RefreshToken: dbLoginToken.RefreshToken, Expiry: dbLoginToken.Expiry, })) - log.Println("-------------") - log.Println("LOGIN SET") - log.Println("-------------") + logger.Info().Msg("LOGIN SET") } // Set Device ID dbDevice := Device{} if err := db.First(&dbDevice).Error; err != nil { // Assume no Device is Set - log.Println("-------------") - log.Println("NO DEVICE SET") - log.Println("-------------") + logger.Info().Msg("NO DEVICE SET") } else { currentDevice.ID = dbDevice.ID currentDevice.Active = false currentDevice.Name = dbDevice.Name currentDevice.Type = dbDevice.Type - log.Println("-------------") - log.Println("DEVICE SET") - log.Println(dbDevice.Name) - log.Println("-------------") + logger.Info().Msg("DEVICE SET") + logger.Info().Msg(dbDevice.Name) } + // Set Minimum Votes + minimumVotes, _ = strconv.ParseInt(os.Getenv("MINIMUM_VOTES_TO_REMOVE"), 10, 64) + // Set Fallback Playlist addToPlaylist, _ := strconv.ParseBool(os.Getenv("FALLBACK_PLAYLIST_ADD_QUEUED")) fallbackPlaylist = FallbackPlaylist{ @@ -132,28 +121,46 @@ func main() { AddToPlaylist: addToPlaylist, } - // Set Minimum Votes - minimumVotes, _ = strconv.ParseInt(os.Getenv("MINIMUM_VOTES_TO_REMOVE"), 10, 64) + // Set Rate Limiting + rateLimit, _ = strconv.ParseUint(os.Getenv("MAXIMUM_VOTES_PER_HOUR"), 10, 32) - // Set Logging to file - logToFile, _ := strconv.ParseBool(os.Getenv("APP_LOG_TO_FILE")) - if logToFile { - file, err := os.OpenFile(os.Getenv("APP_LOG_PATH"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) - if err != nil { - log.Fatal(err) - } - log.SetOutput(file) - } + // Admin Password + adminPassword = os.Getenv("ADMIN_PASSWORD") + + logger.Info().Msg("Initalization Complete") +} + +func main() { + logger.Info().Msg("Starting Jukebox API") + + // Start Listeners and Polling + logger.Info().Msg("Starting GIN Web Server") + // Set Rate Limiting + store := ratelimit.InMemoryStore(&ratelimit.InMemoryOptions{ + Rate: time.Hour, + Limit: uint(rateLimit), + }) + + rateLimitMiddleWare := ratelimit.RateLimiter(store, &ratelimit.Options{ + ErrorHandler: func(c *gin.Context, info ratelimit.Info) { + c.JSON(429, "Too many requests. Try again in "+time.Until(info.ResetTime).String()) + }, + KeyFunc: func(c *gin.Context) string { + return c.ClientIP() + c.Request.UserAgent() + }, + }) - // Start Router r := gin.Default() r.Use(cors.Default()) authorized := r.Group("", gin.BasicAuth(gin.Accounts{ - "admin": os.Getenv("ADMIN_PASSWORD"), + "admin": adminPassword, })) + // Attempt to reboot previous session + go StartPollingSpotifyIfActive() + // Set Routes r.GET("/search/:searchTerm", handleSearch) @@ -176,3 +183,14 @@ func main() { r.Run(":8888") } + +func StartPollingSpotifyIfActive() { + time.Sleep(8 * time.Second) + dbDevice := Device{} + if err := db.First(&dbDevice).Error; err != nil { + logger.Info().Msg("No Device Set - Please Manually Set the Device!") + } else if dbDevice.Active { + logger.Info().Msg("Attempting to Auto Start Spotify") + go pollSpotify() + } +} diff --git a/api/player.go b/api/player.go index b6b76e5..4baba69 100644 --- a/api/player.go +++ b/api/player.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "log" "net/http" "strconv" "strings" @@ -21,7 +20,7 @@ func handlePlayer(c *gin.Context) { ctx := c.Request.Context() action := c.Param("action") - fmt.Println("Got request for:", action) + logger.Info().Msg("Got request for: " + action) if currentDevice.ID == "" { c.JSON(http.StatusInternalServerError, "No Device Set") @@ -39,7 +38,7 @@ func handlePlayer(c *gin.Context) { if pollingSpotify { err = client.PlayOpt(ctx, &playerOpt) if err != nil { - log.Print(err) + logger.Err(err) c.JSON(http.StatusInternalServerError, err) return } @@ -49,7 +48,7 @@ func handlePlayer(c *gin.Context) { case "pause": err = client.PauseOpt(ctx, &playerOpt) if err != nil { - log.Print(err) + logger.Err(err) c.JSON(http.StatusInternalServerError, err) return } @@ -64,7 +63,7 @@ func handlePlayer(c *gin.Context) { } err = client.Volume(ctx, handleTrackVolumeInput.Volume) if err != nil { - log.Print(err) + logger.Err(err) c.JSON(http.StatusInternalServerError, err) return } @@ -74,7 +73,7 @@ func handlePlayer(c *gin.Context) { playerOpt.URIs = []spotify.URI{track.URI} err = client.NextOpt(ctx, &playerOpt) if err != nil { - log.Print(err) + logger.Err(err) c.JSON(http.StatusInternalServerError, err) return } @@ -88,7 +87,6 @@ func handlePlayer(c *gin.Context) { return } } - } c.JSON(http.StatusAccepted, "Ok: "+action) @@ -99,27 +97,25 @@ func pollSpotify() { pollingSpotify = true - fmt.Println("STARTING JUKEBOX WITH DEVICE: " + currentDevice.ID) + logger.Info().Msg(fmt.Sprintf("STARTING JUKEBOX WITH DEVICE: %s", currentDevice.ID)) playerState, err := client.PlayerState(context.Background()) if err != nil { - fmt.Println("SOMETHING WENT WRONG GETTING PLAYER") - fmt.Println(err) + logger.Err(err).Msg("SOMETHING WENT WRONG GETTING PLAYER") } if playerState.Playing { fallbackPlaylist.Active = true currentTrackURI = playerState.Item.URI - fmt.Println("CONTINUING SONG: " + playerState.Item.Name + " - " + playerState.Item.Artists[0].Name) + logger.Info().Msg("CONTINUING SONG: " + playerState.Item.Name + " - " + playerState.Item.Artists[0].Name) } else { track, _ := getNextSong() err := client.PlayOpt(context.Background(), &spotify.PlayOptions{DeviceID: ¤tDevice.ID, URIs: []spotify.URI{track.URI}}) if err != nil { - fmt.Println("SOMETHING WENT WRONG STARTING PLAYER") - fmt.Println(err) + logger.Err(err).Msg("SOMETHING WENT WRONG STARTING PLAYER") } fallbackPlaylist.Active = track.FromFallBackPlaylist currentTrackURI = track.URI - fmt.Println("STARTING SONG: " + track.Name + " - " + track.Artist) + logger.Info().Msg("STARTING SONG: " + track.Name + " - " + track.Artist) } // Update Current Device @@ -134,21 +130,12 @@ func pollSpotify() { // Check Expiry if m, _ := time.ParseDuration("30s"); time.Until(oauthToken.Expiry) < m { // Attempt to reAuth - fmt.Println("OLD TOKEN") - fmt.Println(oauthToken.AccessToken) - fmt.Println(oauthToken.RefreshToken) - client = spotify.New(auth.Client(context.Background(), &oauth2.Token{RefreshToken: oauthToken.RefreshToken})) token, err := client.Token() if err != nil { - fmt.Println("SOMETHING WENT WRONG REFRESHING TOKEN") - fmt.Println(err.Error()) + logger.Err(err).Msg("SOMETHING WENT WRONG REFRESHING TOKEN") } - fmt.Println("NEW TOKEN") - fmt.Println(token.AccessToken) - fmt.Println(token.RefreshToken) - oauthToken.AccessToken = token.AccessToken oauthToken.TokenType = token.TokenType oauthToken.RefreshToken = token.RefreshToken @@ -158,14 +145,12 @@ func pollSpotify() { if err := db.First(&dbLoginToken, LoginToken{}).Error; err == nil { if err := db.Unscoped().Delete(&dbLoginToken).Error; err != nil { - fmt.Println("SOMETHING WENT WRONG DELETING OLD TOKEN") - fmt.Println(err.Error()) + logger.Err(err).Msg("SOMETHING WENT WRONG DELETING OLD TOKEN") } } if err := db.Create(&LoginToken{AccessToken: oauthToken.AccessToken, TokenType: oauthToken.TokenType, RefreshToken: oauthToken.RefreshToken, Expiry: oauthToken.Expiry}).Error; err != nil { - fmt.Println("SOMETHING WENT WRONG SAVING NEW TOKEN") - fmt.Println(err.Error()) + logger.Err(err).Msg("SOMETHING WENT WRONG SAVING NEW TOKEN") } } else { @@ -175,58 +160,66 @@ func pollSpotify() { // Get Player State playerState, err = client.PlayerState(context.Background()) if err != nil { - fmt.Println("SOMETHING WENT GETTING PLAYER STATE") - fmt.Println(err.Error()) + logger.Err(err).Msg("SOMETHING WENT GETTING PLAYER STATE") } - fmt.Println("CURRENT SONG: " + playerState.Item.Name + " - " + playerState.Item.Artists[0].Name) - fmt.Println("CURRENT PROGRESS: " + strconv.Itoa(playerState.Progress)) - fmt.Println("FALLBACK STATUS: " + strconv.FormatBool(fallbackPlaylist.Active)) + logger.Info().Msg(fmt.Sprintf("CURRENT SONG: %s - %s", playerState.Item.Name, playerState.Item.Artists[0].Name)) + logger.Info().Msg(fmt.Sprintf("CURRENT PROGRESS: %s / %s", strconv.Itoa(playerState.Progress), strconv.Itoa(playerState.Item.Duration))) + logger.Info().Msg(fmt.Sprintf("FALLBACK STATUS: %s", strconv.FormatBool(fallbackPlaylist.Active))) // Update Current Device currentDevice.Active = playerState.Device.Active currentDevice.Volume = playerState.Device.Volume + dbDevice := Device{} + if err := db.First(&dbDevice).Error; err != nil { + // Assume no Device is Set + logger.Fatal().Msg("NO DEVICE SET") + } else { + dbDevice.Active = currentDevice.Active + dbDevice.Volume = currentDevice.Volume + db.Save(&dbDevice) + // logger.Info().Msg("DEVICE SET") + // logger.Info().Msg(dbDevice.Name) + } + if playerState.Progress == 0 { - fmt.Println("LOADING NEXT SONG") + logger.Info().Msg("LOADING NEXT SONG") // Remove the track if !fallbackPlaylist.Active { if fallbackPlaylist.AddToPlaylist { // Can't check if song is already in playlist - so just delete it _, err := client.RemoveTracksFromPlaylist(context.Background(), fallbackPlaylist.ID, spotify.ID(strings.Replace(string(currentTrackURI), "spotify:track:", "", -1))) if err != nil { - fmt.Println("SOMETHING WENT WRONG REMOVING ITEM FROM PLAYLIST") - fmt.Println(err) + logger.Err(err).Msg("SOMETHING WENT WRONG REMOVING ITEM FROM PLAYLIST") } - fmt.Println("ADDING TRACK TO FALLBACK PLAYLIST: " + currentTrackURI) + logger.Info().Msg(fmt.Sprintf("ADDING TRACK TO FALLBACK PLAYLIST: %s", currentTrackURI)) _, err = client.AddTracksToPlaylist(context.Background(), fallbackPlaylist.ID, spotify.ID(strings.Replace(string(currentTrackURI), "spotify:track:", "", -1))) if err != nil { - fmt.Println("SOMETHING WENT WRONG ADDING ITEM TO PLAYLIST") - fmt.Println(err) + logger.Err(err).Msg("SOMETHING WENT WRONG ADDING ITEM TO PLAYLIST") } } - fmt.Println("REMOVING TRACK FROM QUEUE: " + currentTrackURI) + logger.Info().Msg(fmt.Sprintf("REMOVING TRACK FROM QUEUE: %s", currentTrackURI)) if err := db.First(&track, Track{URI: currentTrackURI}).Error; err != nil { - fmt.Println(err) + logger.Err(err) } if err := db.Unscoped().Delete(&track).Error; err != nil { - fmt.Println(err) + logger.Err(err) } } // Get the next track track, err = getNextSong() if err != nil { - fmt.Println("SOMETHING WENT WRONG GETTING NEXT SONG") - fmt.Println(err) + logger.Err(err).Msg("SOMETHING WENT WRONG GETTING NEXT SONG") } currentTrackURI = track.URI fallbackPlaylist.Active = track.FromFallBackPlaylist if fallbackPlaylist.Active { - fmt.Println("No More Tracks - Using fall back playlist") + logger.Info().Msg("No More Tracks - Using fall back playlist") } - fmt.Println("NEXT SONG: " + track.Name + " - " + track.Artist) + logger.Info().Msg("NEXT SONG: " + track.Name + " - " + track.Artist) playerOpt := spotify.PlayOptions{ DeviceID: ¤tDevice.ID, @@ -235,7 +228,7 @@ func pollSpotify() { err = client.PlayOpt(context.Background(), &playerOpt) if err != nil { - fmt.Println(err) + logger.Err(err) } } } @@ -251,7 +244,7 @@ func getAllDevices(c *gin.Context) { } func getCurrentDevice(c *gin.Context) { - fmt.Println(currentDevice) + logger.Info().Msg(fmt.Sprintf("%s", currentDevice)) c.JSON(http.StatusAccepted, currentDevice) } diff --git a/api/search.go b/api/search.go index df22f1d..09500ff 100644 --- a/api/search.go +++ b/api/search.go @@ -1,7 +1,6 @@ package main import ( - "log" "net/http" "github.com/gin-gonic/gin" @@ -16,7 +15,7 @@ func handleSearch(c *gin.Context) { results, err := client.Search(ctx, searchTerm, spotify.SearchTypeTrack) if err != nil { - log.Fatal(err) + logger.Fatal().Err(err) } // handle artist results diff --git a/api/tracks.go b/api/tracks.go index b94810f..ff59893 100644 --- a/api/tracks.go +++ b/api/tracks.go @@ -55,31 +55,6 @@ func handleTrack(c *gin.Context) { } c.JSON(http.StatusCreated, track) return - // case "remove": - // playerState, _ = client.PlayerState(ctx) - // if err := db.First(&track, Track{URI: handleTrackInput.URI}).Error; err != nil { - // c.JSON(http.StatusNotFound, "Track Not Found") - // return - // } - // if err := db.Unscoped().Delete(&track).Error; err != nil { - // c.JSON(http.StatusInternalServerError, err) - // return - // } - // // If currently playing is removed - play next in queue - // if playerState.Playing && playerState.Item.URI == track.URI { - // newTrack, _ := getNextSongByVotes() - // playerOpt := spotify.PlayOptions{ - // DeviceID: ¤tDevice.ID, - // URIs: []spotify.URI{newTrack.URI}, - // } - // err := client.PlayOpt(ctx, &playerOpt) - // if err != nil { - // c.JSON(http.StatusInternalServerError, err) - // return - // } - // } - // c.JSON(http.StatusAccepted, track) - // return default: c.JSON(http.StatusBadRequest, "Unknown Action") return diff --git a/api/types.go b/api/types.go index 512d5ed..f82fcef 100644 --- a/api/types.go +++ b/api/types.go @@ -113,4 +113,5 @@ type Device struct { Name string `json:"name"` Type string `json:"type"` Active bool `json:"is_active"` + Volume int `json:"volume"` } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..38d5494 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,52 @@ +version: '3.9' + +services: + api: + build: + context: . + dockerfile: resources/docker/api/Dockerfile + volumes: + - ./api:/app + env_file: $PWD/api/.env + command: go run . + ports: + - 8888:8888 + networks: + - internal + + ui: + build: + context: . + dockerfile: resources/docker/ui/Dockerfile + volumes: + - ./ui:/frontend + env_file: $PWD/ui/.env + command: npm run dev + ports: + - 3000:3000 + networks: + - internal + + npm: + build: + context: . + dockerfile: resources/docker/ui/Dockerfile + entrypoint: [ "npm" ] + working_dir: /frontend + command: [ "-v" ] + volumes: + - ./ui:/app + + go: + build: + context: . + dockerfile: resources/docker/api/Dockerfile + entrypoint: [ "go" ] + working_dir: /app + command: [ "-v" ] + volumes: + - ./api:/app + +networks: + internal: + driver: bridge \ No newline at end of file diff --git a/resources/docker/api/Dockerfile b/resources/docker/api/Dockerfile new file mode 100644 index 0000000..d36e0ce --- /dev/null +++ b/resources/docker/api/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.22-alpine +LABEL maintainer="Thornton Phillis (dev@th0rn0.co.uk)" + +WORKDIR /app + +ENV CGO_ENABLED=1 +ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE" +RUN apk add --no-cache gcc musl-dev + +COPY api/go.mod api/go.sum ./ +RUN go mod download + +COPY api/ . + +RUN go build -ldflags='-s -w -extldflags "-static"' -o /spotify-api + +EXPOSE 8080 + +CMD [ "/spotify-api" ] \ No newline at end of file diff --git a/resources/docker/ui/Dockerfile b/resources/docker/ui/Dockerfile new file mode 100644 index 0000000..06a419b --- /dev/null +++ b/resources/docker/ui/Dockerfile @@ -0,0 +1,14 @@ +FROM node:21-alpine3.18 +LABEL maintainer="Thornton Phillis (dev@th0rn0.co.uk)" + +WORKDIR /frontend + +COPY ui/ . + +RUN npm install + +RUN npm run build + +EXPOSE 3000 + +CMD [ "node", "/frontend/.output/server/index.mjs" ] \ No newline at end of file