8 "golang.org/x/net/websocket"
20 const version
= "v1.0.0"
21 const formatString
= "2006-01-02T15:04"
23 //go:embed templates/*
25 //var content = os.DirFS("../broadcaster-server/")
27 var config ServerConfig
= NewServerConfig()
30 configFlag
:= flag
.String("c", "", "path to configuration file")
31 addUserFlag
:= flag
.Bool("a", false, "interactively add an admin user then exit")
32 versionFlag
:= flag
.Bool("v", false, "print version then exit")
36 fmt
.Println("Broadcaster Server", version
)
39 if *configFlag
== "" {
40 log
.Fatal("must specify a configuration file with -c")
42 config
.LoadFromFile(*configFlag
)
45 defer db
.CloseDatabase()
48 scanner
:= bufio
.NewScanner(os
.Stdin
)
49 fmt
.Println("Enter new admin username:")
53 username
:= scanner
.Text()
54 fmt
.Println("Enter new admin password (will be printed in the clear):")
58 password
:= scanner
.Text()
59 if username
== "" || password
== "" {
60 fmt
.Println("Both username and password must be specified")
63 if err
:= users
.CreateUser(username
, password
, true); err
!= nil {
69 log
.Println("Broadcaster Server", version
, "starting up")
72 InitAudioFiles(config
.AudioFilesPath
)
77 http
.HandleFunc("/login", logInPage
)
78 http
.Handle("/file-downloads/", http
.StripPrefix("/file-downloads/", http
.FileServer(http
.Dir(config
.AudioFilesPath
))))
80 // Authenticated routes
82 http
.HandleFunc("/", homePage
)
83 http
.HandleFunc("/logout", logOutPage
)
84 http
.HandleFunc("/change-password", changePasswordPage
)
86 http
.HandleFunc("/playlists/", playlistSection
)
87 http
.HandleFunc("/files/", fileSection
)
88 http
.HandleFunc("/radios/", radioSection
)
90 http
.Handle("/radio-ws", websocket
.Handler(RadioSync
))
91 http
.Handle("/web-ws", websocket
.Handler(WebSync
))
92 http
.HandleFunc("/stop", stopPage
)
96 err
:= http
.ListenAndServe(config
.BindAddress
+":"+strconv
.Itoa(config
.Port
), nil)
102 type HeaderData
struct {
107 func renderHeader(w http
.ResponseWriter
, selectedMenu
string) {
108 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/header.html"))
110 SelectedMenu
: selectedMenu
,
111 Username
: "username",
113 err
:= tmpl
.Execute(w
, data
)
119 func renderFooter(w http
.ResponseWriter
) {
120 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/footer.html"))
121 err
:= tmpl
.Execute(w
, nil)
127 type HomeData
struct {
132 func homePage(w http
.ResponseWriter
, r
*http
.Request
) {
133 renderHeader(w
, "status")
134 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/index.html"))
139 tmpl
.Execute(w
, data
)
143 type LogInData
struct {
147 func logInPage(w http
.ResponseWriter
, r
*http
.Request
) {
148 log
.Println("Log in page!")
150 username
:= r
.Form
["username"]
151 password
:= r
.Form
["password"]
154 user
, err
:= users
.Authenticate(username
[0], password
[0])
156 errText
= "Incorrect login"
158 createSessionCookie(w
, user
.Username
)
159 http
.Redirect(w
, r
, "/", http
.StatusFound
)
168 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/login.html"))
169 tmpl
.Execute(w
, data
)
173 func playlistSection(w http
.ResponseWriter
, r
*http
.Request
) {
174 path
:= strings
.Split(r
.URL
.Path
, "/")
179 if path
[2] == "new" {
180 editPlaylistPage(w
, r
, 0)
181 } else if path
[2] == "submit" && r
.Method
== "POST" {
183 } else if path
[2] == "delete" && r
.Method
== "POST" {
185 } else if path
[2] == "" {
188 id
, err
:= strconv
.Atoi(path
[2])
193 editPlaylistPage(w
, r
, id
)
197 func fileSection(w http
.ResponseWriter
, r
*http
.Request
) {
198 path
:= strings
.Split(r
.URL
.Path
, "/")
203 if path
[2] == "upload" {
205 } else if path
[2] == "delete" && r
.Method
== "POST" {
207 } else if path
[2] == "" {
215 func radioSection(w http
.ResponseWriter
, r
*http
.Request
) {
216 path
:= strings
.Split(r
.URL
.Path
, "/")
221 if path
[2] == "new" {
222 editRadioPage(w
, r
, 0)
223 } else if path
[2] == "submit" && r
.Method
== "POST" {
225 } else if path
[2] == "delete" && r
.Method
== "POST" {
227 } else if path
[2] == "" {
230 id
, err
:= strconv
.Atoi(path
[2])
235 editRadioPage(w
, r
, id
)
239 type ChangePasswordPageData
struct {
244 func changePasswordPage(w http
.ResponseWriter
, r
*http
.Request
) {
245 user
, err
:= currentUser(w
, r
)
247 http
.Redirect(w
, r
, "/login", http
.StatusFound
)
250 var data ChangePasswordPageData
251 if r
.Method
== "POST" {
254 w
.WriteHeader(http
.StatusBadRequest
)
257 oldPassword
:= r
.Form
.Get("oldPassword")
258 newPassword
:= r
.Form
.Get("newPassword")
259 err
= users
.UpdatePassword(user
.Username
, oldPassword
, newPassword
)
261 data
.Message
= "Failed to change password: " + err
.Error()
264 data
.Message
= "Successfully changed password"
265 data
.ShowForm
= false
266 cookie
, err
:= r
.Cookie("broadcast_session")
268 log
.Println("clearing other sessions for username", user
.Username
, "token", cookie
.Value
)
269 db
.ClearOtherSessions(user
.Username
, cookie
.Value
)
276 renderHeader(w
, "change-password")
277 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/change_password.html"))
278 err
= tmpl
.Execute(w
, data
)
285 type PlaylistsPageData
struct {
289 func playlistsPage(w http
.ResponseWriter
, _
*http
.Request
) {
290 renderHeader(w
, "playlists")
291 data
:= PlaylistsPageData
{
292 Playlists
: db
.GetPlaylists(),
294 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/playlists.html"))
295 err
:= tmpl
.Execute(w
, data
)
302 type RadiosPageData
struct {
306 func radiosPage(w http
.ResponseWriter
, _
*http
.Request
) {
307 renderHeader(w
, "radios")
308 data
:= RadiosPageData
{
309 Radios
: db
.GetRadios(),
311 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/radios.html"))
312 err
:= tmpl
.Execute(w
, data
)
319 type EditPlaylistPageData
struct {
321 Entries
[]PlaylistEntry
325 func editPlaylistPage(w http
.ResponseWriter
, r
*http
.Request
, id
int) {
326 var data EditPlaylistPageData
327 for _
, f
:= range files
.Files() {
328 data
.Files
= append(data
.Files
, f
.Name
)
331 data
.Playlist
.Enabled
= true
332 data
.Playlist
.Name
= "New Playlist"
333 data
.Playlist
.StartTime
= time
.Now().Format(formatString
)
334 data
.Entries
= append(data
.Entries
, PlaylistEntry
{})
336 playlist
, err
:= db
.GetPlaylist(id
)
341 data
.Playlist
= playlist
342 data
.Entries
= db
.GetEntriesForPlaylist(id
)
344 renderHeader(w
, "radios")
345 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/playlist.html"))
346 tmpl
.Execute(w
, data
)
350 func submitPlaylist(w http
.ResponseWriter
, r
*http
.Request
) {
354 id
, err
:= strconv
.Atoi(r
.Form
.Get("playlistId"))
358 _
, err
= time
.Parse(formatString
, r
.Form
.Get("playlistStartTime"))
363 p
.Enabled
= r
.Form
.Get("playlistEnabled") == "1"
364 p
.Name
= r
.Form
.Get("playlistName")
365 p
.StartTime
= r
.Form
.Get("playlistStartTime")
367 delays
:= r
.Form
["delaySeconds"]
368 filenames
:= r
.Form
["filename"]
369 isRelatives
:= r
.Form
["isRelative"]
371 entries
:= make([]PlaylistEntry
, 0)
372 for i
:= range delays
{
374 delay
, err
:= strconv
.Atoi(delays
[i
])
378 e
.DelaySeconds
= delay
380 e
.IsRelative
= isRelatives
[i
] == "1"
381 e
.Filename
= filenames
[i
]
382 entries
= append(entries
, e
)
384 cleanedEntries
:= make([]PlaylistEntry
, 0)
385 for _
, e
:= range entries
{
386 if e
.DelaySeconds
!= 0 || e
.Filename
!= "" {
387 cleanedEntries
= append(cleanedEntries
, e
)
394 id
= db
.CreatePlaylist(p
)
396 db
.SetEntriesForPlaylist(cleanedEntries
, id
)
397 // Notify connected radios
398 playlists
.NotifyChanges()
400 http
.Redirect(w
, r
, "/playlists/", http
.StatusFound
)
403 func deletePlaylist(w http
.ResponseWriter
, r
*http
.Request
) {
406 id
, err
:= strconv
.Atoi(r
.Form
.Get("playlistId"))
410 db
.DeletePlaylist(id
)
411 playlists
.NotifyChanges()
413 http
.Redirect(w
, r
, "/playlists/", http
.StatusFound
)
416 type EditRadioPageData
struct {
420 func editRadioPage(w http
.ResponseWriter
, r
*http
.Request
, id
int) {
421 var data EditRadioPageData
423 data
.Radio
.Name
= "New Radio"
424 data
.Radio
.Token
= generateSession()
426 radio
, err
:= db
.GetRadio(id
)
433 renderHeader(w
, "radios")
434 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/radio.html"))
435 tmpl
.Execute(w
, data
)
439 func submitRadio(w http
.ResponseWriter
, r
*http
.Request
) {
443 id
, err
:= strconv
.Atoi(r
.Form
.Get("radioId"))
448 radio
.Name
= r
.Form
.Get("radioName")
449 radio
.Token
= r
.Form
.Get("radioToken")
451 db
.UpdateRadio(radio
)
453 db
.CreateRadio(radio
)
456 http
.Redirect(w
, r
, "/radios/", http
.StatusFound
)
459 func deleteRadio(w http
.ResponseWriter
, r
*http
.Request
) {
462 id
, err
:= strconv
.Atoi(r
.Form
.Get("radioId"))
468 http
.Redirect(w
, r
, "/radios/", http
.StatusFound
)
471 type FilesPageData
struct {
475 func filesPage(w http
.ResponseWriter
, _
*http
.Request
) {
476 renderHeader(w
, "files")
477 data
:= FilesPageData
{
478 Files
: files
.Files(),
480 log
.Println("file page data", data
)
481 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/files.html"))
482 err
:= tmpl
.Execute(w
, data
)
489 func deleteFile(w http
.ResponseWriter
, r
*http
.Request
) {
492 filename
:= r
.Form
.Get("filename")
496 files
.Delete(filename
)
498 http
.Redirect(w
, r
, "/files/", http
.StatusFound
)
501 func uploadFile(w http
.ResponseWriter
, r
*http
.Request
) {
502 err
:= r
.ParseMultipartForm(100 << 20)
503 file
, handler
, err
:= r
.FormFile("file")
505 path
:= filepath
.Join(files
.Path(), filepath
.Base(handler
.Filename
))
506 f
, _
:= os
.Create(path
)
509 log
.Println("uploaded file to", path
)
512 http
.Redirect(w
, r
, "/files/", http
.StatusFound
)
515 func logOutPage(w http
.ResponseWriter
, r
*http
.Request
) {
516 clearSessionCookie(w
)
518 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/logout.html"))
523 func stopPage(w http
.ResponseWriter
, r
*http
.Request
) {
524 _
, err
:= currentUser(w
, r
)
526 http
.Redirect(w
, r
, "/login", http
.StatusFound
)
530 radioId
, err
:= strconv
.Atoi(r
.Form
.Get("radioId"))
535 commandRouter
.Stop(radioId
)
536 http
.Redirect(w
, r
, "/", http
.StatusFound
)