8 "golang.org/x/net/websocket"
20 const version
= "v1.0.0"
21 const formatString
= "2006-01-02T15:04"
23 //go:embed templates/*
26 var config ServerConfig
= NewServerConfig()
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")
35 fmt
.Println("Broadcaster Server", version
)
38 if *configFlag
== "" {
39 log
.Fatal("must specify a configuration file with -c")
41 config
.LoadFromFile(*configFlag
)
44 defer db
.CloseDatabase()
47 scanner
:= bufio
.NewScanner(os
.Stdin
)
48 fmt
.Println("Enter new admin username:")
52 username
:= scanner
.Text()
53 fmt
.Println("Enter new admin password (will be printed in the clear):")
57 password
:= scanner
.Text()
58 if username
== "" || password
== "" {
59 fmt
.Println("Both username and password must be specified")
62 if err
:= users
.CreateUser(username
, password
, true); err
!= nil {
68 log
.Println("Broadcaster Server", version
, "starting up")
71 InitAudioFiles(config
.AudioFilesPath
)
76 http
.HandleFunc("/login", logInPage
)
77 http
.Handle("/file-downloads/", http
.StripPrefix("/file-downloads/", http
.FileServer(http
.Dir(config
.AudioFilesPath
))))
79 // Authenticated routes
81 http
.HandleFunc("/", homePage
)
82 http
.HandleFunc("/logout", logOutPage
)
83 http
.HandleFunc("/change-password", changePasswordPage
)
85 http
.HandleFunc("/playlists/", playlistSection
)
86 http
.HandleFunc("/files/", fileSection
)
87 http
.HandleFunc("/radios/", radioSection
)
89 http
.Handle("/radio-ws", websocket
.Handler(RadioSync
))
90 http
.Handle("/web-ws", websocket
.Handler(WebSync
))
91 http
.HandleFunc("/stop", stopPage
)
95 err
:= http
.ListenAndServe(config
.BindAddress
+":"+strconv
.Itoa(config
.Port
), nil)
101 type HeaderData
struct {
105 func renderHeader(w http
.ResponseWriter
, selectedMenu
string) {
106 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/header.html"))
108 SelectedMenu
: selectedMenu
,
110 tmpl
.Execute(w
, data
)
113 func renderFooter(w http
.ResponseWriter
) {
114 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/footer.html"))
118 type HomeData
struct {
123 func homePage(w http
.ResponseWriter
, r
*http
.Request
) {
124 renderHeader(w
, "status")
125 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/index.html"))
130 tmpl
.Execute(w
, data
)
134 type LogInData
struct {
138 func logInPage(w http
.ResponseWriter
, r
*http
.Request
) {
139 log
.Println("Log in page!")
141 username
:= r
.Form
["username"]
142 password
:= r
.Form
["password"]
145 user
, err
:= users
.Authenticate(username
[0], password
[0])
147 errText
= "Incorrect login"
149 createSessionCookie(w
, user
.Username
)
150 http
.Redirect(w
, r
, "/", http
.StatusFound
)
159 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/login.html"))
160 tmpl
.Execute(w
, data
)
164 func playlistSection(w http
.ResponseWriter
, r
*http
.Request
) {
165 path
:= strings
.Split(r
.URL
.Path
, "/")
170 if path
[2] == "new" {
171 editPlaylistPage(w
, r
, 0)
172 } else if path
[2] == "submit" && r
.Method
== "POST" {
174 } else if path
[2] == "delete" && r
.Method
== "POST" {
176 } else if path
[2] == "" {
179 id
, err
:= strconv
.Atoi(path
[2])
184 editPlaylistPage(w
, r
, id
)
188 func fileSection(w http
.ResponseWriter
, r
*http
.Request
) {
189 path
:= strings
.Split(r
.URL
.Path
, "/")
194 if path
[2] == "upload" {
196 } else if path
[2] == "delete" && r
.Method
== "POST" {
198 } else if path
[2] == "" {
206 func radioSection(w http
.ResponseWriter
, r
*http
.Request
) {
207 path
:= strings
.Split(r
.URL
.Path
, "/")
212 if path
[2] == "new" {
213 editRadioPage(w
, r
, 0)
214 } else if path
[2] == "submit" && r
.Method
== "POST" {
216 } else if path
[2] == "delete" && r
.Method
== "POST" {
218 } else if path
[2] == "" {
221 id
, err
:= strconv
.Atoi(path
[2])
226 editRadioPage(w
, r
, id
)
230 type ChangePasswordPageData
struct {
235 func changePasswordPage(w http
.ResponseWriter
, r
*http
.Request
) {
236 user
, err
:= currentUser(w
, r
)
238 http
.Redirect(w
, r
, "/login", http
.StatusFound
)
241 var data ChangePasswordPageData
242 if r
.Method
== "POST" {
245 w
.WriteHeader(http
.StatusBadRequest
)
248 oldPassword
:= r
.Form
.Get("oldPassword")
249 newPassword
:= r
.Form
.Get("newPassword")
250 err
= users
.UpdatePassword(user
.Username
, oldPassword
, newPassword
)
252 data
.Message
= "Failed to change password: " + err
.Error()
255 data
.Message
= "Successfully changed password"
256 data
.ShowForm
= false
257 cookie
, err
:= r
.Cookie("broadcast_session")
259 log
.Println("clearing other sessions for username", user
.Username
, "token", cookie
.Value
)
260 db
.ClearOtherSessions(user
.Username
, cookie
.Value
)
267 renderHeader(w
, "change-password")
268 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/change_password.html"))
269 err
= tmpl
.Execute(w
, data
)
276 type PlaylistsPageData
struct {
280 func playlistsPage(w http
.ResponseWriter
, _
*http
.Request
) {
281 renderHeader(w
, "playlists")
282 data
:= PlaylistsPageData
{
283 Playlists
: db
.GetPlaylists(),
285 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/playlists.html"))
286 err
:= tmpl
.Execute(w
, data
)
293 type RadiosPageData
struct {
297 func radiosPage(w http
.ResponseWriter
, _
*http
.Request
) {
298 renderHeader(w
, "radios")
299 data
:= RadiosPageData
{
300 Radios
: db
.GetRadios(),
302 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/radios.html"))
303 err
:= tmpl
.Execute(w
, data
)
310 type EditPlaylistPageData
struct {
312 Entries
[]PlaylistEntry
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
)
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
{})
327 playlist
, err
:= db
.GetPlaylist(id
)
332 data
.Playlist
= playlist
333 data
.Entries
= db
.GetEntriesForPlaylist(id
)
335 renderHeader(w
, "radios")
336 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/playlist.html"))
337 tmpl
.Execute(w
, data
)
341 func submitPlaylist(w http
.ResponseWriter
, r
*http
.Request
) {
345 id
, err
:= strconv
.Atoi(r
.Form
.Get("playlistId"))
349 _
, err
= time
.Parse(formatString
, r
.Form
.Get("playlistStartTime"))
354 p
.Enabled
= r
.Form
.Get("playlistEnabled") == "1"
355 p
.Name
= r
.Form
.Get("playlistName")
356 p
.StartTime
= r
.Form
.Get("playlistStartTime")
358 delays
:= r
.Form
["delaySeconds"]
359 filenames
:= r
.Form
["filename"]
360 isRelatives
:= r
.Form
["isRelative"]
362 entries
:= make([]PlaylistEntry
, 0)
363 for i
:= range delays
{
365 delay
, err
:= strconv
.Atoi(delays
[i
])
369 e
.DelaySeconds
= delay
371 e
.IsRelative
= isRelatives
[i
] == "1"
372 e
.Filename
= filenames
[i
]
373 entries
= append(entries
, e
)
375 cleanedEntries
:= make([]PlaylistEntry
, 0)
376 for _
, e
:= range entries
{
377 if e
.DelaySeconds
!= 0 || e
.Filename
!= "" {
378 cleanedEntries
= append(cleanedEntries
, e
)
385 id
= db
.CreatePlaylist(p
)
387 db
.SetEntriesForPlaylist(cleanedEntries
, id
)
388 // Notify connected radios
389 playlists
.NotifyChanges()
391 http
.Redirect(w
, r
, "/playlists/", http
.StatusFound
)
394 func deletePlaylist(w http
.ResponseWriter
, r
*http
.Request
) {
397 id
, err
:= strconv
.Atoi(r
.Form
.Get("playlistId"))
401 db
.DeletePlaylist(id
)
402 playlists
.NotifyChanges()
404 http
.Redirect(w
, r
, "/playlists/", http
.StatusFound
)
407 type EditRadioPageData
struct {
411 func editRadioPage(w http
.ResponseWriter
, r
*http
.Request
, id
int) {
412 var data EditRadioPageData
414 data
.Radio
.Name
= "New Radio"
415 data
.Radio
.Token
= generateSession()
417 radio
, err
:= db
.GetRadio(id
)
424 renderHeader(w
, "radios")
425 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/radio.html"))
426 tmpl
.Execute(w
, data
)
430 func submitRadio(w http
.ResponseWriter
, r
*http
.Request
) {
434 id
, err
:= strconv
.Atoi(r
.Form
.Get("radioId"))
439 radio
.Name
= r
.Form
.Get("radioName")
440 radio
.Token
= r
.Form
.Get("radioToken")
442 db
.UpdateRadio(radio
)
444 db
.CreateRadio(radio
)
447 http
.Redirect(w
, r
, "/radios/", http
.StatusFound
)
450 func deleteRadio(w http
.ResponseWriter
, r
*http
.Request
) {
453 id
, err
:= strconv
.Atoi(r
.Form
.Get("radioId"))
459 http
.Redirect(w
, r
, "/radios/", http
.StatusFound
)
462 type FilesPageData
struct {
466 func filesPage(w http
.ResponseWriter
, _
*http
.Request
) {
467 renderHeader(w
, "files")
468 data
:= FilesPageData
{
469 Files
: files
.Files(),
471 log
.Println("file page data", data
)
472 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/files.html"))
473 err
:= tmpl
.Execute(w
, data
)
480 func deleteFile(w http
.ResponseWriter
, r
*http
.Request
) {
483 filename
:= r
.Form
.Get("filename")
487 files
.Delete(filename
)
489 http
.Redirect(w
, r
, "/files/", http
.StatusFound
)
492 func uploadFile(w http
.ResponseWriter
, r
*http
.Request
) {
493 err
:= r
.ParseMultipartForm(100 << 20)
494 file
, handler
, err
:= r
.FormFile("file")
496 path
:= filepath
.Join(files
.Path(), filepath
.Base(handler
.Filename
))
497 f
, _
:= os
.Create(path
)
500 log
.Println("uploaded file to", path
)
503 http
.Redirect(w
, r
, "/files/", http
.StatusFound
)
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"))
514 func stopPage(w http
.ResponseWriter
, r
*http
.Request
) {
515 _
, err
:= currentUser(w
, r
)
517 http
.Redirect(w
, r
, "/login", http
.StatusFound
)
521 radioId
, err
:= strconv
.Atoi(r
.Form
.Get("radioId"))
526 commandRouter
.Stop(radioId
)
527 http
.Redirect(w
, r
, "/", http
.StatusFound
)