aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO.md (renamed from TODO)10
-rw-r--r--config.go78
-rw-r--r--main.go95
-rw-r--r--player.go2
-rw-r--r--spotify.go21
-rw-r--r--web/client.html13
-rw-r--r--wrms.go36
7 files changed, 195 insertions, 60 deletions
diff --git a/TODO b/TODO.md
index d44392a..9f72b92 100644
--- a/TODO
+++ b/TODO.md
@@ -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
diff --git a/config.go b/config.go
index a74069d..049145a 100644
--- a/config.go
+++ b/config.go
@@ -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()
+}
diff --git a/main.go b/main.go
index 3e54901..956f374 100644
--- a/main.go
+++ b/main.go
@@ -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()
diff --git a/player.go b/player.go
index 2be55c9..a3232dd 100644
--- a/player.go
+++ b/player.go
@@ -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":
diff --git a/spotify.go b/spotify.go
index 8a32fc6..0e4974a 100644
--- a/spotify.go
+++ b/spotify.go
@@ -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>
diff --git a/wrms.go b/wrms.go
index cf2e20c..51e39f7 100644
--- a/wrms.go
+++ b/wrms.go
@@ -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 {