diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-05-21 20:05:01 +0200 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2023-05-21 20:05:01 +0200 |
| commit | 5ee0fb4aa53e02a4b6c2ffb3d5bfa3a6153c9a06 (patch) | |
| tree | 5268e39568e6146521e4be9e0c1b70d3afca538c | |
| parent | ead811a4ebd0074cc4d42da53daacbb358229fac (diff) | |
| download | WRMS-5ee0fb4aa53e02a4b6c2ffb3d5bfa3a6153c9a06.tar.gz WRMS-5ee0fb4aa53e02a4b6c2ffb3d5bfa3a6153c9a06.zip | |
implement YAML config and admin connections
| -rw-r--r-- | TODO.md (renamed from TODO) | 10 | ||||
| -rw-r--r-- | config.go | 78 | ||||
| -rw-r--r-- | main.go | 95 | ||||
| -rw-r--r-- | player.go | 2 | ||||
| -rw-r--r-- | spotify.go | 21 | ||||
| -rw-r--r-- | web/client.html | 13 | ||||
| -rw-r--r-- | wrms.go | 36 |
7 files changed, 195 insertions, 60 deletions
@@ -1,4 +1,9 @@ +## General + +- [X] add admin + ## Fronted + - [X] support incremental search results - [ ] distinguish different search queries @@ -17,5 +22,6 @@ - [X] Fix youtube search ## Configuration -- [ ] Implement command line flags -- [ ] Implement config file support + +- [X] Implement command line flags +- [X] Implement config file support @@ -1,9 +1,79 @@ package main +import ( + "io" + "os" + "path" + + "github.com/google/uuid" + "gopkg.in/yaml.v3" + "muhq.space/go/wrms/llog" +) + type Config struct { - Port int - Backends string - LocalMusicDir string - UploadDir string + Port int `yaml:"port"` + Backends []string `yaml:"backends"` + LocalMusicDir string `yaml:"music-dir"` + UploadDir string `yaml:"upload-dir"` + Admin uuid.UUID `yaml:"admin"` + spotify *spotifyConfig HasUpload bool } + +func defaultConfig() Config { + c := Config{Port: 8080, UploadDir: "upload"} + return c +} + +func (c Config) IsAdmin(id uuid.UUID) bool { + return c.Admin == id +} + +func findConfig() string { + confDir := os.Getenv("XDG_CONFIG_DIR") + if confDir == "" { + confDir = os.Getenv("HOME") + } + + paths := []string{"config.yml", + path.Join(confDir, "wrms", "config.yml")} + + for _, path := range paths { + fileInfo, err := os.Stat(path) + if !os.IsNotExist(err) && !fileInfo.IsDir() { + return path + } + } + + return "" +} + +func loadConfig(configPath string) Config { + f, err := os.Open(configPath) + if err != nil { + llog.Fatal("Opening config %s failed: %v", configPath, err) + } + defer f.Close() + + data, err := io.ReadAll(f) + if err != nil { + llog.Fatal("Reading config %s failed: %v", configPath, err) + } + + config := defaultConfig() + err = yaml.Unmarshal(data, &config) + if err != nil { + llog.Fatal("Failed to parse config %s: %v", configPath, err) + } + + return config +} + +func newConfig() Config { + configPath := findConfig() + if configPath != "" { + return loadConfig(configPath) + } + + return defaultConfig() +} @@ -6,11 +6,12 @@ import ( "flag" "fmt" "html/template" - "io/ioutil" + "io" "net/http" "strings" "time" + "golang.org/x/exp/slices" "muhq.space/go/wrms/llog" "github.com/google/uuid" @@ -22,32 +23,60 @@ var wrms *Wrms var pageTemplate *template.Template func landingPage(w http.ResponseWriter, r *http.Request) { - if _, err := r.Cookie("UUID"); err != nil { - http.SetCookie(w, &http.Cookie{Name: "UUID", Value: uuid.NewString()}) + var id uuid.UUID + if cookie, err := r.Cookie("UUID"); err != nil { + id, err = uuid.NewRandom() + if err != nil { + llog.Fatal("Failed to generate random uuid: %v", err) + } + + http.SetCookie(w, &http.Cookie{Name: "UUID", Value: id.String()}) + } else { + id, err = uuid.Parse(cookie.Value) + if err != nil { + http.Error(w, "Invalid UUID set in cookie", http.StatusBadRequest) + return + } } - if err := pageTemplate.Execute(w, wrms.Config); err != nil { + tempData := struct { + Config Config + IsAdmin bool + }{wrms.Config, wrms.Config.IsAdmin(id)} + + if err := pageTemplate.Execute(w, tempData); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } -func getConnId(w http.ResponseWriter, r *http.Request) string { +func getConnId(w http.ResponseWriter, r *http.Request) (uuid.UUID, error) { uuidCookie, err := r.Cookie("UUID") if err != nil { http.Error(w, "No connection ID cookie set", http.StatusUnauthorized) - return "" + return uuid.Nil, err } - return uuidCookie.Value + + id, err := uuid.Parse(uuidCookie.Value) + if err != nil { + http.Error(w, "Invalid UUID set in cookie", http.StatusBadRequest) + return uuid.Nil, err + } + + return id, nil } func searchHandler(w http.ResponseWriter, r *http.Request) { - connId := getConnId(w, r) - if connId == "" { + connId, err := getConnId(w, r) + if err != nil { return } conn := wrms.GetConn(connId) + if conn == nil { + http.Error(w, "No websocket connection found", http.StatusInternalServerError) + return + } pattern := r.URL.Query().Get("pattern") @@ -71,8 +100,8 @@ func searchHandler(w http.ResponseWriter, r *http.Request) { } func genericVoteHandler(w http.ResponseWriter, r *http.Request, vote string) { - connId := getConnId(w, r) - if connId == "" { + connId, err := getConnId(w, r) + if err != nil { return } @@ -94,7 +123,7 @@ func unvoteHandler(w http.ResponseWriter, r *http.Request) { } func addHandler(w http.ResponseWriter, r *http.Request) { - data, err := ioutil.ReadAll(r.Body) + data, err := io.ReadAll(r.Body) if err != nil { llog.Warning("Failed to read request body: %s", string(data)) http.Error(w, "Failed to read request body", http.StatusInternalServerError) @@ -113,6 +142,16 @@ func addHandler(w http.ResponseWriter, r *http.Request) { } func playPauseHandler(w http.ResponseWriter, r *http.Request) { + connId, err := getConnId(w, r) + if err != nil { + return + } + + if !wrms.Config.IsAdmin(connId) { + http.Error(w, "Only admin is allowed to control Playback", http.StatusUnauthorized) + return + } + wrms.PlayPause() } @@ -168,9 +207,9 @@ func wsEndpoint(w http.ResponseWriter, r *http.Request) { initialCmds = append(initialCmds, data) for _, song := range wrms.Songs { - if _, ok := song.Upvotes[id.String()]; ok { + if _, ok := song.Upvotes[id]; ok { upvoted = append(upvoted, song) - } else if _, ok := song.Downvotes[id.String()]; ok { + } else if _, ok := song.Downvotes[id]; ok { downvoted = append(downvoted, song) } } @@ -231,16 +270,32 @@ func setupRoutes() { } func main() { - config := Config{} + config := newConfig() + logLevel := flag.String("loglevel", "Warning", "log level") - flag.IntVar(&config.Port, "port", 8080, "port to listen to") - flag.StringVar(&config.Backends, "backends", "dummy youtube spotify", "music backend to use") - flag.StringVar(&config.LocalMusicDir, "serve-music-dir", "", "local music directory to serve") - flag.StringVar(&config.UploadDir, "upload-dir", "uploads/", "directory to upload songs to") + flag.IntVar(&config.Port, "port", config.Port, "port to listen to") + backends := flag.String("backends", "", "music backend to use") + flag.StringVar(&config.LocalMusicDir, + "serve-music-dir", config.LocalMusicDir, "local music directory to serve") + flag.StringVar(&config.UploadDir, "upload-dir", config.UploadDir, "directory to upload songs to") + genAdmin := flag.Bool("generate-admin", false, "generate an admin uuid") flag.Parse() + if *backends != "" { + config.Backends = strings.Split(*backends, " ") + } + + if *genAdmin { + var err error + config.Admin, err = uuid.NewRandom() + if err != nil { + llog.Fatal("Failed to create random UUID: %v", err) + } + llog.Info("Admin uuid: %v", config.Admin) + } + llog.SetLogLevelFromString(*logLevel) - config.HasUpload = strings.Contains(config.Backends, "upload") + config.HasUpload = slices.Contains(config.Backends, "upload") wrms = NewWrms(config) defer wrms.Close() @@ -23,7 +23,7 @@ func NewPlayer(wrms *Wrms, backends []string) Player { for _, backend := range backends { switch backend { case "spotify": - b, err = NewSpotify() + b, err = NewSpotify(wrms.Config.spotify) case "youtube": b = NewYoutubeBackend() case "dummy": @@ -25,23 +25,30 @@ const ( deviceName = "wrms" ) +type spotifyConfig struct { + username string + password string +} + type SpotifyBackend struct { session *core.Session } -func NewSpotify() (*SpotifyBackend, error) { +func NewSpotify(config *spotifyConfig) (*SpotifyBackend, error) { spotify := SpotifyBackend{} + if config != nil { + config = &spotifyConfig{} + } - var err error + config.username = os.Getenv("WRMS_SPOTIFY_USER") + config.password = os.Getenv("WRMS_SPOTIFY_PASSWORD") - username := os.Getenv("WRMS_SPOTIFY_USER") - password := os.Getenv("WRMS_SPOTIFY_PASSWORD") - if username == "" || password == "" { + if config.username == "" || config.password == "" { return nil, errors.New("User and password for spotify must be provided") } - spotify.session, err = librespot.Login(username, password, deviceName) - + var err error + spotify.session, err = librespot.Login(config.username, config.password, deviceName) if err != nil { return nil, err } diff --git a/web/client.html b/web/client.html index 1d82b45..a371c43 100644 --- a/web/client.html +++ b/web/client.html @@ -72,6 +72,7 @@ } } + let isAdmin = false; let songs = []; let playing = {}; let votes = new Map(); @@ -165,16 +166,16 @@ } function handlePause() { - document.getElementById("ppbutton").innerHTML = 'Play'; + {{if .IsAdmin}} document.getElementById("ppbutton").innerHTML = 'Play'; {{end}} } function handleStop() { - document.getElementById("ppbutton").innerHTML = 'Play'; + {{if .IsAdmin}} document.getElementById("ppbutton").innerHTML = 'Play'; {{end}} playing = document.getElementById("playing").innerHTML = ""; } function handlePlay(currentSong) { - document.getElementById("ppbutton").innerHTML = 'Pause'; + {{if .IsAdmin}} document.getElementById("ppbutton").innerHTML = 'Pause'; {{end}} idx = -1; songs.forEach(function(s, i, a) { if (s.uri == currentSong.uri) idx = i; }); @@ -256,7 +257,7 @@ return false; } - {{if .HasUpload}} + {{if .Config.HasUpload}} function submitUpload() { const form = new FormData(document.getElementById("uploadForm")); const file = form.get("song"); @@ -314,7 +315,7 @@ <button id="searchButton">Search</button> </form> - {{if .HasUpload}} + {{if .Config.HasUpload}} <form id="uploadForm" onsubmit="return submitUpload()"> <input id="uploadInput" name="song" type="file"> <button id="uploadButton", type="submit">Upload</button> @@ -333,9 +334,11 @@ <h2>Playing</h2> <p id='playing'></p> + {{if .IsAdmin}} <div id='controls'> <button id="ppbutton">Play</button> </div> + {{end}} <h2>Playlist</h2> <ul id='playlist'></ul> @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "strings" "muhq.space/go/wrms/llog" @@ -12,18 +11,18 @@ import ( ) type Song struct { - Title string `json:"title"` - Artist string `json:"artist"` - Source string `json:"source"` - Uri string `json:"uri"` - Weight int `json:"weight"` - index int `json:"-"` // used by heap.Interface - Upvotes map[string]struct{} `json:"-"` - Downvotes map[string]struct{} `json:"-"` + Title string `json:"title"` + Artist string `json:"artist"` + Source string `json:"source"` + Uri string `json:"uri"` + Weight int `json:"weight"` + index int `json:"-"` // used by heap.Interface + Upvotes map[uuid.UUID]struct{} `json:"-"` + Downvotes map[uuid.UUID]struct{} `json:"-"` } func NewSong(title, artist, source, uri string) Song { - return Song{title, artist, source, uri, 0, 0, map[string]struct{}{}, map[string]struct{}{}} + return Song{title, artist, source, uri, 0, 0, map[uuid.UUID]struct{}{}, map[uuid.UUID]struct{}{}} } func NewSongFromJson(data []byte) (Song, error) { @@ -34,8 +33,8 @@ func NewSongFromJson(data []byte) (Song, error) { return s, err } - s.Upvotes = map[string]struct{}{} - s.Downvotes = map[string]struct{}{} + s.Upvotes = map[uuid.UUID]struct{}{} + s.Downvotes = map[uuid.UUID]struct{}{} return s, nil } @@ -78,17 +77,12 @@ func NewWrms(config Config) *Wrms { wrms := Wrms{} wrms.Config = config wrms.Connections = make(map[uuid.UUID]*Connection) - wrms.Player = NewPlayer(&wrms, strings.Split(config.Backends, " ")) + wrms.Player = NewPlayer(&wrms, config.Backends) return &wrms } -func (wrms *Wrms) GetConn(connId string) *Connection { - id, err := uuid.Parse(connId) - if err != nil { - llog.Fatal("Failed to parse connId %s: %v", connId, err) - } - - return wrms.Connections[id] +func (wrms *Wrms) GetConn(connId uuid.UUID) *Connection { + return wrms.Connections[connId] } func (wrms *Wrms) Close() { @@ -158,7 +152,7 @@ func (wrms *Wrms) PlayPause() { wrms.Broadcast(ev) } -func (wrms *Wrms) AdjustSongWeight(connId string, songUri string, vote string) { +func (wrms *Wrms) AdjustSongWeight(connId uuid.UUID, songUri string, vote string) { for i := 0; i < len(wrms.Songs); i++ { s := &wrms.Songs[i] if s.Uri != songUri { |
