-package main
-
-import (
- "bufio"
- _ "embed"
- "flag"
- "fmt"
- "golang.org/x/net/websocket"
- "html/template"
- "io"
- "log"
- "net/http"
- "os"
- "path/filepath"
- "strconv"
- "strings"
- "time"
-)
-
-const version = "v1.0.0"
-const formatString = "2006-01-02T15:04"
-
-// //go:embed templates/*
-//var content embed.FS
-var content = os.DirFS("../broadcaster-server/")
-
-var config ServerConfig = NewServerConfig()
-
-func main() {
- configFlag := flag.String("c", "", "path to configuration file")
- addUserFlag := flag.Bool("a", false, "interactively add an admin user then exit")
- versionFlag := flag.Bool("v", false, "print version then exit")
- flag.Parse()
-
- if *versionFlag {
- fmt.Println("Broadcaster Server", version)
- os.Exit(0)
- }
- if *configFlag == "" {
- log.Fatal("must specify a configuration file with -c")
- }
- config.LoadFromFile(*configFlag)
-
- InitDatabase()
- defer db.CloseDatabase()
-
- if *addUserFlag {
- scanner := bufio.NewScanner(os.Stdin)
- fmt.Println("Enter new admin username:")
- if !scanner.Scan() {
- os.Exit(1)
- }
- username := scanner.Text()
- fmt.Println("Enter new admin password (will be printed in the clear):")
- if !scanner.Scan() {
- os.Exit(1)
- }
- password := scanner.Text()
- if username == "" || password == "" {
- fmt.Println("Both username and password must be specified")
- os.Exit(1)
- }
- if err := users.CreateUser(username, password, true); err != nil {
- log.Fatal(err)
- }
- os.Exit(0)
- }
-
- log.Println("Broadcaster Server", version, "starting up")
- InitCommandRouter()
- InitPlaylists()
- InitAudioFiles(config.AudioFilesPath)
- InitServerStatus()
-
- // Public routes
-
- http.HandleFunc("/login", logInPage)
- http.Handle("/file-downloads/", http.StripPrefix("/file-downloads/", http.FileServer(http.Dir(config.AudioFilesPath))))
-
- // Authenticated routes
-
- http.HandleFunc("/", homePage)
- http.HandleFunc("/logout", logOutPage)
- http.HandleFunc("/change-password", changePasswordPage)
-
- http.HandleFunc("/playlists/", playlistSection)
- http.HandleFunc("/files/", fileSection)
- http.HandleFunc("/radios/", radioSection)
-
- http.Handle("/radio-ws", websocket.Handler(RadioSync))
- http.Handle("/web-ws", websocket.Handler(WebSync))
- http.HandleFunc("/stop", stopPage)
-
- // Admin routes
-
- err := http.ListenAndServe(config.BindAddress+":"+strconv.Itoa(config.Port), nil)
- if err != nil {
- log.Fatal(err)
- }
-}
-
-type HeaderData struct {
- SelectedMenu string
- Username string
-}
-
-func renderHeader(w http.ResponseWriter, selectedMenu string) {
- tmpl := template.Must(template.ParseFS(content, "templates/header.html"))
- data := HeaderData{
- SelectedMenu: selectedMenu,
- Username: "username",
- }
- err := tmpl.Execute(w, data)
- if err != nil {
- log.Fatal(err)
- }
-}
-
-func renderFooter(w http.ResponseWriter) {
- tmpl := template.Must(template.ParseFS(content, "templates/footer.html"))
- err := tmpl.Execute(w, nil)
- if err != nil {
- log.Fatal(err)
- }
-}
-
-type HomeData struct {
- LoggedIn bool
- Username string
-}
-
-func homePage(w http.ResponseWriter, r *http.Request) {
- renderHeader(w, "status")
- tmpl := template.Must(template.ParseFS(content, "templates/index.html"))
- data := HomeData{
- LoggedIn: true,
- Username: "Bob",
- }
- tmpl.Execute(w, data)
- renderFooter(w)
-}
-
-type LogInData struct {
- Error string
-}
-
-func logInPage(w http.ResponseWriter, r *http.Request) {
- log.Println("Log in page!")
- r.ParseForm()
- username := r.Form["username"]
- password := r.Form["password"]
- errText := ""
- if username != nil {
- user, err := users.Authenticate(username[0], password[0])
- if err != nil {
- errText = "Incorrect login"
- } else {
- createSessionCookie(w, user.Username)
- http.Redirect(w, r, "/", http.StatusFound)
- return
- }
- }
-
- data := LogInData{
- Error: errText,
- }
- renderHeader(w, "")
- tmpl := template.Must(template.ParseFS(content, "templates/login.html"))
- tmpl.Execute(w, data)
- renderFooter(w)
-}
-
-func playlistSection(w http.ResponseWriter, r *http.Request) {
- path := strings.Split(r.URL.Path, "/")
- if len(path) != 3 {
- http.NotFound(w, r)
- return
- }
- if path[2] == "new" {
- editPlaylistPage(w, r, 0)
- } else if path[2] == "submit" && r.Method == "POST" {
- submitPlaylist(w, r)
- } else if path[2] == "delete" && r.Method == "POST" {
- deletePlaylist(w, r)
- } else if path[2] == "" {
- playlistsPage(w, r)
- } else {
- id, err := strconv.Atoi(path[2])
- if err != nil {
- http.NotFound(w, r)
- return
- }
- editPlaylistPage(w, r, id)
- }
-}
-
-func fileSection(w http.ResponseWriter, r *http.Request) {
- path := strings.Split(r.URL.Path, "/")
- if len(path) != 3 {
- http.NotFound(w, r)
- return
- }
- if path[2] == "upload" {
- uploadFile(w, r)
- } else if path[2] == "delete" && r.Method == "POST" {
- deleteFile(w, r)
- } else if path[2] == "" {
- filesPage(w, r)
- } else {
- http.NotFound(w, r)
- return
- }
-}
-
-func radioSection(w http.ResponseWriter, r *http.Request) {
- path := strings.Split(r.URL.Path, "/")
- if len(path) != 3 {
- http.NotFound(w, r)
- return
- }
- if path[2] == "new" {
- editRadioPage(w, r, 0)
- } else if path[2] == "submit" && r.Method == "POST" {
- submitRadio(w, r)
- } else if path[2] == "delete" && r.Method == "POST" {
- deleteRadio(w, r)
- } else if path[2] == "" {
- radiosPage(w, r)
- } else {
- id, err := strconv.Atoi(path[2])
- if err != nil {
- http.NotFound(w, r)
- return
- }
- editRadioPage(w, r, id)
- }
-}
-
-type ChangePasswordPageData struct {
- Message string
- ShowForm bool
-}
-
-func changePasswordPage(w http.ResponseWriter, r *http.Request) {
- user, err := currentUser(w, r)
- if err != nil {
- http.Redirect(w, r, "/login", http.StatusFound)
- return
- }
- var data ChangePasswordPageData
- if r.Method == "POST" {
- err := r.ParseForm()
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- return
- }
- oldPassword := r.Form.Get("oldPassword")
- newPassword := r.Form.Get("newPassword")
- err = users.UpdatePassword(user.Username, oldPassword, newPassword)
- if err != nil {
- data.Message = "Failed to change password: " + err.Error()
- data.ShowForm = true
- } else {
- data.Message = "Successfully changed password"
- data.ShowForm = false
- cookie, err := r.Cookie("broadcast_session")
- if err == nil {
- log.Println("clearing other sessions for username", user.Username, "token", cookie.Value)
- db.ClearOtherSessions(user.Username, cookie.Value)
- }
- }
- } else {
- data.Message = ""
- data.ShowForm = true
- }
- renderHeader(w, "change-password")
- tmpl := template.Must(template.ParseFS(content, "templates/change_password.html"))
- err = tmpl.Execute(w, data)
- if err != nil {
- log.Fatal(err)
- }
- renderFooter(w)
-}
-
-type PlaylistsPageData struct {
- Playlists []Playlist
-}
-
-func playlistsPage(w http.ResponseWriter, _ *http.Request) {
- renderHeader(w, "playlists")
- data := PlaylistsPageData{
- Playlists: db.GetPlaylists(),
- }
- tmpl := template.Must(template.ParseFS(content, "templates/playlists.html"))
- err := tmpl.Execute(w, data)
- if err != nil {
- log.Fatal(err)
- }
- renderFooter(w)
-}
-
-type RadiosPageData struct {
- Radios []Radio
-}
-
-func radiosPage(w http.ResponseWriter, _ *http.Request) {
- renderHeader(w, "radios")
- data := RadiosPageData{
- Radios: db.GetRadios(),
- }
- tmpl := template.Must(template.ParseFS(content, "templates/radios.html"))
- err := tmpl.Execute(w, data)
- if err != nil {
- log.Fatal(err)
- }
- renderFooter(w)
-}
-
-type EditPlaylistPageData struct {
- Playlist Playlist
- Entries []PlaylistEntry
- Files []string
-}
-
-func editPlaylistPage(w http.ResponseWriter, r *http.Request, id int) {
- var data EditPlaylistPageData
- for _, f := range files.Files() {
- data.Files = append(data.Files, f.Name)
- }
- if id == 0 {
- data.Playlist.Enabled = true
- data.Playlist.Name = "New Playlist"
- data.Playlist.StartTime = time.Now().Format(formatString)
- data.Entries = append(data.Entries, PlaylistEntry{})
- } else {
- playlist, err := db.GetPlaylist(id)
- if err != nil {
- http.NotFound(w, r)
- return
- }
- data.Playlist = playlist
- data.Entries = db.GetEntriesForPlaylist(id)
- }
- renderHeader(w, "radios")
- tmpl := template.Must(template.ParseFS(content, "templates/playlist.html"))
- tmpl.Execute(w, data)
- renderFooter(w)
-}
-
-func submitPlaylist(w http.ResponseWriter, r *http.Request) {
- err := r.ParseForm()
- if err == nil {
- var p Playlist
- id, err := strconv.Atoi(r.Form.Get("playlistId"))
- if err != nil {
- return
- }
- _, err = time.Parse(formatString, r.Form.Get("playlistStartTime"))
- if err != nil {
- return
- }
- p.Id = id
- p.Enabled = r.Form.Get("playlistEnabled") == "1"
- p.Name = r.Form.Get("playlistName")
- p.StartTime = r.Form.Get("playlistStartTime")
-
- delays := r.Form["delaySeconds"]
- filenames := r.Form["filename"]
- isRelatives := r.Form["isRelative"]
-
- entries := make([]PlaylistEntry, 0)
- for i := range delays {
- var e PlaylistEntry
- delay, err := strconv.Atoi(delays[i])
- if err != nil {
- return
- }
- e.DelaySeconds = delay
- e.Position = i
- e.IsRelative = isRelatives[i] == "1"
- e.Filename = filenames[i]
- entries = append(entries, e)
- }
- cleanedEntries := make([]PlaylistEntry, 0)
- for _, e := range entries {
- if e.DelaySeconds != 0 || e.Filename != "" {
- cleanedEntries = append(cleanedEntries, e)
- }
- }
-
- if id != 0 {
- db.UpdatePlaylist(p)
- } else {
- id = db.CreatePlaylist(p)
- }
- db.SetEntriesForPlaylist(cleanedEntries, id)
- // Notify connected radios
- playlists.NotifyChanges()
- }
- http.Redirect(w, r, "/playlists/", http.StatusFound)
-}
-
-func deletePlaylist(w http.ResponseWriter, r *http.Request) {
- err := r.ParseForm()
- if err == nil {
- id, err := strconv.Atoi(r.Form.Get("playlistId"))
- if err != nil {
- return
- }
- db.DeletePlaylist(id)
- playlists.NotifyChanges()
- }
- http.Redirect(w, r, "/playlists/", http.StatusFound)
-}
-
-type EditRadioPageData struct {
- Radio Radio
-}
-
-func editRadioPage(w http.ResponseWriter, r *http.Request, id int) {
- var data EditRadioPageData
- if id == 0 {
- data.Radio.Name = "New Radio"
- data.Radio.Token = generateSession()
- } else {
- radio, err := db.GetRadio(id)
- if err != nil {
- http.NotFound(w, r)
- return
- }
- data.Radio = radio
- }
- renderHeader(w, "radios")
- tmpl := template.Must(template.ParseFS(content, "templates/radio.html"))
- tmpl.Execute(w, data)
- renderFooter(w)
-}
-
-func submitRadio(w http.ResponseWriter, r *http.Request) {
- err := r.ParseForm()
- if err == nil {
- var radio Radio
- id, err := strconv.Atoi(r.Form.Get("radioId"))
- if err != nil {
- return
- }
- radio.Id = id
- radio.Name = r.Form.Get("radioName")
- radio.Token = r.Form.Get("radioToken")
- if id != 0 {
- db.UpdateRadio(radio)
- } else {
- db.CreateRadio(radio)
- }
- }
- http.Redirect(w, r, "/radios/", http.StatusFound)
-}
-
-func deleteRadio(w http.ResponseWriter, r *http.Request) {
- err := r.ParseForm()
- if err == nil {
- id, err := strconv.Atoi(r.Form.Get("radioId"))
- if err != nil {
- return
- }
- db.DeleteRadio(id)
- }
- http.Redirect(w, r, "/radios/", http.StatusFound)
-}
-
-type FilesPageData struct {
- Files []FileSpec
-}
-
-func filesPage(w http.ResponseWriter, _ *http.Request) {
- renderHeader(w, "files")
- data := FilesPageData{
- Files: files.Files(),
- }
- log.Println("file page data", data)
- tmpl := template.Must(template.ParseFS(content, "templates/files.html"))
- err := tmpl.Execute(w, data)
- if err != nil {
- log.Fatal(err)
- }
- renderFooter(w)
-}
-
-func deleteFile(w http.ResponseWriter, r *http.Request) {
- err := r.ParseForm()
- if err == nil {
- filename := r.Form.Get("filename")
- if filename == "" {
- return
- }
- files.Delete(filename)
- }
- http.Redirect(w, r, "/files/", http.StatusFound)
-}
-
-func uploadFile(w http.ResponseWriter, r *http.Request) {
- err := r.ParseMultipartForm(100 << 20)
- file, handler, err := r.FormFile("file")
- if err == nil {
- path := filepath.Join(files.Path(), filepath.Base(handler.Filename))
- f, _ := os.Create(path)
- defer f.Close()
- io.Copy(f, file)
- log.Println("uploaded file to", path)
- files.Refresh()
- }
- http.Redirect(w, r, "/files/", http.StatusFound)
-}
-
-func logOutPage(w http.ResponseWriter, r *http.Request) {
- clearSessionCookie(w)
- renderHeader(w, "")
- tmpl := template.Must(template.ParseFS(content, "templates/logout.html"))
- tmpl.Execute(w, nil)
- renderFooter(w)
-}
-
-func stopPage(w http.ResponseWriter, r *http.Request) {
- _, err := currentUser(w, r)
- if err != nil {
- http.Redirect(w, r, "/login", http.StatusFound)
- return
- }
- r.ParseForm()
- radioId, err := strconv.Atoi(r.Form.Get("radioId"))
- if err != nil {
- http.NotFound(w, r)
- return
- }
- commandRouter.Stop(radioId)
- http.Redirect(w, r, "/", http.StatusFound)
-}