Skip to content

Commit

Permalink
Merge pull request #2 from zembrodt/develop
Browse files Browse the repository at this point in the history
Merge develop into main
  • Loading branch information
zembrodt authored Jun 12, 2023
2 parents b14d218 + 21a6fb0 commit 18459cd
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 60 deletions.
86 changes: 74 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,76 @@
# ShowTunes API

Go server used by the ShowTunes app to make API requests where a client secret is needed.

## Requests
* */v1/auth/tokens* (**POST**): Retrieve a Spotify auth token
* Expects *code* and *redirect_uri*
* Returns a new auth token
* */v1/auth/tokens* (**PUT**): Refresh a Spotify auth token
* Expects refresh token as *code*
* Returns an updated auth token
* */v1/color* (**GET**): Retrieve the dominant color of a given album's cover art
* Expects *url* - must be to an image hosted by Spotify (*i.scdn.co*)
* Returns the dominant color in hex
Go server API as an example of a 3rd party API for the ShowTunes app.

See: https://github.com/zembrodt/showtunes

The ShowTunes app can be configured to use a 3rd party API for its authorization requests. It can also be configured to
use a 3rd party API (the same or a different server) to request analysis information on album covers.

## Endpoints

###`/v1/auth/token` POST
Retrieve/Refresh a Spotify auth token

#### Request
| Parameter | Value |
| --------------- | --------------------------------------------------------------------------------- |
| `grant_type` | Must be `authorization_token` or `refresh_token` |
| `code` | The authorization code |
| `redirect_uri` | Must match the `redirect_uri` used when requesting the authorization code |
| `refresh_token` | The token used in place of the authorization code when the auth token has expired |

#### Response
| Parameter | Type | Value |
| --------------- | -------- | ---------------------------------------------------- |
| `access_token` | `string` | The Spotify API access token |
| `token_type` | `string` | How the access token can be used. Always `Bearer` |
| `refresh_token` | `string` | The token used to request a new token after `expiry` |
| `expiry` | `string` | Date formatted string for when this token expires |

---

###`/v1/color` GET
Retrieve the dominant color of a given album's cover art

#### Request
| Parameter | Value |
| --------- | --------------------------------------------------------------------------------- |
| `url` | The url for the image to be used (Must be a domain configured in `VALID_DOMAINS`) |

#### Response
| Parameter | Type | Value |
| --------- | -------- | ---------------------------------- |
| `color` | `string` | The dominant color as a hex string |

---

###`/ping` GET
Retrieve information on the running API server

#### Response
| Parameter | Type | Value |
| --------- | -------- | ----------------------------------|
| `name` | `string` | The name of the application |
| `version` | `string` | The application's current version |
| `api_root` | `string` | The API endpoint root |

## Configurations
*Note*: these can be configured as environment variables or in `config/config.yaml`.

Environment variables must be prefixed with `SHOWTUNES_`

| Config | Default Value | Description |
| ---------------- | ------------- | ------------------------------------------------------------------ |
| `SERVER_ADDRESS` | `localhost` | The address of this API server |
| `SERVER_PORT` | `8000` | The port of this API server |
| `ORIGIN` | `*` | URL for client accessing this API (`Access-Control-Allow-Origin`) |
| `MAX_AGE` | `86400` | Value for `Access-Control-Max-Age` header |
| `CLIENT_ID` | None | The Client ID to retrieve the authorization token with |
| `CLIENT_SECRET` | None | The Client Secret to retrieve the authorization token with |
| `VALID_DOMAINS` | `i.scdn.co` | Comma-separated list of URLs that host the required Spotify images |

## Building and Running the Server
Scripts have been provided to build an executable for the server that can be deployed.
* `resources/scripts/build.bat`
* `resources/scripts/build.sh`
20 changes: 9 additions & 11 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import (
)

var osEnvConfigs = []string{
global.ServerAddress,
global.ServerPort,
global.PublicAddress,
global.ServerAddressKey,
global.ServerPortKey,
global.ClientIdKey,
global.ClientSecretKey,
global.OriginKey,
global.MaxAgeKey,
global.ValidDomainsKey,
}

func main() {
Expand All @@ -36,24 +36,22 @@ func main() {

setConfigurations()

serverAddress := viper.GetString(global.ServerAddress)
serverPort := viper.GetInt(global.ServerPort)

//repo := repository.New(db)
//svc := service.New(repo, expiryTime)
serverAddress := viper.GetString(global.ServerAddressKey)
serverPort := viper.GetInt(global.ServerPortKey)

server := controller.New(viper.GetString(global.ClientIdKey), viper.GetString(global.ClientSecretKey))
server.Start(serverAddress, serverPort)
}

func setConfigurations() {
// Set config defaults
viper.SetDefault(global.ServerAddress, "localhost")
viper.SetDefault(global.ServerPort, 8000)
viper.SetDefault(global.ServerAddressKey, "localhost")
viper.SetDefault(global.ServerPortKey, 8000)
viper.SetDefault(global.ClientIdKey, "")
viper.SetDefault(global.ClientSecretKey, "")
viper.SetDefault(global.OriginKey, "*")
viper.SetDefault(global.MaxAgeKey, "86400")
viper.SetDefault(global.ValidDomainsKey, "i.scdn.co")

// Get config from config.yaml
viper.SetConfigName(global.ConfigFileName)
Expand All @@ -76,7 +74,7 @@ func setConfigurations() {

// Check for environment variables
for _, key := range osEnvConfigs {
val, lookup := os.LookupEnv(key)
val, lookup := os.LookupEnv(global.EnvPrefix + "_" + key)
if lookup {
log.Printf("Adding env variable for %s\n", key)
viper.Set(key, val)
Expand Down
30 changes: 17 additions & 13 deletions controller/auth_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,27 @@ import (

const (
pathAuth = "/auth"
pathToken = pathAuth + "/tokens"
pathToken = pathAuth + "/token"

codeKey = "code"
redirectUriKey = "redirect_uri"
codeKey = "code"
redirectUriKey = "redirect_uri"
grantTypeKey = "grant_type"
refreshTokenKey = "refresh_token"
)

var scopes = []string{
"user-read-playback-state",
"user-modify-playback-state",
func (c *ShowTunesAPIController) createAuthHandlers() {
c.handleFunc(pathToken, c.handleToken, http.MethodPost)
}

func (c *ShowTunesAPIController) createAuthHandlers() {
c.handleFunc(pathToken, c.getAuthTokens, http.MethodPost)
c.handleFunc(pathToken, c.updateAuthToken, http.MethodPut)
func (c *ShowTunesAPIController) handleToken(w http.ResponseWriter, r *http.Request) {
if r.FormValue(grantTypeKey) == refreshTokenKey {
c.refreshToken(w, r)
} else {
c.fetchToken(w, r)
}
}

func (c *ShowTunesAPIController) getAuthTokens(w http.ResponseWriter, r *http.Request) {
func (c *ShowTunesAPIController) fetchToken(w http.ResponseWriter, r *http.Request) {
// Get request parameters
code := r.FormValue(codeKey)
redirectUri := r.FormValue(redirectUriKey)
Expand All @@ -48,12 +52,12 @@ func (c *ShowTunesAPIController) getAuthTokens(w http.ResponseWriter, r *http.Re
respondWithJSON(w, http.StatusOK, token)
}

func (c *ShowTunesAPIController) updateAuthToken(w http.ResponseWriter, r *http.Request) {
func (c *ShowTunesAPIController) refreshToken(w http.ResponseWriter, r *http.Request) {
// Get request parameters
code := r.FormValue(codeKey)
code := r.FormValue(refreshTokenKey)

if len(code) == 0 {
respondWithError(w, http.StatusBadRequest, "Invalid %s", codeKey)
respondWithError(w, http.StatusBadRequest, "Invalid %s", refreshTokenKey)
return
}

Expand Down
16 changes: 8 additions & 8 deletions controller/color_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,15 @@ import (
"strings"
)

type colorResponse struct {
Color string `json:"color"`
}

const (
pathColor = "/color"
urlKey = "url"
)

func validDomains() map[string]bool {
return map[string]bool{
"i.scdn.co": true,
}
}

func (c *ShowTunesAPIController) createColorHandlers() {
c.handleFunc(pathColor, c.getDominateColor, http.MethodGet)
}
Expand All @@ -47,7 +45,7 @@ func (c *ShowTunesAPIController) getDominateColor(w http.ResponseWriter, r *http
return
}
// Check if the domain for this image is an accepted one
if !validDomains()[strings.ToLower(coverArtUrl.Hostname())] {
if !c.validDomains[strings.ToLower(coverArtUrl.Hostname())] {
respondWithError(w, http.StatusBadRequest, "The provided domain in the URL is invalid")
return
}
Expand Down Expand Up @@ -77,5 +75,7 @@ func (c *ShowTunesAPIController) getDominateColor(w http.ResponseWriter, r *http
}

// Return dominant color of the album art
respondWithJSON(w, http.StatusOK, dominantcolor.Hex(dominantcolor.Find(img)))
respondWithJSON(w, http.StatusOK, colorResponse{
Color: dominantcolor.Hex(dominantcolor.Find(img)),
})
}
21 changes: 14 additions & 7 deletions controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/spf13/viper"
"github.com/zembrodt/showtunes-api"
"github.com/zembrodt/showtunes-api/util/global"
"golang.org/x/oauth2"
"golang.org/x/oauth2/spotify"
"log"
"net/http"
"os"
"os/signal"
"reflect"
"strings"
"time"

"github.com/gorilla/mux"
Expand All @@ -23,10 +26,9 @@ type httpResponse struct {
}

type pingResponse struct {
Response httpResponse `json:"response"`
Name string `json:"name"`
Version string `json:"version"`
ApiRoot string `json:"apiRoot"`
ApiRoot string `json:"api_root"`
}

type httpSuccess httpResponse
Expand Down Expand Up @@ -54,6 +56,7 @@ type ShowTunesAPIController struct {
resourcesPath string
clientId string
clientSecret string
validDomains map[string]bool
conf *oauth2.Config
}

Expand All @@ -65,10 +68,10 @@ func New(clientId, clientSecret string) *ShowTunesAPIController {
routerApi: rApi,
clientId: clientId,
clientSecret: clientSecret,
validDomains: fetchValidDomains(),
conf: &oauth2.Config{
ClientID: clientId,
ClientSecret: clientSecret,
Scopes: scopes,
Endpoint: spotify.Endpoint,
},
}
Expand Down Expand Up @@ -128,10 +131,6 @@ func (c *ShowTunesAPIController) createGeneralHandlers() {

func (c *ShowTunesAPIController) ping(w http.ResponseWriter, r *http.Request) {
respondWithJSON(w, http.StatusOK, pingResponse{
Response: httpResponse{
Code: http.StatusOK,
Message: jsonKeySuccess,
},
Name: showtunes.Name,
Version: showtunes.Version,
ApiRoot: showtunes.APIRoot,
Expand Down Expand Up @@ -192,3 +191,11 @@ func respondWithError(w http.ResponseWriter, code int, message string, params ..
Message: fmt.Sprintf(message, params...),
})
}

func fetchValidDomains() map[string]bool {
domains := make(map[string]bool)
for _, domain := range strings.Split(viper.GetString(global.ValidDomainsKey), ",") {
domains[domain] = true
}
return domains
}
24 changes: 23 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
module github.com/zembrodt/showtunes-api

go 1.16
go 1.20

require (
github.com/cenkalti/dominantcolor v0.0.0-20211126221809-b695f665ba35
github.com/gorilla/mux v1.8.0
github.com/spf13/viper v1.7.1
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93
)

require (
github.com/fsnotify/fsnotify v1.4.7 // indirect
github.com/golang/protobuf v1.4.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.1 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/pelletier/go-toml v1.2.0 // indirect
github.com/spf13/afero v1.1.2 // indirect
github.com/spf13/cast v1.3.0 // indirect
github.com/spf13/jwalterweatherman v1.0.0 // indirect
github.com/spf13/pflag v1.0.3 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 // indirect
golang.org/x/text v0.3.3 // indirect
google.golang.org/appengine v1.6.6 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/ini.v1 v1.51.0 // indirect
gopkg.in/yaml.v2 v2.2.4 // indirect
)
2 changes: 1 addition & 1 deletion properties.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ package showtunes

const (
Name = "ShowTunes API"
Version = "1.0.0"
Version = "1.1.0"
APIRoot = "/v1"
)
16 changes: 9 additions & 7 deletions util/global/config_constants.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package global

const (
EnvPrefix = "SHOWTUNES"

// Configuration file
ConfigFileName = "config"
ConfigFileExtension = "yaml"
ConfigFilePath = "config"

// Configuration names
ServerAddress = "ServerAddress"
ServerPort = "PORT"
PublicAddress = "PublicAddress"
OriginKey = "Origin"
MaxAgeKey = "MaxAge"
ServerAddressKey = "SERVER_ADDRESS"
ServerPortKey = "SERVER_PORT"
OriginKey = "ORIGIN"
MaxAgeKey = "MAX_AGE"

// Spotify Configurations
ClientIdKey = "ClientId"
ClientSecretKey = "ClientSecret"
ClientIdKey = "CLIENT_ID"
ClientSecretKey = "CLIENT_SECRET"
ValidDomainsKey = "VALID_DOMAINS"
)

0 comments on commit 18459cd

Please sign in to comment.