]> code.octet-stream.net Git - broadcaster/blob - broadcaster-server/main.go
Split HTML into header/footer templates
[broadcaster] / broadcaster-server / main.go
1 package main
2
3 import (
4 "bufio"
5 "embed"
6 "flag"
7 "fmt"
8 "golang.org/x/net/websocket"
9 "html/template"
10 "io"
11 "log"
12 "net/http"
13 "os"
14 "path/filepath"
15 "strconv"
16 "strings"
17 "time"
18 )
19
20 const version = "v1.0.0"
21 const formatString = "2006-01-02T15:04"
22
23 //go:embed templates/*
24 var content embed.FS
25
26 var config ServerConfig = NewServerConfig()
27
28 func main() {
29 configFlag := flag.String("c", "", "path to configuration file")
30 addUserFlag := flag.Bool("a", false, "interactively add an admin user then exit")
31 versionFlag := flag.Bool("v", false, "print version then exit")
32 flag.Parse()
33
34 if *versionFlag {
35 fmt.Println("Broadcaster Server", version)
36 os.Exit(0)
37 }
38 if *configFlag == "" {
39 log.Fatal("must specify a configuration file with -c")
40 }
41 config.LoadFromFile(*configFlag)
42
43 InitDatabase()
44 defer db.CloseDatabase()
45
46 if *addUserFlag {
47 scanner := bufio.NewScanner(os.Stdin)
48 fmt.Println("Enter new admin username:")
49 if !scanner.Scan() {
50 os.Exit(1)
51 }
52 username := scanner.Text()
53 fmt.Println("Enter new admin password (will be printed in the clear):")
54 if !scanner.Scan() {
55 os.Exit(1)
56 }
57 password := scanner.Text()
58 if username == "" || password == "" {
59 fmt.Println("Both username and password must be specified")
60 os.Exit(1)
61 }
62 if err := users.CreateUser(username, password, true); err != nil {
63 log.Fatal(err)
64 }
65 os.Exit(0)
66 }
67
68 log.Println("Broadcaster Server", version, "starting up")
69 InitCommandRouter()
70 InitPlaylists()
71 InitAudioFiles(config.AudioFilesPath)
72 InitServerStatus()
73
74 // Public routes
75
76 http.HandleFunc("/login", logInPage)
77 http.Handle("/file-downloads/", http.StripPrefix("/file-downloads/", http.FileServer(http.Dir(config.AudioFilesPath))))
78
79 // Authenticated routes
80
81 http.HandleFunc("/", homePage)
82 http.HandleFunc("/logout", logOutPage)
83 http.HandleFunc("/change-password", changePasswordPage)
84
85 http.HandleFunc("/playlists/", playlistSection)
86 http.HandleFunc("/files/", fileSection)
87 http.HandleFunc("/radios/", radioSection)
88
89 http.Handle("/radio-ws", websocket.Handler(RadioSync))
90 http.Handle("/web-ws", websocket.Handler(WebSync))
91 http.HandleFunc("/stop", stopPage)
92
93 // Admin routes
94
95 err := http.ListenAndServe(config.BindAddress+":"+strconv.Itoa(config.Port), nil)
96 if err != nil {
97 log.Fatal(err)
98 }
99 }
100
101 type HeaderData struct {
102 SelectedMenu string
103 }
104
105 func renderHeader(w http.ResponseWriter, selectedMenu string) {
106 tmpl := template.Must(template.ParseFS(content, "templates/header.html"))
107 data := HeaderData{
108 SelectedMenu: selectedMenu,
109 }
110 tmpl.Execute(w, data)
111 }
112
113 func renderFooter(w http.ResponseWriter) {
114 tmpl := template.Must(template.ParseFS(content, "templates/footer.html"))
115 tmpl.Execute(w, nil)
116 }
117
118 type HomeData struct {
119 LoggedIn bool
120 Username string
121 }
122
123 func homePage(w http.ResponseWriter, r *http.Request) {
124 renderHeader(w, "status")
125 tmpl := template.Must(template.ParseFS(content, "templates/index.html"))
126 data := HomeData{
127 LoggedIn: true,
128 Username: "Bob",
129 }
130 tmpl.Execute(w, data)
131 renderFooter(w)
132 }
133
134 type LogInData struct {
135 Error string
136 }
137
138 func logInPage(w http.ResponseWriter, r *http.Request) {
139 log.Println("Log in page!")
140 r.ParseForm()
141 username := r.Form["username"]
142 password := r.Form["password"]
143 errText := ""
144 if username != nil {
145 user, err := users.Authenticate(username[0], password[0])
146 if err != nil {
147 errText = "Incorrect login"
148 } else {
149 createSessionCookie(w, user.Username)
150 http.Redirect(w, r, "/", http.StatusFound)
151 return
152 }
153 }
154
155 data := LogInData{
156 Error: errText,
157 }
158 renderHeader(w, "")
159 tmpl := template.Must(template.ParseFS(content, "templates/login.html"))
160 tmpl.Execute(w, data)
161 renderFooter(w)
162 }
163
164 func playlistSection(w http.ResponseWriter, r *http.Request) {
165 path := strings.Split(r.URL.Path, "/")
166 if len(path) != 3 {
167 http.NotFound(w, r)
168 return
169 }
170 if path[2] == "new" {
171 editPlaylistPage(w, r, 0)
172 } else if path[2] == "submit" && r.Method == "POST" {
173 submitPlaylist(w, r)
174 } else if path[2] == "delete" && r.Method == "POST" {
175 deletePlaylist(w, r)
176 } else if path[2] == "" {
177 playlistsPage(w, r)
178 } else {
179 id, err := strconv.Atoi(path[2])
180 if err != nil {
181 http.NotFound(w, r)
182 return
183 }
184 editPlaylistPage(w, r, id)
185 }
186 }
187
188 func fileSection(w http.ResponseWriter, r *http.Request) {
189 path := strings.Split(r.URL.Path, "/")
190 if len(path) != 3 {
191 http.NotFound(w, r)
192 return
193 }
194 if path[2] == "upload" {
195 uploadFile(w, r)
196 } else if path[2] == "delete" && r.Method == "POST" {
197 deleteFile(w, r)
198 } else if path[2] == "" {
199 filesPage(w, r)
200 } else {
201 http.NotFound(w, r)
202 return
203 }
204 }
205
206 func radioSection(w http.ResponseWriter, r *http.Request) {
207 path := strings.Split(r.URL.Path, "/")
208 if len(path) != 3 {
209 http.NotFound(w, r)
210 return
211 }
212 if path[2] == "new" {
213 editRadioPage(w, r, 0)
214 } else if path[2] == "submit" && r.Method == "POST" {
215 submitRadio(w, r)
216 } else if path[2] == "delete" && r.Method == "POST" {
217 deleteRadio(w, r)
218 } else if path[2] == "" {
219 radiosPage(w, r)
220 } else {
221 id, err := strconv.Atoi(path[2])
222 if err != nil {
223 http.NotFound(w, r)
224 return
225 }
226 editRadioPage(w, r, id)
227 }
228 }
229
230 type ChangePasswordPageData struct {
231 Message string
232 ShowForm bool
233 }
234
235 func changePasswordPage(w http.ResponseWriter, r *http.Request) {
236 user, err := currentUser(w, r)
237 if err != nil {
238 http.Redirect(w, r, "/login", http.StatusFound)
239 return
240 }
241 var data ChangePasswordPageData
242 if r.Method == "POST" {
243 err := r.ParseForm()
244 if err != nil {
245 w.WriteHeader(http.StatusBadRequest)
246 return
247 }
248 oldPassword := r.Form.Get("oldPassword")
249 newPassword := r.Form.Get("newPassword")
250 err = users.UpdatePassword(user.Username, oldPassword, newPassword)
251 if err != nil {
252 data.Message = "Failed to change password: " + err.Error()
253 data.ShowForm = true
254 } else {
255 data.Message = "Successfully changed password"
256 data.ShowForm = false
257 cookie, err := r.Cookie("broadcast_session")
258 if err == nil {
259 log.Println("clearing other sessions for username", user.Username, "token", cookie.Value)
260 db.ClearOtherSessions(user.Username, cookie.Value)
261 }
262 }
263 } else {
264 data.Message = ""
265 data.ShowForm = true
266 }
267 renderHeader(w, "change-password")
268 tmpl := template.Must(template.ParseFS(content, "templates/change_password.html"))
269 err = tmpl.Execute(w, data)
270 if err != nil {
271 log.Fatal(err)
272 }
273 renderFooter(w)
274 }
275
276 type PlaylistsPageData struct {
277 Playlists []Playlist
278 }
279
280 func playlistsPage(w http.ResponseWriter, _ *http.Request) {
281 renderHeader(w, "playlists")
282 data := PlaylistsPageData{
283 Playlists: db.GetPlaylists(),
284 }
285 tmpl := template.Must(template.ParseFS(content, "templates/playlists.html"))
286 err := tmpl.Execute(w, data)
287 if err != nil {
288 log.Fatal(err)
289 }
290 renderFooter(w)
291 }
292
293 type RadiosPageData struct {
294 Radios []Radio
295 }
296
297 func radiosPage(w http.ResponseWriter, _ *http.Request) {
298 renderHeader(w, "radios")
299 data := RadiosPageData{
300 Radios: db.GetRadios(),
301 }
302 tmpl := template.Must(template.ParseFS(content, "templates/radios.html"))
303 err := tmpl.Execute(w, data)
304 if err != nil {
305 log.Fatal(err)
306 }
307 renderFooter(w)
308 }
309
310 type EditPlaylistPageData struct {
311 Playlist Playlist
312 Entries []PlaylistEntry
313 Files []string
314 }
315
316 func editPlaylistPage(w http.ResponseWriter, r *http.Request, id int) {
317 var data EditPlaylistPageData
318 for _, f := range files.Files() {
319 data.Files = append(data.Files, f.Name)
320 }
321 if id == 0 {
322 data.Playlist.Enabled = true
323 data.Playlist.Name = "New Playlist"
324 data.Playlist.StartTime = time.Now().Format(formatString)
325 data.Entries = append(data.Entries, PlaylistEntry{})
326 } else {
327 playlist, err := db.GetPlaylist(id)
328 if err != nil {
329 http.NotFound(w, r)
330 return
331 }
332 data.Playlist = playlist
333 data.Entries = db.GetEntriesForPlaylist(id)
334 }
335 renderHeader(w, "radios")
336 tmpl := template.Must(template.ParseFS(content, "templates/playlist.html"))
337 tmpl.Execute(w, data)
338 renderFooter(w)
339 }
340
341 func submitPlaylist(w http.ResponseWriter, r *http.Request) {
342 err := r.ParseForm()
343 if err == nil {
344 var p Playlist
345 id, err := strconv.Atoi(r.Form.Get("playlistId"))
346 if err != nil {
347 return
348 }
349 _, err = time.Parse(formatString, r.Form.Get("playlistStartTime"))
350 if err != nil {
351 return
352 }
353 p.Id = id
354 p.Enabled = r.Form.Get("playlistEnabled") == "1"
355 p.Name = r.Form.Get("playlistName")
356 p.StartTime = r.Form.Get("playlistStartTime")
357
358 delays := r.Form["delaySeconds"]
359 filenames := r.Form["filename"]
360 isRelatives := r.Form["isRelative"]
361
362 entries := make([]PlaylistEntry, 0)
363 for i := range delays {
364 var e PlaylistEntry
365 delay, err := strconv.Atoi(delays[i])
366 if err != nil {
367 return
368 }
369 e.DelaySeconds = delay
370 e.Position = i
371 e.IsRelative = isRelatives[i] == "1"
372 e.Filename = filenames[i]
373 entries = append(entries, e)
374 }
375 cleanedEntries := make([]PlaylistEntry, 0)
376 for _, e := range entries {
377 if e.DelaySeconds != 0 || e.Filename != "" {
378 cleanedEntries = append(cleanedEntries, e)
379 }
380 }
381
382 if id != 0 {
383 db.UpdatePlaylist(p)
384 } else {
385 id = db.CreatePlaylist(p)
386 }
387 db.SetEntriesForPlaylist(cleanedEntries, id)
388 // Notify connected radios
389 playlists.NotifyChanges()
390 }
391 http.Redirect(w, r, "/playlists/", http.StatusFound)
392 }
393
394 func deletePlaylist(w http.ResponseWriter, r *http.Request) {
395 err := r.ParseForm()
396 if err == nil {
397 id, err := strconv.Atoi(r.Form.Get("playlistId"))
398 if err != nil {
399 return
400 }
401 db.DeletePlaylist(id)
402 playlists.NotifyChanges()
403 }
404 http.Redirect(w, r, "/playlists/", http.StatusFound)
405 }
406
407 type EditRadioPageData struct {
408 Radio Radio
409 }
410
411 func editRadioPage(w http.ResponseWriter, r *http.Request, id int) {
412 var data EditRadioPageData
413 if id == 0 {
414 data.Radio.Name = "New Radio"
415 data.Radio.Token = generateSession()
416 } else {
417 radio, err := db.GetRadio(id)
418 if err != nil {
419 http.NotFound(w, r)
420 return
421 }
422 data.Radio = radio
423 }
424 renderHeader(w, "radios")
425 tmpl := template.Must(template.ParseFS(content, "templates/radio.html"))
426 tmpl.Execute(w, data)
427 renderFooter(w)
428 }
429
430 func submitRadio(w http.ResponseWriter, r *http.Request) {
431 err := r.ParseForm()
432 if err == nil {
433 var radio Radio
434 id, err := strconv.Atoi(r.Form.Get("radioId"))
435 if err != nil {
436 return
437 }
438 radio.Id = id
439 radio.Name = r.Form.Get("radioName")
440 radio.Token = r.Form.Get("radioToken")
441 if id != 0 {
442 db.UpdateRadio(radio)
443 } else {
444 db.CreateRadio(radio)
445 }
446 }
447 http.Redirect(w, r, "/radios/", http.StatusFound)
448 }
449
450 func deleteRadio(w http.ResponseWriter, r *http.Request) {
451 err := r.ParseForm()
452 if err == nil {
453 id, err := strconv.Atoi(r.Form.Get("radioId"))
454 if err != nil {
455 return
456 }
457 db.DeleteRadio(id)
458 }
459 http.Redirect(w, r, "/radios/", http.StatusFound)
460 }
461
462 type FilesPageData struct {
463 Files []FileSpec
464 }
465
466 func filesPage(w http.ResponseWriter, _ *http.Request) {
467 renderHeader(w, "files")
468 data := FilesPageData{
469 Files: files.Files(),
470 }
471 log.Println("file page data", data)
472 tmpl := template.Must(template.ParseFS(content, "templates/files.html"))
473 err := tmpl.Execute(w, data)
474 if err != nil {
475 log.Fatal(err)
476 }
477 renderFooter(w)
478 }
479
480 func deleteFile(w http.ResponseWriter, r *http.Request) {
481 err := r.ParseForm()
482 if err == nil {
483 filename := r.Form.Get("filename")
484 if filename == "" {
485 return
486 }
487 files.Delete(filename)
488 }
489 http.Redirect(w, r, "/files/", http.StatusFound)
490 }
491
492 func uploadFile(w http.ResponseWriter, r *http.Request) {
493 err := r.ParseMultipartForm(100 << 20)
494 file, handler, err := r.FormFile("file")
495 if err == nil {
496 path := filepath.Join(files.Path(), filepath.Base(handler.Filename))
497 f, _ := os.Create(path)
498 defer f.Close()
499 io.Copy(f, file)
500 log.Println("uploaded file to", path)
501 files.Refresh()
502 }
503 http.Redirect(w, r, "/files/", http.StatusFound)
504 }
505
506 func logOutPage(w http.ResponseWriter, r *http.Request) {
507 clearSessionCookie(w)
508 renderHeader(w, "logout")
509 tmpl := template.Must(template.ParseFS(content, "templates/logout.html"))
510 tmpl.Execute(w, nil)
511 renderFooter(w)
512 }
513
514 func stopPage(w http.ResponseWriter, r *http.Request) {
515 _, err := currentUser(w, r)
516 if err != nil {
517 http.Redirect(w, r, "/login", http.StatusFound)
518 return
519 }
520 r.ParseForm()
521 radioId, err := strconv.Atoi(r.Form.Get("radioId"))
522 if err != nil {
523 http.NotFound(w, r)
524 return
525 }
526 commandRouter.Stop(radioId)
527 http.Redirect(w, r, "/", http.StatusFound)
528 }