]> code.octet-stream.net Git - broadcaster/blob - broadcaster-server/main.go
Add licence, etc.
[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 HomeData struct {
102 LoggedIn bool
103 Username string
104 }
105
106 func homePage(w http.ResponseWriter, r *http.Request) {
107 tmpl := template.Must(template.ParseFS(content, "templates/index.html"))
108 data := HomeData{
109 LoggedIn: true,
110 Username: "Bob",
111 }
112 tmpl.Execute(w, data)
113 }
114
115 type LogInData struct {
116 Error string
117 }
118
119 func logInPage(w http.ResponseWriter, r *http.Request) {
120 log.Println("Log in page!")
121 r.ParseForm()
122 username := r.Form["username"]
123 password := r.Form["password"]
124 errText := ""
125 if username != nil {
126 user, err := users.Authenticate(username[0], password[0])
127 if err != nil {
128 errText = "Incorrect login"
129 } else {
130 createSessionCookie(w, user.Username)
131 http.Redirect(w, r, "/", http.StatusFound)
132 return
133 }
134 }
135
136 data := LogInData{
137 Error: errText,
138 }
139
140 tmpl := template.Must(template.ParseFS(content, "templates/login.html"))
141 tmpl.Execute(w, data)
142 }
143
144 func playlistSection(w http.ResponseWriter, r *http.Request) {
145 path := strings.Split(r.URL.Path, "/")
146 if len(path) != 3 {
147 http.NotFound(w, r)
148 return
149 }
150 if path[2] == "new" {
151 editPlaylistPage(w, r, 0)
152 } else if path[2] == "submit" && r.Method == "POST" {
153 submitPlaylist(w, r)
154 } else if path[2] == "delete" && r.Method == "POST" {
155 deletePlaylist(w, r)
156 } else if path[2] == "" {
157 playlistsPage(w, r)
158 } else {
159 id, err := strconv.Atoi(path[2])
160 if err != nil {
161 http.NotFound(w, r)
162 return
163 }
164 editPlaylistPage(w, r, id)
165 }
166 }
167
168 func fileSection(w http.ResponseWriter, r *http.Request) {
169 path := strings.Split(r.URL.Path, "/")
170 if len(path) != 3 {
171 http.NotFound(w, r)
172 return
173 }
174 if path[2] == "upload" {
175 uploadFile(w, r)
176 } else if path[2] == "delete" && r.Method == "POST" {
177 deleteFile(w, r)
178 } else if path[2] == "" {
179 filesPage(w, r)
180 } else {
181 http.NotFound(w, r)
182 return
183 }
184 }
185
186 func radioSection(w http.ResponseWriter, r *http.Request) {
187 path := strings.Split(r.URL.Path, "/")
188 if len(path) != 3 {
189 http.NotFound(w, r)
190 return
191 }
192 if path[2] == "new" {
193 editRadioPage(w, r, 0)
194 } else if path[2] == "submit" && r.Method == "POST" {
195 submitRadio(w, r)
196 } else if path[2] == "delete" && r.Method == "POST" {
197 deleteRadio(w, r)
198 } else if path[2] == "" {
199 radiosPage(w, r)
200 } else {
201 id, err := strconv.Atoi(path[2])
202 if err != nil {
203 http.NotFound(w, r)
204 return
205 }
206 editRadioPage(w, r, id)
207 }
208 }
209
210 type ChangePasswordPageData struct {
211 Message string
212 ShowForm bool
213 }
214
215 func changePasswordPage(w http.ResponseWriter, r *http.Request) {
216 user, err := currentUser(w, r)
217 if err != nil {
218 http.Redirect(w, r, "/login", http.StatusFound)
219 return
220 }
221 var data ChangePasswordPageData
222 if r.Method == "POST" {
223 err := r.ParseForm()
224 if err != nil {
225 w.WriteHeader(http.StatusBadRequest)
226 return
227 }
228 oldPassword := r.Form.Get("oldPassword")
229 newPassword := r.Form.Get("newPassword")
230 err = users.UpdatePassword(user.Username, oldPassword, newPassword)
231 if err != nil {
232 data.Message = "Failed to change password: " + err.Error()
233 data.ShowForm = true
234 } else {
235 data.Message = "Successfully changed password"
236 data.ShowForm = false
237 cookie, err := r.Cookie("broadcast_session")
238 if err == nil {
239 log.Println("clearing other sessions for username", user.Username, "token", cookie.Value)
240 db.ClearOtherSessions(user.Username, cookie.Value)
241 }
242 }
243 } else {
244 data.Message = ""
245 data.ShowForm = true
246 }
247 tmpl := template.Must(template.ParseFS(content, "templates/change_password.html"))
248 err = tmpl.Execute(w, data)
249 if err != nil {
250 log.Fatal(err)
251 }
252 }
253
254 type PlaylistsPageData struct {
255 Playlists []Playlist
256 }
257
258 func playlistsPage(w http.ResponseWriter, _ *http.Request) {
259 data := PlaylistsPageData{
260 Playlists: db.GetPlaylists(),
261 }
262 tmpl := template.Must(template.ParseFS(content, "templates/playlists.html"))
263 err := tmpl.Execute(w, data)
264 if err != nil {
265 log.Fatal(err)
266 }
267 }
268
269 type RadiosPageData struct {
270 Radios []Radio
271 }
272
273 func radiosPage(w http.ResponseWriter, _ *http.Request) {
274 data := RadiosPageData{
275 Radios: db.GetRadios(),
276 }
277 tmpl := template.Must(template.ParseFS(content, "templates/radios.html"))
278 err := tmpl.Execute(w, data)
279 if err != nil {
280 log.Fatal(err)
281 }
282 }
283
284 type EditPlaylistPageData struct {
285 Playlist Playlist
286 Entries []PlaylistEntry
287 Files []string
288 }
289
290 func editPlaylistPage(w http.ResponseWriter, r *http.Request, id int) {
291 var data EditPlaylistPageData
292 for _, f := range files.Files() {
293 data.Files = append(data.Files, f.Name)
294 }
295 if id == 0 {
296 data.Playlist.Enabled = true
297 data.Playlist.Name = "New Playlist"
298 data.Playlist.StartTime = time.Now().Format(formatString)
299 data.Entries = append(data.Entries, PlaylistEntry{})
300 } else {
301 playlist, err := db.GetPlaylist(id)
302 if err != nil {
303 http.NotFound(w, r)
304 return
305 }
306 data.Playlist = playlist
307 data.Entries = db.GetEntriesForPlaylist(id)
308 }
309 tmpl := template.Must(template.ParseFS(content, "templates/playlist.html"))
310 tmpl.Execute(w, data)
311 }
312
313 func submitPlaylist(w http.ResponseWriter, r *http.Request) {
314 err := r.ParseForm()
315 if err == nil {
316 var p Playlist
317 id, err := strconv.Atoi(r.Form.Get("playlistId"))
318 if err != nil {
319 return
320 }
321 _, err = time.Parse(formatString, r.Form.Get("playlistStartTime"))
322 if err != nil {
323 return
324 }
325 p.Id = id
326 p.Enabled = r.Form.Get("playlistEnabled") == "1"
327 p.Name = r.Form.Get("playlistName")
328 p.StartTime = r.Form.Get("playlistStartTime")
329
330 delays := r.Form["delaySeconds"]
331 filenames := r.Form["filename"]
332 isRelatives := r.Form["isRelative"]
333
334 entries := make([]PlaylistEntry, 0)
335 for i := range delays {
336 var e PlaylistEntry
337 delay, err := strconv.Atoi(delays[i])
338 if err != nil {
339 return
340 }
341 e.DelaySeconds = delay
342 e.Position = i
343 e.IsRelative = isRelatives[i] == "1"
344 e.Filename = filenames[i]
345 entries = append(entries, e)
346 }
347 cleanedEntries := make([]PlaylistEntry, 0)
348 for _, e := range entries {
349 if e.DelaySeconds != 0 || e.Filename != "" {
350 cleanedEntries = append(cleanedEntries, e)
351 }
352 }
353
354 if id != 0 {
355 db.UpdatePlaylist(p)
356 } else {
357 id = db.CreatePlaylist(p)
358 }
359 db.SetEntriesForPlaylist(cleanedEntries, id)
360 // Notify connected radios
361 playlists.NotifyChanges()
362 }
363 http.Redirect(w, r, "/playlists/", http.StatusFound)
364 }
365
366 func deletePlaylist(w http.ResponseWriter, r *http.Request) {
367 err := r.ParseForm()
368 if err == nil {
369 id, err := strconv.Atoi(r.Form.Get("playlistId"))
370 if err != nil {
371 return
372 }
373 db.DeletePlaylist(id)
374 playlists.NotifyChanges()
375 }
376 http.Redirect(w, r, "/playlists/", http.StatusFound)
377 }
378
379 type EditRadioPageData struct {
380 Radio Radio
381 }
382
383 func editRadioPage(w http.ResponseWriter, r *http.Request, id int) {
384 var data EditRadioPageData
385 if id == 0 {
386 data.Radio.Name = "New Radio"
387 data.Radio.Token = generateSession()
388 } else {
389 radio, err := db.GetRadio(id)
390 if err != nil {
391 http.NotFound(w, r)
392 return
393 }
394 data.Radio = radio
395 }
396 tmpl := template.Must(template.ParseFS(content, "templates/radio.html"))
397 tmpl.Execute(w, data)
398 }
399
400 func submitRadio(w http.ResponseWriter, r *http.Request) {
401 err := r.ParseForm()
402 if err == nil {
403 var radio Radio
404 id, err := strconv.Atoi(r.Form.Get("radioId"))
405 if err != nil {
406 return
407 }
408 radio.Id = id
409 radio.Name = r.Form.Get("radioName")
410 radio.Token = r.Form.Get("radioToken")
411 if id != 0 {
412 db.UpdateRadio(radio)
413 } else {
414 db.CreateRadio(radio)
415 }
416 }
417 http.Redirect(w, r, "/radios/", http.StatusFound)
418 }
419
420 func deleteRadio(w http.ResponseWriter, r *http.Request) {
421 err := r.ParseForm()
422 if err == nil {
423 id, err := strconv.Atoi(r.Form.Get("radioId"))
424 if err != nil {
425 return
426 }
427 db.DeleteRadio(id)
428 }
429 http.Redirect(w, r, "/radios/", http.StatusFound)
430 }
431
432 type FilesPageData struct {
433 Files []FileSpec
434 }
435
436 func filesPage(w http.ResponseWriter, _ *http.Request) {
437 data := FilesPageData{
438 Files: files.Files(),
439 }
440 log.Println("file page data", data)
441 tmpl := template.Must(template.ParseFS(content, "templates/files.html"))
442 err := tmpl.Execute(w, data)
443 if err != nil {
444 log.Fatal(err)
445 }
446 }
447
448 func deleteFile(w http.ResponseWriter, r *http.Request) {
449 err := r.ParseForm()
450 if err == nil {
451 filename := r.Form.Get("filename")
452 if filename == "" {
453 return
454 }
455 files.Delete(filename)
456 }
457 http.Redirect(w, r, "/files/", http.StatusFound)
458 }
459
460 func uploadFile(w http.ResponseWriter, r *http.Request) {
461 err := r.ParseMultipartForm(100 << 20)
462 file, handler, err := r.FormFile("file")
463 if err == nil {
464 path := filepath.Join(files.Path(), filepath.Base(handler.Filename))
465 f, _ := os.Create(path)
466 defer f.Close()
467 io.Copy(f, file)
468 log.Println("uploaded file to", path)
469 files.Refresh()
470 }
471 http.Redirect(w, r, "/files/", http.StatusFound)
472 }
473
474 func logOutPage(w http.ResponseWriter, r *http.Request) {
475 clearSessionCookie(w)
476 tmpl := template.Must(template.ParseFS(content, "templates/logout.html"))
477 tmpl.Execute(w, nil)
478 }
479
480 func stopPage(w http.ResponseWriter, r *http.Request) {
481 _, err := currentUser(w, r)
482 if err != nil {
483 http.Redirect(w, r, "/login", http.StatusFound)
484 return
485 }
486 r.ParseForm()
487 radioId, err := strconv.Atoi(r.Form.Get("radioId"))
488 if err != nil {
489 http.NotFound(w, r)
490 return
491 }
492 commandRouter.Stop(radioId)
493 http.Redirect(w, r, "/", http.StatusFound)
494 }