Browse Source

Use github.com/zmb3/spotify

Toni Fadjukoff 6 years ago
parent
commit
490689d5ce
5 changed files with 184 additions and 72 deletions
  1. 2 1
      auth.go
  2. 63 43
      bot.go
  3. 76 15
      spotify/spotify.go
  4. 38 13
      web.go
  5. 5 0
      web/index.html

+ 2 - 1
auth.go View File

@@ -17,6 +17,7 @@ import (
17 17
 type SessionData struct {
18 18
 	sid      string
19 19
 	username string
20
+	spotify  string
20 21
 	priority time.Time
21 22
 	index    int
22 23
 }
@@ -162,7 +163,7 @@ func tryLogin(db *DB, username, password string, longerTime bool) (http.Cookie,
162 163
 	}
163 164
 
164 165
 	expiration := time.Now().Add(duration)
165
-	addSession(&SessionData{sid, username, expiration, 0})
166
+	addSession(&SessionData{sid, username, "", expiration, 0})
166 167
 
167 168
 	return loginCookie, nil
168 169
 }

+ 63 - 43
bot.go View File

@@ -6,7 +6,7 @@ import (
6 6
 	"flag"
7 7
 	"fmt"
8 8
 	"github.com/jasonlvhit/gocron"
9
-	"github.com/lamperi/e4bot/spotify"
9
+	"github.com/zmb3/spotify"
10 10
 	"io"
11 11
 	"log"
12 12
 	"os"
@@ -18,9 +18,13 @@ import (
18 18
 )
19 19
 
20 20
 type App struct {
21
-	db            *DB
22
-	credentials   Credentials
23
-	spotifyClient *spotify.SpotifyClient
21
+	db          *DB
22
+	credentials Credentials
23
+	spotify     *AppSpotify
24
+}
25
+type AppSpotify struct {
26
+	auth   *spotify.Authenticator
27
+	client *spotify.Client
24 28
 }
25 29
 
26 30
 type Credentials struct {
@@ -34,16 +38,16 @@ type Credentials struct {
34 38
 	ListenAddr          string
35 39
 }
36 40
 
37
-func (app *App) CreateSpotifyClient() *spotify.SpotifyClient {
38
-	spotifyClient := spotify.NewClient(app.credentials.SpotifyClientID, app.credentials.SpotifyClientSecret)
39
-	spotifyClient.SetupUserAuthenticate(app.credentials.SpotifyCallback)
40
-	return spotifyClient
41
+func (app *App) CreateSpotifyAuthenticator() *spotify.Authenticator {
42
+	auth := spotify.NewAuthenticator(app.credentials.SpotifyCallback, spotify.ScopePlaylistModifyPublic)
43
+	auth.SetAuthInfo(app.credentials.SpotifyClientID, app.credentials.SpotifyClientSecret)
44
+	return &auth
41 45
 }
42 46
 
43 47
 func (app *App) LaunchWeb() {
44
-	app.spotifyClient = app.CreateSpotifyClient()
48
+	app.spotify.auth = app.CreateSpotifyAuthenticator()
45 49
 	go func() {
46
-		webStart(app.credentials.ListenAddr, app.db, app.spotifyClient)
50
+		webStart(app.credentials.ListenAddr, app.db, app.spotify)
47 51
 	}()
48 52
 }
49 53
 
@@ -225,15 +229,15 @@ func appendAverages(wikiText string) string {
225 229
 	return strings.Join(changedLines, "\n")
226 230
 }
227 231
 
228
-func findPlaylist(wikiText string) (string, []string) {
232
+func findPlaylist(wikiText string) (spotify.ID, []spotify.ID) {
229 233
 	const SONG_MARK = "<!-- Kappale -->"
230 234
 	const SPOTIFY_MARK = "https://open.spotify.com/track/"
231 235
 	const SPOTIFY_PLAYLIST_MARK = "/playlist/"
232 236
 	const PLAYLIST_MARK = " Spotify-soittolista]"
233 237
 	lines := strings.Split(wikiText, "\n")
234 238
 
235
-	playlistId := ""
236
-	tracks := make([]string, 0)
239
+	var playlistId spotify.ID
240
+	tracks := make([]spotify.ID, 0)
237 241
 	for _, line := range lines {
238 242
 		if strings.Index(line, SONG_MARK) != -1 {
239 243
 			i := strings.Index(line, SPOTIFY_MARK)
@@ -243,27 +247,28 @@ func findPlaylist(wikiText string) (string, []string) {
243 247
 					j += i
244 248
 				}
245 249
 				trackId := line[i+len(SPOTIFY_MARK) : j]
246
-				tracks = append(tracks, trackId)
250
+				tracks = append(tracks, spotify.ID(trackId))
247 251
 			}
248 252
 		} else if strings.Index(line, SPOTIFY_PLAYLIST_MARK) != -1 && strings.Index(line, PLAYLIST_MARK) != -1 {
249 253
 			i := strings.Index(line, SPOTIFY_PLAYLIST_MARK)
250 254
 			j := strings.Index(line[i:], PLAYLIST_MARK)
251 255
 
252
-			playlistId = line[i+len(SPOTIFY_PLAYLIST_MARK) : i+j]
253
-			q := strings.Index(playlistId, "?")
256
+			playlist := line[i+len(SPOTIFY_PLAYLIST_MARK) : i+j]
257
+			q := strings.Index(playlist, "?")
254 258
 			if q != -1 {
255
-				playlistId = playlistId[:q]
259
+				playlist = playlist[:q]
256 260
 			}
261
+			playlistId = spotify.ID(playlist)
257 262
 		}
258 263
 	}
259 264
 	log.Printf("Found playlist %s and tracks %s\n", playlistId, tracks)
260 265
 
261
-	return playlistId, tracks
266
+	return spotify.ID(playlistId), tracks
262 267
 }
263 268
 
264
-func appendPlaylist(wikiText string, playlist *spotify.PlaylistInfo) string {
269
+func appendPlaylist(wikiText string, playlist *spotify.FullPlaylist) string {
265 270
 	changedText := wikiText + `
266
-	[` + playlist.ExternalUrls.Spotify + ` Spotify-soittolista]
271
+	[` + playlist.ExternalURLs["spotify"] + ` Spotify-soittolista]
267 272
 	`
268 273
 	return changedText
269 274
 }
@@ -273,6 +278,7 @@ func (app *App) AutomateSection(title string) error {
273 278
 
274 279
 	sections, err := wiki.GetWikiPageSections(title)
275 280
 	if err != nil {
281
+		log.Println("Error while reading page sections")
276 282
 		return err
277 283
 	}
278 284
 
@@ -292,6 +298,7 @@ func (app *App) AutomateSection(title string) error {
292 298
 
293 299
 			wikiText, err := wiki.GetWikiPageSectionText(title, section.index)
294 300
 			if err != nil {
301
+				log.Println("Error reading text from wiki")
295 302
 				return err
296 303
 			}
297 304
 			message := ""
@@ -300,41 +307,54 @@ func (app *App) AutomateSection(title string) error {
300 307
 				message = message + fmt.Sprintf("Calculate averages for week %d. ", weekNumber)
301 308
 			}
302 309
 
303
-			if app.spotifyClient.HasUserLogin() {
304
-				playlistId, tracks := findPlaylist(changedWikiText)
305
-				currentTracks, err := app.db.FindPlaylistBySection(section.title)
306
-				if len(tracks) > 0 && (err != nil || reflect.DeepEqual(currentTracks, tracks)) {
310
+			if app.spotify.client != nil {
311
+				token, err := app.spotify.client.Token()
312
+				if err != nil {
313
+					log.Println("No token available")
314
+				} else if token.Expiry.Before(time.Now()) {
315
+					log.Println("Token has expired")
316
+				} else {
317
+					log.Println("Checking if playlist needs updating")
318
+					playlistId, tracks := findPlaylist(changedWikiText)
319
+					currentTracks, err := app.db.FindPlaylistBySection(section.title)
320
+					if len(tracks) > 0 && (err != nil || reflect.DeepEqual(currentTracks, tracks)) {
321
+
322
+						if playlistId == "" {
323
+
324
+							info, err := app.spotify.client.CreatePlaylistForUser(app.credentials.SpotifyUser, title+" "+section.title, true)
325
+							if err != nil {
326
+								log.Println("Error creating playlist to Spotify")
327
+								return err
328
+							}
329
+							playlistId = info.ID
330
+							changedWikiText = appendPlaylist(changedWikiText, info)
331
+							message = message + fmt.Sprintf("Added link to Spotify playlist for week %d.", weekNumber)
332
+						}
333
+						err := app.spotify.client.ReplacePlaylistTracks(app.credentials.SpotifyUser, playlistId, tracks...)
334
+						if err != nil {
335
+							log.Println("Error updating playlist to Spotify")
336
+							return err
337
+						}
307 338
 
308
-					spotify := app.spotifyClient
309
-					if playlistId == "" {
339
+						stringTracks := make([]string, len(tracks))
340
+						for i, t := range tracks {
341
+							stringTracks[i] = string(t)
342
+						}
310 343
 
311
-						info, err := spotify.NewPlaylist(title+" "+section.title, app.credentials.SpotifyUser)
344
+						_, err = app.db.UpdatePlaylistBySection(section.title, stringTracks)
312 345
 						if err != nil {
313
-							log.Println("Error creating playlist")
346
+							log.Println("Error updating playlist to DB")
314 347
 							return err
315 348
 						}
316
-						playlistId = info.Id
317
-						changedWikiText = appendPlaylist(changedWikiText, info)
318
-						message = message + fmt.Sprintf("Added link to Spotify playlist for week %d.", weekNumber)
319
-					}
320
-					err := spotify.UpdatePlaylist(app.credentials.SpotifyUser, playlistId, tracks)
321
-					if err != nil {
322
-						log.Println("Error updating playlist")
323
-						return err
324
-					}
325
-					_, err = app.db.UpdatePlaylistBySection(section.title, tracks)
326
-					if err != nil {
327
-						return err
328 349
 					}
329 350
 				}
330
-
331 351
 			}
332 352
 
333 353
 			if message != "" {
334 354
 
335 355
 				_, err := wiki.EditWikiPageSection(title, section.index, changedWikiText,
336 356
 					fmt.Sprintf("Calculate averages for week %d", weekNumber))
337
-				err = nil
357
+				//err = errors.New("foo")
338 358
 				if err != nil {
339 359
 					return err
340 360
 				}
@@ -397,7 +417,7 @@ func main() {
397 417
 		panic(err)
398 418
 	}
399 419
 
400
-	a := App{InitDatabase(), creds, nil}
420
+	a := App{InitDatabase(), creds, &AppSpotify{}}
401 421
 	a.LaunchWeb()
402 422
 
403 423
 	gocron.Every(1).Minute().Do(a.AutomateSectionTask)

+ 76 - 15
spotify/spotify.go View File

@@ -1,6 +1,7 @@
1 1
 package spotify
2 2
 
3 3
 import (
4
+	"bytes"
4 5
 	"context"
5 6
 	"crypto/tls"
6 7
 	"encoding/json"
@@ -10,8 +11,10 @@ import (
10 11
 	"golang.org/x/oauth2/clientcredentials"
11 12
 	spotifyAuth "golang.org/x/oauth2/spotify"
12 13
 	"io"
14
+	"log"
13 15
 	"net/http"
14 16
 	"net/url"
17
+	"reflect"
15 18
 	"strings"
16 19
 )
17 20
 
@@ -138,6 +141,19 @@ type Followers struct {
138 141
 	Total int    `json:"total"`
139 142
 }
140 143
 
144
+type SpotifyProfile struct {
145
+	Birthdate    string       `json:"birthdate"`
146
+	Country      string       `json:"country"`
147
+	DisplayName  string       `json:"display_name"`
148
+	Email        string       `json:"email"`
149
+	ExternalUrls ExternalUrls `json:"external_urls"`
150
+	Followers    Followers    `json:"followers"`
151
+	Id           string       `json:"id"`
152
+	Product      string       `json:"product"`
153
+	Type         string       `json:"type"`
154
+	Uri          string       `json:"uri"`
155
+}
156
+
141 157
 func NewClient(username, secret string) *SpotifyClient {
142 158
 	baseURL, _ := url.Parse("https://api.spotify.com")
143 159
 	httpClient := &http.Client{}
@@ -157,7 +173,7 @@ func (s *SpotifyClient) Authenticate() error {
157 173
 	}
158 174
 	token, err := config.Token(context.Background())
159 175
 	if err != nil {
160
-		fmt.Println(err)
176
+		log.Println(err)
161 177
 		return err
162 178
 	}
163 179
 	s.bearerAuth = token.AccessToken
@@ -182,7 +198,7 @@ func (s *SpotifyClient) SetupUserAuthenticate(redirectURL string) error {
182 198
 func (s *SpotifyClient) GetTrack(trackId string) (*TrackInfo, error) {
183 199
 	var trackInfo TrackInfo
184 200
 	url := s.urlFromBase(fmt.Sprintf("/v1/tracks/%s", trackId))
185
-	err := s.doRequest("GET", url, &trackInfo, nil)
201
+	err := s.doRequest("GET", url, &trackInfo, nil, nil)
186 202
 	if err != nil {
187 203
 		return nil, err
188 204
 	} else {
@@ -195,7 +211,7 @@ func (s *SpotifyClient) NewPlaylist(name, userId string) (*PlaylistInfo, error)
195 211
 	values := &url.Values{}
196 212
 	values.Add("name", name)
197 213
 	url := s.urlFromBase(fmt.Sprintf("/v1/users/%s/playlists", userId))
198
-	err := s.doRequest("POST", url, &playlistInfo, values)
214
+	err := s.doRequest("POST", url, &playlistInfo, values, nil)
199 215
 	if err != nil {
200 216
 		return nil, err
201 217
 	} else {
@@ -203,47 +219,89 @@ func (s *SpotifyClient) NewPlaylist(name, userId string) (*PlaylistInfo, error)
203 219
 	}
204 220
 }
205 221
 
222
+type Playlist struct {
223
+	Uris []string `json:"uris"`
224
+}
225
+
206 226
 func (s *SpotifyClient) UpdatePlaylist(userId, playlistId string, tracks []string) error {
207
-	values := &url.Values{}
208 227
 	uris := make([]string, 0)
209 228
 	for _, track := range tracks {
210 229
 		uris = append(uris, "spotify:track:"+track)
211 230
 	}
231
+	//playlist := &Playlist{uris}
232
+	values := &url.Values{}
212 233
 	values.Add("uris", strings.Join(uris, ","))
213 234
 
235
+	result := struct {
236
+		SnapshotID string `json:"snapshot_id"`
237
+	}{}
238
+
214 239
 	url := s.urlFromBase(fmt.Sprintf("/v1/users/%s/playlists/%s", userId, playlistId))
215
-	err := s.doRequest("PUT", url, nil, values)
240
+	err := s.doRequest("PUT", url, &result, values, nil)
216 241
 	return err
217 242
 }
218 243
 
244
+func (s *SpotifyClient) GetMe() (*SpotifyProfile, error) {
245
+	var profile SpotifyProfile
246
+	url := s.urlFromBase(fmt.Sprintf("/v1/me"))
247
+	err := s.doRequest("GET", url, &profile, nil, nil)
248
+	if err != nil {
249
+		return nil, err
250
+	} else {
251
+		return &profile, nil
252
+	}
253
+}
254
+
219 255
 func (s *SpotifyClient) urlFromBase(path string) *url.URL {
220 256
 	endpoint := &url.URL{Path: path}
221 257
 	return s.baseURL.ResolveReference(endpoint)
222 258
 }
223 259
 
224
-func (s *SpotifyClient) doRequest(method string, url *url.URL, object interface{}, values *url.Values) error {
260
+func (s *SpotifyClient) doRequest(method string, url *url.URL, object interface{}, values *url.Values, inputBody interface{}) error {
225 261
 	var bodyReader io.Reader
226
-	if values != nil {
262
+	var contentType string
263
+	if inputBody != nil {
264
+
265
+		//pr, pw := io.Pipe()
266
+		//go func() {
267
+		//	err := json.NewEncoder(pw).Encode(&inputBody)
268
+		//	pw.Close()
269
+		//	if err != nil {
270
+		//		log.Printf("Spotify: Encoding body failed: %s\n", err.Error())
271
+		//	}
272
+		//}()
273
+
274
+		var buf bytes.Buffer
275
+		err := json.NewEncoder(&buf).Encode(&inputBody)
276
+		log.Printf("Spotify: Body is '%s'\n", buf.String())
277
+		if err != nil {
278
+			log.Printf("Spotify: Encoding body failed: %s\n", err.Error())
279
+			return err
280
+		}
281
+
282
+		bodyReader = &buf
283
+		contentType = "application/json"
284
+	} else if values != nil {
227 285
 		s := values.Encode()
228 286
 		bodyReader = strings.NewReader(s)
287
+		contentType = "application/x-www-form-urlencoded"
229 288
 	}
230 289
 
231 290
 	req, err := http.NewRequest(method, url.String(), bodyReader)
232 291
 	if err != nil {
233 292
 		return err
234 293
 	}
294
+	req.Header.Set("Content-Type", contentType)
235 295
 
236 296
 	if s.bearerAuth != "" {
237 297
 		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.bearerAuth))
238 298
 	}
239 299
 
240
-	if values != nil {
241
-		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
242
-	}
243
-
244 300
 	req.Header.Set("Accept", "application/json")
245 301
 	req.Header.Set("User-Agent", "github.com/lamperi/e4bot/spotify")
246 302
 
303
+	log.Printf("Spotify: %s %s\n", method, url.Path)
304
+
247 305
 	resp, err := s.httpClient.Do(req)
248 306
 	if err != nil {
249 307
 		return err
@@ -256,9 +314,12 @@ func (s *SpotifyClient) doRequest(method string, url *url.URL, object interface{
256 314
 		return errors.New(fmt.Sprintf("Spotify API returned: %d: %s", resp.StatusCode, errorResp.Error.Message))
257 315
 	}
258 316
 
259
-	err = json.NewDecoder(resp.Body).Decode(object)
260
-	if err != nil {
261
-		return err
317
+	if object != nil {
318
+		log.Printf("Spotify: Trying to decode payload %s\n", reflect.TypeOf(object))
319
+		err = json.NewDecoder(resp.Body).Decode(object)
320
+		if err != nil {
321
+			return err
322
+		}
262 323
 	}
263 324
 	return nil
264 325
 
@@ -286,7 +347,7 @@ func (s *SpotifyClient) HandleToken(state string, r *http.Request) error {
286 347
 		return err
287 348
 	}
288 349
 
289
-	fmt.Println("Got token from Spotify", token)
350
+	log.Println("Got token from Spotify", token)
290 351
 
291 352
 	client := s.config.Client(s.context, token)
292 353
 	s.httpClient = client

+ 38 - 13
web.go View File

@@ -1,11 +1,12 @@
1 1
 package main
2 2
 
3 3
 import (
4
-	"github.com/lamperi/e4bot/spotify"
4
+	"github.com/zmb3/spotify"
5 5
 	"html/template"
6 6
 	"log"
7 7
 	"net/http"
8 8
 	"strings"
9
+	"time"
9 10
 )
10 11
 
11 12
 var (
@@ -20,8 +21,10 @@ func (s *WebService) IndexHandler(w http.ResponseWriter, r *http.Request) {
20 21
 
21 22
 	data := struct {
22 23
 		Username string
24
+		Spotify  string
23 25
 		Songs    []*Song
24 26
 	}{
27
+		"",
25 28
 		"",
26 29
 		make([]*Song, 0),
27 30
 	}
@@ -36,6 +39,7 @@ func (s *WebService) IndexHandler(w http.ResponseWriter, r *http.Request) {
36 39
 		}
37 40
 		data.Songs = songs
38 41
 		data.Username = session.username
42
+		data.Spotify = session.spotify
39 43
 	}
40 44
 
41 45
 	var templates = cachedTemplates
@@ -83,14 +87,22 @@ func (s *WebService) UpdateHandler(w http.ResponseWriter, r *http.Request) {
83 87
 	url := r.Form.Get("url")
84 88
 
85 89
 	if artist == "" && title == "" && url != "" {
86
-		log.Println("Resolving Spotify URL")
87
-		trackID := getTrackID(url)
88
-		err := s.spotifyClient.Authenticate()
90
+		if s.spotify.client == nil {
91
+			http.Error(w, "No Spotify token available", http.StatusInternalServerError)
92
+			return
93
+		}
94
+		token, err := s.spotify.client.Token()
89 95
 		if err != nil {
90 96
 			http.Error(w, err.Error(), http.StatusInternalServerError)
91 97
 			return
98
+		} else if token.Expiry.Before(time.Now()) {
99
+			http.Error(w, "Spotify token expired", http.StatusInternalServerError)
100
+			return
92 101
 		}
93
-		track, err := s.spotifyClient.GetTrack(trackID)
102
+
103
+		log.Println("Resolving Spotify URL")
104
+		trackID := getTrackID(url)
105
+		track, err := s.spotify.client.GetTrack(spotify.ID(trackID))
94 106
 
95 107
 		if err != nil {
96 108
 			http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -107,7 +119,7 @@ func (s *WebService) UpdateHandler(w http.ResponseWriter, r *http.Request) {
107 119
 			}
108 120
 		}
109 121
 		title = track.Name
110
-		url = track.ExternalUrls.Spotify
122
+		url = track.ExternalURLs["spotify"]
111 123
 	}
112 124
 
113 125
 	updated, err := s.db.UpdateEntry(session.username, round, artist, title, url)
@@ -150,28 +162,41 @@ func (s *WebService) LoginHandler(w http.ResponseWriter, r *http.Request) {
150 162
 }
151 163
 
152 164
 func (s *WebService) SpotifyHandler(w http.ResponseWriter, r *http.Request) {
153
-	url := s.spotifyClient.GetAuthUrl("foobar")
165
+	url := s.spotify.auth.AuthURL("foobar")
154 166
 	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
155 167
 }
156 168
 
157 169
 func (s *WebService) CallbackHandler(w http.ResponseWriter, r *http.Request) {
158 170
 	state := "foobar"
159
-	err := s.spotifyClient.HandleToken(state, r)
171
+	token, err := s.spotify.auth.Token(state, r)
160 172
 	if err != nil {
161 173
 		http.Error(w, "Couldn't get token", http.StatusForbidden)
162 174
 		return
163 175
 	}
176
+	session, err := getSession(r)
177
+	if err != nil {
178
+		http.Error(w, "Couldn't get session", http.StatusInternalServerError)
179
+		return
180
+	}
181
+	client := s.spotify.auth.NewClient(token)
182
+	s.spotify.client = &client
183
+	profile, err := s.spotify.client.CurrentUser()
184
+	if err != nil {
185
+		http.Error(w, "Couldn't load profile from Spotify", http.StatusInternalServerError)
186
+		return
187
+	}
188
+	session.spotify = profile.ID
164 189
 	http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
165 190
 }
166 191
 
167 192
 type WebService struct {
168
-	db            *DB
169
-	spotifyClient *spotify.SpotifyClient
170
-	noCache       bool
193
+	db      *DB
194
+	spotify *AppSpotify
195
+	noCache bool
171 196
 }
172 197
 
173
-func webStart(listenAddr string, db *DB, spotifyClient *spotify.SpotifyClient) {
174
-	service := &WebService{db, spotifyClient, true}
198
+func webStart(listenAddr string, db *DB, spotify *AppSpotify) {
199
+	service := &WebService{db, spotify, true}
175 200
 
176 201
 	mux := http.NewServeMux()
177 202
 

+ 5 - 0
web/index.html View File

@@ -33,6 +33,11 @@
33 33
 
34 34
     <div class="container">
35 35
         {{if .Username }}
36
+        {{if .Spotify}}
37
+        <p>You are logged to Spotify as <strong>{{ .Spotify }}</strong></p>
38
+        {{else}}
39
+        <a href="/spotify"><button style="margin-bottom: 1em">Login with Spotify</button></a>
40
+        {{end}}
36 41
         {{range .Songs }}
37 42
         <div class="row">
38 43
             <small style="width: 70px">{{ .RoundName }}</small>