Browse Source

Add initial web interface to modify json db

Toni Fadjukoff 6 years ago
parent
commit
75f3454630
8 changed files with 348 additions and 72 deletions
  1. 59 72
      bot.go
  2. 53 0
      db.go
  3. 115 0
      web.go
  4. 7 0
      web/css/bootstrap.min.css
  5. 4 0
      web/css/jumbotron.css
  6. BIN
      web/favicon.ico
  7. 103 0
      web/index.html
  8. 7 0
      web/js/vendor/bootstrap.min.js

+ 59 - 72
bot.go View File

@@ -5,10 +5,8 @@ import (
5 5
 	"encoding/json"
6 6
 	"errors"
7 7
 	"fmt"
8
-	"github.com/antonholmquist/jason"
9 8
 	"github.com/jasonlvhit/gocron"
10 9
 	"io"
11
-	"io/ioutil"
12 10
 	"log"
13 11
 	"os"
14 12
 	"regexp"
@@ -73,51 +71,21 @@ func addSong(title string, week int, author string, song string) (bool, error) {
73 71
 	return false, errors.New("Could not find matching section")
74 72
 }
75 73
 
76
-type SongsFile struct {
77
-	Songs []*struct {
78
-		Week   int
79
-		Title  string
80
-		Artist string
81
-		URL    string
82
-		Sync   bool
83
-	}
84
-}
85
-
86 74
 func songSynced(syncedWeek int) error {
87
-	var v SongsFile
88
-	f, err := os.Open("songs.json")
75
+	v, err := loadDb()
89 76
 	if err != nil {
90 77
 		return err
91 78
 	}
92
-	defer f.Close()
93
-
94
-	if err != nil {
95
-		log.Fatal(err)
96
-		return nil
97
-	}
98
-	dec := json.NewDecoder(f)
99
-	dec.UseNumber()
100
-	for {
101
-		if err := dec.Decode(&v); err == io.EOF {
102
-			break
103
-		} else if err != nil {
104
-			log.Fatal(err)
105
-			return err
106
-		}
107
-	}
108 79
 
109 80
 	synced := false
110 81
 	for _, song := range v.Songs {
111 82
 		if song.Week == syncedWeek {
112 83
 			song.Sync = true
113 84
 			synced = true
114
-			jsonValue, err := json.Marshal(v)
85
+			err := saveDb(v)
115 86
 			if err != nil {
116
-				log.Fatal(err)
117 87
 				return err
118 88
 			}
119
-			err = ioutil.WriteFile("songs.json", jsonValue, 0644)
120
-			return err
121 89
 		}
122 90
 	}
123 91
 	if !synced {
@@ -127,49 +95,18 @@ func songSynced(syncedWeek int) error {
127 95
 }
128 96
 
129 97
 func getSongs() (SongPriorityQueue, error) {
130
-	f, err := os.Open("songs.json")
131
-	if err != nil {
132
-		return nil, err
133
-	}
134
-	defer f.Close()
135
-	v, err := jason.NewObjectFromReader(f)
98
+	dbSongs, err := loadDb()
136 99
 	if err != nil {
137 100
 		return nil, err
138 101
 	}
139
-	songsArr, err := v.GetObjectArray("Songs")
140
-	if err != nil {
141
-		return nil, err
142
-	}
143
-
144
-	yearStart, _ := time.Parse(time.RFC3339, "2018-01-01T00:00:00+02:00")
145
-
146
-	songs := make(SongPriorityQueue, len(songsArr))
147
-	for index, songObj := range songsArr {
148
-		title, err := songObj.GetString("Title")
149
-		if err != nil {
150
-			return nil, err
151
-		}
152
-		artist, err := songObj.GetString("Artist")
153
-		if err != nil {
154
-			return nil, err
155
-		}
156
-		url, err := songObj.GetString("URL")
157
-		if err != nil {
158
-			return nil, err
159
-		}
160
-		week64, err := songObj.GetInt64("Week")
161
-		if err != nil {
162
-			return nil, err
163
-		}
164
-		week := int(week64)
165
-		sync, _ := songObj.GetBoolean("Sync")
166 102
 
167
-		target := yearStart.AddDate(0, 0, (week-1)*7)
103
+	songs := make(SongPriorityQueue, len(dbSongs.Songs))
104
+	for index, songObj := range dbSongs.Songs {
168 105
 		songs[index] = &Song{
169
-			time:  target,
170
-			song:  "[" + url + " " + artist + " - " + title + "]",
171
-			week:  week,
172
-			sync:  sync,
106
+			time:  targetTime(songObj),
107
+			song:  songEntryWikiText(songObj),
108
+			week:  songObj.Week,
109
+			sync:  songObj.Sync,
173 110
 			index: index,
174 111
 		}
175 112
 	}
@@ -221,6 +158,20 @@ func initCreds() error {
221 158
 	return nil
222 159
 }
223 160
 
161
+func targetTime(entry *SongEntry) time.Time {
162
+	yearStart, _ := time.Parse(time.RFC3339, "2018-01-01T00:00:00+02:00")
163
+	target := yearStart.AddDate(0, 0, (entry.Week-1)*7)
164
+	return target
165
+}
166
+
167
+func songEntryWikiText(entry *SongEntry) string {
168
+	return songWikiText(entry.URL, entry.Artist, entry.Title)
169
+}
170
+
171
+func songWikiText(url string, artist string, title string) string {
172
+	return "[" + url + " " + artist + " - " + title + "]"
173
+}
174
+
224 175
 func main() {
225 176
 	err := initCreds()
226 177
 	if err != nil {
@@ -230,6 +181,42 @@ func main() {
230 181
 	if err != nil {
231 182
 		panic(err)
232 183
 	}
184
+
185
+	modifiedSongChan := make(chan *SongEntry)
186
+
187
+	go func() {
188
+		webStart(modifiedSongChan)
189
+	}()
190
+
191
+	go func() {
192
+		for {
193
+			newSong := <-modifiedSongChan
194
+			matched := false
195
+			for _, song := range songs {
196
+				if song.week == newSong.Week {
197
+					song.song = songEntryWikiText(newSong)
198
+					song.sync = newSong.Sync
199
+					matched = true
200
+					log.Printf("Updated song for week %d, artist: %s, title: %s, URL: %s, time: %v",
201
+						newSong.Week, newSong.Artist, newSong.Title, newSong.URL, song.time)
202
+				}
203
+			}
204
+			if !matched {
205
+				song := &Song{
206
+					time:  targetTime(newSong),
207
+					song:  songEntryWikiText(newSong),
208
+					week:  newSong.Week,
209
+					sync:  newSong.Sync,
210
+					index: len(songs),
211
+				}
212
+				heap.Push(&songs, song)
213
+				log.Printf("Added song for week %d, artist: %s, title: %s, URL: %s, time: %v",
214
+					newSong.Week, newSong.Artist, newSong.Title, newSong.URL, song.time)
215
+			}
216
+		}
217
+	}()
218
+
233 219
 	gocron.Every(1).Second().Do(task)
234 220
 	<-gocron.Start()
221
+
235 222
 }

+ 53 - 0
db.go View File

@@ -0,0 +1,53 @@
1
+package main
2
+
3
+import (
4
+	"encoding/json"
5
+	"io"
6
+	"io/ioutil"
7
+	"os"
8
+)
9
+
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) {
23
+
24
+	var v SongsFile
25
+	f, err := os.Open("songs.json")
26
+	if err != nil {
27
+		return v, err
28
+	}
29
+	defer f.Close()
30
+
31
+	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
+		}
42
+	}
43
+	return v, nil
44
+}
45
+
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
53
+}

+ 115 - 0
web.go View File

@@ -0,0 +1,115 @@
1
+package main
2
+
3
+import (
4
+	"html/template"
5
+	"net/http"
6
+	"strconv"
7
+)
8
+
9
+var (
10
+	cachedTemplates = template.Must(template.ParseFiles("web/index.html"))
11
+	songsChan       chan *SongEntry
12
+)
13
+
14
+func indexHandler(w http.ResponseWriter, r *http.Request) {
15
+	if r.URL.Path != "/" {
16
+		http.Error(w, "Not Found", http.StatusNotFound)
17
+		return
18
+	}
19
+
20
+	songs, err := loadDb()
21
+	if err != nil {
22
+		http.Error(w, err.Error(), http.StatusInternalServerError)
23
+		return
24
+	}
25
+	alsoEmptySongs := SongsFile{
26
+		Songs: make([]*SongEntry, 52),
27
+	}
28
+	for week := 1; week <= 52; week++ {
29
+		alsoEmptySongs.Songs[week-1] = &SongEntry{Week: week}
30
+	}
31
+	for _, song := range songs.Songs {
32
+		alsoEmptySongs.Songs[song.Week-1] = song
33
+	}
34
+
35
+	var templates = cachedTemplates
36
+	if true {
37
+		templates = template.Must(template.ParseFiles("web/index.html"))
38
+	}
39
+	err = templates.ExecuteTemplate(w, "index.html", alsoEmptySongs)
40
+	if err != nil {
41
+		http.Error(w, err.Error(), http.StatusInternalServerError)
42
+	}
43
+}
44
+
45
+func updateHandler(w http.ResponseWriter, r *http.Request) {
46
+	if r.URL.Path != "/update" {
47
+		http.Error(w, "Forbidden", http.StatusForbidden)
48
+		return
49
+	}
50
+	err := r.ParseForm()
51
+	if err != nil {
52
+		http.Error(w, err.Error(), http.StatusBadRequest)
53
+		return
54
+	}
55
+	week, err := strconv.Atoi(r.Form.Get("week"))
56
+	if err != nil {
57
+		http.Error(w, "week parameter not provided", http.StatusBadRequest)
58
+		return
59
+	}
60
+	artist := r.Form.Get("artist")
61
+	title := r.Form.Get("title")
62
+	url := r.Form.Get("url")
63
+	sync := r.Form.Get("sync")
64
+
65
+	songs, err := loadDb()
66
+	if err != nil {
67
+		http.Error(w, err.Error(), http.StatusInternalServerError)
68
+		return
69
+	}
70
+
71
+	matched := false
72
+	for _, song := range songs.Songs {
73
+		if song.Week == week {
74
+			song.Artist = artist
75
+			song.Title = title
76
+			song.URL = url
77
+			if song.Sync && sync != "on" {
78
+				song.Sync = false
79
+			}
80
+			songsChan <- song
81
+			matched = true
82
+		}
83
+	}
84
+	if !matched {
85
+		song := &SongEntry{
86
+			Artist: artist,
87
+			Title:  title,
88
+			URL:    url,
89
+			Week:   week,
90
+		}
91
+		songs.Songs = append(songs.Songs, song)
92
+		songsChan <- song
93
+	}
94
+
95
+	err = saveDb(songs)
96
+	if err != nil {
97
+		http.Error(w, err.Error(), http.StatusInternalServerError)
98
+		return
99
+	}
100
+	http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
101
+}
102
+
103
+func webStart(modifiedSongChan chan *SongEntry) {
104
+	songsChan = modifiedSongChan
105
+
106
+	fs := http.FileServer(http.Dir("web"))
107
+	http.Handle("/css/", fs)
108
+	http.Handle("/fonts/", fs)
109
+	http.Handle("/js/", fs)
110
+	http.Handle("/favicon.ico", fs)
111
+	http.HandleFunc("/", indexHandler)
112
+	http.HandleFunc("/update", updateHandler)
113
+
114
+	http.ListenAndServe("localhost:8080", nil)
115
+}

File diff suppressed because it is too large
+ 7 - 0
web/css/bootstrap.min.css


+ 4 - 0
web/css/jumbotron.css View File

@@ -0,0 +1,4 @@
1
+/* Move down content because we have a fixed navbar that is 50px tall */
2
+body {
3
+  padding-top: 2rem;
4
+}

BIN
web/favicon.ico View File


+ 103 - 0
web/index.html View File

@@ -0,0 +1,103 @@
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
+    <!-- Bootstrap core CSS -->
13
+    <link href="css/bootstrap.min.css" rel="stylesheet">
14
+
15
+    <!-- Custom styles for this template -->
16
+    <link href="css/jumbotron.css" rel="stylesheet">
17
+  </head>
18
+
19
+  <body>
20
+
21
+    <nav class="navbar navbar-toggleable-md navbar-inverse fixed-top bg-inverse">
22
+      <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
23
+        <span class="navbar-toggler-icon"></span>
24
+      </button>
25
+      <a class="navbar-brand" href="#"></a>
26
+
27
+      <div class="collapse navbar-collapse" id="navbarsExampleDefault">
28
+        <ul class="navbar-nav mr-auto">
29
+          <li class="nav-item active">
30
+            <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
31
+          </li>
32
+          <li class="nav-item">
33
+            <a class="nav-link" href="#">Link</a>
34
+          </li>
35
+          <li class="nav-item">
36
+            <a class="nav-link disabled" href="#">Disabled</a>
37
+          </li>
38
+          <li class="nav-item dropdown">
39
+            <a class="nav-link dropdown-toggle" href="http://example.com" id="dropdown01" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a>
40
+            <div class="dropdown-menu" aria-labelledby="dropdown01">
41
+              <a class="dropdown-item" href="#">Action</a>
42
+              <a class="dropdown-item" href="#">Another action</a>
43
+              <a class="dropdown-item" href="#">Something else here</a>
44
+            </div>
45
+          </li>
46
+        </ul>
47
+        <form class="form-inline my-2 my-lg-0">
48
+          <input class="form-control mr-sm-2" type="text" placeholder="Search">
49
+          <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
50
+        </form>
51
+      </div>
52
+    </nav>
53
+
54
+    <!-- Main jumbotron for a primary marketing message or call to action -->
55
+    <div class="jumbotron">
56
+      <div class="container">
57
+        <h1 class="display-3">Hello Lamperi!</h1>
58
+        <p>Please fill in the songs for year 2018.</p>
59
+      </div>
60
+    </div>
61
+
62
+    <div class="container">
63
+        {{range .Songs }}
64
+        <div class="row">
65
+            <small style="width: 70px">Week {{ .Week }}</small>
66
+            <form class="form-inline" method="post" action="/update">
67
+                <label class="sr-only" for="artist">Artist</label>
68
+                <input class="form-control mb-2 mr-sm-2" name="artist" placeholder="Enter Artist"  value="{{ .Artist }}">
69
+                
70
+                <label class="sr-only" for="title">Track title</label>
71
+                <input class="form-control mb-2 mr-sm-2" name="title" placeholder="Enter Track title"  value="{{ .Title }}">
72
+                
73
+                <label class="sr-only" for="url">Track URL</label>
74
+                <input class="form-control mb-2 mr-sm-2" name="url" placeholder="Enter Track title" value="{{ .URL }}">
75
+                
76
+                <div class="form-check mb-2 mr-sm-2">
77
+                    <input class="form-check-input" style="width: 100px" type="checkbox" name="synced" {{ if .Sync }} checked {{ else }} disabled {{ end }}>
78
+                    <label class="form-check-label" for="synced">Track is synced</label>
79
+                </div>
80
+                
81
+                <input type="hidden" name="week" value="{{ .Week }}">
82
+                <button type="submit" class="btn btn-primary mb-2">Update track</button>
83
+            </form>
84
+            
85
+        </div>
86
+        {{end}}
87
+
88
+      <hr>
89
+
90
+      <footer>
91
+        <p>&copy; Lamperi 2018</p>
92
+      </footer>
93
+    </div> <!-- /container -->
94
+
95
+
96
+    <!-- Bootstrap core JavaScript
97
+    ================================================== -->
98
+    <!-- Placed at the end of the document so the pages load faster -->
99
+    <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>
100
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
101
+    <script src="js/vendor/bootstrap.min.js"></script>
102
+  </body>
103
+</html>

File diff suppressed because it is too large
+ 7 - 0
web/js/vendor/bootstrap.min.js