diff --git a/NOTES.md b/NOTES.md index d3ce923..793ee3e 100644 --- a/NOTES.md +++ b/NOTES.md @@ -18,3 +18,14 @@ - [ ] save playlist - [ ] delete playlist +# mpd state + +- [x] state ("play", "stop", "pause") +- [x] repeat +- [ ] shuffle +- [ ] xfade +- [x] volume +- [x] track length/progress + - [x] track progress (seek) +- [ ] track name + diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..3f54744 --- /dev/null +++ b/config.ini @@ -0,0 +1,12 @@ +[mpd] +host = localhost +port = 6600 +#username = +#pasword = + +[ui] +hostname = localhost +port = 8080 +tls = no +cert = cert.pem +key = key.pem diff --git a/favicon.xcf b/favicon.xcf new file mode 100644 index 0000000..65f9164 Binary files /dev/null and b/favicon.xcf differ diff --git a/flake.lock b/flake.lock index bc26a35..37265ac 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1694529238, - "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "owner": "numtide", "repo": "flake-utils", - "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "type": "github" }, "original": { @@ -28,11 +28,11 @@ ] }, "locked": { - "lastModified": 1699950847, - "narHash": "sha256-xN/yVtqHb7kimHA/WvQFrEG5WS38t0K+A/W+j/WhQWM=", + "lastModified": 1701687253, + "narHash": "sha256-qJCMxIKWXonJODPF2oV7mCd0xu7VYVenTucrY0bizto=", "owner": "tweag", "repo": "gomod2nix", - "rev": "05c993c9a5bd55a629cd45ed49951557b7e9c61a", + "rev": "001bbfa22e2adeb87c34c6015e5694e88721cabe", "type": "github" }, "original": { @@ -43,11 +43,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1701237617, - "narHash": "sha256-Ryd8xpNDY9MJnBFDYhB37XSFIxCPVVVXAbInNPa95vs=", + "lastModified": 1702206697, + "narHash": "sha256-vE9oEx3Y8TO5MnWwFlmopjHd1JoEBno+EhsfUCq5iR8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "85306ef2470ba705c97ce72741d56e42d0264015", + "rev": "29d6c96900b9b576c2fb89491452f283aa979819", "type": "github" }, "original": { diff --git a/go.mod b/go.mod index 25cad23..a81631f 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/labstack/echo-contrib v0.15.0 github.com/labstack/echo/v4 v4.11.2 golang.org/x/net v0.17.0 + gopkg.in/ini.v1 v1.67.0 ) require ( diff --git a/go.sum b/go.sum index c6db544..153f4b9 100644 --- a/go.sum +++ b/go.sum @@ -80,6 +80,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/mpd.go b/mpd.go new file mode 100644 index 0000000..12bac1c --- /dev/null +++ b/mpd.go @@ -0,0 +1,190 @@ +package main + +import ( + "github.com/fhs/gompd/v2/mpd" + "github.com/labstack/echo/v4" + "log" + "net/http" + "strconv" + "time" +) + +// MPD API calls + +func previousTrack(c echo.Context) error { + // Connect to MPD server + conn, err := mpd.Dial("tcp", "localhost:6600") + if err != nil { + log.Fatalln(err) + } + defer conn.Close() + + err = conn.Previous() + if err != nil { + log.Fatalln(err) + } + + return c.String(http.StatusOK, "") +} + +func nextTrack(c echo.Context) error { + // Connect to MPD server + conn, err := mpd.Dial("tcp", "localhost:6600") + if err != nil { + log.Fatalln(err) + } + defer conn.Close() + + err = conn.Next() + if err != nil { + log.Fatalln(err) + } + + return c.String(http.StatusOK, "") +} + +func stopPlayback(c echo.Context) error { + // Connect to MPD server + conn, err := mpd.Dial("tcp", "localhost:6600") + if err != nil { + log.Fatalln(err) + } + defer conn.Close() + + err = conn.Stop() + if err != nil { + log.Fatalln(err) + } + + return c.String(http.StatusOK, "") +} + +func resumePlayback(c echo.Context) error { + // Connect to MPD server + conn, err := mpd.Dial("tcp", "localhost:6600") + if err != nil { + log.Fatalln(err) + } + defer conn.Close() + + err = conn.Pause(false) + if err != nil { + log.Fatalln(err) + } + + return c.String(http.StatusOK, "") +} + +func pausePlayback(c echo.Context) error { + // Connect to MPD server + conn, err := mpd.Dial("tcp", "localhost:6600") + if err != nil { + log.Fatalln(err) + } + defer conn.Close() + + err = conn.Pause(true) + if err != nil { + log.Fatalln(err) + } + + return c.String(http.StatusOK, "") +} + +func seek(c echo.Context) error { + // Connect to MPD server + conn, err := mpd.Dial("tcp", "localhost:6600") + if err != nil { + log.Fatalln(err) + } + defer conn.Close() + + seconds, err := strconv.Atoi(c.Param("seconds")) + if err != nil { + log.Fatalln(err) + } + + if seconds < 0 { + return c.String(http.StatusBadRequest, "seconds must be positive integer") + } + + err = conn.SeekCur(time.Duration(seconds)*time.Second, false) + if err != nil { + log.Fatalln(err) + } + + return c.String(http.StatusOK, "") +} + +func toggleRepeat(c echo.Context) error { + // Connect to MPD server + conn, err := mpd.Dial("tcp", "localhost:6600") + if err != nil { + log.Fatalln(err) + } + defer conn.Close() + + status, err := conn.Status() + if err != nil { + log.Fatalln(err) + } + if status["repeat"] == "1" { + err = conn.Repeat(false) + } else { + err = conn.Repeat(true) + } + if err != nil { + log.Fatalln(err) + } + + return c.String(http.StatusOK, "") +} + +func toggleRandom(c echo.Context) error { + // Connect to MPD server + conn, err := mpd.Dial("tcp", "localhost:6600") + if err != nil { + log.Fatalln(err) + } + defer conn.Close() + + status, err := conn.Status() + if err != nil { + log.Fatalln(err) + } + if status["toggleRandom"] == "1" { + err = conn.Random(false) + } else { + err = conn.Random(true) + } + if err != nil { + log.Fatalln(err) + } + + return c.String(http.StatusOK, "") +} + +func setVolume(c echo.Context) error { + // Connect to MPD server + conn, err := mpd.Dial("tcp", "localhost:6600") + if err != nil { + log.Fatalln(err) + } + defer conn.Close() + + level, err := strconv.Atoi(c.Param("level")) + if err != nil { + log.Fatalln(err) + } + + if level > 100 || level < 0 { + return c.String(http.StatusBadRequest, "Volume must be between 0 and 100") + } + + err = conn.SetVolume(level) + if err != nil { + log.Fatalln(err) + } + + return c.String(http.StatusOK, "") +} diff --git a/server.go b/server.go index 80d1eec..fbd02c5 100644 --- a/server.go +++ b/server.go @@ -1,20 +1,52 @@ package main import ( + "encoding/json" "fmt" "github.com/fhs/gompd/v2/mpd" "github.com/labstack/echo-contrib/echoprometheus" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "golang.org/x/net/websocket" + "gopkg.in/ini.v1" "log" "net/http" - "strconv" + "os" + "os/exec" "strings" - "time" ) +type Config struct { + MPD struct { + Hostname string `ini:"hostname"` + Port int `ini:"port"` + Username string `ini:"username"` + Password string `ini:"password"` + } `ini:"mpd"` + UI struct { + Hostname string `ini:"hostname"` + Port int `ini:"port"` + Tls bool `ini:"tls"` + Certificate string `ini:"cert"` + Key string `ini:"key"` + } `ini:"ui"` +} + func main() { + iniData, err := ini.Load("config.ini") + if err != nil { + fmt.Printf("Fail to read configuration file: %v", err) + os.Exit(1) + } + + var config Config + + err = iniData.MapTo(&config) + if err != nil { + fmt.Printf("Fail to parse configuration file: %v", err) + os.Exit(1) + } + e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) @@ -56,17 +88,28 @@ func main() { g.GET("/random", toggleRandom) g.GET("/volume/:level", setVolume) + g.GET("/download", downloadTrack) + e.GET("/ws", wsServe) - e.Logger.Fatal(e.StartTLS(":1323", "cert.pem", "key.pem")) - //e.Logger.Fatal(e.Start(":1323")) + if config.UI.Tls { + e.Logger.Fatal(e.StartTLS(fmt.Sprintf("%s:%d", config.UI.Hostname, config.UI.Port), config.UI.Certificate, config.UI.Key)) + } else { + e.Logger.Fatal(e.Start(fmt.Sprintf("%s:%d", config.UI.Hostname, config.UI.Port))) + } } func wsServe(c echo.Context) error { - fmt.Println("wsServe") websocket.Handler(func(ws *websocket.Conn) { defer ws.Close() - fmt.Println("handler") + + // Connect to MPD server + mpdConn, err := mpd.Dial("tcp", "localhost:6600") + if err != nil { + log.Fatalln(err) + } + defer mpdConn.Close() + for { // Read msg := "" @@ -75,205 +118,66 @@ func wsServe(c echo.Context) error { c.Logger().Error(err) break } else { - if strings.HasPrefix(strings.ToUpper(msg), "MPD#") { - // Forward MPD communication - // TODO: forward request to mpd and response back to client - err := websocket.Message.Send(ws, "MPD command received, processing... processing...") + log.Println(msg) + if strings.ToLower(msg) == "#status" { + // TODO: Get current MPD status and return it + status, err := mpdConn.Status() + if err != nil { + log.Fatalln(err) + } + jsonData, err := json.Marshal(status) + if err != nil { + log.Fatalln(err) + } + err = websocket.Message.Send(ws, fmt.Sprintf("{\"mpd_status\":%s}", string(jsonData))) if err != nil { c.Logger().Error(err) } - } else if strings.HasPrefix(strings.ToUpper(msg), "YT#") { + } else if strings.HasPrefix(strings.ToLower(msg), "#download ") { // Download video link as audio file + uri := strings.SplitN(msg, " ", 2)[1] // TODO: implement yt-dlp integration - err := websocket.Message.Send(ws, "YT-DLP command received, processing... processing...") + err := websocket.Message.Send(ws, fmt.Sprintf("Downloading %s", uri)) if err != nil { c.Logger().Error(err) } } } - //fmt.Println(msg) } }).ServeHTTP(c.Response(), c.Request()) return nil } -// API calls +func downloadTrack(c echo.Context) error { + // yt-dlp \ + // --no-wait-for-video \ + // --no-playlist \ + // --windows-filenames \ + // --newline \ + // --extract-audio \ + // --audio-format mp3 \ + // --audio-quality 0 \ + // -f bestaudio/best \ + // ${video_url} -func previousTrack(c echo.Context) error { - // Connect to MPD server - conn, err := mpd.Dial("tcp", "localhost:6600") - if err != nil { - log.Fatalln(err) - } - defer conn.Close() - - err = conn.Previous() - if err != nil { + cmd := exec.Command( + "yt-dlp", + "--no-wait-for-video", + "--no-playlist", + "--windows-filenames", + "--newline", + "--extract-audio", + "--audio-format", "mp3", + "--audio-quality", "0", + "--format", "bestaudio/best", + c.Param("url"), + ) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { log.Fatalln(err) } - return c.String(http.StatusNoContent, "") -} - -func nextTrack(c echo.Context) error { - // Connect to MPD server - conn, err := mpd.Dial("tcp", "localhost:6600") - if err != nil { - log.Fatalln(err) - } - defer conn.Close() - - err = conn.Next() - if err != nil { - log.Fatalln(err) - } - - return c.String(http.StatusNoContent, "") -} - -func stopPlayback(c echo.Context) error { - // Connect to MPD server - conn, err := mpd.Dial("tcp", "localhost:6600") - if err != nil { - log.Fatalln(err) - } - defer conn.Close() - - err = conn.Stop() - if err != nil { - log.Fatalln(err) - } - - return c.String(http.StatusNoContent, "") -} - -func resumePlayback(c echo.Context) error { - // Connect to MPD server - conn, err := mpd.Dial("tcp", "localhost:6600") - if err != nil { - log.Fatalln(err) - } - defer conn.Close() - - err = conn.Pause(false) - if err != nil { - log.Fatalln(err) - } - - return c.String(http.StatusNoContent, "") -} - -func pausePlayback(c echo.Context) error { - // Connect to MPD server - conn, err := mpd.Dial("tcp", "localhost:6600") - if err != nil { - log.Fatalln(err) - } - defer conn.Close() - - err = conn.Pause(true) - if err != nil { - log.Fatalln(err) - } - - return c.String(http.StatusNoContent, "") -} - -func seek(c echo.Context) error { - // Connect to MPD server - conn, err := mpd.Dial("tcp", "localhost:6600") - if err != nil { - log.Fatalln(err) - } - defer conn.Close() - - seconds, err := strconv.Atoi(c.Param("seconds")) - if err != nil { - log.Fatalln(err) - } - - if seconds < 0 { - return c.String(http.StatusBadRequest, "seconds must be positive integer") - } - - err = conn.SeekCur(time.Duration(seconds)*time.Second, false) - if err != nil { - log.Fatalln(err) - } - - return c.String(http.StatusNoContent, "") -} - -func toggleRepeat(c echo.Context) error { - // Connect to MPD server - conn, err := mpd.Dial("tcp", "localhost:6600") - if err != nil { - log.Fatalln(err) - } - defer conn.Close() - - status, err := conn.Status() - if err != nil { - log.Fatalln(err) - } - if status["repeat"] == "1" { - err = conn.Repeat(false) - } else { - err = conn.Repeat(true) - } - if err != nil { - log.Fatalln(err) - } - - return c.String(http.StatusNoContent, "") -} - -func toggleRandom(c echo.Context) error { - // Connect to MPD server - conn, err := mpd.Dial("tcp", "localhost:6600") - if err != nil { - log.Fatalln(err) - } - defer conn.Close() - - status, err := conn.Status() - if err != nil { - log.Fatalln(err) - } - if status["toggleRandom"] == "1" { - err = conn.Random(false) - } else { - err = conn.Random(true) - } - if err != nil { - log.Fatalln(err) - } - - return c.String(http.StatusNoContent, "") -} - -func setVolume(c echo.Context) error { - // Connect to MPD server - conn, err := mpd.Dial("tcp", "localhost:6600") - if err != nil { - log.Fatalln(err) - } - defer conn.Close() - - level, err := strconv.Atoi(c.Param("level")) - if err != nil { - log.Fatalln(err) - } - - if level > 100 || level < 0 { - return c.String(http.StatusBadRequest, "Volume must be between 0 and 100") - } - - err = conn.SetVolume(level) - if err != nil { - log.Fatalln(err) - } - - return c.String(http.StatusNoContent, "") + return c.String(http.StatusAccepted, "") } diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..6681b0e Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/test.html b/static/test.html new file mode 100644 index 0000000..ed7c9e9 --- /dev/null +++ b/static/test.html @@ -0,0 +1,71 @@ + + +
+