diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6737e08 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +mpd: + mkdir -p /tmp/sanic/{music,playlists} + mpd --no-daemon ./mpd.conf diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..d3ce923 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,20 @@ +# mpd commands + +- [x] previous track +- [x] next track +- [x] stop +- [x] play/pause +- [x] set track progress +- [x] set volume +- [x] repeat queue +- [x] shuffle queue +- [ ] set fade +- [ ] add track to queue +- [ ] rm track from queue +- [ ] move track in queue +- [ ] clear queue +- [ ] list tracks in music db +- [ ] list playlists +- [ ] save playlist +- [ ] delete playlist + diff --git a/flake.lock b/flake.lock index 8edcf61..bc26a35 100644 --- a/flake.lock +++ b/flake.lock @@ -28,11 +28,11 @@ ] }, "locked": { - "lastModified": 1694616124, - "narHash": "sha256-c49BVhQKw3XDRgt+y+uPAbArtgUlMXCET6VxEBmzHXE=", + "lastModified": 1699950847, + "narHash": "sha256-xN/yVtqHb7kimHA/WvQFrEG5WS38t0K+A/W+j/WhQWM=", "owner": "tweag", "repo": "gomod2nix", - "rev": "f95720e89af6165c8c0aa77f180461fe786f3c21", + "rev": "05c993c9a5bd55a629cd45ed49951557b7e9c61a", "type": "github" }, "original": { @@ -43,11 +43,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1697915759, - "narHash": "sha256-WyMj5jGcecD+KC8gEs+wFth1J1wjisZf8kVZH13f1Zo=", + "lastModified": 1701237617, + "narHash": "sha256-Ryd8xpNDY9MJnBFDYhB37XSFIxCPVVVXAbInNPa95vs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "51d906d2341c9e866e48c2efcaac0f2d70bfd43e", + "rev": "85306ef2470ba705c97ce72741d56e42d0264015", "type": "github" }, "original": { diff --git a/server.go b/server.go index 3b3e73e..80d1eec 100644 --- a/server.go +++ b/server.go @@ -1,13 +1,17 @@ package main import ( + "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" + "log" "net/http" + "strconv" "strings" - - "github.com/labstack/echo/v4" + "time" ) func main() { @@ -25,6 +29,7 @@ func main() { e.GET("/metrics", echoprometheus.NewHandler()) e.GET("/", func(c echo.Context) (err error) { + // HTTP/2 Server Push pusher, ok := c.Response().Writer.(http.Pusher) if ok { if err = pusher.Push("/style.css", nil); err != nil { @@ -40,6 +45,17 @@ func main() { return c.File("index.html") }) + g := e.Group("/api") + g.GET("/previous_track", previousTrack) + g.GET("/next_track", nextTrack) + g.GET("/stop", stopPlayback) + g.GET("/play", resumePlayback) + g.GET("/pause", pausePlayback) + g.GET("/seek/:seconds", seek) + g.GET("/repeat", toggleRepeat) + g.GET("/random", toggleRandom) + g.GET("/volume/:level", setVolume) + e.GET("/ws", wsServe) e.Logger.Fatal(e.StartTLS(":1323", "cert.pem", "key.pem")) @@ -47,33 +63,217 @@ func main() { } func wsServe(c echo.Context) error { + fmt.Println("wsServe") websocket.Handler(func(ws *websocket.Conn) { defer ws.Close() + fmt.Println("handler") for { // Read msg := "" err := websocket.Message.Receive(ws, &msg) if err != nil { c.Logger().Error(err) - } - // Forward MPD communication - if strings.HasPrefix(strings.ToUpper(msg), "MPD#") { - // TODO: forward request to mpd and response back to client - err := websocket.Message.Send(ws, "MPD command received, processing... processing...") - if err != nil { - 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...") + if err != nil { + c.Logger().Error(err) + } + + } else if strings.HasPrefix(strings.ToUpper(msg), "YT#") { + // Download video link as audio file + // TODO: implement yt-dlp integration + err := websocket.Message.Send(ws, "YT-DLP command received, processing... processing...") + if err != nil { + c.Logger().Error(err) + } } } - // Download video link as audio file - if strings.HasPrefix(strings.ToUpper(msg), "YT#") { - // TODO: implement yt-dlp integration - err := websocket.Message.Send(ws, "YT-DLP command received, processing... processing...") - if err != nil { - c.Logger().Error(err) - } - } - //fmt.Printf("%s\n", msg) + //fmt.Println(msg) } }).ServeHTTP(c.Response(), c.Request()) return nil } + +// 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.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, "") +} diff --git a/static/flexbox/index.html b/static/flexbox/index.html new file mode 100644 index 0000000..009c080 --- /dev/null +++ b/static/flexbox/index.html @@ -0,0 +1,264 @@ + + + + + Sanic - Flexbox layout + + + + + + +
+
+
+ + +
+
+
+ + + + +
+
+ + +
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+ xfade +
+
+ +
00
+ +
+
+
+
+ + +
+
+
+

Now playing: 00:00:00/100:00:00

+ + + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PositionArtistTitleAlbumGenreTime
1ChakraLove Shines Through (Martin Roth's in Electro Love Remix)undefinedundefined9:29
2ChakraLove Shines Through (Martin Roth's in Electro Love Remix)undefinedundefined9:29
3ChakraLove Shines Through (Martin Roth's in Electro Love Remix)undefinedundefined9:29
4ChakraLove Shines Through (Martin Roth's in Electro Love Remix)undefinedundefined9:29
5ChakraLove Shines Through (Martin Roth's in Electro Love Remix)undefinedundefined9:29
6ChakraLove Shines Through (Martin Roth's in Electro Love Remix)undefinedundefined9:29
7ChakraLove Shines Through (Martin Roth's in Electro Love Remix)undefinedundefined9:29
+
+
+
+
    +
  • +
    + / +
      +
    • +
      + 00_music +
        +
      • autosort
      • +
      • reimport
      • +
      • unsortable
      • +
      • youtube
      • +
      +
      +
    • +
    • +
      + 01_incoming +
        +
      • coon
      • +
      • cascha
      • +
      • Xen
      • +
      +
      +
    • +
    • +
      + 02_megablast +
        +
      • dnb
      • +
      • mix
      • +
      +
      +
    • +
    • +
      + 03_mfs +
        +
      • ambient
      • +
      • electronic
      • +
      +
      +
    • +
    +
    +
  • +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArtistTitleAlbumGenreTime
ChakraLove Shines Through (Martin Roth's in Electro Love Remix)undefinedundefined9:29
ChakraLove Shines Through (Martin Roth's in Electro Love Remix)undefinedundefined9:29
ChakraLove Shines Through (Martin Roth's in Electro Love Remix)undefinedundefined9:29
ChakraLove Shines Through (Martin Roth's in Electro Love Remix)undefinedundefined9:29
ChakraLove Shines Through (Martin Roth's in Electro Love Remix)undefinedundefined9:29
ChakraLove Shines Through (Martin Roth's in Electro Love Remix)undefinedundefined9:29
ChakraLove Shines Through (Martin Roth's in Electro Love Remix)undefinedundefined9:29
+
+
+ +
+ + diff --git a/static/flexbox/rangeinput.css b/static/flexbox/rangeinput.css new file mode 100644 index 0000000..73a4ffd --- /dev/null +++ b/static/flexbox/rangeinput.css @@ -0,0 +1,77 @@ +/* https://www.smashingmagazine.com/2021/12/create-custom-range-input-consistent-browsers/ */ + +/********** Range Input Styles **********/ + +/* Range Reset */ +input[type="range"] { + -webkit-appearance: none; + appearance: none; + background: transparent; + cursor: pointer; +} + +/* Removes default focus */ +input[type="range"]:focus { + outline: none; +} + +/***** Chrome, Safari, Opera and Edge Chromium styles *****/ + +/* slider track */ +input[type="range"]::-webkit-slider-runnable-track { + height: 5px; + width: 70px; + border: 1px solid #1a2445; + border-right-color: #3a506b; + border-bottom-color: #3a506b; + background-color: #2e2e27; + background-repeat: repeat-x; +} + +/* slider thumb */ +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; /* Override default look */ + appearance: none; + margin-top: -12px; /* Centers thumb on the track */ + + /* custom styles */ + height: 11px; + margin-top: -4px; + width: 5px; + background: #640000; + border: 1px solid #812b25; + border-right-color: #21110f; + border-bottom-color: #21110f; +} + +input[type="range"]:focus::-webkit-slider-thumb { + border: 1px solid #21110f; + border-right-color: #812b25; + border-bottom-color: #812b25; +} + +/******** Firefox styles ********/ + +/* slider track */ +input[type="range"]::-moz-range-track { + background-color: #053a5f; + border-radius: 0.5rem; + height: 0.5rem; +} + +/* slider thumb */ +input[type="range"]::-moz-range-thumb { + border: none; /*Removes extra border that FF applies*/ + border-radius: 0; /*Removes default border-radius that FF applies*/ + + /* custom styles */ + background-color: #5cd5eb; + height: 2rem; + width: 1rem; +} + +input[type="range"]:focus::-moz-range-thumb { + border: 1px solid #053a5f; + outline: 3px solid #053a5f; + outline-offset: 0.125rem; +} diff --git a/static/flexbox/sanic.css b/static/flexbox/sanic.css new file mode 100644 index 0000000..7d77655 --- /dev/null +++ b/static/flexbox/sanic.css @@ -0,0 +1,203 @@ +html, body { + background-color: #09101d; + color: #bbb; + height: 99%; +} + +div { + /* border: 1px dashed white; */ + + font-weight: normal; + font-family: Arial, Helvetica, sans-serif; + font-size: 13px; +} + +button { + background-color: #28374a; + color: #bbb; +} + +input { + background-color: #28374a; + color: white; +} + +table { + width: 100%; + table-layout: fixed; + border-spacing: 0pt; +} + +thead { + background-repeat: repeat-x; + background-image: url(../img/table-header-gradient.png); +} + +th { + font-weight: bold; + padding: 2px 2px 2px 14px; + border: solid #1c2c1a; + border-width: 0 1px 0 0; + cursor: pointer; +} + +td { + padding: 1px 1px 1px 0.5em; + border: solid black; + border-width: 0 1px 1px 0; + text-align: left; +} + +#queue-table tr td:first-of-type { + padding-left: 16px; +} + +/* This is probably a better way to generate alternate coloring on tables. However, + background color for selected track is overwritten this way. Therefore the "old + school" way of alternate coloring is used for now. + +table tr:nth-child(odd) td { + background:#1e1f1a; +} + +table tr:nth-child(even) td { + background:#171812; +} +*/ + +table tr.odd { + background-color: #1e1f1a; +} + +table tr.even { + background-color: #171812; +} + +#queue-table tr.playing { + background-color: #490b00; +} + +td.playing { + background-image: url(../img/playback.png); + background-repeat: no-repeat; + background-position: left center; +} + +#flexbox-container { + display: flex; + flex-direction: column; + height: 100%; +} + +#controls-top { + display: flex; + flex-direction: row; + + padding: 5px; + background-repeat: repeat-x; + background-image: url(../img/top-controls-bg.png); +} + +#controls-top div { + border-right: 1px solid black; +} + +#top-left-controls { + display: flex; + flex-direction: column; + width: 100px; +} + +#top-left-controls button { + text-align: left; +} + +#playback-controls { + display: flex; + flex-direction: column; + width: 160px; +} + +#progress { + margin-left: 10px; +} + +#volume { + margin-left: 10px; +} + +#queue-volume-controls { + width: 150px; +} + +#queue-controls { + display: flex; + flex-direction: row; +} + +#queue-xfade-control { + display: flex; + flex-direction: column; + text-align: center; +} + +#queue-xfade-buttons { + display: flex; + flex-direction: row; +} + +#top-logo { + display: flex; + flex-direction: column; + text-align: center; + color: white; + width: 50px; +} + +#top-logo-container { + display: flex; + flex-grow: 1; + justify-content: flex-end; +} + +#sanic-logo { + width: 36px; + padding-left: 8px; +} + +#xfade { + width: 20px; +} + +#queue { + display: flex; + flex-direction: column; + flex-grow: 1; + border-bottom: 4px ridge #3a506b; +} + +#controls_bottom { + display: flex; + flex-direction: row; + flex-grow: 2; +} + +#playlist_controls { + background-color: #171812;; + /* flex-grow: 1; */ + + width: 20%; /* frickel? */ + border-right: 4px ridge #3a506b; +} + +#playlist_tracklist { + display: flex; + flex-direction: column; + + width: 80%; /* frickel? */ +} + +#footer { + background-color: #041936; + text-align: right; +} diff --git a/static/flexbox/sanic.js b/static/flexbox/sanic.js new file mode 100644 index 0000000..e69de29 diff --git a/static/flexbox/treeview.css b/static/flexbox/treeview.css new file mode 100644 index 0000000..6f052b6 --- /dev/null +++ b/static/flexbox/treeview.css @@ -0,0 +1,91 @@ +/* https://iamkate.com/code/tree-views/ */ + +/* Custom properties */ + +.tree { + --spacing : 1.0rem; + --radius : 10px; +} + +/* Padding */ + +.tree li { + display : block; + position : relative; + padding-left : calc(2 * var(--spacing) - var(--radius) - 2px + 10px); +} + +.tree ul { + margin-left : calc(var(--radius) - var(--spacing)); + padding-left : 0; +} + +/* Vertical lines */ + +.tree ul li { + border-left : 2px solid #ddd; +} + +.tree ul li:last-child { + border-color : transparent; +} + +/* Horizontal lines */ + +.tree ul li::before { + content : ''; + display : block; + position : absolute; + top : calc(var(--spacing) / -2); + left : -2px; + width : calc(var(--spacing) + 2px); + height : calc(var(--spacing) + 1px); + border : solid #ddd; + border-width : 0 0 2px 2px; +} + +/* Summaries */ + +.tree summary { + display : block; + cursor : pointer; +} + +.tree summary::marker, +.tree summary::-webkit-details-marker { + display : none; +} + +.tree summary:focus { + outline : none; +} + +.tree summary:focus-visible { + outline : 1px dotted #000; +} + +/* Markers */ + +.tree li::after, +.tree summary::before { + content : ''; + display : block; + position : absolute; + top : calc(var(--spacing) / 2 - var(--radius)); + left : calc(var(--spacing) - var(--radius) - 1px); + width : calc(2 * var(--radius)); + height : calc(2 * var(--radius)); + border-radius : 50%; + background : #ddd; +} + +/* Expand and collapse buttons */ + +.tree summary::before { + z-index : 1; + background : #696 url('../img/expand-collapse.svg') 0 0; +} + +.tree details[open] > summary::before { + background-position : calc(-2 * var(--radius)) 0; +} diff --git a/static/img/expand-collapse.svg b/static/img/expand-collapse.svg new file mode 100644 index 0000000..f34809c --- /dev/null +++ b/static/img/expand-collapse.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/static/img/playback.png b/static/img/playback.png new file mode 100644 index 0000000..eebea58 Binary files /dev/null and b/static/img/playback.png differ diff --git a/static/img/sanic-logo.webp b/static/img/sanic-logo.webp new file mode 100644 index 0000000..4ed3d29 Binary files /dev/null and b/static/img/sanic-logo.webp differ diff --git a/static/img/table-header-gradient.png b/static/img/table-header-gradient.png new file mode 100644 index 0000000..d853129 Binary files /dev/null and b/static/img/table-header-gradient.png differ diff --git a/static/img/top-controls-bg.png b/static/img/top-controls-bg.png new file mode 100644 index 0000000..8107473 Binary files /dev/null and b/static/img/top-controls-bg.png differ