Browse Source

Add login with Spotify, current playlist support

Toni Fadjukoff 6 years ago
parent
commit
7d3a7b5928
6 changed files with 359 additions and 71 deletions
  1. 42 7
      auth.go
  2. 60 25
      bot.go
  3. 71 8
      db.go
  4. 115 28
      web.go
  5. 5 3
      web/index.html
  6. 66 0
      web/round.html

+ 42 - 7
auth.go View File

@@ -7,6 +7,7 @@ import (
7 7
 	"crypto/sha1"
8 8
 	"encoding/base64"
9 9
 	"errors"
10
+	"github.com/zmb3/spotify"
10 11
 	"golang.org/x/crypto/pbkdf2"
11 12
 	"log"
12 13
 	"net/http"
@@ -15,11 +16,12 @@ import (
15 16
 )
16 17
 
17 18
 type SessionData struct {
18
-	sid      string
19
-	username string
20
-	spotify  string
21
-	priority time.Time
22
-	index    int
19
+	sid           string
20
+	username      string
21
+	spotify       string
22
+	priority      time.Time
23
+	index         int
24
+	spotifyClient *spotify.Client
23 25
 }
24 26
 
25 27
 type SessionQueue []*SessionData
@@ -119,6 +121,18 @@ func hashPasswordSalt(password, salt []byte) []byte {
119 121
 	return pbkdf2.Key(password, salt, 4096, 32, sha1.New)
120 122
 }
121 123
 
124
+func spotifyOk(db *DB, spotifyID string) (string, error) {
125
+	username, err := db.FindUserBySpotifyID(spotifyID)
126
+	if err != nil {
127
+		if err.Error() == "sql: no rows in result set" {
128
+			return "", nil
129
+		} else {
130
+			return "", err
131
+		}
132
+	}
133
+	return username, nil
134
+}
135
+
122 136
 func userOk(db *DB, username, password string) (bool, error) {
123 137
 	hash, err := db.FindHashForUser(username)
124 138
 	if err != nil {
@@ -136,6 +150,19 @@ func userOk(db *DB, username, password string) (bool, error) {
136 150
 	return ok, nil
137 151
 }
138 152
 
153
+func tryLoginWithSpotify(db *DB, spotifyID string) (http.Cookie, error) {
154
+	username, err := spotifyOk(db, spotifyID)
155
+	if username == "" {
156
+		if err != nil {
157
+			return http.Cookie{}, err
158
+		}
159
+		return http.Cookie{},
160
+			errors.New("This spotify account is not connected to any account")
161
+	}
162
+
163
+	return addSessionAndReturnMatchingCookie(username, spotifyID, true)
164
+}
165
+
139 166
 func tryLogin(db *DB, username, password string, longerTime bool) (http.Cookie, error) {
140 167
 	if exists, err := userOk(db, username, password); !exists {
141 168
 		if err != nil {
@@ -145,6 +172,10 @@ func tryLogin(db *DB, username, password string, longerTime bool) (http.Cookie,
145 172
 			errors.New("The username or password you entered isn't correct.")
146 173
 	}
147 174
 
175
+	return addSessionAndReturnMatchingCookie(username, "", longerTime)
176
+}
177
+
178
+func addSessionAndReturnMatchingCookie(username, spotifyID string, longerTime bool) (http.Cookie, error) {
148 179
 	sid, err := randString(32)
149 180
 	if err != nil {
150 181
 		return http.Cookie{}, err
@@ -163,9 +194,10 @@ func tryLogin(db *DB, username, password string, longerTime bool) (http.Cookie,
163 194
 	}
164 195
 
165 196
 	expiration := time.Now().Add(duration)
166
-	addSession(&SessionData{sid, username, "", expiration, 0})
197
+	addSession(&SessionData{sid, username, spotifyID, expiration, 0, nil})
167 198
 
168 199
 	return loginCookie, nil
200
+
169 201
 }
170 202
 
171 203
 func getSession(req *http.Request) (*SessionData, error) {
@@ -173,8 +205,11 @@ func getSession(req *http.Request) (*SessionData, error) {
173 205
 	if err != nil {
174 206
 		return nil, err
175 207
 	}
208
+	return getSessionById(cookie.Value)
209
+}
176 210
 
177
-	session, exists := sessions[cookie.Value]
211
+func getSessionById(sid string) (*SessionData, error) {
212
+	session, exists := sessions[sid]
178 213
 	if !exists {
179 214
 		return nil, errors.New("Session expired from server")
180 215
 	}

+ 60 - 25
bot.go View File

@@ -308,7 +308,7 @@ func (app *App) AutomateSection(title string) error {
308 308
 		weekStr := numberReg.FindString(section.title)
309 309
 		if weekStr != "" {
310 310
 			weekNumber, _ := strconv.Atoi(weekStr)
311
-			if weekNumber < currentWeek-1 {
311
+			if weekNumber < currentWeek-4 {
312 312
 				continue
313 313
 			}
314 314
 			if weekNumber > currentWeek {
@@ -340,37 +340,32 @@ func (app *App) AutomateSection(title string) error {
340 340
 						log.Println("Failed to find tracks from DB", err)
341 341
 					}
342 342
 					log.Println("Checking if playlist needs updating", currentTracks, tracks, err)
343
-					if len(tracks) > 0 && (err != nil || !tracksEqual(tracks, currentTracks)) {
344
-
345
-						if playlistID == "" {
346
-							log.Println("Creating new playlist")
347
-							info, err := app.spotify.client.CreatePlaylistForUser(app.credentials.SpotifyUser, title+" "+section.title, true)
348
-							if err != nil {
349
-								log.Println("Error creating playlist to Spotify")
350
-								return err
351
-							}
352
-							playlistID = info.ID
353
-							changedWikiText = appendPlaylist(changedWikiText, info)
354
-							message = message + fmt.Sprintf("Added link to Spotify playlist for week %d.", weekNumber)
355
-						}
356
-						log.Printf("Updating playlist %s for %s with tracks %s\n", playlistID, app.credentials.SpotifyUser, tracks)
357
-						err := app.spotify.client.ReplacePlaylistTracks(app.credentials.SpotifyUser, playlistID, tracks...)
343
+					if app.ShouldUpdate(tracks, currentTracks, err != nil) {
344
+						playlistID, changedWikiText, message, err = app.CreatePlaylist(title+" "+section.title, weekNumber, playlistID, changedWikiText, message)
358 345
 						if err != nil {
359
-							log.Println("Error updating playlist to Spotify")
360 346
 							return err
361 347
 						}
362
-
363
-						stringTracks := make([]string, len(tracks))
364
-						for i, t := range tracks {
365
-							stringTracks[i] = string(t)
366
-						}
367
-
368
-						_, err = app.db.UpdatePlaylistBySection(section.title, stringTracks)
348
+						err = app.UpdatePlaylist(section.title, playlistID, tracks, currentTracks)
369 349
 						if err != nil {
370
-							log.Println("Error updating playlist to DB")
371 350
 							return err
372 351
 						}
373 352
 					}
353
+					if weekNumber == currentWeek {
354
+						// This playlist is same for every week
355
+						// So people don't have to find the new ones from wiki
356
+						playlistID = "4piDy2DQ35Y43yUaInyWfN"
357
+						currentTracks, err = app.db.FindPlaylistBySection("current week")
358
+						if err != nil {
359
+							log.Println("Failed to find current week tracks from DB", err)
360
+						}
361
+						log.Println("Checking if playlist needs updating", currentTracks, tracks, err)
362
+						if app.ShouldUpdate(tracks, currentTracks, err != nil) {
363
+							err = app.UpdatePlaylist("current week", playlistID, tracks, currentTracks)
364
+							if err != nil {
365
+								return err
366
+							}
367
+						}
368
+					}
374 369
 				}
375 370
 			}
376 371
 
@@ -388,6 +383,46 @@ func (app *App) AutomateSection(title string) error {
388 383
 	return nil
389 384
 }
390 385
 
386
+func (app *App) ShouldUpdate(tracks []spotify.ID, currentTracks []string, isNew bool) bool {
387
+	return len(tracks) > 0 && (isNew || !tracksEqual(tracks, currentTracks))
388
+}
389
+
390
+func (app *App) CreatePlaylist(title string, weekNumber int, playlistID spotify.ID, changedWikiText, message string) (spotify.ID, string, string, error) {
391
+	if playlistID == "" {
392
+		log.Println("Creating new playlist")
393
+		info, err := app.spotify.client.CreatePlaylistForUser(app.credentials.SpotifyUser, title, true)
394
+		if err != nil {
395
+			log.Println("Error creating playlist to Spotify")
396
+			return "", "", "", err
397
+		}
398
+		playlistID = info.ID
399
+		changedWikiText = appendPlaylist(changedWikiText, info)
400
+		message = message + fmt.Sprintf("Added link to Spotify playlist for week %d.", weekNumber)
401
+	}
402
+	return playlistID, changedWikiText, message, nil
403
+}
404
+
405
+func (app *App) UpdatePlaylist(section string, playlistID spotify.ID, tracks []spotify.ID, currentTracks []string) error {
406
+	log.Printf("Updating playlist %s for %s with tracks %s\n", playlistID, app.credentials.SpotifyUser, tracks)
407
+	err := app.spotify.client.ReplacePlaylistTracks(app.credentials.SpotifyUser, playlistID, tracks...)
408
+	if err != nil {
409
+		log.Println("Error updating playlist to Spotify")
410
+		return err
411
+	}
412
+
413
+	stringTracks := make([]string, len(tracks))
414
+	for i, t := range tracks {
415
+		stringTracks[i] = string(t)
416
+	}
417
+
418
+	_, err = app.db.UpdatePlaylistBySection(section, stringTracks)
419
+	if err != nil {
420
+		log.Println("Error updating playlist to DB")
421
+		return err
422
+	}
423
+	return nil
424
+}
425
+
391 426
 func (app *App) AutomateSectionTask() {
392 427
 	panels, err := app.db.FindAllPanels()
393 428
 	if err != nil {

+ 71 - 8
db.go View File

@@ -7,6 +7,7 @@ import (
7 7
 	_ "github.com/lib/pq"
8 8
 	"log"
9 9
 	"os"
10
+	"time"
10 11
 )
11 12
 
12 13
 type DB struct {
@@ -34,6 +35,12 @@ func InitDatabase() *DB {
34 35
 	return &DB{database}
35 36
 }
36 37
 
38
+func (db *DB) FindUserBySpotifyID(username string) (string, error) {
39
+	var user string
40
+	err := db.database.QueryRow("SELECT u.username FROM public.user u WHERE u.spotify_id = $1", username).Scan(&user)
41
+	return user, err
42
+}
43
+
37 44
 func (db *DB) FindHashForUser(username string) (string, error) {
38 45
 	var hash string
39 46
 	err := db.database.QueryRow("SELECT u.password FROM public.user u WHERE lower(u.username) = lower($1)", username).Scan(&hash)
@@ -67,7 +74,7 @@ func (db *DB) FindEntriesToSync() ([]*EntryToSync, error) {
67 74
 	JOIN public."user" u ON u.id = e.user_id
68 75
 	JOIN public.round r ON r.id = e.round_id
69 76
 	JOIN public.panel p ON p.id = r.panel_id
70
-	WHERE r.start < current_timestamp AND e.synced = false`
77
+	WHERE r.start < current_timestamp AND e.synced = false AND p.sync_enabled = true`
71 78
 	rows, err := db.database.Query(query)
72 79
 
73 80
 	if err != nil {
@@ -101,27 +108,79 @@ type EntryToSync struct {
101 108
 	artist, title, spotifyURL, article, username, section string
102 109
 }
103 110
 
111
+func (db *DB) FindRoundInfo(roundNum int) (*RoundInfo, error) {
112
+	query := `
113
+		SELECT r.section, p.name, p.article, r.start, r.end
114
+		FROM public.round r
115
+                JOIN public.panel p ON p.id = r.panel_id
116
+		WHERE r.id = $1`
117
+	rows, err := db.database.Query(query, roundNum)
118
+	if err != nil {
119
+		return nil, err
120
+	}
121
+	defer rows.Close()
122
+	var (
123
+		section, panelName, panelArticle string
124
+		start, end                       *time.Time
125
+	)
126
+	if !rows.Next() {
127
+		return nil, nil
128
+	}
129
+	err = rows.Scan(&section, &panelName, &panelArticle, &start, &end)
130
+	if err != nil {
131
+		log.Println("Error while scanning row:", err)
132
+		return nil, err
133
+	}
134
+	return &RoundInfo{section, panelName, panelArticle, start, end}, nil
135
+}
136
+
137
+type RoundInfo struct {
138
+	Section, PanelName, PanelArticle string
139
+	Start, End                       *time.Time
140
+}
141
+
142
+func (db *DB) FindAllRoundEntries(round int) ([]*Song, error) {
143
+	query := `
144
+		SELECT r.id, r.section, e.artist, e.title, e.spotify_url, e.synced, u.username
145
+		FROM public.round r
146
+                JOIN public.panel p ON p.id = r.panel_id
147
+		JOIN public.entry e ON r.id = e.round_id
148
+		JOIN public."user" u ON u.id = e.user_id
149
+		WHERE r.id = $1`
150
+	rows, err := db.database.Query(query, round)
151
+	if err != nil {
152
+		return nil, err
153
+	}
154
+	defer rows.Close()
155
+	return db.rowsToSong(rows)
156
+}
157
+
104 158
 func (db *DB) FindAllEntries(username string) ([]*Song, error) {
105
-	var songs []*Song
106 159
 	query := `
107
-		SELECT r.id, r.section, e.artist, e.title, e.spotify_url, e.synced
160
+		SELECT r.id, r.section, e.artist, e.title, e.spotify_url, e.synced, u.username
108 161
 		FROM public.round r
109
-		LEFT JOIN public.entry e ON r.id = e.round_id
110
-		LEFT JOIN public."user" u ON u.id = e.user_id AND lower(u.username) = lower($1)
162
+		LEFT JOIN public.entry e ON r.id = e.round_id AND e.user_id = 
163
+                  (SELECT u2.id FROM public."user" u2 WHERE lower(u2.username) = lower($1))
164
+		LEFT JOIN public."user" u ON r.id = e.round_id AND u.id = e.user_id
111 165
 		ORDER BY r.start ASC`
112 166
 	rows, err := db.database.Query(query, username)
113 167
 	if err != nil {
114 168
 		return nil, err
115 169
 	}
116 170
 	defer rows.Close()
171
+	return db.rowsToSong(rows)
172
+}
173
+
174
+func (db *DB) rowsToSong(rows *sql.Rows) ([]*Song, error) {
175
+	var songs []*Song
117 176
 	for i := 0; rows.Next(); i++ {
118 177
 		song := &Song{}
119 178
 		songs = append(songs, song)
120 179
 		var (
121
-			artist, title, url *string
122
-			sync               *bool
180
+			artist, title, url, username *string
181
+			sync                         *bool
123 182
 		)
124
-		err = rows.Scan(&songs[i].RoundID, &songs[i].RoundName, &artist, &title, &url, &sync)
183
+		err := rows.Scan(&songs[i].RoundID, &songs[i].RoundName, &artist, &title, &url, &sync, &username)
125 184
 		if err != nil {
126 185
 			return nil, err
127 186
 		}
@@ -137,6 +196,9 @@ func (db *DB) FindAllEntries(username string) ([]*Song, error) {
137 196
 		if sync != nil {
138 197
 			song.Sync = *sync
139 198
 		}
199
+		if username != nil {
200
+			song.Submitter = *username
201
+		}
140 202
 	}
141 203
 	return songs, nil
142 204
 }
@@ -148,6 +210,7 @@ type Song struct {
148 210
 	Artist    string
149 211
 	URL       string
150 212
 	Sync      bool
213
+	Submitter string
151 214
 }
152 215
 
153 216
 func (db *DB) UpdateEntry(username, round, artist, title, url string) (bool, error) {

+ 115 - 28
web.go View File

@@ -5,14 +5,29 @@ import (
5 5
 	"html/template"
6 6
 	"log"
7 7
 	"net/http"
8
+	"strconv"
8 9
 	"strings"
9 10
 	"time"
10 11
 )
11 12
 
12 13
 var (
13
-	cachedTemplates = template.Must(template.ParseFiles("web/index.html"))
14
+	cachedTemplates = template.Must(template.ParseFiles("web/index.html", "web/round.html"))
14 15
 )
15 16
 
17
+func (s *WebService) serveTemplate(w http.ResponseWriter, data interface{}, templateName string) {
18
+	var templates = cachedTemplates
19
+	if s.noCache {
20
+		templates = template.Must(template.ParseFiles("web/index.html", "web/round.html"))
21
+	}
22
+
23
+	err := templates.ExecuteTemplate(w, templateName, data)
24
+	if err != nil {
25
+		log.Println("Error while rendering template", err)
26
+		http.Error(w, err.Error(), http.StatusInternalServerError)
27
+	}
28
+
29
+}
30
+
16 31
 func (s *WebService) IndexHandler(w http.ResponseWriter, r *http.Request) {
17 32
 	if r.URL.Path != "/" {
18 33
 		http.Error(w, "Not Found", http.StatusNotFound)
@@ -31,8 +46,9 @@ func (s *WebService) IndexHandler(w http.ResponseWriter, r *http.Request) {
31 46
 		make([]*Song, 0),
32 47
 	}
33 48
 
34
-	session, err := getSession(r)
49
+	session, _ := getSession(r)
35 50
 	if session != nil {
51
+		log.Println("Find all songs for user", session.username)
36 52
 		songs, err := s.db.FindAllEntries(session.username)
37 53
 		if err != nil {
38 54
 			log.Println("Error while reading entries from database", err)
@@ -42,25 +58,15 @@ func (s *WebService) IndexHandler(w http.ResponseWriter, r *http.Request) {
42 58
 		data.Songs = songs
43 59
 		data.Username = session.username
44 60
 		data.Spotify = session.spotify
45
-	}
46 61
 
47
-	if s.spotify.client != nil {
48
-		token, err := s.spotify.client.Token()
49
-		if err == nil {
50
-			data.SpotifyExpiry = token.Expiry.Format(time.RFC822Z)
62
+		if session.spotifyClient != nil {
63
+			token, err := session.spotifyClient.Token()
64
+			if err == nil {
65
+				data.SpotifyExpiry = token.Expiry.Format(time.RFC822Z)
66
+			}
51 67
 		}
52 68
 	}
53
-
54
-	var templates = cachedTemplates
55
-	if s.noCache {
56
-		templates = template.Must(template.ParseFiles("web/index.html"))
57
-	}
58
-
59
-	err = templates.ExecuteTemplate(w, "index.html", data)
60
-	if err != nil {
61
-		log.Println("Error while rendering template", err)
62
-		http.Error(w, err.Error(), http.StatusInternalServerError)
63
-	}
69
+	s.serveTemplate(w, data, "index.html")
64 70
 }
65 71
 
66 72
 func getTrackID(url string) string {
@@ -96,11 +102,15 @@ func (s *WebService) UpdateHandler(w http.ResponseWriter, r *http.Request) {
96 102
 	url := r.Form.Get("url")
97 103
 
98 104
 	if artist == "" && title == "" && url != "" {
99
-		if s.spotify.client == nil {
105
+		var client = s.spotify.client
106
+		if client == nil {
107
+			client = session.spotifyClient
108
+		}
109
+		if client == nil {
100 110
 			http.Error(w, "No Spotify token available", http.StatusInternalServerError)
101 111
 			return
102 112
 		}
103
-		token, err := s.spotify.client.Token()
113
+		token, err := client.Token()
104 114
 		if err != nil {
105 115
 			http.Error(w, err.Error(), http.StatusInternalServerError)
106 116
 			return
@@ -111,7 +121,7 @@ func (s *WebService) UpdateHandler(w http.ResponseWriter, r *http.Request) {
111 121
 
112 122
 		log.Println("Resolving Spotify URL")
113 123
 		trackID := getTrackID(url)
114
-		track, err := s.spotify.client.GetTrack(spotify.ID(trackID))
124
+		track, err := client.GetTrack(spotify.ID(trackID))
115 125
 
116 126
 		if err != nil {
117 127
 			http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -182,20 +192,96 @@ func (s *WebService) CallbackHandler(w http.ResponseWriter, r *http.Request) {
182 192
 		http.Error(w, "Couldn't get token", http.StatusForbidden)
183 193
 		return
184 194
 	}
195
+	client := s.spotify.auth.NewClient(token)
196
+	profile, err := client.CurrentUser()
197
+	if err != nil {
198
+		http.Error(w, "Couldn't load profile from Spotify", http.StatusInternalServerError)
199
+		return
200
+	}
185 201
 	session, err := getSession(r)
202
+	if session == nil {
203
+		if err != nil {
204
+			log.Println("Error while trying to read current session", err)
205
+		}
206
+		log.Println("Trying to login with spotify ID", profile.ID)
207
+		cookie, err := tryLoginWithSpotify(s.db, profile.ID)
208
+		if err != nil {
209
+			http.Error(w, "Couldn't login with Spotify profile", http.StatusInternalServerError)
210
+			return
211
+		}
212
+		session, _ := getSessionById(cookie.Value)
213
+		session.spotifyClient = &client
214
+
215
+		log.Println("Logged in user spotify ID", profile.ID)
216
+		http.SetCookie(w, &cookie)
217
+		http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
218
+		return
219
+	} else {
220
+		session.spotify = profile.ID
221
+		session.spotifyClient = &client
222
+		http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
223
+		return
224
+	}
225
+	// TODO: when?
226
+	if profile.ID == "lamperi" {
227
+		s.spotify.client = &client
228
+	}
229
+}
230
+
231
+func (s *WebService) RoundHandler(w http.ResponseWriter, r *http.Request) {
232
+	data := struct {
233
+		Username        string
234
+		Spotify         string
235
+		SpotifyExpiry   string
236
+		Songs           []*Song
237
+		SubmitterPublic bool
238
+		RoundInfo       *RoundInfo
239
+	}{
240
+		"",
241
+		"",
242
+		"",
243
+		make([]*Song, 0),
244
+		false,
245
+		nil,
246
+	}
247
+
248
+	round := strings.TrimPrefix(r.URL.Path, "/round/")
249
+	roundNum, err := strconv.Atoi(round)
186 250
 	if err != nil {
187
-		http.Error(w, "Couldn't get session", http.StatusInternalServerError)
251
+		http.Error(w, err.Error(), http.StatusBadRequest)
188 252
 		return
189 253
 	}
190
-	client := s.spotify.auth.NewClient(token)
191
-	s.spotify.client = &client
192
-	profile, err := s.spotify.client.CurrentUser()
254
+	roundInfo, err := s.db.FindRoundInfo(roundNum)
193 255
 	if err != nil {
194
-		http.Error(w, "Couldn't load profile from Spotify", http.StatusInternalServerError)
256
+		http.Error(w, err.Error(), http.StatusInternalServerError)
195 257
 		return
196 258
 	}
197
-	session.spotify = profile.ID
198
-	http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
259
+	if roundInfo == nil {
260
+		http.Error(w, "No such panel", http.StatusNotFound)
261
+		return
262
+	}
263
+	data.RoundInfo = roundInfo
264
+	session, err := getSession(r)
265
+	if session != nil {
266
+		data.Username = session.username
267
+		data.Spotify = session.spotify
268
+	}
269
+	songs, err := s.db.FindAllRoundEntries(roundNum)
270
+	if err != nil {
271
+		log.Println("Error while reading entries from database", err)
272
+		http.Error(w, err.Error(), http.StatusInternalServerError)
273
+		return
274
+	}
275
+	data.Songs = songs
276
+	data.SubmitterPublic = roundInfo.End.Before(time.Now())
277
+
278
+	if s.spotify.client != nil {
279
+		token, err := s.spotify.client.Token()
280
+		if err == nil {
281
+			data.SpotifyExpiry = token.Expiry.Format(time.RFC822Z)
282
+		}
283
+	}
284
+	s.serveTemplate(w, data, "round.html")
199 285
 }
200 286
 
201 287
 type WebService struct {
@@ -215,6 +301,7 @@ func webStart(listenAddr string, db *DB, spotify *AppSpotify) {
215 301
 	mux.Handle("/js/", fs)
216 302
 	mux.Handle("/favicon.ico", fs)
217 303
 	mux.HandleFunc("/", service.IndexHandler)
304
+	mux.HandleFunc("/round/", service.RoundHandler)
218 305
 	mux.HandleFunc("/update", service.UpdateHandler)
219 306
 	mux.HandleFunc("/login", service.LoginHandler)
220 307
 	mux.HandleFunc("/spotify", service.SpotifyHandler)

+ 5 - 3
web/index.html View File

@@ -40,7 +40,7 @@
40 40
         {{end}}
41 41
         {{range .Songs }}
42 42
         <div class="row">
43
-            <small style="width: 70px">{{ .RoundName }}</small>
43
+            <small style="width: 70px"><a href="/round/{{ .RoundID }}">{{ .RoundName }}</a></small>
44 44
             <form class="form-inline" method="post" action="/update">
45 45
                 <label class="sr-only" for="artist">Artist</label>
46 46
                 <input class="form-control mb-2 mr-sm-2" name="artist" placeholder="Enter Artist"  value="{{ .Artist }}">
@@ -49,7 +49,7 @@
49 49
                 <input class="form-control mb-2 mr-sm-2" name="title" placeholder="Enter Track title"  value="{{ .Title }}">
50 50
                 
51 51
                 <label class="sr-only" for="url">Track URL</label>
52
-                <input class="form-control mb-2 mr-sm-2" name="url" placeholder="Enter Track title" value="{{ .URL }}">
52
+                <input class="form-control mb-2 mr-sm-2" name="url" placeholder="Enter Track URL" value="{{ .URL }}">
53 53
                 
54 54
                 <div class="form-check mb-2 mr-sm-2">
55 55
                     <input class="form-check-input" style="width: 100px" type="checkbox" name="synced" {{ if .Sync }} checked {{ else }} disabled {{ end }}>
@@ -76,12 +76,14 @@
76 76
             </div>
77 77
             <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
78 78
         </form>
79
+        <hr/>
80
+        <a href="/spotify"><button class="btn btn-lg btn-primary btn-block" style="margin-bottom: 1em">Sign in with Spotify</button></a>
79 81
     {{end}}
80 82
 
81 83
       <hr>
82 84
 
83 85
       <footer>
84
-        <p>&copy; Lamperi 2018</p>
86
+        <p>&copy; Lamperi 2018-2019</p>
85 87
       </footer>
86 88
     </div> <!-- /container -->
87 89
 

+ 66 - 0
web/round.html View File

@@ -0,0 +1,66 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+  <head>
4
+    <meta charset="utf-8">
5
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6
+    <meta name="description" content="">
7
+    <meta name="author" content="">
8
+    <link rel="icon" href="favicon.ico">
9
+
10
+    <title>LevyraatiBot</title>
11
+
12
+    <base href="..">
13
+    <!-- Bootstrap core CSS -->
14
+    <link href="css/bootstrap.min.css" rel="stylesheet">
15
+
16
+    <!-- Custom styles for this template -->
17
+    <link href="css/jumbotron.css" rel="stylesheet">
18
+  </head>
19
+
20
+  <body>
21
+
22
+    <!-- Main jumbotron for a primary marketing message or call to action -->
23
+    <div class="jumbotron">
24
+      <div class="container">
25
+        {{if .RoundInfo}}
26
+        <h1 class="display-3">{{ .RoundInfo.PanelName }} {{ .RoundInfo.Section }}</h1>
27
+        {{if or (gt (len .Songs) 3) .SubmitterPublic }}
28
+        <p>Tässä viikon kattaus, olkaa hyvä</p>
29
+        {{else}}
30
+        <p>Odotetaan vielä muutamaa laulua.</p>
31
+        {{end}}
32
+        {{else}}
33
+        <h1 class="display-3">Please log in</h1>
34
+        <p>Login now.</p>
35
+        {{end}}
36
+      </div>
37
+    </div>
38
+
39
+    <div class="container">
40
+        {{if or (gt (len .Songs) 3) .SubmitterPublic }}
41
+	{{ $submitterPublic := .SubmitterPublic }}
42
+        {{range .Songs }}
43
+        <div class="row">
44
+                <span>{{ .Artist }} / {{ .Title }} / <a href="{{ .URL }}">Kuuntele</a>
45
+    		{{ if $submitterPublic }}({{ .Submitter }}){{ end }}
46
+        </div>
47
+        {{end}}
48
+        {{else}}
49
+	<p>Vasta {{ len .Songs }} laulu{{if ne (len .Songs) 1}}a{{end}} lisätty.</p>
50
+        {{end}}
51
+      <hr>
52
+
53
+      <footer>
54
+        <p>&copy; Lamperi 2018-2019</p>
55
+      </footer>
56
+    </div> <!-- /container -->
57
+
58
+
59
+    <!-- Bootstrap core JavaScript
60
+    ================================================== -->
61
+    <!-- Placed at the end of the document so the pages load faster -->
62
+    <script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
63
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
64
+    <script src="js/vendor/bootstrap.min.js"></script>
65
+  </body>
66
+</html>