From 99dc857d4191ee82e4da2f72772199af90714bc8 Mon Sep 17 00:00:00 2001 From: "Ricardo (XenGi) Band" Date: Sun, 10 Dec 2023 21:33:54 +0100 Subject: [PATCH] add mpd status messages --- NOTES.md | 11 ++ favicon.xcf | Bin 0 -> 27944 bytes flake.lock | 18 ++-- mpd.go | 190 +++++++++++++++++++++++++++++++++++ server.go | 244 +++++++++++---------------------------------- static/favicon.ico | Bin 0 -> 67814 bytes static/test.html | 71 +++++++++++++ 7 files changed, 338 insertions(+), 196 deletions(-) create mode 100644 favicon.xcf create mode 100644 mpd.go create mode 100644 static/favicon.ico create mode 100644 static/test.html 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/favicon.xcf b/favicon.xcf new file mode 100644 index 0000000000000000000000000000000000000000..65f9164de573be349169f8d00295bb7666f678c2 GIT binary patch literal 27944 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 IJ;dby0Vq|hxc~qF literal 0 HcmV?d00001 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/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..aa1a168 100644 --- a/server.go +++ b/server.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "fmt" "github.com/fhs/gompd/v2/mpd" "github.com/labstack/echo-contrib/echoprometheus" @@ -9,9 +10,9 @@ import ( "golang.org/x/net/websocket" "log" "net/http" - "strconv" + "os" + "os/exec" "strings" - "time" ) func main() { @@ -56,17 +57,25 @@ 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")) + //e.Logger.Fatal(e.StartTLS(":1323", "cert.pem", "key.pem")) + e.Logger.Fatal(e.Start(":1323")) } 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 +84,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 0000000000000000000000000000000000000000..6681b0e8122e0b2e2e9061e0d35bd90590142923 GIT binary patch literal 67814 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 qPt5rV-?LD}P{V)LNbmJEr_gJPdE^ANK + + + API test + + + + + + + + +