diff --git a/.forgejo/workflows/tests.yml b/.forgejo/workflows/tests.yml deleted file mode 100644 index 592b843..0000000 --- a/.forgejo/workflows/tests.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Run tests - -on: - push: - pull_request: - workflow_dispatch: - -jobs: - nix: - runs-on: docker - container: - image: nix - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Nix doctor - run: nix doctor - - name: Check flake - run: nix flake check --all-systems - - name: Build flake - run: nix build --print-out-paths - - name: Flake info - run: nix flake info - tests: - runs-on: docker - container: - image: debian:stable - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Go - uses: actions/setup-go@v4 - with: - #go-version: "stable" - go-version-file: "go.mod" - check-latest: true - - name: Run Linter - uses: dominikh/staticcheck-action@v1 - with: - version: "latest" - install-go: false - - name: Run tests - run: make test diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..65fbd27 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,73 @@ +include: + - template: Security/Dependency-Scanning.gitlab-ci.yml + - template: Security/SAST.gitlab-ci.yml + - template: Security/Secret-Detection.gitlab-ci.yml + - template: Security/Container-Scanning.gitlab-ci.yml + +variables: + # Use TLS https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#tls-enabled + DOCKER_HOST: tcp://docker:2376 + DOCKER_TLS_CERTDIR: "/certs" + CONTAINER_TEST_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG + CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest + +container_scanning: + variables: + CS_DEFAULT_BRANCH_IMAGE: $CI_REGISTRY_IMAGE/$CI_DEFAULT_BRANCH:$CI_COMMIT_SHA + +image: golang:latest + +stages: + - test + - build + - release + +format: + stage: test + script: + - go fmt $(go list ./...) + +vet: + stage: test + script: + - go vet $(go list ./...) + +test: + stage: test + script: + - go test -race $(go list ./...) + +sast: + stage: test + +compile: + stage: build + script: + - mkdir -p bin + - go build -v -o bin ./... + artifacts: + paths: + - bin + +build: + stage: build + image: docker:stable + services: + - docker:stable-dind + script: + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker build --pull -t $CONTAINER_TEST_IMAGE . + - docker push $CONTAINER_TEST_IMAGE + +release: + stage: release + image: docker:stable + services: + - docker:stable-dind + script: + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker pull $CONTAINER_TEST_IMAGE + - docker tag $CONTAINER_TEST_IMAGE $CONTAINER_RELEASE_IMAGE + - docker push $CONTAINER_RELEASE_IMAGE + only: + - main diff --git a/Makefile b/Makefile index f8eee1e..2a89362 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ PROJECT := sanic mpd: ## Run mpd test instance mkdir -p /tmp/${PROJECT}/{music,playlists} + cp *.mp3 /tmp/${PROJECT}/music/ touch /tmp/${PROJECT}/mpd_db mpd --no-daemon ./mpd.conf @@ -17,7 +18,7 @@ build: ## Compile project update: ## Update go dependencies go get -u - which gomod2nix && gomod2nix + which gomod2nix && gomod2nix # sync go deps with nix tidy: ## Add missing and remove unused modules go mod tidy diff --git a/README.md b/README.md index 95a23d8..5ae986e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Example flake setup (untested): inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; sanic = { - url = "git.berlin.ccc.de/cccb/sanic"; + url = "gitlab.com/XenGi/sanic"; inputs.nixpkgs.follows = "nixpkgs"; }; }; @@ -52,10 +52,18 @@ Example flake setup (untested): Install from the AUR: -``` +```shell yay -S sanic ``` +### Podman + +Run as daemon: + +```shell +podman run -d -v ./config.ini:/config.ini -p 8443:8443 registry.gitlab.com/XenGi/sanic:latest +``` + ## 🛠️ Development sanic is developed using [Nix][nix], but you can also just use the usual Golang tooling. diff --git a/config.ini b/config.ini index 132ef26..4c3b3fa 100644 --- a/config.ini +++ b/config.ini @@ -6,7 +6,7 @@ port = 6600 [ui] hostname = [::1] -port = 8080 -tls = no +port = 8443 +tls = yes cert = cert.pem key = key.pem diff --git a/flake.lock b/flake.lock index c570239..5273d21 100644 --- a/flake.lock +++ b/flake.lock @@ -28,11 +28,11 @@ ] }, "locked": { - "lastModified": 1717050755, - "narHash": "sha256-C9IEHABulv2zEDFA+Bf0E1nmfN4y6MIUe5eM2RCrDC0=", + "lastModified": 1722589758, + "narHash": "sha256-sbbA8b6Q2vB/t/r1znHawoXLysCyD4L/6n6/RykiSnA=", "owner": "tweag", "repo": "gomod2nix", - "rev": "31b6d2e40b36456e792cd6cf50d5a8ddd2fa59a1", + "rev": "4e08ca09253ef996bd4c03afa383b23e35fe28a1", "type": "github" }, "original": { @@ -43,11 +43,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1720955038, - "narHash": "sha256-GaliJqfFwyYxReFywxAa8orCO+EnDq2NK2F+5aSc8vo=", + "lastModified": 1722957468, + "narHash": "sha256-SQ0TCC4aklOhN/OzcztrKqDLY8SjpIZcyvTulzhDXs0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "aa247c0c90ecf4ae7a032c54fdc21b91ca274062", + "rev": "2a13929e1f191b3690dd2f2db13098b04adb9043", "type": "github" }, "original": { diff --git a/go.mod b/go.mod index 5a4ffed..2b14430 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.22 require ( github.com/fhs/gompd/v2 v2.3.0 - github.com/labstack/echo-contrib v0.17.0 + github.com/labstack/echo-contrib v0.17.1 github.com/labstack/echo/v4 v4.12.0 - golang.org/x/net v0.24.0 + golang.org/x/net v0.28.0 gopkg.in/ini.v1 v1.67.0 ) @@ -17,15 +17,16 @@ require ( github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/prometheus/client_golang v1.19.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.52.3 // indirect - github.com/prometheus/procfs v0.13.0 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.5.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/time v0.6.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/go.sum b/go.sum index 8e8f1d0..ddf2549 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/labstack/echo-contrib v0.17.0 h1:xam8wakZOsiQYM14Z0og1xF3w/heWNeDF5AtC5PlX8E= github.com/labstack/echo-contrib v0.17.0/go.mod h1:mjX5VB3OqJcroIEycptBOY9Hr7rK+unq79W8QFKGNV0= +github.com/labstack/echo-contrib v0.17.1 h1:7I/he7ylVKsDUieaGRZ9XxxTYOjfQwVzHzUYrNykfCU= +github.com/labstack/echo-contrib v0.17.1/go.mod h1:SnsCZtwHBAZm5uBSAtQtXQHI3wqEA73hvTn0bYMKnZA= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -21,16 +23,24 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA= github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -39,18 +49,30 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/gomod2nix.toml b/gomod2nix.toml index 9f3b3c0..4f81dd8 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -14,8 +14,8 @@ schema = 3 version = "v3.2.2+incompatible" hash = "sha256-LOkpuXhWrFayvVf1GOaOmZI5YKEsgqVSb22aF8LnCEM=" [mod."github.com/labstack/echo-contrib"] - version = "v0.17.0" - hash = "sha256-J5S8vO8Zg9uV9A3zbILswn/oPdwLKRXncsXdEjDOmU8=" + version = "v0.17.1" + hash = "sha256-OkO1gWz1xL8Rw2wy4RMJjsLGhABAqAsjpVkQ3b6G+L4=" [mod."github.com/labstack/echo/v4"] version = "v4.12.0" hash = "sha256-TPXJv/6C53bnmcEYxa9g5Mft8u/rLT96q64tQ9+RtKU=" @@ -28,18 +28,21 @@ schema = 3 [mod."github.com/mattn/go-isatty"] version = "v0.0.20" hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ=" + [mod."github.com/munnerz/goautoneg"] + version = "v0.0.0-20191010083416-a7dc8b61c822" + hash = "sha256-79URDDFenmGc9JZu+5AXHToMrtTREHb3BC84b/gym9Q=" [mod."github.com/prometheus/client_golang"] - version = "v1.19.0" - hash = "sha256-YV8sxMPR+xorTUCriTfcFsaV2b7PZfPJDQmOgUYOZJo=" + version = "v1.19.1" + hash = "sha256-MSLsMDi89uQc7Pa2fhqeamyfPJpenGj3r+eB/UotK7w=" [mod."github.com/prometheus/client_model"] version = "v0.6.1" hash = "sha256-rIDyUzNfxRA934PIoySR0EhuBbZVRK/25Jlc/r8WODw=" [mod."github.com/prometheus/common"] - version = "v0.52.3" - hash = "sha256-JzNAt7pimXZMnPxv8droAHJq+5OKWi9BYkGtMvqpyd0=" + version = "v0.55.0" + hash = "sha256-qzvCnc+hnAB5dq2MYy8GlPxgyNnyn9kFVlN2CXZe9T0=" [mod."github.com/prometheus/procfs"] - version = "v0.13.0" - hash = "sha256-J31K36TkIiQU2EGOcmqDa+dkoKXiVuxafPVT4rKbEsg=" + version = "v0.15.1" + hash = "sha256-H+WXJemFFwdoglmD6p7JRjrJJZmIVAmJwYmLbZ8Q9sw=" [mod."github.com/valyala/bytebufferpool"] version = "v1.0.0" hash = "sha256-I9FPZ3kCNRB+o0dpMwBnwZ35Fj9+ThvITn8a3Jr8mAY=" @@ -47,23 +50,23 @@ schema = 3 version = "v1.2.2" hash = "sha256-gp+lNXE8zjO+qJDM/YbS6V43HFsYP6PKn4ux1qa5lZ0=" [mod."golang.org/x/crypto"] - version = "v0.22.0" - hash = "sha256-2+u9nd32+Bi7EEv7QFc12CRTbfV7DApNv+yKIr7+lTw=" + version = "v0.26.0" + hash = "sha256-Iicrsb65fCmjfPILKoSLyBZMwe2VUcoTF5SpYTCQEuk=" [mod."golang.org/x/net"] - version = "v0.24.0" - hash = "sha256-w1c21ljta5wNIyel9CSIn/crPzwOCRofNKhqmfs4aEQ=" + version = "v0.28.0" + hash = "sha256-WdH/mgsX/CB+CiYtXEwJAXHN8FgtW2YhFcWwrrHNBLo=" [mod."golang.org/x/sys"] - version = "v0.19.0" - hash = "sha256-cmuL31TYLJmDm/fDnI2Sn0wB88cpdOHV1+urorsJWx4=" + version = "v0.23.0" + hash = "sha256-tC6QVLu72bADgINz26FUGdmYqKgsU45bHPg7sa0ZV7w=" [mod."golang.org/x/text"] - version = "v0.14.0" - hash = "sha256-yh3B0tom1RfzQBf1RNmfdNWF1PtiqxV41jW1GVS6JAg=" + version = "v0.17.0" + hash = "sha256-R8JbsP7KX+KFTHH7SjRnUGCdvtagylVOfngWEnVSqBc=" [mod."golang.org/x/time"] - version = "v0.5.0" - hash = "sha256-W6RgwgdYTO3byIPOFxrP2IpAZdgaGowAaVfYby7AULU=" + version = "v0.6.0" + hash = "sha256-gW9TVK9HjLk52lzfo5rBzSunc01gS0+SG2nk0X1w55M=" [mod."google.golang.org/protobuf"] - version = "v1.33.0" - hash = "sha256-cWwQjtUwSIEkAlAadrlxK1PYZXTRrV4NKzt7xDpJgIU=" + version = "v1.34.2" + hash = "sha256-nMTlrDEE2dbpWz50eQMPBQXCyQh4IdjrTIccaU0F3m0=" [mod."gopkg.in/ini.v1"] version = "v1.67.0" hash = "sha256-V10ahGNGT+NLRdKUyRg1dos5RxLBXBk1xutcnquc/+4=" diff --git a/server.go b/server.go index ae2e3c8..233c4dc 100644 --- a/server.go +++ b/server.go @@ -1,19 +1,14 @@ 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" "os" "os/exec" - "strings" ) // Config holds the configuration for the mpd connection and for the web server. @@ -78,6 +73,21 @@ func main() { return c.File("index.html") }) + // echo back request to check if HTTP/2 works etc + e.GET("/echo", func(c echo.Context) error { + req := c.Request() + format := ` + + Protocol: %s
+ Host: %s
+ Remote Address: %s
+ Method: %s
+ Path: %s
+
+` + return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path)) + }) + g := e.Group("/api") g.GET("/update_db", updateDb) g.GET("/previous_track", previousTrack) @@ -104,7 +114,7 @@ func main() { g.GET("/download", downloadTrack) - e.GET("/ws", wsServe) + e.GET("/sse", serveSSE) 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)) @@ -113,90 +123,8 @@ func main() { } } -// wsServe handles websocket connections. -func wsServe(c echo.Context) error { - websocket.Handler(func(ws *websocket.Conn) { - defer ws.Close() - - // Connect to MPD server - mpdConn, err := mpd.Dial("tcp", "localhost:6600") - if err != nil { - //log.Fatalln(err) - c.Logger().Error(err) - err = websocket.Message.Send(ws, fmt.Sprintf("{\"mpd_error\":\"%s\"}", err.Error())) - if err != nil { - c.Logger().Error(err) - } - } - defer mpdConn.Close() - - for { - // Read - msg := "" - err := websocket.Message.Receive(ws, &msg) - if err != nil { - c.Logger().Error(err) - break - } else { - // log.Print(msg) - if strings.ToLower(msg) == "#status" { - status, err := mpdConn.Status() - if err != nil { - c.Logger().Error(err) - } - currentsong, err := mpdConn.CurrentSong() - if err != nil { - c.Logger().Error(err) - } - queue, err := mpdConn.PlaylistInfo(-1, -1) - if err != nil { - c.Logger().Error(err) - } - jsonStatus, err := json.Marshal(status) - if err != nil { - c.Logger().Error(err) - } - jsonCurrentSong, err := json.Marshal(currentsong) - if err != nil { - c.Logger().Error(err) - } - jsonQueue, err := json.Marshal(queue) - if err != nil { - c.Logger().Error(err) - } - err = websocket.Message.Send(ws, fmt.Sprintf("{\"mpd_status\":%s,\"mpd_current_song\":%s,\"mpd_queue\":%s}", string(jsonStatus), string(jsonCurrentSong), string(jsonQueue))) - if err != nil { - c.Logger().Error(err) - } - - } 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, fmt.Sprintf("Downloading %s", uri)) - if err != nil { - c.Logger().Error(err) - } - } - } - } - }).ServeHTTP(c.Response(), c.Request()) - return nil -} - // downloadTrack tries to download a given URL and saves the song to the database. 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} - cmd := exec.Command( "yt-dlp", "--no-wait-for-video", @@ -212,7 +140,7 @@ func downloadTrack(c echo.Context) error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - log.Fatalln(err) + c.Logger().Fatal(err) } return c.String(http.StatusAccepted, "") diff --git a/sse.go b/sse.go new file mode 100644 index 0000000..299463d --- /dev/null +++ b/sse.go @@ -0,0 +1,191 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/fhs/gompd/v2/mpd" + "github.com/labstack/echo/v4" + "io" + "time" +) + +// Event represents Server-Sent Event. +// SSE explanation: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format +type Event struct { + // ID is used to set the EventSource object's last event ID value. + ID []byte + // Data field is for the message. When the EventSource receives multiple consecutive lines + // that begin with data:, it concatenates them, inserting a newline character between each one. + // Trailing newlines are removed. + Data []byte + // Event is a string identifying the type of event described. If this is specified, an event + // will be dispatched on the browser to the listener for the specified event name; the website + // source code should use addEventListener() to listen for named events. The onmessage handler + // is called if no event name is specified for a message. + Event []byte + // Retry is the reconnection time. If the connection to the server is lost, the browser will + // wait for the specified time before attempting to reconnect. This must be an integer, specifying + // the reconnection time in milliseconds. If a non-integer value is specified, the field is ignored. + Retry []byte + // Comment line can be used to prevent connections from timing out; a server can send a comment + // periodically to keep the connection alive. + Comment []byte +} + +// MarshalTo marshals Event to given Writer +func (ev *Event) MarshalTo(w io.Writer) error { + // Marshalling part is taken from: https://github.com/r3labs/sse/blob/c6d5381ee3ca63828b321c16baa008fd6c0b4564/http.go#L16 + if len(ev.Data) == 0 && len(ev.Comment) == 0 { + return nil + } + + if len(ev.Data) > 0 { + if _, err := fmt.Fprintf(w, "id: %s\n", ev.ID); err != nil { + return err + } + + sd := bytes.Split(ev.Data, []byte("\n")) + for i := range sd { + if _, err := fmt.Fprintf(w, "data: %s\n", sd[i]); err != nil { + return err + } + } + + if len(ev.Event) > 0 { + if _, err := fmt.Fprintf(w, "event: %s\n", ev.Event); err != nil { + return err + } + } + + if len(ev.Retry) > 0 { + if _, err := fmt.Fprintf(w, "retry: %s\n", ev.Retry); err != nil { + return err + } + } + } + + if len(ev.Comment) > 0 { + if _, err := fmt.Fprintf(w, ": %s\n", ev.Comment); err != nil { + return err + } + } + + if _, err := fmt.Fprint(w, "\n"); err != nil { + return err + } + + return nil +} + +// serveSSE handles sending Server-Sent-Events. +func serveSSE(c echo.Context) error { + // TODO: figure out how to retrieve IP from Forwarded header behind proxy: https://echo.labstack.com/docs/ip-address + c.Logger().Printf("SSE client connected, ip: %v", c.RealIP()) + + w := c.Response() + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + // Connect to MPD server + mpdConn, err := mpd.Dial("tcp", "localhost:6600") + if err != nil { + c.Logger().Error(err) + event := Event{ + Event: []byte("mpd"), + Data: []byte(fmt.Sprintf("connection error: %s", err.Error())), + } + if err := event.MarshalTo(w); err != nil { + return err + } + w.Flush() + } + defer mpdConn.Close() + + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + var lastJsonStatus []byte + var lastJsonCurrentSong []byte + var lastJsonQueue []byte + for { + select { + case <-c.Request().Context().Done(): + c.Logger().Printf("SSE client disconnected, ip: %v", c.RealIP()) + return nil + case <-ticker.C: + c.Logger().Printf("Getting MPD status for %v", c.RealIP()) + + status, err := mpdConn.Status() + if err != nil { + c.Logger().Error(err) + } + jsonStatus, err := json.Marshal(status) + if err != nil { + c.Logger().Error(err) + } + // Only send new event if different from last time + if !bytes.Equal(jsonStatus, lastJsonStatus) { + statusEvent := Event{ + Event: []byte("status"), + Data: []byte(string(jsonStatus)), + } + if err := statusEvent.MarshalTo(w); err != nil { + return err + } + lastJsonStatus = jsonStatus + } + + currentsong, err := mpdConn.CurrentSong() + if err != nil { + c.Logger().Error(err) + } + jsonCurrentSong, err := json.Marshal(currentsong) + if err != nil { + c.Logger().Error(err) + } + // Only send new event if different from last time + if !bytes.Equal(jsonCurrentSong, lastJsonCurrentSong) { + currentSongEvent := Event{ + Event: []byte("status"), + Data: []byte(string(jsonCurrentSong)), + } + if err := currentSongEvent.MarshalTo(w); err != nil { + return err + } + lastJsonCurrentSong = jsonCurrentSong + } + + queue, err := mpdConn.PlaylistInfo(-1, -1) + if err != nil { + c.Logger().Error(err) + } + jsonQueue, err := json.Marshal(queue) + if err != nil { + c.Logger().Error(err) + } + // Only send new event if different from last time + if !bytes.Equal(jsonQueue, lastJsonQueue) { + queueEvent := Event{ + Event: []byte("status"), + Data: []byte(string(jsonQueue)), + } + if err := queueEvent.MarshalTo(w); err != nil { + return err + } + lastJsonQueue = jsonQueue + } + + // Ping to prevent timeout + pingEvent := Event{ + Comment: []byte("ping"), + } + if err := pingEvent.MarshalTo(w); err != nil { + return err + } + + w.Flush() + } + } +} diff --git a/static/index.html b/static/index.html index 31b34ce..728bce6 100644 --- a/static/index.html +++ b/static/index.html @@ -197,7 +197,6 @@