Browse Source

Implements integration with Spotify

Toni Fadjukoff 7 years ago
parent
commit
b9016adf51
4 changed files with 285 additions and 6 deletions
  1. 9 4
      bot.go
  2. 177 0
      spotify/spotify.go
  3. 49 0
      spotify/spotify_test.go
  4. 50 2
      web.go

+ 9 - 4
bot.go View File

@@ -6,6 +6,7 @@ import (
6 6
 	"errors"
7 7
 	"fmt"
8 8
 	"github.com/jasonlvhit/gocron"
9
+	"github.com/lamperi/e4bot/spotify"
9 10
 	"io"
10 11
 	"log"
11 12
 	"os"
@@ -18,9 +19,11 @@ import (
18 19
 var songs SongPriorityQueue
19 20
 
20 21
 var credentials struct {
21
-	APIURL   string
22
-	UserName string
23
-	Password string
22
+	APIURL              string
23
+	UserName            string
24
+	Password            string
25
+	SpotifyClientID     string
26
+	SpotifyClientSecret string
24 27
 }
25 28
 
26 29
 func appendSong(wikiText string, author string, newSong string) string {
@@ -317,8 +320,10 @@ func main() {
317 320
 
318 321
 	modifiedSongChan := make(chan *SongEntry)
319 322
 
323
+	spotifyClient := spotify.NewClient(credentials.SpotifyClientID, credentials.SpotifyClientSecret)
320 324
 	go func() {
321
-		webStart(modifiedSongChan)
325
+
326
+		webStart(modifiedSongChan, spotifyClient)
322 327
 	}()
323 328
 
324 329
 	go func() {

+ 177 - 0
spotify/spotify.go View File

@@ -0,0 +1,177 @@
1
+package spotify
2
+
3
+import (
4
+	"context"
5
+	"encoding/json"
6
+	"errors"
7
+	"fmt"
8
+	"golang.org/x/oauth2/clientcredentials"
9
+	spotifyAuth "golang.org/x/oauth2/spotify"
10
+	"io"
11
+	"net/http"
12
+	"net/url"
13
+	"strings"
14
+)
15
+
16
+type SpotifyClient struct {
17
+	clientID     string
18
+	clientSecret string
19
+	bearerAuth   string
20
+	baseURL      *url.URL
21
+	httpClient   *http.Client
22
+}
23
+
24
+type TrackInfo struct {
25
+	Album        AlbumInfo    `json:"album"`
26
+	Artists      []ArtistInfo `json:"artists"`
27
+	DiskNumber   int          `json:"disc_number"`
28
+	DurationMS   int          `json:"duration_ms"`
29
+	Explicit     bool         `json:"explicit"`
30
+	ExternalIds  ExternalIds  `json:"external_ids"`
31
+	ExternalUrls ExternalUrls `json:"external_urls"`
32
+	Href         string       `json:"href"`
33
+	ID           string       `json:"id"`
34
+	IsPlayable   bool         `json:"is_playable"`
35
+	Name         string       `json:"name"`
36
+	Popularity   int          `json:"popularity"`
37
+	PreviewUrl   string       `json:"preview_url"`
38
+	TrackNumber  int          `json:"track_number"`
39
+	Type         string       `json:"type"`
40
+	Uri          string       `json:"uri"`
41
+}
42
+
43
+type AlbumInfo struct {
44
+	AlbumType    string       `json:"album_type"`
45
+	Artists      []ArtistInfo `json:"artists"`
46
+	ExternalUrls ExternalUrls `json:"external_urls"`
47
+	Href         string       `json:"href"`
48
+	ID           string       `json:"id"`
49
+	Images       []ImageInfo  `json:"images"`
50
+	Name         string       `json:"name"`
51
+	Type         string       `json:"type"`
52
+	Uri          string       `json:"uri"`
53
+}
54
+
55
+type ArtistInfo struct {
56
+	ExternalUrls ExternalUrls `json:"external_urls"`
57
+	Href         string       `json:"href"`
58
+	ID           string       `json:"id"`
59
+	Name         string       `json:"name"`
60
+	Type         string       `json:"type"`
61
+	Uri          string       `json:"uri"`
62
+}
63
+
64
+type ImageInfo struct {
65
+	Height int    `json:"height"`
66
+	URL    string `json:url`
67
+	Width  int    `json:"width"`
68
+}
69
+
70
+type ExternalIds struct {
71
+	ISRC string `json:"isrc"`
72
+}
73
+type ExternalUrls struct {
74
+	Spotify string `json:"spotify"`
75
+}
76
+
77
+type TokenInfo struct {
78
+	AccessToken string `json:"access_token"`
79
+	TokenType   string `json:"token_type"`
80
+	ExpiresIn   int    `json:"expires_in"`
81
+}
82
+
83
+type AuthErrorResponse struct {
84
+	Error            string `json:"error"`
85
+	ErrorDescription string `json:"error_description"`
86
+}
87
+
88
+type ErrorResponse struct {
89
+	Error ErrorInfo `json:"error"`
90
+}
91
+
92
+type ErrorInfo struct {
93
+	Status  int    `json:"status"`
94
+	Message string `json:"message"`
95
+}
96
+
97
+func NewClient(username, secret string) *SpotifyClient {
98
+	baseURL, _ := url.Parse("https://api.spotify.com")
99
+	httpClient := &http.Client{}
100
+	return &SpotifyClient{
101
+		clientID:     username,
102
+		clientSecret: secret,
103
+		baseURL:      baseURL,
104
+		httpClient:   httpClient,
105
+	}
106
+}
107
+
108
+func (s *SpotifyClient) Authenticate() error {
109
+	config := &clientcredentials.Config{
110
+		ClientID:     s.clientID,
111
+		ClientSecret: s.clientSecret,
112
+		TokenURL:     spotifyAuth.Endpoint.TokenURL,
113
+	}
114
+	token, err := config.Token(context.Background())
115
+	if err != nil {
116
+		fmt.Println(err)
117
+		return err
118
+	}
119
+	s.bearerAuth = token.AccessToken
120
+	return nil
121
+}
122
+
123
+func (s *SpotifyClient) GetTrack(trackId string) (*TrackInfo, error) {
124
+	var trackInfo TrackInfo
125
+	url := s.urlFromBase(fmt.Sprintf("/v1/tracks/%s", trackId))
126
+	err := s.doRequest("GET", url, &trackInfo, nil)
127
+	if err != nil {
128
+		return nil, err
129
+	} else {
130
+		return &trackInfo, nil
131
+	}
132
+}
133
+
134
+func (s *SpotifyClient) urlFromBase(path string) *url.URL {
135
+	endpoint := &url.URL{Path: path}
136
+	return s.baseURL.ResolveReference(endpoint)
137
+}
138
+
139
+func (s *SpotifyClient) doRequest(method string, url *url.URL, object interface{}, values *url.Values) error {
140
+	var bodyReader io.Reader
141
+	if values != nil {
142
+		s := values.Encode()
143
+		bodyReader = strings.NewReader(s)
144
+	}
145
+
146
+	req, err := http.NewRequest(method, url.String(), bodyReader)
147
+	if err != nil {
148
+		return err
149
+	}
150
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.bearerAuth))
151
+
152
+	if values != nil {
153
+		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
154
+	}
155
+
156
+	req.Header.Set("Accept", "application/json")
157
+	req.Header.Set("User-Agent", "github.com/lamperi/e4bot/spotify")
158
+
159
+	resp, err := s.httpClient.Do(req)
160
+	if err != nil {
161
+		return err
162
+	}
163
+	defer resp.Body.Close()
164
+
165
+	if resp.StatusCode != 200 {
166
+		var errorResp ErrorResponse
167
+		err = json.NewDecoder(resp.Body).Decode(&errorResp)
168
+		return errors.New(fmt.Sprintf("Spotify API returned: %d: %s", resp.StatusCode, errorResp.Error.Message))
169
+	}
170
+
171
+	err = json.NewDecoder(resp.Body).Decode(object)
172
+	if err != nil {
173
+		return err
174
+	}
175
+	return nil
176
+
177
+}

+ 49 - 0
spotify/spotify_test.go View File

@@ -0,0 +1,49 @@
1
+package spotify
2
+
3
+import (
4
+	"os"
5
+	"strings"
6
+	"testing"
7
+)
8
+
9
+func getAuthorization() (string, string) {
10
+	envVars := os.Environ()
11
+	for _, envVar := range envVars {
12
+		key_val := strings.Split(envVar, "=")
13
+		if key_val[0] == "SPOTIFY_TOKEN" {
14
+			username_secret := strings.Split(key_val[1], ":")
15
+			return username_secret[0], username_secret[1]
16
+		}
17
+	}
18
+	return "", ""
19
+}
20
+
21
+func createClient(t *testing.T) *SpotifyClient {
22
+	username, secret := getAuthorization()
23
+	if username == "" {
24
+		t.Fatal("Spotify authorization token not available, please use it as env variable SPOTIFY_TOKEN")
25
+	}
26
+	client := NewClient(username, secret)
27
+	err := client.Authenticate()
28
+	if err != nil {
29
+		t.Fatal("Spotify authentication failed", err)
30
+	}
31
+	return client
32
+}
33
+
34
+func TestGetTrackInfo(t *testing.T) {
35
+	client := createClient(t)
36
+	track, err := client.GetTrack("5fLiaYOzIAk0PpwJ2VQUpT")
37
+	if err != nil {
38
+		t.Error("Error while reading Track", err)
39
+	}
40
+	if track.Artists[0].Name != "Vesala" {
41
+		t.Error("Expected track artist to be 'Vesala'")
42
+	}
43
+	if track.Name != "Rakkaus ja maailmanloppu" {
44
+		t.Error("Expected track artist to be 'Rakkaus ja maailmanloppu'")
45
+	}
46
+	if track.ExternalUrls.Spotify != "https://open.spotify.com/track/5fLiaYOzIAk0PpwJ2VQUpT" {
47
+		t.Error("Expected track artist to be 'https://open.spotify.com/track/5fLiaYOzIAk0PpwJ2VQUpT'")
48
+	}
49
+}

+ 50 - 2
web.go View File

@@ -1,14 +1,18 @@
1 1
 package main
2 2
 
3 3
 import (
4
+	"github.com/lamperi/e4bot/spotify"
4 5
 	"html/template"
6
+	"log"
5 7
 	"net/http"
6 8
 	"strconv"
9
+	"strings"
7 10
 )
8 11
 
9 12
 var (
10 13
 	cachedTemplates = template.Must(template.ParseFiles("web/index.html"))
11 14
 	songsChan       chan *SongEntry
15
+	spotifyClient   *spotify.SpotifyClient
12 16
 )
13 17
 
14 18
 func indexHandler(w http.ResponseWriter, r *http.Request) {
@@ -55,13 +59,29 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
55 59
 	}
56 60
 }
57 61
 
62
+func getTrackID(url string) string {
63
+	const externalURLPrefix = "https://open.spotify.com/track/"
64
+	const trackURIPrefix = "spotify:track:"
65
+	if strings.HasPrefix(url, externalURLPrefix) {
66
+		return strings.Split(url, externalURLPrefix)[1]
67
+	} else if strings.HasPrefix(url, trackURIPrefix) {
68
+		return strings.Split(url, trackURIPrefix)[1]
69
+	}
70
+	return url
71
+}
72
+
58 73
 func updateHandler(w http.ResponseWriter, r *http.Request) {
59 74
 	if r.URL.Path != "/update" {
60 75
 		http.Error(w, "Forbidden", http.StatusForbidden)
61 76
 		return
62 77
 	}
78
+	session, err := getSession(r)
79
+	if session == nil {
80
+		http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
81
+		return
82
+	}
63 83
 
64
-	err := r.ParseForm()
84
+	err = r.ParseForm()
65 85
 	if err != nil {
66 86
 		http.Error(w, err.Error(), http.StatusBadRequest)
67 87
 		return
@@ -76,6 +96,33 @@ func updateHandler(w http.ResponseWriter, r *http.Request) {
76 96
 	url := r.Form.Get("url")
77 97
 	sync := r.Form.Get("sync")
78 98
 
99
+	if artist == "" && title == "" && url != "" {
100
+		log.Println("Resolving Spotify URL")
101
+		trackID := getTrackID(url)
102
+		err := spotifyClient.Authenticate()
103
+		if err != nil {
104
+			http.Error(w, err.Error(), http.StatusInternalServerError)
105
+			return
106
+		}
107
+		track, err := spotifyClient.GetTrack(trackID)
108
+
109
+		if err != nil {
110
+			http.Error(w, err.Error(), http.StatusInternalServerError)
111
+			return
112
+		}
113
+		log.Printf("Found Track with name %v and artists %v\n", track.Name, track.Artists)
114
+		for index, trackArtist := range track.Artists {
115
+			if index == 0 {
116
+				artist = trackArtist.Name
117
+			} else if index == 1 {
118
+				artist = artist + " feat. " + trackArtist.Name
119
+			} else {
120
+				artist = artist + ", " + trackArtist.Name
121
+			}
122
+		}
123
+		title = track.Name
124
+	}
125
+
79 126
 	songs, err := loadDb()
80 127
 	if err != nil {
81 128
 		http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -136,8 +183,9 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
136 183
 	http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
137 184
 }
138 185
 
139
-func webStart(modifiedSongChan chan *SongEntry) {
186
+func webStart(modifiedSongChan chan *SongEntry, spot *spotify.SpotifyClient) {
140 187
 	songsChan = modifiedSongChan
188
+	spotifyClient = spot
141 189
 
142 190
 	mux := http.NewServeMux()
143 191