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 HomeData
struct {
106 func homePage(w http
.ResponseWriter
, r
*http
.Request
) {
107 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/index.html"))
112 tmpl
.Execute(w
, data
)
115 type LogInData
struct {
119 func logInPage(w http
.ResponseWriter
, r
*http
.Request
) {
120 log
.Println("Log in page!")
122 username
:= r
.Form
["username"]
123 password
:= r
.Form
["password"]
126 user
, err
:= users
.Authenticate(username
[0], password
[0])
128 errText
= "Incorrect login"
130 createSessionCookie(w
, user
.Username
)
131 http
.Redirect(w
, r
, "/", http
.StatusFound
)
140 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/login.html"))
141 tmpl
.Execute(w
, data
)
144 func playlistSection(w http
.ResponseWriter
, r
*http
.Request
) {
145 path
:= strings
.Split(r
.URL
.Path
, "/")
150 if path
[2] == "new" {
151 editPlaylistPage(w
, r
, 0)
152 } else if path
[2] == "submit" && r
.Method
== "POST" {
154 } else if path
[2] == "delete" && r
.Method
== "POST" {
156 } else if path
[2] == "" {
159 id
, err
:= strconv
.Atoi(path
[2])
164 editPlaylistPage(w
, r
, id
)
168 func fileSection(w http
.ResponseWriter
, r
*http
.Request
) {
169 path
:= strings
.Split(r
.URL
.Path
, "/")
174 if path
[2] == "upload" {
176 } else if path
[2] == "delete" && r
.Method
== "POST" {
178 } else if path
[2] == "" {
186 func radioSection(w http
.ResponseWriter
, r
*http
.Request
) {
187 path
:= strings
.Split(r
.URL
.Path
, "/")
192 if path
[2] == "new" {
193 editRadioPage(w
, r
, 0)
194 } else if path
[2] == "submit" && r
.Method
== "POST" {
196 } else if path
[2] == "delete" && r
.Method
== "POST" {
198 } else if path
[2] == "" {
201 id
, err
:= strconv
.Atoi(path
[2])
206 editRadioPage(w
, r
, id
)
210 type ChangePasswordPageData
struct {
215 func changePasswordPage(w http
.ResponseWriter
, r
*http
.Request
) {
216 user
, err
:= currentUser(w
, r
)
218 http
.Redirect(w
, r
, "/login", http
.StatusFound
)
221 var data ChangePasswordPageData
222 if r
.Method
== "POST" {
225 w
.WriteHeader(http
.StatusBadRequest
)
228 oldPassword
:= r
.Form
.Get("oldPassword")
229 newPassword
:= r
.Form
.Get("newPassword")
230 err
= users
.UpdatePassword(user
.Username
, oldPassword
, newPassword
)
232 data
.Message
= "Failed to change password: " + err
.Error()
235 data
.Message
= "Successfully changed password"
236 data
.ShowForm
= false
237 cookie
, err
:= r
.Cookie("broadcast_session")
239 log
.Println("clearing other sessions for username", user
.Username
, "token", cookie
.Value
)
240 db
.ClearOtherSessions(user
.Username
, cookie
.Value
)
247 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/change_password.html"))
248 err
= tmpl
.Execute(w
, data
)
254 type PlaylistsPageData
struct {
258 func playlistsPage(w http
.ResponseWriter
, _
*http
.Request
) {
259 data
:= PlaylistsPageData
{
260 Playlists
: db
.GetPlaylists(),
262 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/playlists.html"))
263 err
:= tmpl
.Execute(w
, data
)
269 type RadiosPageData
struct {
273 func radiosPage(w http
.ResponseWriter
, _
*http
.Request
) {
274 data
:= RadiosPageData
{
275 Radios
: db
.GetRadios(),
277 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/radios.html"))
278 err
:= tmpl
.Execute(w
, data
)
284 type EditPlaylistPageData
struct {
286 Entries
[]PlaylistEntry
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
)
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
{})
301 playlist
, err
:= db
.GetPlaylist(id
)
306 data
.Playlist
= playlist
307 data
.Entries
= db
.GetEntriesForPlaylist(id
)
309 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/playlist.html"))
310 tmpl
.Execute(w
, data
)
313 func submitPlaylist(w http
.ResponseWriter
, r
*http
.Request
) {
317 id
, err
:= strconv
.Atoi(r
.Form
.Get("playlistId"))
321 _
, err
= time
.Parse(formatString
, r
.Form
.Get("playlistStartTime"))
326 p
.Enabled
= r
.Form
.Get("playlistEnabled") == "1"
327 p
.Name
= r
.Form
.Get("playlistName")
328 p
.StartTime
= r
.Form
.Get("playlistStartTime")
330 delays
:= r
.Form
["delaySeconds"]
331 filenames
:= r
.Form
["filename"]
332 isRelatives
:= r
.Form
["isRelative"]
334 entries
:= make([]PlaylistEntry
, 0)
335 for i
:= range delays
{
337 delay
, err
:= strconv
.Atoi(delays
[i
])
341 e
.DelaySeconds
= delay
343 e
.IsRelative
= isRelatives
[i
] == "1"
344 e
.Filename
= filenames
[i
]
345 entries
= append(entries
, e
)
347 cleanedEntries
:= make([]PlaylistEntry
, 0)
348 for _
, e
:= range entries
{
349 if e
.DelaySeconds
!= 0 || e
.Filename
!= "" {
350 cleanedEntries
= append(cleanedEntries
, e
)
357 id
= db
.CreatePlaylist(p
)
359 db
.SetEntriesForPlaylist(cleanedEntries
, id
)
360 // Notify connected radios
361 playlists
.NotifyChanges()
363 http
.Redirect(w
, r
, "/playlists/", http
.StatusFound
)
366 func deletePlaylist(w http
.ResponseWriter
, r
*http
.Request
) {
369 id
, err
:= strconv
.Atoi(r
.Form
.Get("playlistId"))
373 db
.DeletePlaylist(id
)
374 playlists
.NotifyChanges()
376 http
.Redirect(w
, r
, "/playlists/", http
.StatusFound
)
379 type EditRadioPageData
struct {
383 func editRadioPage(w http
.ResponseWriter
, r
*http
.Request
, id
int) {
384 var data EditRadioPageData
386 data
.Radio
.Name
= "New Radio"
387 data
.Radio
.Token
= generateSession()
389 radio
, err
:= db
.GetRadio(id
)
396 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/radio.html"))
397 tmpl
.Execute(w
, data
)
400 func submitRadio(w http
.ResponseWriter
, r
*http
.Request
) {
404 id
, err
:= strconv
.Atoi(r
.Form
.Get("radioId"))
409 radio
.Name
= r
.Form
.Get("radioName")
410 radio
.Token
= r
.Form
.Get("radioToken")
412 db
.UpdateRadio(radio
)
414 db
.CreateRadio(radio
)
417 http
.Redirect(w
, r
, "/radios/", http
.StatusFound
)
420 func deleteRadio(w http
.ResponseWriter
, r
*http
.Request
) {
423 id
, err
:= strconv
.Atoi(r
.Form
.Get("radioId"))
429 http
.Redirect(w
, r
, "/radios/", http
.StatusFound
)
432 type FilesPageData
struct {
436 func filesPage(w http
.ResponseWriter
, _
*http
.Request
) {
437 data
:= FilesPageData
{
438 Files
: files
.Files(),
440 log
.Println("file page data", data
)
441 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/files.html"))
442 err
:= tmpl
.Execute(w
, data
)
448 func deleteFile(w http
.ResponseWriter
, r
*http
.Request
) {
451 filename
:= r
.Form
.Get("filename")
455 files
.Delete(filename
)
457 http
.Redirect(w
, r
, "/files/", http
.StatusFound
)
460 func uploadFile(w http
.ResponseWriter
, r
*http
.Request
) {
461 err
:= r
.ParseMultipartForm(100 << 20)
462 file
, handler
, err
:= r
.FormFile("file")
464 path
:= filepath
.Join(files
.Path(), filepath
.Base(handler
.Filename
))
465 f
, _
:= os
.Create(path
)
468 log
.Println("uploaded file to", path
)
471 http
.Redirect(w
, r
, "/files/", http
.StatusFound
)
474 func logOutPage(w http
.ResponseWriter
, r
*http
.Request
) {
475 clearSessionCookie(w
)
476 tmpl
:= template
.Must(template
.ParseFS(content
, "templates/logout.html"))
480 func stopPage(w http
.ResponseWriter
, r
*http
.Request
) {
481 _
, err
:= currentUser(w
, r
)
483 http
.Redirect(w
, r
, "/login", http
.StatusFound
)
487 radioId
, err
:= strconv
.Atoi(r
.Form
.Get("radioId"))
492 commandRouter
.Stop(radioId
)
493 http
.Redirect(w
, r
, "/", http
.StatusFound
)