Parcourir la source

Refactor to use instances

Toni Fadjukoff il y a 6 ans
Parent
révision
dcd28bbfa6
5 fichiers modifiés avec 227 ajouts et 211 suppressions
  1. 4 5
      auth.go
  2. 55 67
      bot.go
  3. 142 6
      db.go
  4. 0 47
      songs.go
  5. 26 86
      web.go

+ 4 - 5
auth.go Voir le fichier

@@ -118,9 +118,8 @@ func hashPasswordSalt(password, salt []byte) []byte {
118 118
 	return pbkdf2.Key(password, salt, 4096, 32, sha1.New)
119 119
 }
120 120
 
121
-func userOk(username, password string) (bool, error) {
122
-	var hash string
123
-	err := getDb().QueryRow("SELECT u.password FROM public.user u WHERE lower(u.username) = lower($1)", username).Scan(&hash)
121
+func userOk(db *DB, username, password string) (bool, error) {
122
+	hash, err := db.FindHashForUser(username)
124 123
 	if err != nil {
125 124
 		if err.Error() == "sql: no rows in result set" {
126 125
 			return false, nil
@@ -136,8 +135,8 @@ func userOk(username, password string) (bool, error) {
136 135
 	return ok, nil
137 136
 }
138 137
 
139
-func tryLogin(username, password string, longerTime bool) (http.Cookie, error) {
140
-	if exists, err := userOk(username, password); !exists {
138
+func tryLogin(db *DB, username, password string, longerTime bool) (http.Cookie, error) {
139
+	if exists, err := userOk(db, username, password); !exists {
141 140
 		if err != nil {
142 141
 			return http.Cookie{}, err
143 142
 		}

+ 55 - 67
bot.go Voir le fichier

@@ -3,6 +3,7 @@ package main
3 3
 import (
4 4
 	"encoding/json"
5 5
 	"errors"
6
+	"flag"
6 7
 	"fmt"
7 8
 	"github.com/jasonlvhit/gocron"
8 9
 	"github.com/lamperi/e4bot/spotify"
@@ -15,9 +16,12 @@ import (
15 16
 	"time"
16 17
 )
17 18
 
18
-var songs SongPriorityQueue
19
+type App struct {
20
+	db          *DB
21
+	credentials Credentials
22
+}
19 23
 
20
-var credentials struct {
24
+type Credentials struct {
21 25
 	APIURL              string
22 26
 	UserName            string
23 27
 	Password            string
@@ -26,6 +30,18 @@ var credentials struct {
26 30
 	ListenAddr          string
27 31
 }
28 32
 
33
+func (app *App) CreateSpotifyClient() *spotify.SpotifyClient {
34
+	spotifyClient := spotify.NewClient(app.credentials.SpotifyClientID, app.credentials.SpotifyClientSecret)
35
+	return spotifyClient
36
+}
37
+
38
+func (app *App) LaunchWeb() {
39
+	spotifyClient := app.CreateSpotifyClient()
40
+	go func() {
41
+		webStart(app.credentials.ListenAddr, app.db, spotifyClient)
42
+	}()
43
+}
44
+
29 45
 func appendSong(wikiText string, author string, newSong string) string {
30 46
 	const AUTHOR_MARK = "<!-- Lisääjä -->"
31 47
 	const SONG_MARK = "<!-- Kappale -->"
@@ -46,8 +62,13 @@ func appendSong(wikiText string, author string, newSong string) string {
46 62
 	return strings.Join(changedLines, "\n")
47 63
 }
48 64
 
49
-func addSong(updateTitle, updateSection, author, song string) (bool, error) {
50
-	wiki := CreateWikiClient(credentials.APIURL, credentials.UserName, credentials.Password)
65
+func (app *App) wikiClient() *WikiClient {
66
+	wiki := CreateWikiClient(app.credentials.APIURL, app.credentials.UserName, app.credentials.Password)
67
+	return wiki
68
+}
69
+
70
+func (app *App) AddSong(updateTitle, updateSection, author, song string) (bool, error) {
71
+	wiki := app.wikiClient()
51 72
 
52 73
 	sections, err := wiki.GetWikiPageSections(updateTitle)
53 74
 	if err != nil {
@@ -76,69 +97,34 @@ func addSong(updateTitle, updateSection, author, song string) (bool, error) {
76 97
 	return false, errors.New("Could not find matching section")
77 98
 }
78 99
 
79
-func songSynced(userId, roundId int) error {
80
-	query := `UPDATE public.entry SET synced = true WHERE user_id = $1 AND round_id = $2`
81
-	res, err := getDb().Exec(query, userId, roundId)
82
-
83
-	if err != nil {
84
-		return err
85
-	}
86
-
87
-	affected, err := res.RowsAffected()
88
-	if err != nil {
89
-		return err
90
-	}
91
-
92
-	if affected != 1 {
93
-		return errors.New("Unknown entry ID")
94
-	}
95
-	return nil
100
+func (app *App) SongSynced(userId, roundId int) error {
101
+	_, err := app.db.EntrySynced(userId, roundId)
102
+	return err
96 103
 }
97 104
 
98
-func submitSong() {
99
-	query := `
100
-	SELECT e.user_id, e.round_id, e.artist, e.title, e.spotify_url, p.article, u.username, r.section
101
-	FROM public.entry e 
102
-	JOIN public."user" u ON u.id = e.user_id
103
-	JOIN public.round r ON r.id = e.round_id
104
-	JOIN public.panel p ON p.id = r.panel_id
105
-	WHERE r.start < current_timestamp AND e.synced = false`
106
-	rows, err := getDb().Query(query)
105
+func (app *App) SubmitSongs() {
106
+	entries, err := app.db.FindEntriesToSync()
107 107
 	if err != nil {
108
-		log.Println("Error while reading songs from database:", err)
108
+		log.Println("Error while finding entries to sync:", err)
109 109
 		return
110 110
 	}
111
-	defer rows.Close()
112
-	for rows.Next() {
113
-		var (
114
-			userId, roundId                                       int
115
-			artist, title, spotifyURL, article, username, section string
116
-		)
117
-		err := rows.Scan(&userId, &roundId, &artist, &title, &spotifyURL, &article, &username, &section)
118
-		if err != nil {
119
-			log.Println("Error while scanning row:", err)
120
-			continue
121
-		}
122
-		song := songWikiText(spotifyURL, artist, title)
111
+
112
+	for _, entry := range entries {
113
+		song := songWikiText(entry.spotifyURL, entry.artist, entry.title)
123 114
 
124 115
 		fmt.Println("Time has passed for " + song)
125 116
 
126
-		success, err := addSong(article, section, username, song)
117
+		success, err := app.AddSong(entry.article, entry.section, entry.username, song)
127 118
 		if err != nil {
128 119
 			log.Println("Error while adding song:", err)
129 120
 		}
130 121
 		if success {
131
-			err = songSynced(userId, roundId)
122
+			err = app.SongSynced(entry.userId, entry.roundId)
132 123
 			if err != nil {
133 124
 				fmt.Println("Error received:", err)
134 125
 			}
135 126
 		}
136 127
 	}
137
-	err = rows.Err()
138
-	if err != nil {
139
-		log.Println("Error after reading cursor:", err)
140
-		return
141
-	}
142 128
 }
143 129
 
144 130
 func isCurrentAuthor(line, author string) bool {
@@ -229,8 +215,8 @@ func appendAverages(wikiText string) string {
229 215
 	return strings.Join(changedLines, "\n")
230 216
 }
231 217
 
232
-func fixAverages(title string) error {
233
-	wiki := CreateWikiClient(credentials.APIURL, credentials.UserName, credentials.Password)
218
+func (app *App) FixAverages(title string) error {
219
+	wiki := app.wikiClient()
234 220
 
235 221
 	sections, err := wiki.GetWikiPageSections(title)
236 222
 	if err != nil {
@@ -270,23 +256,28 @@ func fixAverages(title string) error {
270 256
 	return nil
271 257
 }
272 258
 
273
-func fixAveragesTask() {
274
-	err := fixAverages("Levyraati 2018")
259
+func (app *App) FixAveragesTask() {
260
+	err := app.FixAverages("Levyraati 2018")
275 261
 	if err != nil {
276 262
 		fmt.Println("Error while calculating averages:", err)
277 263
 	}
278 264
 }
279 265
 
280
-func initCreds() error {
281
-	f, err := os.Open("credentials.json")
266
+func initCreds() (Credentials, error) {
267
+	var credsFile string
268
+	flag.StringVar(&credsFile, "credentials", "credentials.json", "JSON config to hold app credentials")
269
+	flag.Parse()
270
+
271
+	var credentials Credentials
272
+	f, err := os.Open(credsFile)
282 273
 	if err != nil {
283
-		return err
274
+		return credentials, err
284 275
 	}
285 276
 	defer f.Close()
286 277
 
287 278
 	if err != nil {
288 279
 		log.Fatal(err)
289
-		return err
280
+		return credentials, err
290 281
 	}
291 282
 	dec := json.NewDecoder(f)
292 283
 	for {
@@ -296,7 +287,7 @@ func initCreds() error {
296 287
 			log.Fatal(err)
297 288
 		}
298 289
 	}
299
-	return nil
290
+	return credentials, nil
300 291
 }
301 292
 
302 293
 func songWikiText(url string, artist string, title string) string {
@@ -304,19 +295,16 @@ func songWikiText(url string, artist string, title string) string {
304 295
 }
305 296
 
306 297
 func main() {
307
-	err := initCreds()
298
+	creds, err := initCreds()
308 299
 	if err != nil {
309 300
 		panic(err)
310 301
 	}
311 302
 
312
-	spotifyClient := spotify.NewClient(credentials.SpotifyClientID, credentials.SpotifyClientSecret)
313
-	go func() {
314
-		webStart(credentials.ListenAddr, spotifyClient)
315
-	}()
316
-
317
-	gocron.Every(1).Hour().Do(fixAveragesTask)
303
+	a := App{InitDatabase(), creds}
304
+	a.LaunchWeb()
318 305
 
319
-	gocron.Every(1).Second().Do(submitSong)
306
+	gocron.Every(1).Hour().Do(a.FixAveragesTask)
307
+	gocron.Every(1).Second().Do(a.SubmitSongs)
320 308
 	<-gocron.Start()
321 309
 
322 310
 }

+ 142 - 6
db.go Voir le fichier

@@ -2,18 +2,19 @@ package main
2 2
 
3 3
 import (
4 4
 	"database/sql"
5
+	"errors"
5 6
 	_ "github.com/lib/pq"
6 7
 	"log"
7 8
 )
8 9
 
9
-var (
10
+type DB struct {
10 11
 	database *sql.DB
11
-)
12
+}
12 13
 
13
-func init() {
14
+func InitDatabase() *DB {
14 15
 	connStr := "user=levyraati dbname=levyraati sslmode=disable"
15 16
 	var err error
16
-	database, err = sql.Open("postgres", connStr)
17
+	database, err := sql.Open("postgres", connStr)
17 18
 	if err != nil {
18 19
 		log.Fatal(err)
19 20
 	}
@@ -21,8 +22,143 @@ func init() {
21 22
 	if err != nil {
22 23
 		log.Fatal(err)
23 24
 	}
25
+	return &DB{database}
26
+}
27
+
28
+func (db *DB) FindHashForUser(username string) (string, error) {
29
+	var hash string
30
+	err := db.database.QueryRow("SELECT u.password FROM public.user u WHERE lower(u.username) = lower($1)", username).Scan(&hash)
31
+	return hash, err
32
+}
33
+
34
+func (db *DB) EntrySynced(userId, roundId int) (bool, error) {
35
+	query := `UPDATE public.entry SET synced = true WHERE user_id = $1 AND round_id = $2`
36
+	res, err := db.database.Exec(query, userId, roundId)
37
+
38
+	if err != nil {
39
+		return false, err
40
+	}
41
+
42
+	affected, err := res.RowsAffected()
43
+	if err != nil {
44
+		return false, err
45
+	}
46
+
47
+	if affected != 1 {
48
+		return false, errors.New("Unknown entry ID")
49
+	}
50
+	return true, nil
51
+
52
+}
53
+
54
+func (db *DB) FindEntriesToSync() ([]*EntryToSync, error) {
55
+	query := `
56
+	SELECT e.user_id, e.round_id, e.artist, e.title, e.spotify_url, p.article, u.username, r.section
57
+	FROM public.entry e 
58
+	JOIN public."user" u ON u.id = e.user_id
59
+	JOIN public.round r ON r.id = e.round_id
60
+	JOIN public.panel p ON p.id = r.panel_id
61
+	WHERE r.start < current_timestamp AND e.synced = false`
62
+	rows, err := db.database.Query(query)
63
+
64
+	if err != nil {
65
+		log.Println("Error while reading songs from database:", err)
66
+		return nil, err
67
+	}
68
+	defer rows.Close()
69
+	var entries []*EntryToSync
70
+	for rows.Next() {
71
+		var (
72
+			userId, roundId                                       int
73
+			artist, title, spotifyURL, article, username, section string
74
+		)
75
+		err := rows.Scan(&userId, &roundId, &artist, &title, &spotifyURL, &article, &username, &section)
76
+		if err != nil {
77
+			log.Println("Error while scanning row:", err)
78
+			return nil, err
79
+		}
80
+		entries = append(entries, &EntryToSync{userId, roundId, artist, title, spotifyURL, article, username, section})
81
+	}
82
+	err = rows.Err()
83
+	if err != nil {
84
+		log.Println("Error after reading cursor:", err)
85
+		return nil, err
86
+	}
87
+	return entries, nil
88
+}
89
+
90
+type EntryToSync struct {
91
+	userId, roundId                                       int
92
+	artist, title, spotifyURL, article, username, section string
24 93
 }
25 94
 
26
-func getDb() *sql.DB {
27
-	return database
95
+func (db *DB) FindAllEntries(username string) ([]*Song, error) {
96
+	var songs []*Song
97
+	query := `
98
+		SELECT r.id, r.section, e.artist, e.title, e.spotify_url, e.synced
99
+		FROM public.round r
100
+		LEFT JOIN public.entry e ON r.id = e.round_id
101
+		LEFT JOIN public."user" u ON u.id = e.user_id AND lower(u.username) = lower($1)
102
+		ORDER BY r.start ASC`
103
+	rows, err := db.database.Query(query, username)
104
+	if err != nil {
105
+		return nil, err
106
+	}
107
+	defer rows.Close()
108
+	for i := 0; rows.Next(); i++ {
109
+		song := &Song{}
110
+		songs = append(songs, song)
111
+		var (
112
+			artist, title, url *string
113
+			sync               *bool
114
+		)
115
+		err = rows.Scan(&songs[i].RoundID, &songs[i].RoundName, &artist, &title, &url, &sync)
116
+		if err != nil {
117
+			return nil, err
118
+		}
119
+		if artist != nil {
120
+			song.Artist = *artist
121
+		}
122
+		if title != nil {
123
+			song.Title = *title
124
+		}
125
+		if url != nil {
126
+			song.URL = *url
127
+		}
128
+		if sync != nil {
129
+			song.Sync = *sync
130
+		}
131
+	}
132
+	return songs, nil
133
+}
134
+
135
+type Song struct {
136
+	RoundID   int
137
+	RoundName string
138
+	Title     string
139
+	Artist    string
140
+	URL       string
141
+	Sync      bool
142
+}
143
+
144
+func (db *DB) UpdateEntry(username, round, artist, title, url string) (bool, error) {
145
+	query := `
146
+	INSERT INTO public.entry
147
+	SELECT id, $2, $3, $4, $5, false
148
+	FROM public."user" u
149
+	WHERE lower(u.username) = lower($1)
150
+	ON CONFLICT (user_id, round_id) DO UPDATE SET artist = EXCLUDED.artist, title = EXCLUDED.title, spotify_url = EXCLUDED.spotify_url, synced = EXCLUDED.synced`
151
+	res, err := db.database.Exec(query, username, round, artist, title, url)
152
+
153
+	if err != nil {
154
+		return false, err
155
+	}
156
+	affected, err := res.RowsAffected()
157
+	if err != nil {
158
+		return false, err
159
+	}
160
+	if affected != 1 {
161
+		return false, nil
162
+	}
163
+	return true, nil
28 164
 }

+ 0 - 47
songs.go Voir le fichier

@@ -1,47 +0,0 @@
1
-package main
2
-
3
-import (
4
-	"time"
5
-)
6
-
7
-// From https://golang.org/pkg/container/heap/
8
-
9
-type Song struct {
10
-	time  time.Time
11
-	song  string
12
-	week  int
13
-	sync  bool
14
-	index int
15
-}
16
-
17
-type SongPriorityQueue []*Song
18
-
19
-func (pq SongPriorityQueue) Len() int {
20
-	return len(pq)
21
-}
22
-
23
-func (pq SongPriorityQueue) Less(i, j int) bool {
24
-	return pq[i].time.Before(pq[j].time)
25
-
26
-}
27
-func (pq SongPriorityQueue) Swap(i, j int) {
28
-	pq[i], pq[j] = pq[j], pq[i]
29
-	pq[i].index = i
30
-	pq[j].index = j
31
-}
32
-
33
-func (pq *SongPriorityQueue) Push(x interface{}) {
34
-	n := len(*pq)
35
-	item := x.(*Song)
36
-	item.index = n
37
-	*pq = append(*pq, item)
38
-}
39
-
40
-func (pq *SongPriorityQueue) Pop() interface{} {
41
-	old := *pq
42
-	n := len(old)
43
-	item := old[n-1]
44
-	item.index = -1 // for safety
45
-	*pq = old[0 : n-1]
46
-	return item
47
-}

+ 26 - 86
web.go Voir le fichier

@@ -10,10 +10,9 @@ import (
10 10
 
11 11
 var (
12 12
 	cachedTemplates = template.Must(template.ParseFiles("web/index.html"))
13
-	spotifyClient   *spotify.SpotifyClient
14 13
 )
15 14
 
16
-func indexHandler(w http.ResponseWriter, r *http.Request) {
15
+func (s *WebService) IndexHandler(w http.ResponseWriter, r *http.Request) {
17 16
 	if r.URL.Path != "/" {
18 17
 		http.Error(w, "Not Found", http.StatusNotFound)
19 18
 		return
@@ -21,79 +20,26 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
21 20
 
22 21
 	data := struct {
23 22
 		Username string
24
-		Songs    []*struct {
25
-			RoundID   int
26
-			RoundName string
27
-			Title     string
28
-			Artist    string
29
-			URL       string
30
-			Sync      bool
31
-		}
23
+		Songs    []*Song
32 24
 	}{
33 25
 		"",
34
-		make([]*struct {
35
-			RoundID   int
36
-			RoundName string
37
-			Title     string
38
-			Artist    string
39
-			URL       string
40
-			Sync      bool
41
-		}, 0),
26
+		make([]*Song, 0),
42 27
 	}
43 28
 
44 29
 	session, err := getSession(r)
45 30
 	if session != nil {
46
-		query := `
47
-		SELECT r.id, r.section, e.artist, e.title, e.spotify_url, e.synced
48
-		FROM public.round r
49
-		LEFT JOIN public.entry e ON r.id = e.round_id
50
-		LEFT JOIN public."user" u ON u.id = e.user_id AND lower(u.username) = lower($1)
51
-		ORDER BY r.start ASC`
52
-		rows, err := getDb().Query(query, session.username)
31
+		songs, err := s.db.FindAllEntries(session.username)
53 32
 		if err != nil {
54
-			log.Println("Error while executing query", err)
33
+			log.Println("Error while reading entries from database", err)
55 34
 			http.Error(w, err.Error(), http.StatusInternalServerError)
56 35
 			return
57 36
 		}
58
-		defer rows.Close()
59
-		for i := 0; rows.Next(); i++ {
60
-			row := &struct {
61
-				RoundID   int
62
-				RoundName string
63
-				Title     string
64
-				Artist    string
65
-				URL       string
66
-				Sync      bool
67
-			}{}
68
-			data.Songs = append(data.Songs, row)
69
-			var (
70
-				artist, title, url *string
71
-				sync               *bool
72
-			)
73
-			err = rows.Scan(&data.Songs[i].RoundID, &data.Songs[i].RoundName, &artist, &title, &url, &sync)
74
-			if err != nil {
75
-				log.Println("Error while scanning cursor", err)
76
-				http.Error(w, err.Error(), http.StatusInternalServerError)
77
-				return
78
-			}
79
-			if artist != nil {
80
-				row.Artist = *artist
81
-			}
82
-			if title != nil {
83
-				row.Title = *title
84
-			}
85
-			if url != nil {
86
-				row.URL = *url
87
-			}
88
-			if sync != nil {
89
-				row.Sync = *sync
90
-			}
91
-		}
37
+		data.Songs = songs
92 38
 		data.Username = session.username
93 39
 	}
94 40
 
95 41
 	var templates = cachedTemplates
96
-	if true {
42
+	if s.noCache {
97 43
 		templates = template.Must(template.ParseFiles("web/index.html"))
98 44
 	}
99 45
 
@@ -115,7 +61,7 @@ func getTrackID(url string) string {
115 61
 	return url
116 62
 }
117 63
 
118
-func updateHandler(w http.ResponseWriter, r *http.Request) {
64
+func (s *WebService) UpdateHandler(w http.ResponseWriter, r *http.Request) {
119 65
 	if r.URL.Path != "/update" {
120 66
 		http.Error(w, "Forbidden", http.StatusForbidden)
121 67
 		return
@@ -139,12 +85,12 @@ func updateHandler(w http.ResponseWriter, r *http.Request) {
139 85
 	if artist == "" && title == "" && url != "" {
140 86
 		log.Println("Resolving Spotify URL")
141 87
 		trackID := getTrackID(url)
142
-		err := spotifyClient.Authenticate()
88
+		err := s.spotifyClient.Authenticate()
143 89
 		if err != nil {
144 90
 			http.Error(w, err.Error(), http.StatusInternalServerError)
145 91
 			return
146 92
 		}
147
-		track, err := spotifyClient.GetTrack(trackID)
93
+		track, err := s.spotifyClient.GetTrack(trackID)
148 94
 
149 95
 		if err != nil {
150 96
 			http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -164,25 +110,13 @@ func updateHandler(w http.ResponseWriter, r *http.Request) {
164 110
 		url = track.ExternalUrls.Spotify
165 111
 	}
166 112
 
167
-	query := `
168
-	INSERT INTO public.entry
169
-	SELECT id, $2, $3, $4, $5, false
170
-	FROM public."user" u
171
-	WHERE lower(u.username) = lower($1)
172
-	ON CONFLICT (user_id, round_id) DO UPDATE SET artist = EXCLUDED.artist, title = EXCLUDED.title, spotify_url = EXCLUDED.spotify_url, synced = EXCLUDED.synced`
173
-	res, err := getDb().Exec(query, session.username, round, artist, title, url)
174
-
113
+	updated, err := s.db.UpdateEntry(session.username, round, artist, title, url)
175 114
 	if err != nil {
176 115
 		http.Error(w, err.Error(), http.StatusInternalServerError)
177 116
 		return
178 117
 	}
179
-	affected, err := res.RowsAffected()
180
-	if err != nil {
181
-		http.Error(w, err.Error(), http.StatusInternalServerError)
182
-		return
183
-	}
184
-	if affected != 1 {
185
-		http.Error(w, err.Error(), http.StatusInternalServerError)
118
+	if !updated {
119
+		http.Error(w, "No round in DB", http.StatusNotFound)
186 120
 		return
187 121
 	}
188 122
 
@@ -192,7 +126,7 @@ func updateHandler(w http.ResponseWriter, r *http.Request) {
192 126
 	http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
193 127
 }
194 128
 
195
-func loginHandler(w http.ResponseWriter, r *http.Request) {
129
+func (s *WebService) LoginHandler(w http.ResponseWriter, r *http.Request) {
196 130
 	err := r.ParseForm()
197 131
 	if err != nil {
198 132
 		http.Error(w, err.Error(), http.StatusBadRequest)
@@ -203,7 +137,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
203 137
 	remember := r.Form.Get("remember")
204 138
 	longer := remember == "remember-me"
205 139
 
206
-	cookie, err := tryLogin(username, password, longer)
140
+	cookie, err := tryLogin(s.db, username, password, longer)
207 141
 
208 142
 	if err != nil {
209 143
 		log.Println("Error while trying to login", err.Error())
@@ -215,8 +149,14 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
215 149
 	http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
216 150
 }
217 151
 
218
-func webStart(listenAddr string, spot *spotify.SpotifyClient) {
219
-	spotifyClient = spot
152
+type WebService struct {
153
+	db            *DB
154
+	spotifyClient *spotify.SpotifyClient
155
+	noCache       bool
156
+}
157
+
158
+func webStart(listenAddr string, db *DB, spotifyClient *spotify.SpotifyClient) {
159
+	service := &WebService{db, spotifyClient, true}
220 160
 
221 161
 	mux := http.NewServeMux()
222 162
 
@@ -225,9 +165,9 @@ func webStart(listenAddr string, spot *spotify.SpotifyClient) {
225 165
 	mux.Handle("/fonts/", fs)
226 166
 	mux.Handle("/js/", fs)
227 167
 	mux.Handle("/favicon.ico", fs)
228
-	mux.HandleFunc("/", indexHandler)
229
-	mux.HandleFunc("/update", updateHandler)
230
-	mux.HandleFunc("/login", loginHandler)
168
+	mux.HandleFunc("/", service.IndexHandler)
169
+	mux.HandleFunc("/update", service.UpdateHandler)
170
+	mux.HandleFunc("/login", service.LoginHandler)
231 171
 
232 172
 	http.ListenAndServe(listenAddr, mux)
233 173
 }