Browse Source

Convert to postgresql backend

Toni Fadjukoff 6 years ago
parent
commit
f3411137f9
6 changed files with 220 additions and 216 deletions
  1. 1 15
      auth.go
  2. 60 104
      bot.go
  3. 15 40
      db.go
  4. 59 1
      sql/database.sql
  5. 83 54
      web.go
  6. 2 2
      web/index.html

+ 1 - 15
auth.go View File

@@ -5,10 +5,8 @@ import (
5 5
 	"container/heap"
6 6
 	"crypto/rand"
7 7
 	"crypto/sha1"
8
-	"database/sql"
9 8
 	"encoding/base64"
10 9
 	"errors"
11
-	_ "github.com/lib/pq"
12 10
 	"golang.org/x/crypto/pbkdf2"
13 11
 	"log"
14 12
 	"net/http"
@@ -58,7 +56,6 @@ func (pq *SessionQueue) Pop() interface{} {
58 56
 var (
59 57
 	sessions     = make(map[string]*SessionData)
60 58
 	sessionQueue = make(SessionQueue, 0)
61
-	db           *sql.DB
62 59
 )
63 60
 
64 61
 func sessionExpirer() {
@@ -73,17 +70,6 @@ func sessionExpirer() {
73 70
 
74 71
 func init() {
75 72
 	go sessionExpirer()
76
-
77
-	connStr := "user=levyraati dbname=levyraati sslmode=disable"
78
-	var err error
79
-	db, err = sql.Open("postgres", connStr)
80
-	if err != nil {
81
-		log.Fatal(err)
82
-	}
83
-	_, err = db.Query("SELECT 1")
84
-	if err != nil {
85
-		log.Fatal(err)
86
-	}
87 73
 }
88 74
 
89 75
 func addSession(data *SessionData) {
@@ -134,7 +120,7 @@ func hashPasswordSalt(password, salt []byte) []byte {
134 120
 
135 121
 func userOk(username, password string) (bool, error) {
136 122
 	var hash string
137
-	err := db.QueryRow("SELECT u.password FROM public.user u WHERE lower(u.username) = lower($1)", username).Scan(&hash)
123
+	err := getDb().QueryRow("SELECT u.password FROM public.user u WHERE lower(u.username) = lower($1)", username).Scan(&hash)
138 124
 	if err != nil {
139 125
 		if err.Error() == "sql: no rows in result set" {
140 126
 			return false, nil

+ 60 - 104
bot.go View File

@@ -1,7 +1,6 @@
1 1
 package main
2 2
 
3 3
 import (
4
-	"container/heap"
5 4
 	"encoding/json"
6 5
 	"errors"
7 6
 	"fmt"
@@ -47,97 +46,99 @@ func appendSong(wikiText string, author string, newSong string) string {
47 46
 	return strings.Join(changedLines, "\n")
48 47
 }
49 48
 
50
-func addSong(title string, week int, author string, song string) (bool, error) {
49
+func addSong(updateTitle, updateSection, author, song string) (bool, error) {
51 50
 	wiki := CreateWikiClient(credentials.APIURL, credentials.UserName, credentials.Password)
52 51
 
53
-	sections, err := wiki.GetWikiPageSections(title)
52
+	sections, err := wiki.GetWikiPageSections(updateTitle)
54 53
 	if err != nil {
55 54
 		return false, err
56 55
 	}
57 56
 
58
-	numberReg, _ := regexp.Compile("\\d+")
59 57
 	for _, section := range sections {
60
-		weekStr := numberReg.FindString(section.title)
61
-		if weekStr != "" {
62
-			weekNumber, _ := strconv.Atoi(weekStr)
63
-			if weekNumber == week {
64
-				wikiText, err := wiki.GetWikiPageSectionText(title, section.index)
65
-				if err != nil {
66
-					return false, err
67
-				}
68
-				changedWikiText := appendSong(wikiText, author, song)
58
+		if updateSection == section.title {
59
+			wikiText, err := wiki.GetWikiPageSectionText(updateTitle, section.index)
60
+			if err != nil {
61
+				return false, err
62
+			}
63
+			changedWikiText := appendSong(wikiText, author, song)
69 64
 
70
-				return wiki.EditWikiPageSection(title, section.index, changedWikiText,
71
-					fmt.Sprintf("Added week %d song for %s", week, author))
65
+			if false {
66
+				// Stub
67
+				fmt.Println("Pretend to update wiki text to ", updateTitle, section.index, changedWikiText,
68
+					fmt.Sprintf("Added %s song for %s", updateSection, author))
69
+				return true, nil
72 70
 			}
71
+
72
+			return wiki.EditWikiPageSection(updateTitle, section.index, changedWikiText,
73
+				fmt.Sprintf("Added %s song for %s", updateSection, author))
73 74
 		}
74 75
 	}
75 76
 	return false, errors.New("Could not find matching section")
76 77
 }
77 78
 
78
-func songSynced(syncedWeek int) error {
79
-	v, err := loadDb()
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
+
80 83
 	if err != nil {
81 84
 		return err
82 85
 	}
83 86
 
84
-	synced := false
85
-	for _, song := range v.Songs {
86
-		if song.Week == syncedWeek {
87
-			song.Sync = true
88
-			synced = true
89
-			err := saveDb(v)
90
-			if err != nil {
91
-				return err
92
-			}
93
-		}
87
+	affected, err := res.RowsAffected()
88
+	if err != nil {
89
+		return err
94 90
 	}
95
-	if !synced {
96
-		return errors.New("No week matched from JSON for synced song")
91
+
92
+	if affected != 1 {
93
+		return errors.New("Unknown entry ID")
97 94
 	}
98
-	return errors.New("Week not found")
95
+	return nil
99 96
 }
100 97
 
101
-func getSongs() (SongPriorityQueue, error) {
102
-	dbSongs, err := loadDb()
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)
103 107
 	if err != nil {
104
-		return nil, err
108
+		log.Println("Error while reading songs from database:", err)
109
+		return
105 110
 	}
106
-
107
-	songs := make(SongPriorityQueue, len(dbSongs.Songs))
108
-	for index, songObj := range dbSongs.Songs {
109
-		songs[index] = &Song{
110
-			time:  targetTime(songObj),
111
-			song:  songEntryWikiText(songObj),
112
-			week:  songObj.Week,
113
-			sync:  songObj.Sync,
114
-			index: index,
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
115 121
 		}
116
-	}
117
-	heap.Init(&songs)
118
-	for len(songs) > 0 && songs[0].sync {
119
-		heap.Pop(&songs)
120
-	}
121
-	return songs, nil
122
-}
122
+		song := songWikiText(spotifyURL, artist, title)
123 123
 
124
-func submitSong() {
125
-	now := time.Now()
126
-	if len(songs) > 0 && songs[0].time.Before(now) {
127
-		fmt.Println("Time has passed for " + songs[0].song)
128
-		success, err := addSong("Levyraati 2018", songs[0].week, "Lamperi", songs[0].song)
124
+		fmt.Println("Time has passed for " + song)
125
+
126
+		success, err := addSong(article, section, username, song)
129 127
 		if err != nil {
130 128
 			log.Println("Error while adding song:", err)
131 129
 		}
132 130
 		if success {
133
-			err := songSynced(songs[0].week)
134
-			if err == nil {
135
-				heap.Pop(&songs)
136
-			} else {
131
+			err = songSynced(userId, roundId)
132
+			if err != nil {
137 133
 				fmt.Println("Error received:", err)
138 134
 			}
139 135
 		}
140 136
 	}
137
+	err = rows.Err()
138
+	if err != nil {
139
+		log.Println("Error after reading cursor:", err)
140
+		return
141
+	}
141 142
 }
142 143
 
143 144
 func isCurrentAuthor(line, author string) bool {
@@ -298,16 +299,6 @@ func initCreds() error {
298 299
 	return nil
299 300
 }
300 301
 
301
-func targetTime(entry *SongEntry) time.Time {
302
-	yearStart, _ := time.Parse(time.RFC3339, "2018-01-01T00:00:00+02:00")
303
-	target := yearStart.AddDate(0, 0, (entry.Week-1)*7)
304
-	return target
305
-}
306
-
307
-func songEntryWikiText(entry *SongEntry) string {
308
-	return songWikiText(entry.URL, entry.Artist, entry.Title)
309
-}
310
-
311 302
 func songWikiText(url string, artist string, title string) string {
312 303
 	return "[" + url + " " + artist + " - " + title + "]"
313 304
 }
@@ -317,45 +308,10 @@ func main() {
317 308
 	if err != nil {
318 309
 		panic(err)
319 310
 	}
320
-	songs, err = getSongs()
321
-	if err != nil {
322
-		panic(err)
323
-	}
324
-
325
-	modifiedSongChan := make(chan *SongEntry)
326 311
 
327 312
 	spotifyClient := spotify.NewClient(credentials.SpotifyClientID, credentials.SpotifyClientSecret)
328 313
 	go func() {
329
-
330
-		webStart(credentials.ListenAddr, modifiedSongChan, spotifyClient)
331
-	}()
332
-
333
-	go func() {
334
-		for {
335
-			newSong := <-modifiedSongChan
336
-			matched := false
337
-			for _, song := range songs {
338
-				if song.week == newSong.Week {
339
-					song.song = songEntryWikiText(newSong)
340
-					song.sync = newSong.Sync
341
-					matched = true
342
-					log.Printf("Updated song for week %d, artist: %s, title: %s, URL: %s, time: %v",
343
-						newSong.Week, newSong.Artist, newSong.Title, newSong.URL, song.time)
344
-				}
345
-			}
346
-			if !matched {
347
-				song := &Song{
348
-					time:  targetTime(newSong),
349
-					song:  songEntryWikiText(newSong),
350
-					week:  newSong.Week,
351
-					sync:  newSong.Sync,
352
-					index: len(songs),
353
-				}
354
-				heap.Push(&songs, song)
355
-				log.Printf("Added song for week %d, artist: %s, title: %s, URL: %s, time: %v",
356
-					newSong.Week, newSong.Artist, newSong.Title, newSong.URL, song.time)
357
-			}
358
-		}
314
+		webStart(credentials.ListenAddr, spotifyClient)
359 315
 	}()
360 316
 
361 317
 	gocron.Every(1).Hour().Do(fixAveragesTask)

+ 15 - 40
db.go View File

@@ -1,53 +1,28 @@
1 1
 package main
2 2
 
3 3
 import (
4
-	"encoding/json"
5
-	"io"
6
-	"io/ioutil"
7
-	"os"
4
+	"database/sql"
5
+	_ "github.com/lib/pq"
6
+	"log"
8 7
 )
9 8
 
10
-type SongsFile struct {
11
-	Songs []*SongEntry
12
-}
13
-
14
-type SongEntry struct {
15
-	Week   int
16
-	Title  string
17
-	Artist string
18
-	URL    string
19
-	Sync   bool
20
-}
21
-
22
-func loadDb() (SongsFile, error) {
9
+var (
10
+	database *sql.DB
11
+)
23 12
 
24
-	var v SongsFile
25
-	f, err := os.Open("songs.json")
13
+func init() {
14
+	connStr := "user=levyraati dbname=levyraati sslmode=disable"
15
+	var err error
16
+	database, err = sql.Open("postgres", connStr)
26 17
 	if err != nil {
27
-		return v, err
18
+		log.Fatal(err)
28 19
 	}
29
-	defer f.Close()
30
-
20
+	_, err = database.Query("SELECT 1")
31 21
 	if err != nil {
32
-		return v, err
33
-	}
34
-	dec := json.NewDecoder(f)
35
-	dec.UseNumber()
36
-	for {
37
-		if err := dec.Decode(&v); err == io.EOF {
38
-			break
39
-		} else if err != nil {
40
-			return v, err
41
-		}
22
+		log.Fatal(err)
42 23
 	}
43
-	return v, nil
44 24
 }
45 25
 
46
-func saveDb(v SongsFile) error {
47
-	jsonValue, err := json.Marshal(v)
48
-	if err != nil {
49
-		return err
50
-	}
51
-	err = ioutil.WriteFile("songs.json", jsonValue, 0644)
52
-	return err
26
+func getDb() *sql.DB {
27
+	return database
53 28
 }

+ 59 - 1
sql/database.sql View File

@@ -16,4 +16,62 @@ CREATE TABLE public."user"
16 16
 
17 17
 CREATE UNIQUE INDEX user_lower_idx
18 18
     ON public."user"
19
-    (lower(username));
19
+    (lower(username));
20
+
21
+-- Table: public.panel
22
+
23
+-- DROP TABLE public.panel;
24
+
25
+CREATE TABLE public.panel
26
+(
27
+    id serial,
28
+    name text NOT NULL,
29
+    article text NOT NULL,
30
+    CONSTRAINT panel_pkey PRIMARY KEY (id)
31
+);
32
+
33
+-- Table: public.user_panel
34
+
35
+-- DROP TABLE public.user_panel;
36
+
37
+CREATE TABLE public.user_panel
38
+(
39
+    user_id integer NOT NULL,
40
+    panel_id integer NOT NULL,
41
+    CONSTRAINT user_panel_pkey PRIMARY KEY (user_id, panel_id),
42
+    CONSTRAINT panel_id FOREIGN KEY (user_id)
43
+        REFERENCES public.panel (id),
44
+    CONSTRAINT user_id FOREIGN KEY (user_id)
45
+        REFERENCES public."user" (id)
46
+);
47
+
48
+-- Table: public.round
49
+
50
+-- DROP TABLE public.round;
51
+
52
+CREATE TABLE public.round
53
+(
54
+    id serial,
55
+    start timestamp with time zone NOT NULL,
56
+    "end" timestamp with time zone NOT NULL,
57
+    panel_id integer NOT NULL,
58
+    section text NOT NULL,
59
+    CONSTRAINT round_pkey PRIMARY KEY (id),
60
+    CONSTRAINT panel_id FOREIGN KEY (panel_id)
61
+        REFERENCES public.panel (id)
62
+);
63
+
64
+-- Table: public.entry
65
+
66
+-- DROP TABLE public.entry;
67
+
68
+CREATE TABLE public.entry
69
+(
70
+    user_id integer NOT NULL,
71
+    round_id integer NOT NULL,
72
+    artist text,
73
+    title text,
74
+    spotify_url text,
75
+    synced boolean NOT NULL DEFAULT false,
76
+    CONSTRAINT entry_pkey PRIMARY KEY (user_id, round_id)
77
+);

+ 83 - 54
web.go View File

@@ -5,13 +5,11 @@ import (
5 5
 	"html/template"
6 6
 	"log"
7 7
 	"net/http"
8
-	"strconv"
9 8
 	"strings"
10 9
 )
11 10
 
12 11
 var (
13 12
 	cachedTemplates = template.Must(template.ParseFiles("web/index.html"))
14
-	songsChan       chan *SongEntry
15 13
 	spotifyClient   *spotify.SpotifyClient
16 14
 )
17 15
 
@@ -21,24 +19,77 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
21 19
 		return
22 20
 	}
23 21
 
24
-	alsoEmptySongs := SongsFile{
25
-		Songs: make([]*SongEntry, 52),
26
-	}
27
-	for week := 1; week <= 52; week++ {
28
-		alsoEmptySongs.Songs[week-1] = &SongEntry{Week: week}
22
+	data := struct {
23
+		Username string
24
+		Songs    []*struct {
25
+			RoundID   int
26
+			RoundName string
27
+			Title     string
28
+			Artist    string
29
+			URL       string
30
+			Sync      bool
31
+		}
32
+	}{
33
+		"",
34
+		make([]*struct {
35
+			RoundID   int
36
+			RoundName string
37
+			Title     string
38
+			Artist    string
39
+			URL       string
40
+			Sync      bool
41
+		}, 0),
29 42
 	}
30
-	username := ""
43
+
31 44
 	session, err := getSession(r)
32 45
 	if session != nil {
33
-		songs, err := loadDb()
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)
34 53
 		if err != nil {
54
+			log.Println("Error while executing query", err)
35 55
 			http.Error(w, err.Error(), http.StatusInternalServerError)
36 56
 			return
37 57
 		}
38
-		for _, song := range songs.Songs {
39
-			alsoEmptySongs.Songs[song.Week-1] = song
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
+			}
40 91
 		}
41
-		username = session.username
92
+		data.Username = session.username
42 93
 	}
43 94
 
44 95
 	var templates = cachedTemplates
@@ -46,15 +97,9 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
46 97
 		templates = template.Must(template.ParseFiles("web/index.html"))
47 98
 	}
48 99
 
49
-	data := struct {
50
-		Username string
51
-		Songs    []*SongEntry
52
-	}{
53
-		username,
54
-		alsoEmptySongs.Songs,
55
-	}
56 100
 	err = templates.ExecuteTemplate(w, "index.html", data)
57 101
 	if err != nil {
102
+		log.Println("Error while rendering template", err)
58 103
 		http.Error(w, err.Error(), http.StatusInternalServerError)
59 104
 	}
60 105
 }
@@ -86,15 +131,10 @@ func updateHandler(w http.ResponseWriter, r *http.Request) {
86 131
 		http.Error(w, err.Error(), http.StatusBadRequest)
87 132
 		return
88 133
 	}
89
-	week, err := strconv.Atoi(r.Form.Get("week"))
90
-	if err != nil {
91
-		http.Error(w, "week parameter not provided", http.StatusBadRequest)
92
-		return
93
-	}
134
+	round := r.Form.Get("round")
94 135
 	artist := r.Form.Get("artist")
95 136
 	title := r.Form.Get("title")
96 137
 	url := r.Form.Get("url")
97
-	sync := r.Form.Get("sync")
98 138
 
99 139
 	if artist == "" && title == "" && url != "" {
100 140
 		log.Println("Resolving Spotify URL")
@@ -124,41 +164,31 @@ func updateHandler(w http.ResponseWriter, r *http.Request) {
124 164
 		url = track.ExternalUrls.Spotify
125 165
 	}
126 166
 
127
-	songs, err := loadDb()
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
+
128 175
 	if err != nil {
129 176
 		http.Error(w, err.Error(), http.StatusInternalServerError)
130 177
 		return
131 178
 	}
132
-
133
-	matched := false
134
-	for _, song := range songs.Songs {
135
-		if song.Week == week {
136
-			song.Artist = artist
137
-			song.Title = title
138
-			song.URL = url
139
-			if song.Sync && sync != "on" {
140
-				song.Sync = false
141
-			}
142
-			songsChan <- song
143
-			matched = true
144
-		}
145
-	}
146
-	if !matched {
147
-		song := &SongEntry{
148
-			Artist: artist,
149
-			Title:  title,
150
-			URL:    url,
151
-			Week:   week,
152
-		}
153
-		songs.Songs = append(songs.Songs, song)
154
-		songsChan <- song
155
-	}
156
-
157
-	err = saveDb(songs)
179
+	affected, err := res.RowsAffected()
158 180
 	if err != nil {
159 181
 		http.Error(w, err.Error(), http.StatusInternalServerError)
160 182
 		return
161 183
 	}
184
+	if affected != 1 {
185
+		http.Error(w, err.Error(), http.StatusInternalServerError)
186
+		return
187
+	}
188
+
189
+	log.Printf("Updated song for round %s, artist: %s, title: %s, URL: %s",
190
+		round, artist, title, url)
191
+
162 192
 	http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
163 193
 }
164 194
 
@@ -185,8 +215,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
185 215
 	http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
186 216
 }
187 217
 
188
-func webStart(listenAddr string, modifiedSongChan chan *SongEntry, spot *spotify.SpotifyClient) {
189
-	songsChan = modifiedSongChan
218
+func webStart(listenAddr string, spot *spotify.SpotifyClient) {
190 219
 	spotifyClient = spot
191 220
 
192 221
 	mux := http.NewServeMux()

+ 2 - 2
web/index.html View File

@@ -35,7 +35,7 @@
35 35
         {{if .Username }}
36 36
         {{range .Songs }}
37 37
         <div class="row">
38
-            <small style="width: 70px">Week {{ .Week }}</small>
38
+            <small style="width: 70px">{{ .RoundName }}</small>
39 39
             <form class="form-inline" method="post" action="/update">
40 40
                 <label class="sr-only" for="artist">Artist</label>
41 41
                 <input class="form-control mb-2 mr-sm-2" name="artist" placeholder="Enter Artist"  value="{{ .Artist }}">
@@ -51,7 +51,7 @@
51 51
                     <label class="form-check-label" for="synced">Track is synced</label>
52 52
                 </div>
53 53
                 
54
-                <input type="hidden" name="week" value="{{ .Week }}">
54
+                <input type="hidden" name="round" value="{{ .RoundID }}">
55 55
                 <button type="submit" class="btn btn-primary mb-2">Update track</button>
56 56
             </form>
57 57